@@ -41,5 +41,24 @@ The AzureServiceBus configuration options provided directly by the CAP: | |||||
NAME | DESCRIPTION | TYPE | DEFAULT | NAME | DESCRIPTION | TYPE | DEFAULT | ||||
:---|:---|---|:--- | :---|:---|---|:--- | ||||
ConnectionString | Endpoint address | string | | ConnectionString | Endpoint address | string | | ||||
EnableSessions | Enable [Service bus sessions](https://docs.microsoft.com/en-us/azure/service-bus-messaging/message-sessions) | bool | false | |||||
TopicPath | Topic entity path | string | cap | TopicPath | Topic entity path | string | cap | ||||
ManagementTokenProvider | Token provider | ITokenProvider | null | |||||
ManagementTokenProvider | Token provider | ITokenProvider | null | |||||
#### Sessions | |||||
When sessions are enabled (see `EnableSessions` option above), every message sent will have a session id. To control the session id, include | |||||
an extra header with name `AzureServiceBusHeaders.SessionId` when publishing events: | |||||
```csharp | |||||
ICapPublisher capBus = ...; | |||||
string yourEventName = ...; | |||||
YourEventType yourEvent = ...; | |||||
Dictionary<string, string> extraHeaders = new Dictionary<string, string>(); | |||||
extraHeaders.Add(AzureServiceBusHeaders.SessionId, <your-session-id>); | |||||
capBus.Publish(yourEventName, yourEvent, extraHeaders); | |||||
``` | |||||
If no session id header is present, the message id will be used as the session id. |
@@ -78,13 +78,25 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
{ | { | ||||
ConnectAsync().GetAwaiter().GetResult(); | ConnectAsync().GetAwaiter().GetResult(); | ||||
_consumerClient.RegisterMessageHandler(OnConsumerReceived, | |||||
new MessageHandlerOptions(OnExceptionReceived) | |||||
{ | |||||
AutoComplete = false, | |||||
MaxConcurrentCalls = 10, | |||||
MaxAutoRenewDuration = TimeSpan.FromSeconds(30) | |||||
}); | |||||
if (_asbOptions.EnableSessions) | |||||
{ | |||||
_consumerClient.RegisterSessionHandler(OnConsumerReceivedWithSession, | |||||
new SessionHandlerOptions(OnExceptionReceived) | |||||
{ | |||||
AutoComplete = false, | |||||
MaxAutoRenewDuration = TimeSpan.FromSeconds(30) | |||||
}); | |||||
} | |||||
else | |||||
{ | |||||
_consumerClient.RegisterMessageHandler(OnConsumerReceived, | |||||
new MessageHandlerOptions(OnExceptionReceived) | |||||
{ | |||||
AutoComplete = false, | |||||
MaxConcurrentCalls = 10, | |||||
MaxAutoRenewDuration = TimeSpan.FromSeconds(30) | |||||
}); | |||||
} | |||||
while (true) | while (true) | ||||
{ | { | ||||
@@ -96,7 +108,15 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
public void Commit(object sender) | public void Commit(object sender) | ||||
{ | { | ||||
_consumerClient.CompleteAsync((string)sender); | |||||
var commitInput = (AzureServiceBusConsumerCommitInput) sender; | |||||
if (_asbOptions.EnableSessions) | |||||
{ | |||||
commitInput.Session.CompleteAsync(commitInput.LockToken); | |||||
} | |||||
else | |||||
{ | |||||
_consumerClient.CompleteAsync(commitInput.LockToken); | |||||
} | |||||
} | } | ||||
public void Reject(object sender) | public void Reject(object sender) | ||||
@@ -141,7 +161,13 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
if (!await mClient.SubscriptionExistsAsync(_asbOptions.TopicPath, _subscriptionName)) | if (!await mClient.SubscriptionExistsAsync(_asbOptions.TopicPath, _subscriptionName)) | ||||
{ | { | ||||
await mClient.CreateSubscriptionAsync(_asbOptions.TopicPath, _subscriptionName); | |||||
var subscriptionDescription = | |||||
new SubscriptionDescription(_asbOptions.TopicPath, _subscriptionName) | |||||
{ | |||||
RequiresSession = _asbOptions.EnableSessions | |||||
}; | |||||
await mClient.CreateSubscriptionAsync(subscriptionDescription); | |||||
_logger.LogInformation($"Azure Service Bus topic {_asbOptions.TopicPath} created subscription: {_subscriptionName}"); | _logger.LogInformation($"Azure Service Bus topic {_asbOptions.TopicPath} created subscription: {_subscriptionName}"); | ||||
} | } | ||||
@@ -157,14 +183,28 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
#region private methods | #region private methods | ||||
private Task OnConsumerReceived(Message message, CancellationToken token) | |||||
private TransportMessage ConvertMessage(Message message) | |||||
{ | { | ||||
var header = message.UserProperties.ToDictionary(x => x.Key, y => y.Value?.ToString()); | var header = message.UserProperties.ToDictionary(x => x.Key, y => y.Value?.ToString()); | ||||
header.Add(Headers.Group, _subscriptionName); | header.Add(Headers.Group, _subscriptionName); | ||||
var context = new TransportMessage(header, message.Body); | |||||
return new TransportMessage(header, message.Body); | |||||
} | |||||
private Task OnConsumerReceivedWithSession(IMessageSession session, Message message, CancellationToken token) | |||||
{ | |||||
var context = ConvertMessage(message); | |||||
OnMessageReceived?.Invoke(new AzureServiceBusConsumerCommitInput(message.SystemProperties.LockToken, session), context); | |||||
return Task.CompletedTask; | |||||
} | |||||
private Task OnConsumerReceived(Message message, CancellationToken token) | |||||
{ | |||||
var context = ConvertMessage(message); | |||||
OnMessageReceived?.Invoke(message.SystemProperties.LockToken, context); | |||||
OnMessageReceived?.Invoke(new AzureServiceBusConsumerCommitInput(message.SystemProperties.LockToken), context); | |||||
return Task.CompletedTask; | return Task.CompletedTask; | ||||
} | } | ||||
@@ -0,0 +1,16 @@ | |||||
using Microsoft.Azure.ServiceBus; | |||||
namespace DotNetCore.CAP.AzureServiceBus | |||||
{ | |||||
public class AzureServiceBusConsumerCommitInput | |||||
{ | |||||
public AzureServiceBusConsumerCommitInput(string lockToken, IMessageSession session = null) | |||||
{ | |||||
LockToken = lockToken; | |||||
Session = session; | |||||
} | |||||
public IMessageSession Session { get; set; } | |||||
public string LockToken { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,7 @@ | |||||
namespace DotNetCore.CAP.AzureServiceBus | |||||
{ | |||||
public static class AzureServiceBusHeaders | |||||
{ | |||||
public const string SessionId = "cap-session-id"; | |||||
} | |||||
} |
@@ -1,6 +1,7 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | // Copyright (c) .NET Core Community. All rights reserved. | ||||
// Licensed under the MIT License. See License.txt in the project root for license information. | // Licensed under the MIT License. See License.txt in the project root for license information. | ||||
using DotNetCore.CAP.AzureServiceBus; | |||||
using Microsoft.Azure.ServiceBus.Primitives; | using Microsoft.Azure.ServiceBus.Primitives; | ||||
// ReSharper disable once CheckNamespace | // ReSharper disable once CheckNamespace | ||||
@@ -21,6 +22,12 @@ namespace DotNetCore.CAP | |||||
/// </summary> | /// </summary> | ||||
public string ConnectionString { get; set; } | public string ConnectionString { get; set; } | ||||
/// <summary> | |||||
/// Whether Service Bus sessions are enabled. If enabled, all messages must contain a | |||||
/// <see cref="AzureServiceBusHeaders.SessionId"/> header. Defaults to false. | |||||
/// </summary> | |||||
public bool EnableSessions { get; set; } = false; | |||||
/// <summary> | /// <summary> | ||||
/// The name of the topic relative to the service namespace base address. | /// The name of the topic relative to the service namespace base address. | ||||
/// </summary> | /// </summary> | ||||
@@ -46,6 +46,12 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
CorrelationId = transportMessage.GetCorrelationId() | CorrelationId = transportMessage.GetCorrelationId() | ||||
}; | }; | ||||
if (_asbOptions.Value.EnableSessions) | |||||
{ | |||||
transportMessage.Headers.TryGetValue(AzureServiceBusHeaders.SessionId, out var sessionId); | |||||
message.SessionId = string.IsNullOrEmpty(sessionId) ? transportMessage.GetId() : sessionId; | |||||
} | |||||
foreach (var header in transportMessage.Headers) | foreach (var header in transportMessage.Headers) | ||||
{ | { | ||||
message.UserProperties.Add(header.Key, header.Value); | message.UserProperties.Add(header.Key, header.Value); | ||||