diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 933eb6e..27e9745 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## 5.5.0
+- Added Outbox pattern integration
+- Changed
+ - Optimized sending of messages by sending batches in parallel
+
+## 5.4.0
+- Added Isolation feature V2
+
## 5.3.1
- Changed
- Fix usage of sent counter on receiver instead of received counter
diff --git a/src/Ev.ServiceBus.Abstractions/Ev.ServiceBus.Abstractions.csproj b/src/Ev.ServiceBus.Abstractions/Ev.ServiceBus.Abstractions.csproj
index 0741c8d..a3fc703 100644
--- a/src/Ev.ServiceBus.Abstractions/Ev.ServiceBus.Abstractions.csproj
+++ b/src/Ev.ServiceBus.Abstractions/Ev.ServiceBus.Abstractions.csproj
@@ -1,7 +1,7 @@
- netstandard2.1;net8.0
+ net8.0
true
MIT
@@ -15,12 +15,7 @@
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/Ev.ServiceBus.Abstractions/Exceptions/SenderNotFoundException.cs b/src/Ev.ServiceBus.Abstractions/Exceptions/SenderNotFoundException.cs
index 6458e77..5419593 100644
--- a/src/Ev.ServiceBus.Abstractions/Exceptions/SenderNotFoundException.cs
+++ b/src/Ev.ServiceBus.Abstractions/Exceptions/SenderNotFoundException.cs
@@ -5,13 +5,13 @@ namespace Ev.ServiceBus.Abstractions;
[Serializable]
public class SenderNotFoundException : Exception
{
- public SenderNotFoundException(ClientType clientType, string topicName)
+ public SenderNotFoundException(string resourceId)
: base(
- $"The {clientType.ToString()} '{topicName}' you tried to retrieve was not found. "
- + $"Verify your configuration to make sure the {clientType.ToString()} is properly registered.")
+ $"The '{resourceId}' you tried to retrieve was not found. "
+ + $"Verify your configuration to make sure the resource is properly registered.")
{
- TopicName = topicName;
+ ResourceId = resourceId;
}
- public string TopicName { get; }
+ public string ResourceId { get; }
}
\ No newline at end of file
diff --git a/src/Ev.ServiceBus.Abstractions/IMessagePublisher.cs b/src/Ev.ServiceBus.Abstractions/IMessagePublisher.cs
index 37f7aac..6ec1f3c 100644
--- a/src/Ev.ServiceBus.Abstractions/IMessagePublisher.cs
+++ b/src/Ev.ServiceBus.Abstractions/IMessagePublisher.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
namespace Ev.ServiceBus.Abstractions;
@@ -9,7 +10,7 @@ public interface IMessagePublisher
///
/// The object to send through Service Bus
/// A type of object that is registered within Ev.ServiceBus
- void Publish(TMessagePayload messageDto);
+ Task Publish(TMessagePayload messageDto);
///
/// Temporarily stores the object to send through Service Bus until is called.
@@ -17,7 +18,7 @@ public interface IMessagePublisher
/// The object to send through Service Bus
/// The sessionId to attach to the outgoing message
/// A type of object that is registered within Ev.ServiceBus
- void Publish(TMessagePayload messageDto, string sessionId);
+ Task Publish(TMessagePayload messageDto, string sessionId);
///
/// Temporarily stores the object to send through Service Bus until is called.
@@ -25,5 +26,5 @@ public interface IMessagePublisher
/// The object to send through Service Bus
/// Configurator of message context
/// A type of object that is registered within Ev.ServiceBus
- void Publish(TMessagePayload messageDto, Action messageContextConfiguration);
+ Task Publish(TMessagePayload messageDto, Action messageContextConfiguration);
}
\ No newline at end of file
diff --git a/src/Ev.ServiceBus.Abstractions/IsExternalInit.cs b/src/Ev.ServiceBus.Abstractions/IsExternalInit.cs
new file mode 100644
index 0000000..37f9b76
--- /dev/null
+++ b/src/Ev.ServiceBus.Abstractions/IsExternalInit.cs
@@ -0,0 +1,11 @@
+using System.ComponentModel;
+
+namespace System.Runtime.CompilerServices;
+
+///
+/// Reserved to be used by the compiler for tracking metadata.
+/// This class should not be used by developers in source code.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+internal static class IsExternalInit {
+}
diff --git a/src/Ev.ServiceBus/Dispatch/DispatchSender.cs b/src/Ev.ServiceBus/Dispatch/DispatchSender.cs
index 480402a..afdd22c 100644
--- a/src/Ev.ServiceBus/Dispatch/DispatchSender.cs
+++ b/src/Ev.ServiceBus/Dispatch/DispatchSender.cs
@@ -3,40 +3,21 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Azure.Messaging.ServiceBus;
using Ev.ServiceBus.Abstractions;
-using Ev.ServiceBus.Abstractions.Extensions;
-using Ev.ServiceBus.Abstractions.MessageReception;
-using Ev.ServiceBus.Diagnostics;
-using Ev.ServiceBus.Management;
-using Microsoft.Extensions.Options;
namespace Ev.ServiceBus.Dispatch;
public class DispatchSender : IDispatchSender
{
- private const int MaxMessagePerSend = 100;
- private readonly IMessagePayloadSerializer _messagePayloadSerializer;
- private readonly ServiceBusRegistry _dispatchRegistry;
- private readonly ServiceBusRegistry _registry;
- private readonly IMessageMetadataAccessor _messageMetadataAccessor;
- private readonly IEnumerable _dispatchCustomizers;
- private readonly ServiceBusOptions _serviceBusOptions;
+ private readonly ServiceBusMessageFactory _messageFactory;
+ private readonly ServiceBusMessageSender _serviceBusMessageSender;
public DispatchSender(
- ServiceBusRegistry registry,
- IMessagePayloadSerializer messagePayloadSerializer,
- ServiceBusRegistry dispatchRegistry,
- IMessageMetadataAccessor messageMetadataAccessor,
- IEnumerable dispatchCustomizers,
- IOptions serviceBusOptions)
+ ServiceBusMessageFactory messageFactory,
+ ServiceBusMessageSender serviceBusMessageSender)
{
- _registry = registry;
- _messagePayloadSerializer = messagePayloadSerializer;
- _dispatchRegistry = dispatchRegistry;
- _messageMetadataAccessor = messageMetadataAccessor;
- _dispatchCustomizers = dispatchCustomizers;
- _serviceBusOptions = serviceBusOptions.Value;
+ _messageFactory = messageFactory;
+ _serviceBusMessageSender = serviceBusMessageSender;
}
///
@@ -50,20 +31,14 @@ public async Task SendDispatch(object messagePayload, CancellationToken token =
///
public async Task SendDispatch(Abstractions.Dispatch messagePayload, CancellationToken token = default)
{
- var dispatches = CreateMessagesToSend([messagePayload]);
+ var dispatches = _messageFactory.CreateMessagesToSend([messagePayload]);
- foreach (var messagePerResource in dispatches)
- {
- var message = messagePerResource.Messages.Single();
+ var messagePerResource = dispatches.Single();
- await messagePerResource.Sender.SendMessageAsync(message.Message, token);
- ServiceBusMeter.IncrementSentCounter(
- 1,
- messagePerResource.Sender.ClientType.ToString(),
- messagePerResource.Sender.Name,
- message.Message.ApplicationProperties[UserProperties.PayloadTypeIdProperty]?.ToString()
- );
- }
+ await _serviceBusMessageSender.SendMessages(
+ messagePerResource.ResourceId,
+ messagePerResource.Messages,
+ token);
}
///
@@ -86,48 +61,10 @@ public async Task SendDispatches(IEnumerable messagePaylo
throw new ArgumentNullException(nameof(messagePayloads));
}
- var dispatches = CreateMessagesToSend(messagePayloads);
+ var dispatches = _messageFactory.CreateMessagesToSend(messagePayloads);
foreach (var messagesPerResource in dispatches)
{
- await BatchAndSendMessages(messagesPerResource, token, async (sender, batch) =>
- {
- await sender.SendMessagesAsync(batch, token);
- });
- }
- }
-
- private async Task BatchAndSendMessages(MessagesPerResource dispatches, CancellationToken token, Func senderAction)
- {
- var batches = new List();
- var batch = await dispatches.Sender.CreateMessageBatchAsync(token);
- batches.Add(batch);
- foreach (var messageToSend in dispatches.Messages)
- {
- ServiceBusMeter.IncrementSentCounter(
- 1,
- dispatches.Sender.ClientType.ToString(),
- dispatches.Sender.Name,
- messageToSend.Message.ApplicationProperties[UserProperties.PayloadTypeIdProperty]?.ToString()
- );
-
- if (batch.TryAddMessage(messageToSend.Message))
- {
- continue;
- }
- batch = await dispatches.Sender.CreateMessageBatchAsync(token);
- batches.Add(batch);
- if (batch.TryAddMessage(messageToSend.Message))
- {
- continue;
- }
-
- throw new ArgumentOutOfRangeException("A message is too big to fit in a single batch");
- }
-
- foreach (var pageMessages in batches)
- {
- await senderAction.Invoke(dispatches.Sender, pageMessages);
- pageMessages.Dispose();
+ await _serviceBusMessageSender.SendMessages(messagesPerResource.ResourceId, messagesPerResource.Messages, token);
}
}
@@ -151,132 +88,10 @@ public async Task ScheduleDispatches(IEnumerable messageP
throw new ArgumentNullException(nameof(messagePayloads));
}
- var dispatches = CreateMessagesToSend(messagePayloads);
+ var dispatches = _messageFactory.CreateMessagesToSend(messagePayloads);
foreach (var messagesPerResource in dispatches)
{
- await PaginateAndSendMessages(messagesPerResource, async (sender, page) =>
- {
- await sender.ScheduleMessagesAsync(page, scheduledEnqueueTime, token);
- });
- }
- }
-
- private async Task PaginateAndSendMessages(MessagesPerResource dispatches, Func, Task> senderAction)
- {
- var paginatedMessages = dispatches.Messages.Select(o => o.Message)
- .Select((x, i) => new
- {
- Item = x,
- Index = i
- })
- .GroupBy(x => x.Index / MaxMessagePerSend, x => x.Item);
-
- foreach (var pageMessages in paginatedMessages)
- {
- foreach (var message in pageMessages)
- {
- ServiceBusMeter.IncrementSentCounter(
- 1,
- dispatches.Sender.ClientType.ToString(),
- dispatches.Sender.Name,
- message.ApplicationProperties[UserProperties.PayloadTypeIdProperty]?.ToString()
- );
- }
-
- await senderAction.Invoke(dispatches.Sender, pageMessages.Select(m => m).ToArray());
- }
- }
-
- private class MessagesPerResource
- {
- public MessageToSend[] Messages { get; set; }
- public ClientType ClientType { get; set; }
- public string ResourceId { get; set; }
- public IMessageSender Sender { get; set; }
- }
-
- private class MessageToSend
- {
- public MessageToSend(ServiceBusMessage message, MessageDispatchRegistration registration)
- {
- Message = message;
- Registration = registration;
- }
-
- public ServiceBusMessage Message { get; }
- public MessageDispatchRegistration Registration { get; }
- }
-
- private MessagesPerResource[] CreateMessagesToSend(IEnumerable messagePayloads)
- {
- var dispatches =
- (
- from dispatch in messagePayloads
- // the same dispatch can be published to several senders
- let registrations = _dispatchRegistry.GetDispatchRegistrations(dispatch.Payload.GetType())
- from eventPublicationRegistration in registrations
- let message = CreateMessage(eventPublicationRegistration, dispatch)
- select new MessageToSend(message, eventPublicationRegistration)
- )
- .ToArray();
-
- var messagesPerResource = (
- from dispatch in dispatches
- group dispatch by new { dispatch.Registration.Options.ClientType, dispatch.Registration.Options.ResourceId } into gr
- let sender = _registry.GetMessageSender(gr.Key.ClientType, gr.Key.ResourceId)
- select new MessagesPerResource()
- {
- Messages = gr.ToArray(),
- ClientType = gr.Key.ClientType,
- ResourceId = gr.Key.ResourceId,
- Sender = sender
- }).ToArray();
-
- return messagesPerResource;
- }
-
- private ServiceBusMessage CreateMessage(
- MessageDispatchRegistration registration,
- Abstractions.Dispatch dispatch)
- {
- var result = _messagePayloadSerializer.SerializeBody(dispatch.Payload);
- var message = MessageHelper.CreateMessage(result.ContentType, result.Body, registration.PayloadTypeId);
-
- dispatch.ApplicationProperties.Remove(UserProperties.PayloadTypeIdProperty);
- foreach (var dispatchApplicationProperty in dispatch.ApplicationProperties)
- {
- message.ApplicationProperties[dispatchApplicationProperty.Key] = dispatchApplicationProperty.Value;
- }
-
- message.SessionId = dispatch.SessionId;
-
- var originalCorrelationId = _messageMetadataAccessor.Metadata?.CorrelationId ?? Guid.NewGuid().ToString();
- message.CorrelationId = dispatch.CorrelationId ?? originalCorrelationId;
-
- var originalIsolationKey = _messageMetadataAccessor.Metadata?.ApplicationProperties.GetIsolationKey();
- message.SetIsolationKey(originalIsolationKey ?? _serviceBusOptions.Settings.IsolationSettings.IsolationKey);
-
- var originalIsolationApps = _messageMetadataAccessor.Metadata?.ApplicationProperties.GetIsolationApps() ?? [];
- message.SetIsolationApps(originalIsolationApps);
-
- if (dispatch.DiagnosticId != null)
- {
- message.SetDiagnosticIdIfIsNot(dispatch.DiagnosticId);
- }
- if (!string.IsNullOrWhiteSpace(dispatch.MessageId))
- {
- message.MessageId = dispatch.MessageId;
- }
-
- foreach (var customizer in registration.OutgoingMessageCustomizers)
- {
- customizer?.Invoke(message, dispatch.Payload);
- }
-
- foreach (var dispatchCustomizer in _dispatchCustomizers)
- {
- dispatchCustomizer.ExtendDispatch(message, dispatch.Payload);
+ await _serviceBusMessageSender.ScheduleMessages(messagesPerResource.ResourceId, messagesPerResource.Messages, scheduledEnqueueTime, token);
}
- return message;
}
}
\ No newline at end of file
diff --git a/src/Ev.ServiceBus/Dispatch/IServiceBusMessageSender.cs b/src/Ev.ServiceBus/Dispatch/IServiceBusMessageSender.cs
new file mode 100644
index 0000000..3ce9aa5
--- /dev/null
+++ b/src/Ev.ServiceBus/Dispatch/IServiceBusMessageSender.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Messaging.ServiceBus;
+
+namespace Ev.ServiceBus.Dispatch;
+
+public interface IServiceBusMessageSender
+{
+ Task SendMessages(string resourceId, ServiceBusMessage[] messages, CancellationToken token);
+
+ Task ScheduleMessages(
+ string resourceId,
+ ServiceBusMessage[] messages,
+ DateTimeOffset scheduledEnqueueTime,
+ CancellationToken token);
+}
\ No newline at end of file
diff --git a/src/Ev.ServiceBus/Dispatch/MessageDispatcher.cs b/src/Ev.ServiceBus/Dispatch/MessageDispatcher.cs
index 89552a3..0bb5796 100644
--- a/src/Ev.ServiceBus/Dispatch/MessageDispatcher.cs
+++ b/src/Ev.ServiceBus/Dispatch/MessageDispatcher.cs
@@ -31,7 +31,7 @@ public async Task ExecuteDispatches(CancellationToken token)
}
///
- public void Publish(TMessageDto messageDto)
+ public Task Publish(TMessageDto messageDto)
{
if (messageDto == null)
{
@@ -42,10 +42,12 @@ public void Publish(TMessageDto messageDto)
{
DiagnosticId = Activity.Current?.Id
});
+
+ return Task.CompletedTask;
}
///
- public void Publish(TMessagePayload messageDto, string sessionId)
+ public Task Publish(TMessagePayload messageDto, string sessionId)
{
if (messageDto == null)
{
@@ -62,10 +64,12 @@ public void Publish(TMessagePayload messageDto, string sessionI
SessionId = sessionId,
DiagnosticId = Activity.Current?.Id
});
+
+ return Task.CompletedTask;
}
///
- public void Publish(
+ public Task Publish(
TMessagePayload messageDto,
Action messageContextConfiguration)
{
@@ -90,5 +94,7 @@ public void Publish(
MessageId = context.MessageId,
DiagnosticId = context.DiagnosticId ?? Activity.Current?.Id
});
+
+ return Task.CompletedTask;
}
}
\ No newline at end of file
diff --git a/src/Ev.ServiceBus/Dispatch/Outbox/IOutboxService.cs b/src/Ev.ServiceBus/Dispatch/Outbox/IOutboxService.cs
new file mode 100644
index 0000000..70b40de
--- /dev/null
+++ b/src/Ev.ServiceBus/Dispatch/Outbox/IOutboxService.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Messaging.ServiceBus;
+
+namespace Ev.ServiceBus.Dispatch.Outbox;
+
+public interface IOutboxService
+{
+ Task StoreMessage(string resourceId, ServiceBusMessage message, CancellationToken token);
+ Task StoreScheduledMessage(string resourceId, DateTimeOffset scheduledEnqueueTime, ServiceBusMessage message, CancellationToken token);
+ Task EagerlySendStoredMessages(CancellationToken token);
+}
diff --git a/src/Ev.ServiceBus/Dispatch/Outbox/OutboxDispatchSender.cs b/src/Ev.ServiceBus/Dispatch/Outbox/OutboxDispatchSender.cs
new file mode 100644
index 0000000..8a8b9a0
--- /dev/null
+++ b/src/Ev.ServiceBus/Dispatch/Outbox/OutboxDispatchSender.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Ev.ServiceBus.Abstractions;
+
+namespace Ev.ServiceBus.Dispatch.Outbox;
+
+public class OutboxDispatchSender : IDispatchSender
+{
+ private readonly IDispatchSender _underlyingDispatchSender;
+ private readonly IOutboxService _outboxService;
+ private readonly ServiceBusMessageFactory _messageFactory;
+
+ public OutboxDispatchSender(
+ IDispatchSender underlyingDispatchSender,
+ IOutboxService outboxService,
+ ServiceBusMessageFactory serviceBusMessageFactory)
+ {
+ _underlyingDispatchSender = underlyingDispatchSender;
+ _outboxService = outboxService;
+ _messageFactory = serviceBusMessageFactory;
+ }
+
+ public async Task SendDispatch(object messagePayload, CancellationToken token = default)
+ {
+ await SendDispatch(new Abstractions.Dispatch(messagePayload), token);
+ }
+
+ public async Task SendDispatch(Abstractions.Dispatch messagePayload, CancellationToken token = default)
+ {
+ var messagesPerResources = _messageFactory.CreateMessagesToSend([messagePayload]);
+
+ foreach (var messagesPerResource in messagesPerResources)
+ {
+ foreach (var message in messagesPerResource.Messages)
+ {
+ await _outboxService.StoreMessage(messagesPerResource.ResourceId, message, token);
+ }
+ }
+ }
+
+ public async Task SendDispatches(IEnumerable