From 27fa1dbb971ab0685c4102f36828bb6351e6a37b Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Tue, 27 Jul 2021 17:48:00 +0200 Subject: [PATCH 01/34] first draft of transaction support --- .../FunctionEndpoint.cs | 107 ++++++++++++++++-- .../IFunctionEndpoint.cs | 6 + 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index cae7cb57..a2b00963 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -6,10 +6,12 @@ using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; + using System.Transactions; using AzureFunctions.InProcess.ServiceBus; using Extensibility; using Logging; using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.ServiceBus.Core; using Microsoft.Extensions.Logging; using Transport; using ExecutionContext = Microsoft.Azure.WebJobs.ExecutionContext; @@ -28,6 +30,89 @@ internal FunctionEndpoint(IStartableEndpointWithExternallyManagedContainer exter endpointFactory = _ => externallyManagedContainerEndpoint.Start(serviceProvider); } + /// + /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. All messages are committed transactionally with the successful processing of the incoming message. + /// + public async Task ProcessTransactional(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null) + { + var functionExecutionContext = new FunctionExecutionContext(executionContext, functionsLogger); + + try + { + try + { + using (var transaction = new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout })) + { + TransportTransaction transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); + + MessageContext messageContext = CreateMessageContext(message, transportTransaction); + + //TODO: Could also be done earlier since the token here doesn't need to come from the context (it's a new one anyway) + await InitializeEndpointIfNecessary(functionExecutionContext, + messageContext.ReceiveCancellationTokenSource.Token).ConfigureAwait(false); + + await pipeline.PushMessage(messageContext).ConfigureAwait(false); + + using (var scope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) + { + await messageReceiver.CompleteAsync(message.SystemProperties.LockToken).ConfigureAwait(false); + scope.Complete(); + } + + transaction.Commit(); + } + + } + catch (Exception exception) + { + using (var transaction = new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout })) + { + var transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); + + var errorContext = new ErrorContext( + exception, + message.GetHeaders(), + message.MessageId, + message.Body, + transportTransaction, + message.SystemProperties.DeliveryCount); + + var errorHandleResult = await pipeline.PushFailedMessage(errorContext).ConfigureAwait(false); + + if (errorHandleResult == ErrorHandleResult.Handled) + { + using (var scope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) + { + // return to signal to the Functions host it can complete the incoming message + await messageReceiver.CompleteAsync(message.SystemProperties.LockToken).ConfigureAwait(false); + scope.Complete(); + } + + transaction.Commit(); + return; + } + + throw; + } + } + } + catch (Exception) + { + // abandon message outside of a transaction scope + await messageReceiver.AbandonAsync(message.SystemProperties.LockToken).ConfigureAwait(false); + throw; + } + } + + static TransportTransaction CreateTransportTransaction(Message message, IMessageReceiver messageReceiver, CommittableTransaction transaction) + { + var transportTransaction = new TransportTransaction(); + transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); + transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); + transportTransaction.Set(transaction); + return transportTransaction; + } + /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. /// @@ -35,7 +120,7 @@ public async Task Process(Message message, ExecutionContext executionContext, IL { FunctionsLoggerFactory.Instance.SetCurrentLogger(functionsLogger); - var messageContext = CreateMessageContext(message); + var messageContext = CreateMessageContext(message, new TransportTransaction()); var functionExecutionContext = new FunctionExecutionContext(executionContext, functionsLogger); await InitializeEndpointIfNecessary(functionExecutionContext, @@ -65,19 +150,17 @@ await InitializeEndpointIfNecessary(functionExecutionContext, throw; } - - MessageContext CreateMessageContext(Message originalMessage) - { - return new MessageContext( - originalMessage.GetMessageId(), - originalMessage.GetHeaders(), - originalMessage.Body, - new TransportTransaction(), - new CancellationTokenSource(), - new ContextBag()); - } } + static MessageContext CreateMessageContext(Message originalMessage, TransportTransaction transportTransaction) => + new MessageContext( + originalMessage.GetMessageId(), + originalMessage.GetHeaders(), + originalMessage.Body, + transportTransaction, + new CancellationTokenSource(), + new ContextBag()); + /// /// Allows to forcefully initialize the endpoint if it hasn't been initialized yet. /// diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs index b9be6404..496d5f40 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; @@ -12,6 +13,11 @@ /// public interface IFunctionEndpoint { + /// + /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. All messages are committed transactionally with the successful processing of the incoming message. + /// + Task ProcessTransactional(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null); + /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. /// From f848ef7a328b1f0a5b155dcc2f0047d88b343acc Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Wed, 28 Jul 2021 10:01:55 +0200 Subject: [PATCH 02/34] add missing logging integration --- .../FunctionEndpoint.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index a2b00963..f9bdd7a2 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -35,6 +35,8 @@ internal FunctionEndpoint(IStartableEndpointWithExternallyManagedContainer exter /// public async Task ProcessTransactional(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null) { + FunctionsLoggerFactory.Instance.SetCurrentLogger(functionsLogger); + var functionExecutionContext = new FunctionExecutionContext(executionContext, functionsLogger); try From 440e356124f9c6c1f1aee63098617b70ccd39a22 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Wed, 28 Jul 2021 10:23:14 +0200 Subject: [PATCH 03/34] tweaks --- .../FunctionEndpoint.cs | 39 ++++++++----------- .../MessageReceiverExtensions.cs | 21 ++++++++++ 2 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index f9bdd7a2..1fccc9f4 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -43,34 +43,26 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi { try { - using (var transaction = new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout })) - { - TransportTransaction transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); - - MessageContext messageContext = CreateMessageContext(message, transportTransaction); + await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) + .ConfigureAwait(false); - //TODO: Could also be done earlier since the token here doesn't need to come from the context (it's a new one anyway) - await InitializeEndpointIfNecessary(functionExecutionContext, - messageContext.ReceiveCancellationTokenSource.Token).ConfigureAwait(false); + using (var transaction = CreateTransaction()) + { + var transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); + var messageContext = CreateMessageContext(message, transportTransaction); await pipeline.PushMessage(messageContext).ConfigureAwait(false); - using (var scope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) - { - await messageReceiver.CompleteAsync(message.SystemProperties.LockToken).ConfigureAwait(false); - scope.Complete(); - } - + await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); transaction.Commit(); } } catch (Exception exception) { - using (var transaction = new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout })) + using (var transaction = CreateTransaction()) { var transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); - var errorContext = new ErrorContext( exception, message.GetHeaders(), @@ -83,27 +75,28 @@ await InitializeEndpointIfNecessary(functionExecutionContext, if (errorHandleResult == ErrorHandleResult.Handled) { - using (var scope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) - { - // return to signal to the Functions host it can complete the incoming message - await messageReceiver.CompleteAsync(message.SystemProperties.LockToken).ConfigureAwait(false); - scope.Complete(); - } + await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); transaction.Commit(); return; } + // handled by outer try-catch throw; } } } catch (Exception) { - // abandon message outside of a transaction scope + // abandon message outside of a transaction scope to ensure the abandon operation can't be rolled back await messageReceiver.AbandonAsync(message.SystemProperties.LockToken).ConfigureAwait(false); throw; } + + CommittableTransaction CreateTransaction() + { + return new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout }); + } } static TransportTransaction CreateTransportTransaction(Message message, IMessageReceiver messageReceiver, CommittableTransaction transaction) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs new file mode 100644 index 00000000..96909af0 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs @@ -0,0 +1,21 @@ +namespace NServiceBus.AzureFunctions.InProcess.ServiceBus +{ + using System.Threading.Tasks; + using System.Transactions; + using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.ServiceBus.Core; + + static class MessageReceiverExtensions + { + public static async Task SafeCompleteAsync(this IMessageReceiver messageReceiver, Message message, CommittableTransaction transaction) + { + // open short-lived TransactionScope connected to the committable transaction to ensure the message operation has a scope to enlist. + using (var scope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) + { + await messageReceiver.CompleteAsync(message.SystemProperties.LockToken) + .ConfigureAwait(false); + scope.Complete(); + } + } + } +} \ No newline at end of file From 12a57dcb64f66e08111c803745a5493a1b830378 Mon Sep 17 00:00:00 2001 From: Sean Feldman Date: Thu, 29 Jul 2021 02:15:22 -0600 Subject: [PATCH 04/34] Add support for cross-entity transactions configuration via attribute --- .../NServiceBusTriggerFunctionAttribute.cs | 38 +++ ...verride_trigger_function_name.approved.txt | 27 ++ ...rApprovals2.NameIsStringValue.approved.txt | 27 ++ ...not_generate_trigger_function.approved.txt | 0 ...ls2.One_optional_out_of_order.approved.txt | 27 ++ ..._optional_with_parameter_name.approved.txt | 27 ++ ...s2.Two_optionals_out_of_order.approved.txt | 27 ++ ...orApprovals2.Use_one_optional.approved.txt | 27 ++ ...rApprovals2.Use_two_optionals.approved.txt | 27 ++ ...ngFullyQualifiedAttributeName.approved.txt | 27 ++ ...atorApprovals2.UsingNamespace.approved.txt | 27 ++ .../SourceGeneratorApproval2.cs | 252 ++++++++++++++++++ .../TriggerFunctionGenerator2.cs | 139 ++++++++++ 13 files changed, 672 insertions(+) create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.No_attribute_should_not_generate_trigger_function.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs create mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs new file mode 100644 index 00000000..1e4219a2 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs @@ -0,0 +1,38 @@ +namespace NServiceBus +{ + /// + /// Assembly attribute to specify NServiceBus logical endpoint name. + /// The attribute is used to wire up an auto-generated Service Bus trigger function, responding to messages in the queue specified by the name provided. + /// + [System.AttributeUsage(System.AttributeTargets.Assembly)] + public sealed class NServiceBusTriggerFunctionAttribute : System.Attribute + { + /// + /// Endpoint name that is the input queue name. + /// + public string Name { get; } + + /// + /// Override trigger function name. + /// + public string TriggerFunctionName { get; set; } + + /// + /// Enable cross-entity transactions. + /// + public bool EnableCrossEntityTransactions { get; set; } + + /// + /// Endpoint logical name. + /// + /// Endpoint name that is the input queue name. + /// Name given to the auto-generated trigger function. + /// Enable cross-entity transactions. + public NServiceBusTriggerFunctionAttribute(string name, string triggerFunctionName = default, bool enableCrossEntityTransactions = default) + { + Name = name; + TriggerFunctionName = triggerFunctionName; + EnableCrossEntityTransactions = enableCrossEntityTransactions; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt new file mode 100644 index 00000000..7c3f6569 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("trigger")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt new file mode 100644 index 00000000..20f47ce5 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.No_attribute_should_not_generate_trigger_function.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.No_attribute_should_not_generate_trigger_function.approved.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt new file mode 100644 index 00000000..edbb24fc --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: false)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt new file mode 100644 index 00000000..7c3f6569 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("trigger")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt new file mode 100644 index 00000000..d70715dc --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("trigger")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: false)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt new file mode 100644 index 00000000..7c3f6569 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("trigger")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt new file mode 100644 index 00000000..d70715dc --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("trigger")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: false)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt new file mode 100644 index 00000000..20f47ce5 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt new file mode 100644 index 00000000..20f47ce5 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt @@ -0,0 +1,27 @@ +// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + { + this.endpoint = endpoint; + } + + [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] + public async Task Run( + [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + { + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs new file mode 100644 index 00000000..0e42ea29 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs @@ -0,0 +1,252 @@ +namespace NServiceBus.AzureFunctions.SourceGenerator.Tests +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using NUnit.Framework; + using Particular.Approvals; + + [TestFixture] + public class SourceGeneratorApprovals2 + { + [Test] + public void UsingNamespace() + { + var source = +@"using NServiceBus; + +[assembly: NServiceBusTriggerFunction(Foo.Startup.EndpointName)] + +namespace Foo +{ + public class Startup + { + public const string EndpointName = ""endpoint""; + } +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void UsingFullyQualifiedAttributeName() + { + var source = +@"[assembly: NServiceBus.NServiceBusTriggerFunction(Foo.Startup.EndpointName)] + +namespace Foo +{ + public class Startup + { + public const string EndpointName = ""endpoint""; + } +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void NameIsStringValue() + { + var source = @"[assembly: NServiceBus.NServiceBusTriggerFunction(""endpoint"")]"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void No_attribute_should_not_generate_trigger_function() + { + var source = @""; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void No_attribute_should_not_generate_compilation_error() + { + var source = @"using NServiceBus;"; + var (output, diagnostics) = GetGeneratedOutput(source); + + Assert.False(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)); + } + + [Test] + public void Can_override_trigger_function_name() + { + var source = + @"using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""endpoint"", ""trigger"")] + +public class Startup +{ +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void Invalid_name_should_cause_an_error(string endpointName) + { + var source = @" +using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""" + endpointName + @""")] +"; + var (output, diagnostics) = GetGeneratedOutput(source, suppressGeneratedDiagnosticsErrors: true); + + Assert.True(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error && d.Id == TriggerFunctionGenerator2.InvalidEndpointNameError.Id)); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void Invalid_trigger_function_name_should_cause_an_error(string triggerFunctionName) + { + var source = @" +using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""endpoint"", """ + triggerFunctionName + @""")] +"; + var (output, diagnostics) = GetGeneratedOutput(source, suppressGeneratedDiagnosticsErrors: true); + + Assert.True(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error && d.Id == TriggerFunctionGenerator2.InvalidTriggerFunctionNameError.Id)); + } + + [Test] + public void Use_one_optional() + { + var source = @" +using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""endpoint"", ""trigger"")] + +public class Startup +{ +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void Use_two_optionals() + { + var source = @" +using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""endpoint"", ""trigger"", true)] + +public class Startup +{ +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void One_optional_with_parameter_name() + { + var source = @" +using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""endpoint"", triggerFunctionName: ""trigger"")] + +public class Startup +{ +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void One_optional_out_of_order() + { + var source = @" +using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""endpoint"", EnableCrossEntityTransactions = true)] + +public class Startup +{ +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [Test] + public void Two_optionals_out_of_order() + { + var source = @" +using NServiceBus; + +[assembly: NServiceBusTriggerFunction(""endpoint"", EnableCrossEntityTransactions = true, TriggerFunctionName = ""trigger"")] + +public class Startup +{ +}"; + var (output, _) = GetGeneratedOutput(source); + + Approver.Verify(output); + } + + [OneTimeSetUp] + public void Init() + { + // For the unit tests to work, the compilation used by the source generator needs to know that NServiceBusTriggerFunction + // is an attribute from NServiceBus namespace and its full name is NServiceBus.NServiceBusTriggerFunctionAttribute. + // By referencing NServiceBusTriggerFunctionAttribute here, NServiceBus.AzureFunctions.InProcess.ServiceBus is forced to load and participate in the compilation. + _ = new NServiceBusTriggerFunctionAttribute(name: "test"); + _ = new NServiceBusTriggerFunctionAttribute(name: "test", triggerFunctionName: "trigger"); + _ = new NServiceBusTriggerFunctionAttribute(name: "test", enableCrossEntityTransactions: true); + _ = new NServiceBusTriggerFunctionAttribute(name: "test", triggerFunctionName: "trigger", enableCrossEntityTransactions: true); + _ = new NServiceBusTriggerFunctionAttribute(name: "test", enableCrossEntityTransactions: true, triggerFunctionName: "trigger"); + } + + static (string output, ImmutableArray diagnostics) GetGeneratedOutput(string source, bool suppressGeneratedDiagnosticsErrors = false) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var references = new List(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var assembly in assemblies) + { + if (!assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) + { + references.Add(MetadataReference.CreateFromFile(assembly.Location)); + } + } + + var compilation = CSharpCompilation.Create("foo", new[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + // Fail tests when the injected program isn't valid _before_ running generators + var compileDiagnostics = compilation.GetDiagnostics(); + Assert.False(compileDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), "Failed: " + compileDiagnostics.FirstOrDefault()?.GetMessage()); + + var generator = new TriggerFunctionGenerator2(); + + var driver = CSharpGeneratorDriver.Create(generator); + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics); + + if (!suppressGeneratedDiagnosticsErrors) + { + Assert.False(generateDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), "Failed: " + generateDiagnostics.FirstOrDefault()?.GetMessage()); + } + + return (outputCompilation.SyntaxTrees.Last().ToString(), generateDiagnostics); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs new file mode 100644 index 00000000..839b9cfa --- /dev/null +++ b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs @@ -0,0 +1,139 @@ +namespace NServiceBus.AzureFunctions.SourceGenerator +{ + using System.Text; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Text; + + [Generator] + public class TriggerFunctionGenerator2 : ISourceGenerator + { + internal static readonly DiagnosticDescriptor InvalidEndpointNameError = new DiagnosticDescriptor(id: "NSBFUNC003", + title: "Invalid Endpoint Name", + messageFormat: "Endpoint name is invalid and cannot be used to generate trigger function", + category: "TriggerFunctionGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor InvalidTriggerFunctionNameError = new DiagnosticDescriptor(id: "NSBFUNC004", + title: "Invalid Trigger Function Name", + messageFormat: "Trigger function name is invalid and cannot be used to generate trigger function", + category: "TriggerFunctionGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + class SyntaxReceiver : ISyntaxContextReceiver + { + internal string endpointName; + internal string triggerFunctionName; + internal bool enableCrossEntityTransactions; + internal bool attributeFound; + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is AttributeSyntax attributeSyntax + && IsNServiceBusEndpointNameAttribute(context.SemanticModel.GetTypeInfo(attributeSyntax).Type?.ToDisplayString())) + { + attributeFound = true; + + // Assign guaranteed endpoint/queue name and handle the defaults + endpointName = AttributeParameterAtPosition(0); + triggerFunctionName = $"NServiceBusFunctionEndpointTrigger-{endpointName}"; + enableCrossEntityTransactions = false; + + var attributeParametersCount = AttributeParametersCount(); + + if (attributeParametersCount == 1) + { + return; + } + + if (bool.TryParse(AttributeParameterAtPosition(1), out enableCrossEntityTransactions)) + { + // 2nd parameter was cross entity transaction flag + // 3rd parameter might be trigger function name + triggerFunctionName = attributeParametersCount == 3 + ? AttributeParameterAtPosition(2) + : $"NServiceBusFunctionEndpointTrigger-{endpointName}"; + } + else + { + // 2nd parameter was triggerFunctionName + triggerFunctionName = AttributeParameterAtPosition(1); + + // 3rd parameter might be cross entity transaction flag + enableCrossEntityTransactions = attributeParametersCount == 3 && bool.Parse(AttributeParameterAtPosition(2)); + } + } + + bool IsNServiceBusEndpointNameAttribute(string value) => value.Equals("NServiceBus.NServiceBusTriggerFunctionAttribute"); + string AttributeParameterAtPosition(int position) => context.SemanticModel.GetConstantValue(attributeSyntax.ArgumentList.Arguments[position].Expression).ToString(); + int AttributeParametersCount() => attributeSyntax.ArgumentList.Arguments.Count; + } + } + + public void Execute(GeneratorExecutionContext context) + { + // Short circuit if this is a different syntax receiver + if (!(context.SyntaxContextReceiver is SyntaxReceiver syntaxReceiver)) + { + return; + } + + // Skip processing if no attribute was found + if (!syntaxReceiver.attributeFound) + { + return; + } + + // Generate an error if empty/null/space is used as endpoint name + if (string.IsNullOrWhiteSpace(syntaxReceiver.endpointName)) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidEndpointNameError, Location.None, syntaxReceiver.endpointName)); + return; + } + + // Generate an error if empty/null/space is used as trigger function name + if (string.IsNullOrWhiteSpace(syntaxReceiver.triggerFunctionName)) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidTriggerFunctionNameError, Location.None, syntaxReceiver.triggerFunctionName)); + return; + } + + var source = +$@"// +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using NServiceBus; + +class FunctionEndpointTrigger +{{ + readonly IFunctionEndpoint endpoint; + + public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + {{ + this.endpoint = endpoint; + }} + + [FunctionName(""{syntaxReceiver.triggerFunctionName}"")] + public async Task Run( + [ServiceBusTrigger(queueName: ""{syntaxReceiver.endpointName}"", AutoComplete: {(!syntaxReceiver.enableCrossEntityTransactions).ToString().ToLowerInvariant()})] + Message message, + MessageReceiver messageReceiver, + ILogger logger, + ExecutionContext executionContext) + {{ + await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + }} +}}"; + context.AddSource("NServiceBus__FunctionEndpointTrigger", SourceText.From(source, Encoding.UTF8)); + } + } +} \ No newline at end of file From 69dc5223fed1515ac9715a3fd4f356282a1f4fd1 Mon Sep 17 00:00:00 2001 From: Sean Feldman Date: Thu, 29 Jul 2021 02:27:46 -0600 Subject: [PATCH 05/34] Approve API --- .../ApprovalFiles/APIApprovals.Approve.approved.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt index 51ca02fe..7eca5a99 100644 --- a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt +++ b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt @@ -4,6 +4,7 @@ namespace NServiceBus public class FunctionEndpoint : NServiceBus.IFunctionEndpoint { public System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } + public System.Threading.Tasks.Task ProcessTransactional(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task Publish(object message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions options, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task Publish(System.Action messageConstructor, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } @@ -30,6 +31,7 @@ namespace NServiceBus public interface IFunctionEndpoint { System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); + System.Threading.Tasks.Task ProcessTransactional(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null); System.Threading.Tasks.Task Publish(object message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions options, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); System.Threading.Tasks.Task Publish(System.Action messageConstructor, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); @@ -51,6 +53,14 @@ namespace NServiceBus public string Name { get; } public string TriggerFunctionName { get; } } + [System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.All)] + public sealed class NServiceBusTriggerFunctionAttribute : System.Attribute + { + public NServiceBusTriggerFunctionAttribute(string name, string triggerFunctionName = null, bool enableCrossEntityTransactions = false) { } + public bool EnableCrossEntityTransactions { get; set; } + public string Name { get; } + public string TriggerFunctionName { get; set; } + } public class ServiceBusTriggeredEndpointConfiguration { public ServiceBusTriggeredEndpointConfiguration(string endpointName, string connectionStringName = null) { } From 31a8f1b5783ec4993cd8f60e304bc82601e955dd Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 11:45:32 +0200 Subject: [PATCH 06/34] switch invoked method based on transaction setting --- ...Approvals2.Can_override_trigger_function_name.approved.txt | 2 +- .../SourceGeneratorApprovals2.NameIsStringValue.approved.txt | 2 +- ...orApprovals2.One_optional_with_parameter_name.approved.txt | 2 +- .../SourceGeneratorApprovals2.Use_one_optional.approved.txt | 2 +- ...orApprovals2.UsingFullyQualifiedAttributeName.approved.txt | 2 +- .../SourceGeneratorApprovals2.UsingNamespace.approved.txt | 2 +- .../TriggerFunctionGenerator2.cs | 4 +++- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt index 7c3f6569..0753f682 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt @@ -22,6 +22,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + await endpoint.Process(message, executionContext, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt index 20f47ce5..45dc9cfe 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt @@ -22,6 +22,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + await endpoint.Process(message, executionContext, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt index 7c3f6569..0753f682 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt @@ -22,6 +22,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + await endpoint.Process(message, executionContext, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt index 7c3f6569..0753f682 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt @@ -22,6 +22,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + await endpoint.Process(message, executionContext, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt index 20f47ce5..45dc9cfe 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt @@ -22,6 +22,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + await endpoint.Process(message, executionContext, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt index 20f47ce5..45dc9cfe 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt @@ -22,6 +22,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + await endpoint.Process(message, executionContext, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs index 839b9cfa..6f3c70ce 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs @@ -130,7 +130,9 @@ public async Task Run( ILogger logger, ExecutionContext executionContext) {{ - await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger); + {(syntaxReceiver.enableCrossEntityTransactions + ? "await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger);" + : "await endpoint.Process(message, executionContext, logger);")} }} }}"; context.AddSource("NServiceBus__FunctionEndpointTrigger", SourceText.From(source, Encoding.UTF8)); From 1873644d8bae4cf7f9eaaec7e79995bfbe251391 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 11:46:52 +0200 Subject: [PATCH 07/34] please code style --- .../SourceGeneratorApproval2.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs index 0e42ea29..2cfda9f6 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs @@ -72,7 +72,7 @@ public void No_attribute_should_not_generate_trigger_function() public void No_attribute_should_not_generate_compilation_error() { var source = @"using NServiceBus;"; - var (output, diagnostics) = GetGeneratedOutput(source); + var (_, diagnostics) = GetGeneratedOutput(source); Assert.False(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)); } @@ -103,7 +103,7 @@ public void Invalid_name_should_cause_an_error(string endpointName) [assembly: NServiceBusTriggerFunction(""" + endpointName + @""")] "; - var (output, diagnostics) = GetGeneratedOutput(source, suppressGeneratedDiagnosticsErrors: true); + var (_, diagnostics) = GetGeneratedOutput(source, suppressGeneratedDiagnosticsErrors: true); Assert.True(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error && d.Id == TriggerFunctionGenerator2.InvalidEndpointNameError.Id)); } @@ -118,7 +118,7 @@ public void Invalid_trigger_function_name_should_cause_an_error(string triggerFu [assembly: NServiceBusTriggerFunction(""endpoint"", """ + triggerFunctionName + @""")] "; - var (output, diagnostics) = GetGeneratedOutput(source, suppressGeneratedDiagnosticsErrors: true); + var (_, diagnostics) = GetGeneratedOutput(source, suppressGeneratedDiagnosticsErrors: true); Assert.True(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error && d.Id == TriggerFunctionGenerator2.InvalidTriggerFunctionNameError.Id)); } From efcc04426bcce6960744e7f41e00a4ee5e6ba862 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 13:41:51 +0200 Subject: [PATCH 08/34] make the generated code valid syntax --- .../TriggerFunctionGenerator2.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs index 6f3c70ce..98ab503b 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs @@ -108,6 +108,7 @@ public void Execute(GeneratorExecutionContext context) var source = $@"// using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -124,7 +125,7 @@ public FunctionEndpointTrigger(IFunctionEndpoint endpoint) [FunctionName(""{syntaxReceiver.triggerFunctionName}"")] public async Task Run( - [ServiceBusTrigger(queueName: ""{syntaxReceiver.endpointName}"", AutoComplete: {(!syntaxReceiver.enableCrossEntityTransactions).ToString().ToLowerInvariant()})] + [ServiceBusTrigger(queueName: ""{syntaxReceiver.endpointName}"", AutoComplete = {(!syntaxReceiver.enableCrossEntityTransactions).ToString().ToLowerInvariant()})] Message message, MessageReceiver messageReceiver, ILogger logger, From 88492903bbe9c8e2d0039acaa82e6a8be317193c Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 13:44:39 +0200 Subject: [PATCH 09/34] update approval files --- ...rApprovals2.Can_override_trigger_function_name.approved.txt | 3 ++- .../SourceGeneratorApprovals2.NameIsStringValue.approved.txt | 3 ++- ...eGeneratorApprovals2.One_optional_out_of_order.approved.txt | 3 ++- ...torApprovals2.One_optional_with_parameter_name.approved.txt | 3 ++- ...GeneratorApprovals2.Two_optionals_out_of_order.approved.txt | 3 ++- .../SourceGeneratorApprovals2.Use_one_optional.approved.txt | 3 ++- .../SourceGeneratorApprovals2.Use_two_optionals.approved.txt | 3 ++- ...torApprovals2.UsingFullyQualifiedAttributeName.approved.txt | 3 ++- .../SourceGeneratorApprovals2.UsingNamespace.approved.txt | 3 ++- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt index 0753f682..efad0593 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("trigger")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt index 45dc9cfe..99e48622 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt index edbb24fc..bfdbfd4a 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: false)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = false)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt index 0753f682..efad0593 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("trigger")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt index d70715dc..bbb25c31 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("trigger")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: false)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = false)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt index 0753f682..efad0593 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("trigger")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt index d70715dc..bbb25c31 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("trigger")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: false)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = false)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt index 45dc9cfe..99e48622 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] Message message, MessageReceiver messageReceiver, ILogger logger, diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt index 45dc9cfe..99e48622 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt @@ -1,5 +1,6 @@ // using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -16,7 +17,7 @@ class FunctionEndpointTrigger [FunctionName("NServiceBusFunctionEndpointTrigger-endpoint")] public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete: true)] + [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] Message message, MessageReceiver messageReceiver, ILogger logger, From e944564b3a6063abb434f62f763714f96771777a Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 13:58:57 +0200 Subject: [PATCH 10/34] verify generated trigger compiles without errors --- .../SourceGeneratorApproval2.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs index 2cfda9f6..74e1bd3e 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs @@ -4,8 +4,11 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; + using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.WebJobs; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; + using Microsoft.Extensions.Logging; using NUnit.Framework; using Particular.Approvals; @@ -230,17 +233,23 @@ public void Init() } } - var compilation = CSharpCompilation.Create("foo", new[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - // Fail tests when the injected program isn't valid _before_ running generators - var compileDiagnostics = compilation.GetDiagnostics(); - Assert.False(compileDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), "Failed: " + compileDiagnostics.FirstOrDefault()?.GetMessage()); + var compilation = Compile(new[] + { + syntaxTree + }, references); var generator = new TriggerFunctionGenerator2(); var driver = CSharpGeneratorDriver.Create(generator); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics); + // add necessary references for the generated trigger + references.Add(MetadataReference.CreateFromFile(typeof(ServiceBusTriggerAttribute).Assembly.Location)); + references.Add(MetadataReference.CreateFromFile(typeof(ExecutionContext).Assembly.Location)); + references.Add(MetadataReference.CreateFromFile(typeof(Message).Assembly.Location)); + references.Add(MetadataReference.CreateFromFile(typeof(ILogger).Assembly.Location)); + Compile(outputCompilation.SyntaxTrees, references); + if (!suppressGeneratedDiagnosticsErrors) { Assert.False(generateDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), "Failed: " + generateDiagnostics.FirstOrDefault()?.GetMessage()); @@ -248,5 +257,18 @@ public void Init() return (outputCompilation.SyntaxTrees.Last().ToString(), generateDiagnostics); } + + static CSharpCompilation Compile(IEnumerable syntaxTrees, IEnumerable references) + { + var compilation = CSharpCompilation.Create("result", syntaxTrees, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + // Verify the code compiled: + var compilationErrors = compilation + .GetDiagnostics() + .Where(d => d.Severity >= DiagnosticSeverity.Warning); + Assert.IsEmpty(compilationErrors, compilationErrors.FirstOrDefault()?.GetMessage()); + + return compilation; + } } } \ No newline at end of file From 3cd1b6c2a366d6a78d3b7b919acc1f6ccf8401eb Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 14:24:39 +0200 Subject: [PATCH 11/34] remove optional ctor parameter for public attribute properties --- .../NServiceBusTriggerFunctionAttribute.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs index 1e4219a2..344b1aec 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs @@ -26,13 +26,9 @@ public sealed class NServiceBusTriggerFunctionAttribute : System.Attribute /// Endpoint logical name. /// /// Endpoint name that is the input queue name. - /// Name given to the auto-generated trigger function. - /// Enable cross-entity transactions. - public NServiceBusTriggerFunctionAttribute(string name, string triggerFunctionName = default, bool enableCrossEntityTransactions = default) + public NServiceBusTriggerFunctionAttribute(string name) { Name = name; - TriggerFunctionName = triggerFunctionName; - EnableCrossEntityTransactions = enableCrossEntityTransactions; } } } \ No newline at end of file From 9ecd740392e7961b3bfe80e5727b4bc03bf46b2f Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 14:29:14 +0200 Subject: [PATCH 12/34] update tests --- .../SourceGeneratorApproval2.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs index 74e1bd3e..d3bd6a96 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs @@ -86,7 +86,7 @@ public void Can_override_trigger_function_name() var source = @"using NServiceBus; -[assembly: NServiceBusTriggerFunction(""endpoint"", ""trigger"")] +[assembly: NServiceBusTriggerFunction(""endpoint"", TriggerFunctionName = ""trigger"")] public class Startup { @@ -119,7 +119,7 @@ public void Invalid_trigger_function_name_should_cause_an_error(string triggerFu var source = @" using NServiceBus; -[assembly: NServiceBusTriggerFunction(""endpoint"", """ + triggerFunctionName + @""")] +[assembly: NServiceBusTriggerFunction(""endpoint"", TriggerFunctionName = """ + triggerFunctionName + @""")] "; var (_, diagnostics) = GetGeneratedOutput(source, suppressGeneratedDiagnosticsErrors: true); @@ -132,7 +132,7 @@ public void Use_one_optional() var source = @" using NServiceBus; -[assembly: NServiceBusTriggerFunction(""endpoint"", ""trigger"")] +[assembly: NServiceBusTriggerFunction(""endpoint"", TriggerFunctionName = ""trigger"")] public class Startup { @@ -143,12 +143,12 @@ public class Startup } [Test] - public void Use_two_optionals() + public void One_optional_with_parameter_name() { var source = @" using NServiceBus; -[assembly: NServiceBusTriggerFunction(""endpoint"", ""trigger"", true)] +[assembly: NServiceBusTriggerFunction(""endpoint"", TriggerFunctionName = ""trigger"")] public class Startup { @@ -159,12 +159,12 @@ public class Startup } [Test] - public void One_optional_with_parameter_name() + public void One_optional_out_of_order() { var source = @" using NServiceBus; -[assembly: NServiceBusTriggerFunction(""endpoint"", triggerFunctionName: ""trigger"")] +[assembly: NServiceBusTriggerFunction(""endpoint"", EnableCrossEntityTransactions = true)] public class Startup { @@ -175,12 +175,12 @@ public class Startup } [Test] - public void One_optional_out_of_order() + public void Use_two_optionals() { var source = @" using NServiceBus; -[assembly: NServiceBusTriggerFunction(""endpoint"", EnableCrossEntityTransactions = true)] +[assembly: NServiceBusTriggerFunction(""endpoint"", TriggerFunctionName = ""trigger"", EnableCrossEntityTransactions = true)] public class Startup { @@ -213,10 +213,6 @@ public void Init() // is an attribute from NServiceBus namespace and its full name is NServiceBus.NServiceBusTriggerFunctionAttribute. // By referencing NServiceBusTriggerFunctionAttribute here, NServiceBus.AzureFunctions.InProcess.ServiceBus is forced to load and participate in the compilation. _ = new NServiceBusTriggerFunctionAttribute(name: "test"); - _ = new NServiceBusTriggerFunctionAttribute(name: "test", triggerFunctionName: "trigger"); - _ = new NServiceBusTriggerFunctionAttribute(name: "test", enableCrossEntityTransactions: true); - _ = new NServiceBusTriggerFunctionAttribute(name: "test", triggerFunctionName: "trigger", enableCrossEntityTransactions: true); - _ = new NServiceBusTriggerFunctionAttribute(name: "test", enableCrossEntityTransactions: true, triggerFunctionName: "trigger"); } static (string output, ImmutableArray diagnostics) GetGeneratedOutput(string source, bool suppressGeneratedDiagnosticsErrors = false) From 3124cc06cc7027f9029ee6ad46a2b584e7ef8e73 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 14:38:06 +0200 Subject: [PATCH 13/34] cleanup tests --- ...als2.Can_enable_transactions.approved.txt} | 0 ..._optional_with_parameter_name.approved.txt | 28 --------------- ...orApprovals2.Use_one_optional.approved.txt | 28 --------------- .../SourceGeneratorApproval2.cs | 34 +------------------ 4 files changed, 1 insertion(+), 89 deletions(-) rename src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/{SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt => SourceGeneratorApprovals2.Can_enable_transactions.approved.txt} (100%) delete mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt delete mode 100644 src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_enable_transactions.approved.txt similarity index 100% rename from src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_out_of_order.approved.txt rename to src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_enable_transactions.approved.txt diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt deleted file mode 100644 index efad0593..00000000 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.One_optional_with_parameter_name.approved.txt +++ /dev/null @@ -1,28 +0,0 @@ -// -using Microsoft.Azure.ServiceBus; -using Microsoft.Azure.ServiceBus.Core; -using Microsoft.Azure.WebJobs; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using NServiceBus; - -class FunctionEndpointTrigger -{ - readonly IFunctionEndpoint endpoint; - - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) - { - this.endpoint = endpoint; - } - - [FunctionName("trigger")] - public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] - Message message, - MessageReceiver messageReceiver, - ILogger logger, - ExecutionContext executionContext) - { - await endpoint.Process(message, executionContext, logger); - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt deleted file mode 100644 index efad0593..00000000 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_one_optional.approved.txt +++ /dev/null @@ -1,28 +0,0 @@ -// -using Microsoft.Azure.ServiceBus; -using Microsoft.Azure.ServiceBus.Core; -using Microsoft.Azure.WebJobs; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using NServiceBus; - -class FunctionEndpointTrigger -{ - readonly IFunctionEndpoint endpoint; - - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) - { - this.endpoint = endpoint; - } - - [FunctionName("trigger")] - public async Task Run( - [ServiceBusTrigger(queueName: "endpoint", AutoComplete = true)] - Message message, - MessageReceiver messageReceiver, - ILogger logger, - ExecutionContext executionContext) - { - await endpoint.Process(message, executionContext, logger); - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs index d3bd6a96..8410ce63 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs @@ -127,39 +127,7 @@ public void Invalid_trigger_function_name_should_cause_an_error(string triggerFu } [Test] - public void Use_one_optional() - { - var source = @" -using NServiceBus; - -[assembly: NServiceBusTriggerFunction(""endpoint"", TriggerFunctionName = ""trigger"")] - -public class Startup -{ -}"; - var (output, _) = GetGeneratedOutput(source); - - Approver.Verify(output); - } - - [Test] - public void One_optional_with_parameter_name() - { - var source = @" -using NServiceBus; - -[assembly: NServiceBusTriggerFunction(""endpoint"", TriggerFunctionName = ""trigger"")] - -public class Startup -{ -}"; - var (output, _) = GetGeneratedOutput(source); - - Approver.Verify(output); - } - - [Test] - public void One_optional_out_of_order() + public void Can_enable_transactions() { var source = @" using NServiceBus; From 4ecebe13525367de66008bcbd12a465bba79187c Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 15:42:33 +0200 Subject: [PATCH 14/34] obsolete NServiceBusEndpointNameAttribute --- .../FodyWeavers.xml | 4 ++++ .../NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj | 1 + .../NServiceBusEndpointNameAttribute.cs | 4 ++++ .../ApprovalFiles/APIApprovals.Approve.approved.txt | 4 +++- 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FodyWeavers.xml diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FodyWeavers.xml b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FodyWeavers.xml new file mode 100644 index 00000000..a988537c --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj index 6e3203e5..9359d294 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj @@ -16,6 +16,7 @@ + diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusEndpointNameAttribute.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusEndpointNameAttribute.cs index 37b15050..ddad5e57 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusEndpointNameAttribute.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusEndpointNameAttribute.cs @@ -4,6 +4,10 @@ /// Assembly attribute to specify NServiceBus logical endpoint name. /// This name is used to wire up an auto-generated service bus trigger function, responding to messages in the queue specified by the name provided. /// + [ObsoleteEx( + ReplacementTypeOrMember = nameof(NServiceBusTriggerFunctionAttribute), + TreatAsErrorFromVersion = "2", + RemoveInVersion = "3")] [System.AttributeUsage(System.AttributeTargets.Assembly)] public sealed class NServiceBusEndpointNameAttribute : System.Attribute { diff --git a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt index 7eca5a99..b7783608 100644 --- a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt +++ b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt @@ -46,6 +46,8 @@ namespace NServiceBus System.Threading.Tasks.Task Unsubscribe(System.Type eventType, NServiceBus.UnsubscribeOptions options, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); } [System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.All)] + [System.Obsolete("Use `NServiceBusTriggerFunctionAttribute` instead. Will be treated as an error fr" + + "om version 2.0.0. Will be removed in version 3.0.0.", false)] public sealed class NServiceBusEndpointNameAttribute : System.Attribute { public NServiceBusEndpointNameAttribute(string name) { } @@ -56,7 +58,7 @@ namespace NServiceBus [System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.All)] public sealed class NServiceBusTriggerFunctionAttribute : System.Attribute { - public NServiceBusTriggerFunctionAttribute(string name, string triggerFunctionName = null, bool enableCrossEntityTransactions = false) { } + public NServiceBusTriggerFunctionAttribute(string name) { } public bool EnableCrossEntityTransactions { get; set; } public string Name { get; } public string TriggerFunctionName { get; set; } From 3c64b9a5b8193f5341eb48630bd42a614d232252 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Thu, 29 Jul 2021 15:48:10 +0200 Subject: [PATCH 15/34] suppress CS0618 --- .../SourceGeneratorApprovals.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApprovals.cs b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApprovals.cs index 46f1b7b1..193aac7f 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApprovals.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApprovals.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AzureFunctions.SourceGenerator.Tests +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0618 // Disable obsolete warning for NServiceBusEndpointNameAttribute +namespace NServiceBus.AzureFunctions.SourceGenerator.Tests { using System; using System.Collections.Generic; @@ -166,4 +168,6 @@ public void Init() return (outputCompilation.SyntaxTrees.Last().ToString(), generateDiagnostics); } } -} \ No newline at end of file +} +#pragma warning restore CS0618 +#pragma warning restore IDE0079 // Remove unnecessary suppression \ No newline at end of file From cd5171df0686411a4579ad26b9d3e97240eb86ed Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 2 Aug 2021 15:45:51 +0200 Subject: [PATCH 16/34] use single process implementation --- .../FunctionEndpoint.cs | 153 ++++++++---------- 1 file changed, 71 insertions(+), 82 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 1fccc9f4..7ba38b27 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -41,50 +41,12 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi try { - try - { - await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) - .ConfigureAwait(false); - - using (var transaction = CreateTransaction()) - { - var transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); - var messageContext = CreateMessageContext(message, transportTransaction); - - await pipeline.PushMessage(messageContext).ConfigureAwait(false); - - await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); - transaction.Commit(); - } - - } - catch (Exception exception) - { - using (var transaction = CreateTransaction()) - { - var transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); - var errorContext = new ErrorContext( - exception, - message.GetHeaders(), - message.MessageId, - message.Body, - transportTransaction, - message.SystemProperties.DeliveryCount); - - var errorHandleResult = await pipeline.PushFailedMessage(errorContext).ConfigureAwait(false); - - if (errorHandleResult == ErrorHandleResult.Handled) - { - await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); - - transaction.Commit(); - return; - } - - // handled by outer try-catch - throw; - } - } + await Process(message, + functionExecutionContext, + tx => messageReceiver.SafeCompleteAsync(message, tx), + () => CreateTransaction(), + tx => CreateTransportTransaction(tx)) + .ConfigureAwait(false); } catch (Exception) { @@ -93,21 +55,23 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. throw; } - CommittableTransaction CreateTransaction() + CommittableTransaction CreateTransaction() => + new CommittableTransaction(new TransactionOptions + { + IsolationLevel = IsolationLevel.Serializable, + Timeout = TransactionManager.MaximumTimeout + }); + + TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) { - return new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout }); + var transportTransaction = new TransportTransaction(); + transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); + transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); + transportTransaction.Set(transaction); + return transportTransaction; } } - static TransportTransaction CreateTransportTransaction(Message message, IMessageReceiver messageReceiver, CommittableTransaction transaction) - { - var transportTransaction = new TransportTransaction(); - transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); - transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); - transportTransaction.Set(transaction); - return transportTransaction; - } - /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. /// @@ -115,46 +79,71 @@ public async Task Process(Message message, ExecutionContext executionContext, IL { FunctionsLoggerFactory.Instance.SetCurrentLogger(functionsLogger); - var messageContext = CreateMessageContext(message, new TransportTransaction()); var functionExecutionContext = new FunctionExecutionContext(executionContext, functionsLogger); - await InitializeEndpointIfNecessary(functionExecutionContext, - messageContext.ReceiveCancellationTokenSource.Token).ConfigureAwait(false); + await Process(message, + functionExecutionContext, + _ => Task.CompletedTask, + () => null, + _ => new TransportTransaction()) + .ConfigureAwait(false); + } + + async Task Process(Message message, FunctionExecutionContext functionExecutionContext, Func onComplete, Func transactionFactory, Func transportTransactionFactory) + { + await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) + .ConfigureAwait(false); try { - await pipeline.PushMessage(messageContext).ConfigureAwait(false); + using (var transaction = transactionFactory()) + { + var transportTransaction = transportTransactionFactory(transaction); + var messageContext = CreateMessageContext(transportTransaction); + + await pipeline.PushMessage(messageContext).ConfigureAwait(false); + + await onComplete(transaction).ConfigureAwait(false); + //await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); + transaction?.Commit(); + } } catch (Exception exception) { - var errorContext = new ErrorContext( - exception, - message.GetHeaders(), - messageContext.MessageId, - messageContext.Body, - new TransportTransaction(), - message.SystemProperties.DeliveryCount); + using (var transaction = transactionFactory()) + { + var transportTransaction = transportTransactionFactory(transaction); + var errorContext = new ErrorContext( + exception, + message.GetHeaders(), + message.MessageId, + message.Body, + transportTransaction, + message.SystemProperties.DeliveryCount); + + var errorHandleResult = await pipeline.PushFailedMessage(errorContext).ConfigureAwait(false); + + if (errorHandleResult == ErrorHandleResult.Handled) + { + await onComplete(transaction).ConfigureAwait(false); - var errorHandleResult = await pipeline.PushFailedMessage(errorContext).ConfigureAwait(false); + transaction?.Commit(); + return; + } - if (errorHandleResult == ErrorHandleResult.Handled) - { - // return to signal to the Functions host it can complete the incoming message - return; + throw; } - - throw; } - } - static MessageContext CreateMessageContext(Message originalMessage, TransportTransaction transportTransaction) => - new MessageContext( - originalMessage.GetMessageId(), - originalMessage.GetHeaders(), - originalMessage.Body, - transportTransaction, - new CancellationTokenSource(), - new ContextBag()); + MessageContext CreateMessageContext(TransportTransaction transportTransaction) => + new MessageContext( + message.GetMessageId(), + message.GetHeaders(), + message.Body, + transportTransaction, + new CancellationTokenSource(), + new ContextBag()); + } /// /// Allows to forcefully initialize the endpoint if it hasn't been initialized yet. From ae0b19fefbd491123d1b654b44346ad991810b8f Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 2 Aug 2021 15:48:45 +0200 Subject: [PATCH 17/34] delete comment --- .../FunctionEndpoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 7ba38b27..6fd931d4 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -104,7 +104,7 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. await pipeline.PushMessage(messageContext).ConfigureAwait(false); await onComplete(transaction).ConfigureAwait(false); - //await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); + transaction?.Commit(); } } From 566b21e450be4c4d7125b6f7cb5de68e951fe247 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 3 Aug 2021 11:01:43 +0800 Subject: [PATCH 18/34] Native message may not have a messageId --- .../FunctionEndpoint.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 6fd931d4..91ec8170 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -94,6 +94,8 @@ async Task Process(Message message, FunctionExecutionContext functionExecutionCo await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); + var messageId = message.GetMessageId(); + try { using (var transaction = transactionFactory()) @@ -116,7 +118,7 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. var errorContext = new ErrorContext( exception, message.GetHeaders(), - message.MessageId, + messageId, message.Body, transportTransaction, message.SystemProperties.DeliveryCount); @@ -137,7 +139,7 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. MessageContext CreateMessageContext(TransportTransaction transportTransaction) => new MessageContext( - message.GetMessageId(), + messageId, message.GetHeaders(), message.Body, transportTransaction, From 2c0d749ddee156302ca0b4b3ce09c262f195f636 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 3 Aug 2021 11:31:25 +0800 Subject: [PATCH 19/34] Extract transaction strategy --- .../FunctionEndpoint.cs | 39 +++++-------------- .../FunctionTransactionStrategy.cs | 18 +++++++++ ...sageReceiverFunctionTransactionStrategy.cs | 39 +++++++++++++++++++ 3 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 91ec8170..310bfa22 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -6,7 +6,6 @@ using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; - using System.Transactions; using AzureFunctions.InProcess.ServiceBus; using Extensibility; using Logging; @@ -43,9 +42,7 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi { await Process(message, functionExecutionContext, - tx => messageReceiver.SafeCompleteAsync(message, tx), - () => CreateTransaction(), - tx => CreateTransportTransaction(tx)) + new MessageReceiverFunctionTransactionStrategy(message, messageReceiver)) .ConfigureAwait(false); } catch (Exception) @@ -54,22 +51,6 @@ await Process(message, await messageReceiver.AbandonAsync(message.SystemProperties.LockToken).ConfigureAwait(false); throw; } - - CommittableTransaction CreateTransaction() => - new CommittableTransaction(new TransactionOptions - { - IsolationLevel = IsolationLevel.Serializable, - Timeout = TransactionManager.MaximumTimeout - }); - - TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) - { - var transportTransaction = new TransportTransaction(); - transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); - transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); - transportTransaction.Set(transaction); - return transportTransaction; - } } /// @@ -83,13 +64,11 @@ public async Task Process(Message message, ExecutionContext executionContext, IL await Process(message, functionExecutionContext, - _ => Task.CompletedTask, - () => null, - _ => new TransportTransaction()) + FunctionTransactionStrategy.None) .ConfigureAwait(false); } - async Task Process(Message message, FunctionExecutionContext functionExecutionContext, Func onComplete, Func transactionFactory, Func transportTransactionFactory) + async Task Process(Message message, FunctionExecutionContext functionExecutionContext, FunctionTransactionStrategy transactionStrategy) { await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); @@ -98,23 +77,23 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. try { - using (var transaction = transactionFactory()) + using (var transaction = transactionStrategy.CreateTransaction()) { - var transportTransaction = transportTransactionFactory(transaction); + var transportTransaction = transactionStrategy.CreateTransportTransaction(transaction); var messageContext = CreateMessageContext(transportTransaction); await pipeline.PushMessage(messageContext).ConfigureAwait(false); - await onComplete(transaction).ConfigureAwait(false); + await transactionStrategy.Complete(transaction).ConfigureAwait(false); transaction?.Commit(); } } catch (Exception exception) { - using (var transaction = transactionFactory()) + using (var transaction = transactionStrategy.CreateTransaction()) { - var transportTransaction = transportTransactionFactory(transaction); + var transportTransaction = transactionStrategy.CreateTransportTransaction(transaction); var errorContext = new ErrorContext( exception, message.GetHeaders(), @@ -127,7 +106,7 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. if (errorHandleResult == ErrorHandleResult.Handled) { - await onComplete(transaction).ConfigureAwait(false); + await transactionStrategy.Complete(transaction).ConfigureAwait(false); transaction?.Commit(); return; diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs new file mode 100644 index 00000000..640a8bb7 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs @@ -0,0 +1,18 @@ +namespace NServiceBus +{ + using System.Threading.Tasks; + using System.Transactions; + using Transport; + + class FunctionTransactionStrategy + { + public virtual CommittableTransaction CreateTransaction() => null; + + public virtual TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) => + new TransportTransaction(); + + public virtual Task Complete(CommittableTransaction transaction) => Task.CompletedTask; + + public static FunctionTransactionStrategy None { get; } = new FunctionTransactionStrategy(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs new file mode 100644 index 00000000..46058e99 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs @@ -0,0 +1,39 @@ +namespace NServiceBus +{ + using System.Threading.Tasks; + using System.Transactions; + using AzureFunctions.InProcess.ServiceBus; + using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.ServiceBus.Core; + using Transport; + + class MessageReceiverFunctionTransactionStrategy : FunctionTransactionStrategy + { + readonly Message message; + readonly IMessageReceiver messageReceiver; + + public MessageReceiverFunctionTransactionStrategy(Message message, IMessageReceiver messageReceiver) + { + this.message = message; + this.messageReceiver = messageReceiver; + } + + public override CommittableTransaction CreateTransaction() => + new CommittableTransaction(new TransactionOptions + { + IsolationLevel = IsolationLevel.Serializable, + Timeout = TransactionManager.MaximumTimeout + }); + + public override TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) + { + var transportTransaction = new TransportTransaction(); + transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); + transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); + transportTransaction.Set(transaction); + return transportTransaction; + } + + public override Task Complete(CommittableTransaction transaction) => messageReceiver.SafeCompleteAsync(message, transaction); + } +} \ No newline at end of file From b6c8700e3cb9166c81a11d2d7cc58e2e091e06ea Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 2 Aug 2021 15:45:51 +0200 Subject: [PATCH 20/34] use single process implementation --- .../FunctionEndpoint.cs | 153 ++++++++---------- 1 file changed, 71 insertions(+), 82 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 1fccc9f4..7ba38b27 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -41,50 +41,12 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi try { - try - { - await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) - .ConfigureAwait(false); - - using (var transaction = CreateTransaction()) - { - var transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); - var messageContext = CreateMessageContext(message, transportTransaction); - - await pipeline.PushMessage(messageContext).ConfigureAwait(false); - - await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); - transaction.Commit(); - } - - } - catch (Exception exception) - { - using (var transaction = CreateTransaction()) - { - var transportTransaction = CreateTransportTransaction(message, messageReceiver, transaction); - var errorContext = new ErrorContext( - exception, - message.GetHeaders(), - message.MessageId, - message.Body, - transportTransaction, - message.SystemProperties.DeliveryCount); - - var errorHandleResult = await pipeline.PushFailedMessage(errorContext).ConfigureAwait(false); - - if (errorHandleResult == ErrorHandleResult.Handled) - { - await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); - - transaction.Commit(); - return; - } - - // handled by outer try-catch - throw; - } - } + await Process(message, + functionExecutionContext, + tx => messageReceiver.SafeCompleteAsync(message, tx), + () => CreateTransaction(), + tx => CreateTransportTransaction(tx)) + .ConfigureAwait(false); } catch (Exception) { @@ -93,21 +55,23 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. throw; } - CommittableTransaction CreateTransaction() + CommittableTransaction CreateTransaction() => + new CommittableTransaction(new TransactionOptions + { + IsolationLevel = IsolationLevel.Serializable, + Timeout = TransactionManager.MaximumTimeout + }); + + TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) { - return new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout }); + var transportTransaction = new TransportTransaction(); + transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); + transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); + transportTransaction.Set(transaction); + return transportTransaction; } } - static TransportTransaction CreateTransportTransaction(Message message, IMessageReceiver messageReceiver, CommittableTransaction transaction) - { - var transportTransaction = new TransportTransaction(); - transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); - transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); - transportTransaction.Set(transaction); - return transportTransaction; - } - /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. /// @@ -115,46 +79,71 @@ public async Task Process(Message message, ExecutionContext executionContext, IL { FunctionsLoggerFactory.Instance.SetCurrentLogger(functionsLogger); - var messageContext = CreateMessageContext(message, new TransportTransaction()); var functionExecutionContext = new FunctionExecutionContext(executionContext, functionsLogger); - await InitializeEndpointIfNecessary(functionExecutionContext, - messageContext.ReceiveCancellationTokenSource.Token).ConfigureAwait(false); + await Process(message, + functionExecutionContext, + _ => Task.CompletedTask, + () => null, + _ => new TransportTransaction()) + .ConfigureAwait(false); + } + + async Task Process(Message message, FunctionExecutionContext functionExecutionContext, Func onComplete, Func transactionFactory, Func transportTransactionFactory) + { + await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) + .ConfigureAwait(false); try { - await pipeline.PushMessage(messageContext).ConfigureAwait(false); + using (var transaction = transactionFactory()) + { + var transportTransaction = transportTransactionFactory(transaction); + var messageContext = CreateMessageContext(transportTransaction); + + await pipeline.PushMessage(messageContext).ConfigureAwait(false); + + await onComplete(transaction).ConfigureAwait(false); + //await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); + transaction?.Commit(); + } } catch (Exception exception) { - var errorContext = new ErrorContext( - exception, - message.GetHeaders(), - messageContext.MessageId, - messageContext.Body, - new TransportTransaction(), - message.SystemProperties.DeliveryCount); + using (var transaction = transactionFactory()) + { + var transportTransaction = transportTransactionFactory(transaction); + var errorContext = new ErrorContext( + exception, + message.GetHeaders(), + message.MessageId, + message.Body, + transportTransaction, + message.SystemProperties.DeliveryCount); + + var errorHandleResult = await pipeline.PushFailedMessage(errorContext).ConfigureAwait(false); + + if (errorHandleResult == ErrorHandleResult.Handled) + { + await onComplete(transaction).ConfigureAwait(false); - var errorHandleResult = await pipeline.PushFailedMessage(errorContext).ConfigureAwait(false); + transaction?.Commit(); + return; + } - if (errorHandleResult == ErrorHandleResult.Handled) - { - // return to signal to the Functions host it can complete the incoming message - return; + throw; } - - throw; } - } - static MessageContext CreateMessageContext(Message originalMessage, TransportTransaction transportTransaction) => - new MessageContext( - originalMessage.GetMessageId(), - originalMessage.GetHeaders(), - originalMessage.Body, - transportTransaction, - new CancellationTokenSource(), - new ContextBag()); + MessageContext CreateMessageContext(TransportTransaction transportTransaction) => + new MessageContext( + message.GetMessageId(), + message.GetHeaders(), + message.Body, + transportTransaction, + new CancellationTokenSource(), + new ContextBag()); + } /// /// Allows to forcefully initialize the endpoint if it hasn't been initialized yet. From 2def40001e6697b33c279f0f2257ccd657a5611d Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 2 Aug 2021 15:48:45 +0200 Subject: [PATCH 21/34] delete comment --- .../FunctionEndpoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 7ba38b27..6fd931d4 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -104,7 +104,7 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. await pipeline.PushMessage(messageContext).ConfigureAwait(false); await onComplete(transaction).ConfigureAwait(false); - //await messageReceiver.SafeCompleteAsync(message, transaction).ConfigureAwait(false); + transaction?.Commit(); } } From 8ec81e73c7d75f425fde484e66cdd96e9c55cbb5 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 3 Aug 2021 11:01:43 +0800 Subject: [PATCH 22/34] Native message may not have a messageId --- .../FunctionEndpoint.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 6fd931d4..91ec8170 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -94,6 +94,8 @@ async Task Process(Message message, FunctionExecutionContext functionExecutionCo await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); + var messageId = message.GetMessageId(); + try { using (var transaction = transactionFactory()) @@ -116,7 +118,7 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. var errorContext = new ErrorContext( exception, message.GetHeaders(), - message.MessageId, + messageId, message.Body, transportTransaction, message.SystemProperties.DeliveryCount); @@ -137,7 +139,7 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. MessageContext CreateMessageContext(TransportTransaction transportTransaction) => new MessageContext( - message.GetMessageId(), + messageId, message.GetHeaders(), message.Body, transportTransaction, From d47414e2f41eb7ab275febf01526dc42832509e9 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Tue, 3 Aug 2021 12:12:12 +0200 Subject: [PATCH 23/34] add some unit tests around the Process implementation --- .../FunctionEndpoint.cs | 19 +- .../FunctionEndpointComponent.cs | 37 +--- src/ServiceBus.Tests/FunctionEndpointTests.cs | 169 ++++++++++++++++++ src/ServiceBus.Tests/MessageHelper.cs | 43 +++++ 4 files changed, 224 insertions(+), 44 deletions(-) create mode 100644 src/ServiceBus.Tests/FunctionEndpointTests.cs create mode 100644 src/ServiceBus.Tests/MessageHelper.cs diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 91ec8170..859c0def 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -41,11 +41,14 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi try { + await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) + .ConfigureAwait(false); + await Process(message, - functionExecutionContext, tx => messageReceiver.SafeCompleteAsync(message, tx), () => CreateTransaction(), - tx => CreateTransportTransaction(tx)) + tx => CreateTransportTransaction(tx), + pipeline) .ConfigureAwait(false); } catch (Exception) @@ -81,19 +84,19 @@ public async Task Process(Message message, ExecutionContext executionContext, IL var functionExecutionContext = new FunctionExecutionContext(executionContext, functionsLogger); + await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) + .ConfigureAwait(false); + await Process(message, - functionExecutionContext, _ => Task.CompletedTask, () => null, - _ => new TransportTransaction()) + _ => new TransportTransaction(), + pipeline) .ConfigureAwait(false); } - async Task Process(Message message, FunctionExecutionContext functionExecutionContext, Func onComplete, Func transactionFactory, Func transportTransactionFactory) + internal static async Task Process(Message message, Func onComplete, Func transactionFactory, Func transportTransactionFactory, PipelineInvoker pipeline) { - await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) - .ConfigureAwait(false); - var messageId = message.GetMessageId(); try diff --git a/src/ServiceBus.Tests/FunctionEndpointComponent.cs b/src/ServiceBus.Tests/FunctionEndpointComponent.cs index 85872d14..3aa61dbf 100644 --- a/src/ServiceBus.Tests/FunctionEndpointComponent.cs +++ b/src/ServiceBus.Tests/FunctionEndpointComponent.cs @@ -2,20 +2,14 @@ { using System; using System.Collections.Generic; - using System.IO; using System.Linq; - using System.Reflection; using System.Threading; using System.Threading.Tasks; - using Microsoft.Azure.ServiceBus; using Microsoft.Extensions.DependencyInjection; using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.AcceptanceTesting.Customization; using NServiceBus.AcceptanceTesting.Support; - using NServiceBus.MessageInterfaces.MessageMapper.Reflection; - using NServiceBus.Serialization; - using NServiceBus.Settings; using ExecutionContext = Microsoft.Azure.WebJobs.ExecutionContext; abstract class FunctionEndpointComponent : IComponentBehavior @@ -57,9 +51,6 @@ public FunctionRunner( this.scenarioContext = scenarioContext; this.functionComponentType = functionComponentType; Name = functionComponentType.FullName; - - var serializer = new NewtonsoftSerializer(); - messageSerializer = serializer.Configure(new SettingsHolder())(new MessageMapper()); } public override string Name { get; } @@ -107,7 +98,7 @@ public override async Task ComponentsStarted(CancellationToken token) { foreach (var message in messages) { - var transportMessage = GenerateMessage(message); + var transportMessage = MessageHelper.GenerateMessage(message); var context = new ExecutionContext(); await endpoint.Process(transportMessage, context); } @@ -123,37 +114,11 @@ public override Task Stop() return base.Stop(); } - Message GenerateMessage(object message) - { - Message asbMessage; - using (var stream = new MemoryStream()) - { - messageSerializer.Serialize(message, stream); - asbMessage = new Message(stream.ToArray()); - } - - asbMessage.UserProperties["NServiceBus.EnclosedMessageTypes"] = message.GetType().FullName; - - var systemProperties = new Message.SystemPropertiesCollection(); - // sequence number is required to prevent SystemPropertiesCollection from throwing on the getters - var fieldInfo = typeof(Message.SystemPropertiesCollection).GetField("sequenceNumber", BindingFlags.NonPublic | BindingFlags.Instance); - fieldInfo.SetValue(systemProperties, 123); - // set delivery count to 1 - var deliveryCountProperty = typeof(Message.SystemPropertiesCollection).GetProperty("DeliveryCount"); - deliveryCountProperty.SetValue(systemProperties, 1); - // assign test message mocked system properties - var property = typeof(Message).GetProperty("SystemProperties"); - property.SetValue(asbMessage, systemProperties); - - return asbMessage; - } - readonly Action configurationCustomization; readonly ScenarioContext scenarioContext; readonly Type functionComponentType; IList messages; FunctionEndpoint endpoint; - IMessageSerializer messageSerializer; } } } \ No newline at end of file diff --git a/src/ServiceBus.Tests/FunctionEndpointTests.cs b/src/ServiceBus.Tests/FunctionEndpointTests.cs new file mode 100644 index 00000000..c1693989 --- /dev/null +++ b/src/ServiceBus.Tests/FunctionEndpointTests.cs @@ -0,0 +1,169 @@ +namespace ServiceBus.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using NServiceBus; + using NServiceBus.AzureFunctions.InProcess.ServiceBus; + using NServiceBus.Transport; + using NUnit.Framework; + + [TestFixture] + public class FunctionEndpointTests + { + [Test] + public async Task When_processing_successful_should_complete_message() + { + var onCompleteCalled = false; + var message = MessageHelper.GenerateMessage(new TestMessage()); + MessageContext messageContext = null; + TransportTransaction transportTransaction = null; + var pipelineInvoker = await CreatePipeline( + ctx => + { + messageContext = ctx; + return Task.CompletedTask; + }); + + + await FunctionEndpoint.Process( + message, + tx => + { + onCompleteCalled = true; + return Task.CompletedTask; + }, () => null, + _ => transportTransaction = new TransportTransaction(), + pipelineInvoker); + + Assert.IsTrue(onCompleteCalled); + Assert.AreSame(message.GetMessageId(), messageContext.MessageId); + Assert.AreSame(message.Body, messageContext.Body); + CollectionAssert.IsSubsetOf(message.GetHeaders(), messageContext.Headers); // the IncomingMessage has an additional MessageId header + Assert.AreSame(transportTransaction, messageContext.TransportTransaction); + } + + [Test] + public async Task When_processing_fails_should_provide_error_context() + { + var message = MessageHelper.GenerateMessage(new TestMessage()); + var pipelineException = new Exception("test exception"); + var transportTransactions = new List(); + ErrorContext errorContext = null; + var pipelineInvoker = await CreatePipeline( + _ => throw pipelineException, + errCtx => + { + errorContext = errCtx; + return Task.FromResult(ErrorHandleResult.Handled); + }); + + await FunctionEndpoint.Process( + message, + tx => Task.CompletedTask, + () => null, + _ => + { + var tx = new TransportTransaction(); + transportTransactions.Add(tx); + return tx; + }, + pipelineInvoker); + + Assert.AreSame(pipelineException, errorContext.Exception); + Assert.AreSame(message.GetMessageId(), errorContext.Message.NativeMessageId); + Assert.AreSame(message.Body, errorContext.Message.Body); + CollectionAssert.IsSubsetOf(message.GetHeaders(), errorContext.Message.Headers); // the IncomingMessage has an additional MessageId header + Assert.AreSame(transportTransactions.Last(), errorContext.TransportTransaction); // verify usage of the correct transport transaction instance + Assert.AreEqual(2, transportTransactions.Count); // verify that a new transport transaction has been created for the error handling + } + + [Test] + public async Task When_error_pipeline_fails_should_throw() + { + var onCompleteCalled = false; + var errorPipelineException = new Exception("error pipeline failure"); + var pipelineInvoker = await CreatePipeline( + _ => throw new Exception("main pipeline failure"), + _ => throw errorPipelineException); + + var exception = Assert.ThrowsAsync(async () => + await FunctionEndpoint.Process( + MessageHelper.GenerateMessage(new TestMessage()), + tx => + { + onCompleteCalled = true; + return Task.CompletedTask; + }, + () => null, + _ => new TransportTransaction(), + pipelineInvoker)); + + Assert.IsFalse(onCompleteCalled); + Assert.AreSame(errorPipelineException, exception); + } + + [Test] + public async Task When_error_pipeline_handles_error_should_complete_message() + { + var onCompleteCalled = false; + var pipelineInvoker = await CreatePipeline( + _ => throw new Exception("main pipeline failure"), + _ => Task.FromResult(ErrorHandleResult.Handled)); + + await FunctionEndpoint.Process( + MessageHelper.GenerateMessage(new TestMessage()), + tx => + { + onCompleteCalled = true; + return Task.CompletedTask; + }, + () => null, + _ => new TransportTransaction(), + pipelineInvoker); + + Assert.IsTrue(onCompleteCalled); + } + + [Test] + public async Task When_error_pipeline_requires_retry_should_throw() + { + var onCompleteCalled = false; + var mainPipelineException = new Exception("main pipeline failure"); + var pipelineInvoker = await CreatePipeline( + _ => throw mainPipelineException, + _ => Task.FromResult(ErrorHandleResult.RetryRequired)); + + var exception = Assert.ThrowsAsync(async () => + await FunctionEndpoint.Process( + MessageHelper.GenerateMessage(new TestMessage()), + tx => + { + onCompleteCalled = true; + return Task.CompletedTask; + }, + () => null, + _ => new TransportTransaction(), + pipelineInvoker)); + + Assert.IsFalse(onCompleteCalled); + Assert.AreSame(mainPipelineException, exception); + } + + static async Task CreatePipeline(Func mainPipeline = null, Func> errorPipeline = null) + { + var pipelineInvoker = new PipelineInvoker(); + await (pipelineInvoker as IPushMessages) + .Init( + mainPipeline ?? (_ => Task.CompletedTask), + errorPipeline ?? (_ => Task.FromResult(ErrorHandleResult.Handled)), + null, null); + return pipelineInvoker; + } + + class TestMessage + { + } + } +} \ No newline at end of file diff --git a/src/ServiceBus.Tests/MessageHelper.cs b/src/ServiceBus.Tests/MessageHelper.cs new file mode 100644 index 00000000..8416a293 --- /dev/null +++ b/src/ServiceBus.Tests/MessageHelper.cs @@ -0,0 +1,43 @@ +namespace ServiceBus.Tests +{ + using System; + using System.IO; + using System.Reflection; + using Microsoft.Azure.ServiceBus; + using NServiceBus; + using NServiceBus.MessageInterfaces.MessageMapper.Reflection; + using NServiceBus.Serialization; + using NServiceBus.Settings; + + public class MessageHelper + { + static NewtonsoftSerializer serializer = new NewtonsoftSerializer(); + static IMessageSerializer messageSerializer = serializer.Configure(new SettingsHolder())(new MessageMapper()); + + public static Message GenerateMessage(object message) + { + Message asbMessage; + using (var stream = new MemoryStream()) + { + messageSerializer.Serialize(message, stream); + asbMessage = new Message(stream.ToArray()); + } + + asbMessage.UserProperties["NServiceBus.EnclosedMessageTypes"] = message.GetType().FullName; + + var systemProperties = new Message.SystemPropertiesCollection(); + // sequence number is required to prevent SystemPropertiesCollection from throwing on the getters + var fieldInfo = typeof(Message.SystemPropertiesCollection).GetField("sequenceNumber", BindingFlags.NonPublic | BindingFlags.Instance); + fieldInfo.SetValue(systemProperties, 123); + // set delivery count to 1 + var deliveryCountProperty = typeof(Message.SystemPropertiesCollection).GetProperty("DeliveryCount"); + deliveryCountProperty.SetValue(systemProperties, 1); + // assign test message mocked system properties + var property = typeof(Message).GetProperty("SystemProperties"); + property.SetValue(asbMessage, systemProperties); + + asbMessage.MessageId = Guid.NewGuid().ToString("N"); + return asbMessage; + } + } +} \ No newline at end of file From 27a71863241d6a4998c7c73525511b4b3e59d9c0 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 3 Aug 2021 11:31:25 +0800 Subject: [PATCH 24/34] Extract transaction strategy --- .../FunctionEndpoint.cs | 43 ++++--------------- .../FunctionTransactionStrategy.cs | 18 ++++++++ ...sageReceiverFunctionTransactionStrategy.cs | 39 +++++++++++++++++ 3 files changed, 66 insertions(+), 34 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 859c0def..45975d1b 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -6,7 +6,6 @@ using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; - using System.Transactions; using AzureFunctions.InProcess.ServiceBus; using Extensibility; using Logging; @@ -44,11 +43,7 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); - await Process(message, - tx => messageReceiver.SafeCompleteAsync(message, tx), - () => CreateTransaction(), - tx => CreateTransportTransaction(tx), - pipeline) + await Process(message, new MessageReceiverFunctionTransactionStrategy(message, messageReceiver)) .ConfigureAwait(false); } catch (Exception) @@ -57,22 +52,6 @@ await Process(message, await messageReceiver.AbandonAsync(message.SystemProperties.LockToken).ConfigureAwait(false); throw; } - - CommittableTransaction CreateTransaction() => - new CommittableTransaction(new TransactionOptions - { - IsolationLevel = IsolationLevel.Serializable, - Timeout = TransactionManager.MaximumTimeout - }); - - TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) - { - var transportTransaction = new TransportTransaction(); - transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); - transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); - transportTransaction.Set(transaction); - return transportTransaction; - } } /// @@ -87,37 +66,33 @@ public async Task Process(Message message, ExecutionContext executionContext, IL await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); - await Process(message, - _ => Task.CompletedTask, - () => null, - _ => new TransportTransaction(), - pipeline) + await Process(message, FunctionTransactionStrategy.None) .ConfigureAwait(false); } - internal static async Task Process(Message message, Func onComplete, Func transactionFactory, Func transportTransactionFactory, PipelineInvoker pipeline) + async Task Process(Message message, FunctionTransactionStrategy transactionStrategy) { var messageId = message.GetMessageId(); try { - using (var transaction = transactionFactory()) + using (var transaction = transactionStrategy.CreateTransaction()) { - var transportTransaction = transportTransactionFactory(transaction); + var transportTransaction = transactionStrategy.CreateTransportTransaction(transaction); var messageContext = CreateMessageContext(transportTransaction); await pipeline.PushMessage(messageContext).ConfigureAwait(false); - await onComplete(transaction).ConfigureAwait(false); + await transactionStrategy.Complete(transaction).ConfigureAwait(false); transaction?.Commit(); } } catch (Exception exception) { - using (var transaction = transactionFactory()) + using (var transaction = transactionStrategy.CreateTransaction()) { - var transportTransaction = transportTransactionFactory(transaction); + var transportTransaction = transactionStrategy.CreateTransportTransaction(transaction); var errorContext = new ErrorContext( exception, message.GetHeaders(), @@ -130,7 +105,7 @@ internal static async Task Process(Message message, Func null; + + public virtual TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) => + new TransportTransaction(); + + public virtual Task Complete(CommittableTransaction transaction) => Task.CompletedTask; + + public static FunctionTransactionStrategy None { get; } = new FunctionTransactionStrategy(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs new file mode 100644 index 00000000..46058e99 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs @@ -0,0 +1,39 @@ +namespace NServiceBus +{ + using System.Threading.Tasks; + using System.Transactions; + using AzureFunctions.InProcess.ServiceBus; + using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.ServiceBus.Core; + using Transport; + + class MessageReceiverFunctionTransactionStrategy : FunctionTransactionStrategy + { + readonly Message message; + readonly IMessageReceiver messageReceiver; + + public MessageReceiverFunctionTransactionStrategy(Message message, IMessageReceiver messageReceiver) + { + this.message = message; + this.messageReceiver = messageReceiver; + } + + public override CommittableTransaction CreateTransaction() => + new CommittableTransaction(new TransactionOptions + { + IsolationLevel = IsolationLevel.Serializable, + Timeout = TransactionManager.MaximumTimeout + }); + + public override TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) + { + var transportTransaction = new TransportTransaction(); + transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); + transportTransaction.Set("IncomingQueue.PartitionKey", message.PartitionKey); + transportTransaction.Set(transaction); + return transportTransaction; + } + + public override Task Complete(CommittableTransaction transaction) => messageReceiver.SafeCompleteAsync(message, transaction); + } +} \ No newline at end of file From 2f9e9dbab8b04fcb0d7518b3fd1e0ed7690ae17c Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 6 Aug 2021 10:20:28 +0800 Subject: [PATCH 25/34] Fix tests --- .../FunctionEndpoint.cs | 6 +- src/ServiceBus.Tests/FunctionEndpointTests.cs | 99 ++++++++++--------- 2 files changed, 54 insertions(+), 51 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 45975d1b..640a6c3a 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -43,7 +43,7 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); - await Process(message, new MessageReceiverFunctionTransactionStrategy(message, messageReceiver)) + await Process(message, new MessageReceiverFunctionTransactionStrategy(message, messageReceiver), pipeline) .ConfigureAwait(false); } catch (Exception) @@ -66,11 +66,11 @@ public async Task Process(Message message, ExecutionContext executionContext, IL await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); - await Process(message, FunctionTransactionStrategy.None) + await Process(message, FunctionTransactionStrategy.None, pipeline) .ConfigureAwait(false); } - async Task Process(Message message, FunctionTransactionStrategy transactionStrategy) + internal static async Task Process(Message message, FunctionTransactionStrategy transactionStrategy, PipelineInvoker pipeline) { var messageId = message.GetMessageId(); diff --git a/src/ServiceBus.Tests/FunctionEndpointTests.cs b/src/ServiceBus.Tests/FunctionEndpointTests.cs index c1693989..c955884f 100644 --- a/src/ServiceBus.Tests/FunctionEndpointTests.cs +++ b/src/ServiceBus.Tests/FunctionEndpointTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using System.Transactions; using NServiceBus; using NServiceBus.AzureFunctions.InProcess.ServiceBus; using NServiceBus.Transport; @@ -15,10 +16,8 @@ public class FunctionEndpointTests [Test] public async Task When_processing_successful_should_complete_message() { - var onCompleteCalled = false; var message = MessageHelper.GenerateMessage(new TestMessage()); MessageContext messageContext = null; - TransportTransaction transportTransaction = null; var pipelineInvoker = await CreatePipeline( ctx => { @@ -26,22 +25,19 @@ public async Task When_processing_successful_should_complete_message() return Task.CompletedTask; }); + var transactionStrategy = new TestableFunctionTransactionStrategy(); await FunctionEndpoint.Process( message, - tx => - { - onCompleteCalled = true; - return Task.CompletedTask; - }, () => null, - _ => transportTransaction = new TransportTransaction(), + transactionStrategy, pipelineInvoker); - Assert.IsTrue(onCompleteCalled); + Assert.IsTrue(transactionStrategy.OnCompleteCalled); Assert.AreSame(message.GetMessageId(), messageContext.MessageId); Assert.AreSame(message.Body, messageContext.Body); CollectionAssert.IsSubsetOf(message.GetHeaders(), messageContext.Headers); // the IncomingMessage has an additional MessageId header - Assert.AreSame(transportTransaction, messageContext.TransportTransaction); + Assert.AreEqual(1, transactionStrategy.CreatedTransportTransactions.Count); + Assert.AreSame(transactionStrategy.CreatedTransportTransactions[0], messageContext.TransportTransaction); } [Test] @@ -49,7 +45,6 @@ public async Task When_processing_fails_should_provide_error_context() { var message = MessageHelper.GenerateMessage(new TestMessage()); var pipelineException = new Exception("test exception"); - var transportTransactions = new List(); ErrorContext errorContext = null; var pipelineInvoker = await CreatePipeline( _ => throw pipelineException, @@ -59,95 +54,75 @@ public async Task When_processing_fails_should_provide_error_context() return Task.FromResult(ErrorHandleResult.Handled); }); + var transactionStrategy = new TestableFunctionTransactionStrategy(); + await FunctionEndpoint.Process( message, - tx => Task.CompletedTask, - () => null, - _ => - { - var tx = new TransportTransaction(); - transportTransactions.Add(tx); - return tx; - }, + transactionStrategy, pipelineInvoker); Assert.AreSame(pipelineException, errorContext.Exception); Assert.AreSame(message.GetMessageId(), errorContext.Message.NativeMessageId); Assert.AreSame(message.Body, errorContext.Message.Body); CollectionAssert.IsSubsetOf(message.GetHeaders(), errorContext.Message.Headers); // the IncomingMessage has an additional MessageId header - Assert.AreSame(transportTransactions.Last(), errorContext.TransportTransaction); // verify usage of the correct transport transaction instance - Assert.AreEqual(2, transportTransactions.Count); // verify that a new transport transaction has been created for the error handling + Assert.AreSame(transactionStrategy.CreatedTransportTransactions.Last(), errorContext.TransportTransaction); // verify usage of the correct transport transaction instance + Assert.AreEqual(2, transactionStrategy.CreatedTransportTransactions.Count); // verify that a new transport transaction has been created for the error handling } [Test] public async Task When_error_pipeline_fails_should_throw() { - var onCompleteCalled = false; var errorPipelineException = new Exception("error pipeline failure"); var pipelineInvoker = await CreatePipeline( _ => throw new Exception("main pipeline failure"), _ => throw errorPipelineException); + var transactionStrategy = new TestableFunctionTransactionStrategy(); + var exception = Assert.ThrowsAsync(async () => await FunctionEndpoint.Process( MessageHelper.GenerateMessage(new TestMessage()), - tx => - { - onCompleteCalled = true; - return Task.CompletedTask; - }, - () => null, - _ => new TransportTransaction(), + transactionStrategy, pipelineInvoker)); - Assert.IsFalse(onCompleteCalled); + Assert.IsFalse(transactionStrategy.OnCompleteCalled); Assert.AreSame(errorPipelineException, exception); } [Test] public async Task When_error_pipeline_handles_error_should_complete_message() { - var onCompleteCalled = false; var pipelineInvoker = await CreatePipeline( _ => throw new Exception("main pipeline failure"), _ => Task.FromResult(ErrorHandleResult.Handled)); + var transactionStrategy = new TestableFunctionTransactionStrategy(); + await FunctionEndpoint.Process( MessageHelper.GenerateMessage(new TestMessage()), - tx => - { - onCompleteCalled = true; - return Task.CompletedTask; - }, - () => null, - _ => new TransportTransaction(), + transactionStrategy, pipelineInvoker); - Assert.IsTrue(onCompleteCalled); + Assert.IsTrue(transactionStrategy.OnCompleteCalled); } [Test] public async Task When_error_pipeline_requires_retry_should_throw() { - var onCompleteCalled = false; var mainPipelineException = new Exception("main pipeline failure"); var pipelineInvoker = await CreatePipeline( _ => throw mainPipelineException, _ => Task.FromResult(ErrorHandleResult.RetryRequired)); + var transactionStrategy = new TestableFunctionTransactionStrategy(); + var exception = Assert.ThrowsAsync(async () => await FunctionEndpoint.Process( MessageHelper.GenerateMessage(new TestMessage()), - tx => - { - onCompleteCalled = true; - return Task.CompletedTask; - }, - () => null, - _ => new TransportTransaction(), + transactionStrategy, pipelineInvoker)); - Assert.IsFalse(onCompleteCalled); + Assert.IsFalse(transactionStrategy.OnCompleteCalled); Assert.AreSame(mainPipelineException, exception); } @@ -165,5 +140,33 @@ static async Task CreatePipeline(Func mai class TestMessage { } + + class TestableFunctionTransactionStrategy : FunctionTransactionStrategy + { + public bool OnCompleteCalled { get; private set; } + public CommittableTransaction CompletedTransaction { get; private set; } + public CommittableTransaction CreatedTransaction { get; private set; } + public List CreatedTransportTransactions { get; } = new List(); + + public override Task Complete(CommittableTransaction transaction) + { + OnCompleteCalled = true; + CompletedTransaction = transaction; + return Task.CompletedTask; + } + + public override CommittableTransaction CreateTransaction() + { + CreatedTransaction = new CommittableTransaction(); + return CreatedTransaction; + } + + public override TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) + { + var transportTransaction = new TransportTransaction(); + CreatedTransportTransactions.Add(transportTransaction); + return transportTransaction; + } + } } } \ No newline at end of file From d42d229fc25a834768531b2b43478945887d2316 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 6 Aug 2021 10:36:02 +0800 Subject: [PATCH 26/34] Refactor --- .../FunctionEndpoint.cs | 6 +++--- .../Transactions/ITransactionStrategy.cs | 13 +++++++++++++ ...egy.cs => MessageReceiverTransactionStrategy.cs} | 10 +++++----- ...nsactionStrategy.cs => NoTransactionStrategy.cs} | 4 ++-- src/ServiceBus.Tests/FunctionEndpointTests.cs | 8 ++++---- 5 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/ITransactionStrategy.cs rename src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/{MessageReceiverFunctionTransactionStrategy.cs => MessageReceiverTransactionStrategy.cs} (67%) rename src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/{FunctionTransactionStrategy.cs => NoTransactionStrategy.cs} (75%) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index 640a6c3a..d147f1ee 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -43,7 +43,7 @@ public async Task ProcessTransactional(Message message, ExecutionContext executi await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); - await Process(message, new MessageReceiverFunctionTransactionStrategy(message, messageReceiver), pipeline) + await Process(message, new MessageReceiverTransactionStrategy(message, messageReceiver), pipeline) .ConfigureAwait(false); } catch (Exception) @@ -66,11 +66,11 @@ public async Task Process(Message message, ExecutionContext executionContext, IL await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken.None) .ConfigureAwait(false); - await Process(message, FunctionTransactionStrategy.None, pipeline) + await Process(message, NoTransactionStrategy.Instance, pipeline) .ConfigureAwait(false); } - internal static async Task Process(Message message, FunctionTransactionStrategy transactionStrategy, PipelineInvoker pipeline) + internal static async Task Process(Message message, ITransactionStrategy transactionStrategy, PipelineInvoker pipeline) { var messageId = message.GetMessageId(); diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/ITransactionStrategy.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/ITransactionStrategy.cs new file mode 100644 index 00000000..771abeb1 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/ITransactionStrategy.cs @@ -0,0 +1,13 @@ +namespace NServiceBus +{ + using System.Threading.Tasks; + using System.Transactions; + using Transport; + + interface ITransactionStrategy + { + CommittableTransaction CreateTransaction(); + TransportTransaction CreateTransportTransaction(CommittableTransaction transaction); + Task Complete(CommittableTransaction transaction); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverTransactionStrategy.cs similarity index 67% rename from src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs rename to src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverTransactionStrategy.cs index 46058e99..22932a2f 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverFunctionTransactionStrategy.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverTransactionStrategy.cs @@ -7,25 +7,25 @@ using Microsoft.Azure.ServiceBus.Core; using Transport; - class MessageReceiverFunctionTransactionStrategy : FunctionTransactionStrategy + class MessageReceiverTransactionStrategy : ITransactionStrategy { readonly Message message; readonly IMessageReceiver messageReceiver; - public MessageReceiverFunctionTransactionStrategy(Message message, IMessageReceiver messageReceiver) + public MessageReceiverTransactionStrategy(Message message, IMessageReceiver messageReceiver) { this.message = message; this.messageReceiver = messageReceiver; } - public override CommittableTransaction CreateTransaction() => + public CommittableTransaction CreateTransaction() => new CommittableTransaction(new TransactionOptions { IsolationLevel = IsolationLevel.Serializable, Timeout = TransactionManager.MaximumTimeout }); - public override TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) + public TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) { var transportTransaction = new TransportTransaction(); transportTransaction.Set((messageReceiver.ServiceBusConnection, messageReceiver.Path)); @@ -34,6 +34,6 @@ public override TransportTransaction CreateTransportTransaction(CommittableTrans return transportTransaction; } - public override Task Complete(CommittableTransaction transaction) => messageReceiver.SafeCompleteAsync(message, transaction); + public Task Complete(CommittableTransaction transaction) => messageReceiver.SafeCompleteAsync(message, transaction); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/NoTransactionStrategy.cs similarity index 75% rename from src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs rename to src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/NoTransactionStrategy.cs index 640a8bb7..b2c34f04 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/FunctionTransactionStrategy.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/NoTransactionStrategy.cs @@ -4,7 +4,7 @@ using System.Transactions; using Transport; - class FunctionTransactionStrategy + class NoTransactionStrategy : ITransactionStrategy { public virtual CommittableTransaction CreateTransaction() => null; @@ -13,6 +13,6 @@ public virtual TransportTransaction CreateTransportTransaction(CommittableTransa public virtual Task Complete(CommittableTransaction transaction) => Task.CompletedTask; - public static FunctionTransactionStrategy None { get; } = new FunctionTransactionStrategy(); + public static NoTransactionStrategy Instance { get; } = new NoTransactionStrategy(); } } \ No newline at end of file diff --git a/src/ServiceBus.Tests/FunctionEndpointTests.cs b/src/ServiceBus.Tests/FunctionEndpointTests.cs index c955884f..9cfff3e0 100644 --- a/src/ServiceBus.Tests/FunctionEndpointTests.cs +++ b/src/ServiceBus.Tests/FunctionEndpointTests.cs @@ -141,27 +141,27 @@ class TestMessage { } - class TestableFunctionTransactionStrategy : FunctionTransactionStrategy + class TestableFunctionTransactionStrategy : ITransactionStrategy { public bool OnCompleteCalled { get; private set; } public CommittableTransaction CompletedTransaction { get; private set; } public CommittableTransaction CreatedTransaction { get; private set; } public List CreatedTransportTransactions { get; } = new List(); - public override Task Complete(CommittableTransaction transaction) + public Task Complete(CommittableTransaction transaction) { OnCompleteCalled = true; CompletedTransaction = transaction; return Task.CompletedTask; } - public override CommittableTransaction CreateTransaction() + public CommittableTransaction CreateTransaction() { CreatedTransaction = new CommittableTransaction(); return CreatedTransaction; } - public override TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) + public TransportTransaction CreateTransportTransaction(CommittableTransaction transaction) { var transportTransaction = new TransportTransaction(); CreatedTransportTransactions.Add(transportTransaction); From aeb893f43772a69f1f9e576e96dcf1935822f914 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Fri, 6 Aug 2021 13:31:59 +0200 Subject: [PATCH 27/34] Auto-detect AutoComplete setting (#244) * locate attribute to read AutoComplete value * rename public APIs * remove obsolete method usage * extract reflection code into helper * add more details to the reflection helper exception message * merge AttributeDiscoverer and ReflectionHelper methods --- .../AttributeDiscoverer.cs | 38 ----- .../FunctionEndpoint.cs | 19 +++ .../IFunctionEndpoint.cs | 8 +- .../ReflectionHelper.cs | 50 ++++++ ...erviceBusTriggeredEndpointConfiguration.cs | 7 +- ...vals2.Can_enable_transactions.approved.txt | 4 +- ...verride_trigger_function_name.approved.txt | 6 +- ...rApprovals2.NameIsStringValue.approved.txt | 6 +- ...s2.Two_optionals_out_of_order.approved.txt | 4 +- ...rApprovals2.Use_two_optionals.approved.txt | 4 +- ...ngFullyQualifiedAttributeName.approved.txt | 6 +- ...atorApprovals2.UsingNamespace.approved.txt | 6 +- .../TriggerFunctionGenerator2.cs | 6 +- .../APIApprovals.Approve.approved.txt | 8 +- .../FunctionEndpointComponent.cs | 2 +- src/ServiceBus.Tests/ReflectionHelperTests.cs | 148 ++++++++++++++++++ src/ServiceBus.Tests/ServiceBus.Tests.csproj | 1 + ...shipping_handlers_in_dedicated_assembly.cs | 2 +- 18 files changed, 257 insertions(+), 68 deletions(-) delete mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/AttributeDiscoverer.cs create mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ReflectionHelper.cs create mode 100644 src/ServiceBus.Tests/ReflectionHelperTests.cs diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/AttributeDiscoverer.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/AttributeDiscoverer.cs deleted file mode 100644 index 1a1434c7..00000000 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/AttributeDiscoverer.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace NServiceBus.AzureFunctions.InProcess.ServiceBus -{ - using System; - using System.Diagnostics; - using System.Reflection; - using Microsoft.Azure.WebJobs; - - static class TriggerDiscoverer - { - /// - /// Attempts to derive the required configuration information from the Azure Function and trigger attributes via reflection. - /// - public static TTransportTriggerAttribute TryGet() where TTransportTriggerAttribute : Attribute - { - var frames = new StackTrace().GetFrames(); - foreach (var stackFrame in frames) - { - var method = stackFrame.GetMethod(); - var functionAttribute = method.GetCustomAttribute(false); - if (functionAttribute != null) - { - foreach (var parameter in method.GetParameters()) - { - var triggerConfiguration = parameter.GetCustomAttribute(false); - if (triggerConfiguration != null) - { - return triggerConfiguration; - } - } - - return null; - } - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index d147f1ee..f390ae12 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -11,6 +11,7 @@ using Logging; using Microsoft.Azure.ServiceBus; using Microsoft.Azure.ServiceBus.Core; + using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using Transport; using ExecutionContext = Microsoft.Azure.WebJobs.ExecutionContext; @@ -29,8 +30,17 @@ internal FunctionEndpoint(IStartableEndpointWithExternallyManagedContainer exter endpointFactory = _ => externallyManagedContainerEndpoint.Start(serviceProvider); } + /// + /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. This method will lookup the setting to determine whether to use transactional or non-transactional processing. + /// + public Task Process(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null) => + ReflectionHelper.GetAutoCompleteValue() + ? ProcessNonTransactional(message, executionContext, messageReceiver, functionsLogger) + : ProcessTransactional(message, executionContext, messageReceiver, functionsLogger); + /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. All messages are committed transactionally with the successful processing of the incoming message. + /// Requires to be set to false! /// public async Task ProcessTransactional(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null) { @@ -57,6 +67,15 @@ await InitializeEndpointIfNecessary(functionExecutionContext, CancellationToken. /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. /// + public Task ProcessNonTransactional(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null) => Process(message, executionContext, functionsLogger); + + /// + /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. + /// + [ObsoleteEx( + ReplacementTypeOrMember = "Process(Message, ExecutionContext, IMessageReceiver, ILogger)", + TreatAsErrorFromVersion = "2", + RemoveInVersion = "3")] public async Task Process(Message message, ExecutionContext executionContext, ILogger functionsLogger = null) { FunctionsLoggerFactory.Instance.SetCurrentLogger(functionsLogger); diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs index 496d5f40..5b7de981 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/IFunctionEndpoint.cs @@ -14,13 +14,17 @@ public interface IFunctionEndpoint { /// - /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. All messages are committed transactionally with the successful processing of the incoming message. + /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. This method will lookup the setting to determine whether to use transactional or non-transactional processing. /// - Task ProcessTransactional(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null); + Task Process(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null); /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. /// + [ObsoleteEx( + ReplacementTypeOrMember = "Process(Message, ExecutionContext, IMessageReceiver, ILogger)", + TreatAsErrorFromVersion = "2", + RemoveInVersion = "3")] Task Process(Message message, ExecutionContext executionContext, ILogger functionsLogger = null); /// diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ReflectionHelper.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ReflectionHelper.cs new file mode 100644 index 00000000..21d7bd08 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ReflectionHelper.cs @@ -0,0 +1,50 @@ +namespace NServiceBus.AzureFunctions.InProcess.ServiceBus +{ + using System; + using System.Diagnostics; + using System.Reflection; + using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.WebJobs; + + class ReflectionHelper + { + public static bool GetAutoCompleteValue() + { + var triggerAttribute = FindTriggerAttributeInternal(); + if (triggerAttribute != null) + { + return triggerAttribute.AutoComplete; + } + + throw new Exception($"Could not locate {nameof(ServiceBusTriggerAttribute)} to infer the AutoComplete setting. Make sure that the function trigger contains a parameter decorated with {nameof(ServiceBusTriggerAttribute)} or use the advanced APIs exposed via the {nameof(FunctionEndpoint)} type instead."); + } + + public static ServiceBusTriggerAttribute FindBusTriggerAttribute() => FindTriggerAttributeInternal(); + + static ServiceBusTriggerAttribute FindTriggerAttributeInternal() + { + var st = new StackTrace(skipFrames: 2); // skip first two frames because it is this method + the public method + var frames = st.GetFrames(); + foreach (var frame in frames) + { + var method = frame?.GetMethod(); + if (method?.GetCustomAttribute(false) != null) + { + foreach (var parameter in method.GetParameters()) + { + ServiceBusTriggerAttribute serviceBusTriggerAttribute; + if (parameter.ParameterType == typeof(Message) + && (serviceBusTriggerAttribute = parameter.GetCustomAttribute(false)) != null) + { + return serviceBusTriggerAttribute; + } + } + + return null; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ServiceBusTriggeredEndpointConfiguration.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ServiceBusTriggeredEndpointConfiguration.cs index 0b3a6cc0..3a2f544e 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ServiceBusTriggeredEndpointConfiguration.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/ServiceBusTriggeredEndpointConfiguration.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using AzureFunctions.InProcess.ServiceBus; using Logging; - using Microsoft.Azure.WebJobs; using Serialization; using Transport; @@ -77,10 +76,10 @@ public ServiceBusTriggeredEndpointConfiguration(string endpointName, string conn /// public static ServiceBusTriggeredEndpointConfiguration FromAttributes() { - var configuration = TriggerDiscoverer.TryGet(); - if (configuration != null) + var serviceBusTriggerAttribute = ReflectionHelper.FindBusTriggerAttribute(); + if (serviceBusTriggerAttribute != null) { - return new ServiceBusTriggeredEndpointConfiguration(configuration.QueueName, configuration.Connection); + return new ServiceBusTriggeredEndpointConfiguration(serviceBusTriggerAttribute.QueueName, serviceBusTriggerAttribute.Connection); } throw new Exception( diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_enable_transactions.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_enable_transactions.approved.txt index bfdbfd4a..4c8ea9d2 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_enable_transactions.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_enable_transactions.approved.txt @@ -8,9 +8,9 @@ using NServiceBus; class FunctionEndpointTrigger { - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) { this.endpoint = endpoint; } diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt index efad0593..fd792927 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Can_override_trigger_function_name.approved.txt @@ -8,9 +8,9 @@ using NServiceBus; class FunctionEndpointTrigger { - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) { this.endpoint = endpoint; } @@ -23,6 +23,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.Process(message, executionContext, logger); + await endpoint.ProcessNonTransactional(message, executionContext, messageReceiver, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt index 99e48622..e16a7f0f 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.NameIsStringValue.approved.txt @@ -8,9 +8,9 @@ using NServiceBus; class FunctionEndpointTrigger { - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) { this.endpoint = endpoint; } @@ -23,6 +23,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.Process(message, executionContext, logger); + await endpoint.ProcessNonTransactional(message, executionContext, messageReceiver, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt index bbb25c31..70cc6c8a 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Two_optionals_out_of_order.approved.txt @@ -8,9 +8,9 @@ using NServiceBus; class FunctionEndpointTrigger { - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) { this.endpoint = endpoint; } diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt index bbb25c31..70cc6c8a 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.Use_two_optionals.approved.txt @@ -8,9 +8,9 @@ using NServiceBus; class FunctionEndpointTrigger { - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) { this.endpoint = endpoint; } diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt index 99e48622..e16a7f0f 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingFullyQualifiedAttributeName.approved.txt @@ -8,9 +8,9 @@ using NServiceBus; class FunctionEndpointTrigger { - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) { this.endpoint = endpoint; } @@ -23,6 +23,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.Process(message, executionContext, logger); + await endpoint.ProcessNonTransactional(message, executionContext, messageReceiver, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt index 99e48622..e16a7f0f 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/ApprovalFiles/SourceGeneratorApprovals2.UsingNamespace.approved.txt @@ -8,9 +8,9 @@ using NServiceBus; class FunctionEndpointTrigger { - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) { this.endpoint = endpoint; } @@ -23,6 +23,6 @@ class FunctionEndpointTrigger ILogger logger, ExecutionContext executionContext) { - await endpoint.Process(message, executionContext, logger); + await endpoint.ProcessNonTransactional(message, executionContext, messageReceiver, logger); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs index 98ab503b..47805cb6 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs @@ -116,9 +116,9 @@ public void Execute(GeneratorExecutionContext context) class FunctionEndpointTrigger {{ - readonly IFunctionEndpoint endpoint; + readonly FunctionEndpoint endpoint; - public FunctionEndpointTrigger(IFunctionEndpoint endpoint) + public FunctionEndpointTrigger(FunctionEndpoint endpoint) {{ this.endpoint = endpoint; }} @@ -133,7 +133,7 @@ public async Task Run( {{ {(syntaxReceiver.enableCrossEntityTransactions ? "await endpoint.ProcessTransactional(message, executionContext, messageReceiver, logger);" - : "await endpoint.Process(message, executionContext, logger);")} + : "await endpoint.ProcessNonTransactional(message, executionContext, messageReceiver, logger);")} }} }}"; context.AddSource("NServiceBus__FunctionEndpointTrigger", SourceText.From(source, Encoding.UTF8)); diff --git a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt index b7783608..d46ca1f2 100644 --- a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt +++ b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt @@ -3,7 +3,11 @@ namespace NServiceBus { public class FunctionEndpoint : NServiceBus.IFunctionEndpoint { + [System.Obsolete("Use `Process(Message, ExecutionContext, IMessageReceiver, ILogger)` instead. Will" + + " be treated as an error from version 2.0.0. Will be removed in version 3.0.0.", false)] public System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } + public System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } + public System.Threading.Tasks.Task ProcessNonTransactional(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task ProcessTransactional(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task Publish(object message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions options, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } @@ -30,8 +34,10 @@ namespace NServiceBus } public interface IFunctionEndpoint { + [System.Obsolete("Use `Process(Message, ExecutionContext, IMessageReceiver, ILogger)` instead. Will" + + " be treated as an error from version 2.0.0. Will be removed in version 3.0.0.", false)] System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); - System.Threading.Tasks.Task ProcessTransactional(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null); + System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null); System.Threading.Tasks.Task Publish(object message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions options, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); System.Threading.Tasks.Task Publish(System.Action messageConstructor, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null); diff --git a/src/ServiceBus.Tests/FunctionEndpointComponent.cs b/src/ServiceBus.Tests/FunctionEndpointComponent.cs index 3aa61dbf..a5b5f21e 100644 --- a/src/ServiceBus.Tests/FunctionEndpointComponent.cs +++ b/src/ServiceBus.Tests/FunctionEndpointComponent.cs @@ -100,7 +100,7 @@ public override async Task ComponentsStarted(CancellationToken token) { var transportMessage = MessageHelper.GenerateMessage(message); var context = new ExecutionContext(); - await endpoint.Process(transportMessage, context); + await endpoint.ProcessNonTransactional(transportMessage, context, null); } } diff --git a/src/ServiceBus.Tests/ReflectionHelperTests.cs b/src/ServiceBus.Tests/ReflectionHelperTests.cs new file mode 100644 index 00000000..bea7620f --- /dev/null +++ b/src/ServiceBus.Tests/ReflectionHelperTests.cs @@ -0,0 +1,148 @@ +namespace ServiceBus.Tests +{ + using System; + using Microsoft.Azure.ServiceBus; + using Microsoft.Azure.WebJobs; + using NServiceBus.AzureFunctions.InProcess.ServiceBus; + using NUnit.Framework; + + [TestFixture] + public class ReflectionHelperTests + { + [Test] + public void When_no_attributes_defined_should_throw() + { + var exception = Assert.Throws(() => ReflectionHelper.GetAutoCompleteValue()); + + StringAssert.Contains($"Could not locate {nameof(ServiceBusTriggerAttribute)} to infer the AutoComplete setting.", exception.Message); + } + + [Test] + public void When_no_function_name_attribute_defined_should_throw() + { + var exception = Assert.Throws(() => FunctionWithNoFunctionNameAttribute(null)); + + StringAssert.Contains($"Could not locate {nameof(ServiceBusTriggerAttribute)} to infer the AutoComplete setting.", exception.Message); + + void FunctionWithNoFunctionNameAttribute( + [ServiceBusTrigger("queueName", "subscriptionname", AutoComplete = true)] Message _) + { + ReflectionHelper.GetAutoCompleteValue(); + } + } + + [Test] + public void When_no_trigger_attribute_defined_should_throw() + { + var exception = Assert.Throws(() => FunctionWithNoServiceBusTriggerAttribute(null)); + + StringAssert.Contains($"Could not locate {nameof(ServiceBusTriggerAttribute)} to infer the AutoComplete setting.", exception.Message); + + [FunctionName("TestFunction")] + void FunctionWithNoServiceBusTriggerAttribute( + Message _) + { + ReflectionHelper.GetAutoCompleteValue(); + } + } + + [Test] + public void When_auto_complete_set_to_false_should_return_false() + { + FunctionTriggerWithAutoCompleteExplicitlySetToFalse(null); + + [FunctionName("TestFunction")] + void FunctionTriggerWithAutoCompleteExplicitlySetToFalse( + [ServiceBusTrigger("queueName", "subscriptionname", AutoComplete = false)] Message _) + { + Assert.IsFalse(ReflectionHelper.GetAutoCompleteValue()); + } + } + + [Test] + public void When_auto_complete_set_to_true_should_return_true() + { + FunctionTriggerWithAutoCompleteExplicitlySetToTrue(null); + + [FunctionName("TestFunction")] + void FunctionTriggerWithAutoCompleteExplicitlySetToTrue( + [ServiceBusTrigger("queueName", "subscriptionname", AutoComplete = true)] Message _) + { + Assert.IsTrue(ReflectionHelper.GetAutoCompleteValue()); + } + } + + [Test] + public void When_auto_complete_not_set_should_return_true() + { + FunctionTriggerWithoutAutoCompleteConfiguration(null); + + [FunctionName("TestFunction")] + void FunctionTriggerWithoutAutoCompleteConfiguration( + [ServiceBusTrigger("queueName", "subscriptionname")] Message _) + { + Assert.True(ReflectionHelper.GetAutoCompleteValue()); + } + } + + [Test] + public void When_helper_invoked_in_nested_methods() + { + NestedTrigger(null); + + [FunctionName("TestFunction")] + void NestedTrigger( + [ServiceBusTrigger("queueName", "subscriptionname", AutoComplete = false)] Message _) + { + One(); + } + + void One() + { + Two(); + } + + void Two() + { + Three(); + } + + void Three() + { + Assert.IsFalse(ReflectionHelper.GetAutoCompleteValue()); + } + } + + [Test] + public void When_helper_invoked_in_local_function() + { + LocalFunction(null); + + [FunctionName("TestFunction")] + void LocalFunction( + [ServiceBusTrigger("queueName", "subscriptionname", AutoComplete = false)] Message _) + { + LocalFunction(); + + void LocalFunction() + { + Assert.IsFalse(ReflectionHelper.GetAutoCompleteValue()); + } + } + } + + [Test] + public void When_helper_invoked_in_expression() + { + Expression(null); + + [FunctionName("TestFunction")] + void Expression( + [ServiceBusTrigger("queueName", "subscriptionname", AutoComplete = false)] Message _) + { + Func expression = () => ReflectionHelper.GetAutoCompleteValue(); + Assert.IsFalse(expression()); + } + } + } +} \ No newline at end of file diff --git a/src/ServiceBus.Tests/ServiceBus.Tests.csproj b/src/ServiceBus.Tests/ServiceBus.Tests.csproj index 22cc3d0f..dfe67d94 100644 --- a/src/ServiceBus.Tests/ServiceBus.Tests.csproj +++ b/src/ServiceBus.Tests/ServiceBus.Tests.csproj @@ -4,6 +4,7 @@ netcoreapp3.1;net5.0 true ..\Test.snk + 9 diff --git a/src/ServiceBus.Tests/When_shipping_handlers_in_dedicated_assembly.cs b/src/ServiceBus.Tests/When_shipping_handlers_in_dedicated_assembly.cs index da2e6ec5..23dd7501 100644 --- a/src/ServiceBus.Tests/When_shipping_handlers_in_dedicated_assembly.cs +++ b/src/ServiceBus.Tests/When_shipping_handlers_in_dedicated_assembly.cs @@ -42,7 +42,7 @@ public async Task Should_load_handlers_from_assembly_when_using_FunctionsHostBui // we need to process an actual message to have the endpoint being created - await endpoint.Process(GenerateMessage(), new ExecutionContext()); + await endpoint.ProcessNonTransactional(GenerateMessage(), new ExecutionContext(), null); // The message handler assembly should be loaded now because scanning should find and load the handler assembly Assert.True(AppDomain.CurrentDomain.GetAssemblies().Any(a => a.FullName == "Testing.Handlers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")); From 0a5895f3a4178ea29fe308d2d8e5c6514495040c Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 9 Aug 2021 09:24:33 +0200 Subject: [PATCH 28/34] Update src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs Co-authored-by: Mike Minutillo --- .../NServiceBusTriggerFunctionAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs index 344b1aec..b00e6c55 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs @@ -1,7 +1,7 @@ namespace NServiceBus { /// - /// Assembly attribute to specify NServiceBus logical endpoint name. + /// Assembly attribute to configure generated NServiceBus Azure Function. /// The attribute is used to wire up an auto-generated Service Bus trigger function, responding to messages in the queue specified by the name provided. /// [System.AttributeUsage(System.AttributeTargets.Assembly)] @@ -31,4 +31,4 @@ public NServiceBusTriggerFunctionAttribute(string name) Name = name; } } -} \ No newline at end of file +} From 64e5453942675589e01cfba12b0b2b496c265a64 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 9 Aug 2021 09:34:45 +0200 Subject: [PATCH 29/34] use explicit interface impl. to hide Process on FunctionEndpoint --- .../FunctionEndpoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs index f390ae12..bcb5321a 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/FunctionEndpoint.cs @@ -33,7 +33,7 @@ internal FunctionEndpoint(IStartableEndpointWithExternallyManagedContainer exter /// /// Processes a message received from an AzureServiceBus trigger using the NServiceBus message pipeline. This method will lookup the setting to determine whether to use transactional or non-transactional processing. /// - public Task Process(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger = null) => + Task IFunctionEndpoint.Process(Message message, ExecutionContext executionContext, IMessageReceiver messageReceiver, ILogger functionsLogger) => ReflectionHelper.GetAutoCompleteValue() ? ProcessNonTransactional(message, executionContext, messageReceiver, functionsLogger) : ProcessTransactional(message, executionContext, messageReceiver, functionsLogger); From 48aa83d7647853247a10b19d2a0587f5d6c489fa Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 9 Aug 2021 09:35:12 +0200 Subject: [PATCH 30/34] inline SafeCompleteAsync --- .../MessageReceiverExtensions.cs | 21 ------------------- .../MessageReceiverTransactionStrategy.cs | 12 +++++++++-- 2 files changed, 10 insertions(+), 23 deletions(-) delete mode 100644 src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs deleted file mode 100644 index 96909af0..00000000 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/MessageReceiverExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace NServiceBus.AzureFunctions.InProcess.ServiceBus -{ - using System.Threading.Tasks; - using System.Transactions; - using Microsoft.Azure.ServiceBus; - using Microsoft.Azure.ServiceBus.Core; - - static class MessageReceiverExtensions - { - public static async Task SafeCompleteAsync(this IMessageReceiver messageReceiver, Message message, CommittableTransaction transaction) - { - // open short-lived TransactionScope connected to the committable transaction to ensure the message operation has a scope to enlist. - using (var scope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) - { - await messageReceiver.CompleteAsync(message.SystemProperties.LockToken) - .ConfigureAwait(false); - scope.Complete(); - } - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverTransactionStrategy.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverTransactionStrategy.cs index 22932a2f..b5beb92d 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverTransactionStrategy.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/Transactions/MessageReceiverTransactionStrategy.cs @@ -2,7 +2,6 @@ { using System.Threading.Tasks; using System.Transactions; - using AzureFunctions.InProcess.ServiceBus; using Microsoft.Azure.ServiceBus; using Microsoft.Azure.ServiceBus.Core; using Transport; @@ -34,6 +33,15 @@ public TransportTransaction CreateTransportTransaction(CommittableTransaction tr return transportTransaction; } - public Task Complete(CommittableTransaction transaction) => messageReceiver.SafeCompleteAsync(message, transaction); + public async Task Complete(CommittableTransaction transaction) + { + // open short-lived TransactionScope connected to the committable transaction to ensure the message operation has a scope to enlist. + using (var scope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) + { + await messageReceiver.CompleteAsync(message.SystemProperties.LockToken) + .ConfigureAwait(false); + scope.Complete(); + } + } } } \ No newline at end of file From 942c089cb3db6ea8b09cc174b6ce2fa3ad4570cd Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 9 Aug 2021 09:39:22 +0200 Subject: [PATCH 31/34] update attribute property name --- .../NServiceBusTriggerFunctionAttribute.cs | 10 +++++----- .../SourceGeneratorApproval2.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs index b00e6c55..21a555fb 100644 --- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs +++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBusTriggerFunctionAttribute.cs @@ -10,7 +10,7 @@ public sealed class NServiceBusTriggerFunctionAttribute : System.Attribute /// /// Endpoint name that is the input queue name. /// - public string Name { get; } + public string EndpointName { get; } /// /// Override trigger function name. @@ -25,10 +25,10 @@ public sealed class NServiceBusTriggerFunctionAttribute : System.Attribute /// /// Endpoint logical name. /// - /// Endpoint name that is the input queue name. - public NServiceBusTriggerFunctionAttribute(string name) + /// Endpoint name that is the input queue name. + public NServiceBusTriggerFunctionAttribute(string endpointName) { - Name = name; + EndpointName = endpointName; } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs index 8410ce63..9217ceae 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator.Tests/SourceGeneratorApproval2.cs @@ -180,7 +180,7 @@ public void Init() // For the unit tests to work, the compilation used by the source generator needs to know that NServiceBusTriggerFunction // is an attribute from NServiceBus namespace and its full name is NServiceBus.NServiceBusTriggerFunctionAttribute. // By referencing NServiceBusTriggerFunctionAttribute here, NServiceBus.AzureFunctions.InProcess.ServiceBus is forced to load and participate in the compilation. - _ = new NServiceBusTriggerFunctionAttribute(name: "test"); + _ = new NServiceBusTriggerFunctionAttribute(endpointName: "test"); } static (string output, ImmutableArray diagnostics) GetGeneratedOutput(string source, bool suppressGeneratedDiagnosticsErrors = false) From f058b333058fe538cf4ef763520d919b3d1bf0af Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 9 Aug 2021 09:40:55 +0200 Subject: [PATCH 32/34] use existing function name --- .../TriggerFunctionGenerator2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs index 47805cb6..b7ff6178 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs @@ -59,7 +59,7 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) // 3rd parameter might be trigger function name triggerFunctionName = attributeParametersCount == 3 ? AttributeParameterAtPosition(2) - : $"NServiceBusFunctionEndpointTrigger-{endpointName}"; + : triggerFunctionName; } else { From df97eed3d8df35d5dc2f5f9981d97da2338e0754 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 9 Aug 2021 09:41:33 +0200 Subject: [PATCH 33/34] approve API changes --- .../ApprovalFiles/APIApprovals.Approve.approved.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt index d46ca1f2..c510a5cb 100644 --- a/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt +++ b/src/ServiceBus.Tests/ApprovalFiles/APIApprovals.Approve.approved.txt @@ -6,7 +6,6 @@ namespace NServiceBus [System.Obsolete("Use `Process(Message, ExecutionContext, IMessageReceiver, ILogger)` instead. Will" + " be treated as an error from version 2.0.0. Will be removed in version 3.0.0.", false)] public System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } - public System.Threading.Tasks.Task Process(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task ProcessNonTransactional(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task ProcessTransactional(Microsoft.Azure.ServiceBus.Message message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Azure.ServiceBus.Core.IMessageReceiver messageReceiver, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } public System.Threading.Tasks.Task Publish(object message, Microsoft.Azure.WebJobs.ExecutionContext executionContext, Microsoft.Extensions.Logging.ILogger functionsLogger = null) { } @@ -64,9 +63,9 @@ namespace NServiceBus [System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.All)] public sealed class NServiceBusTriggerFunctionAttribute : System.Attribute { - public NServiceBusTriggerFunctionAttribute(string name) { } + public NServiceBusTriggerFunctionAttribute(string endpointName) { } public bool EnableCrossEntityTransactions { get; set; } - public string Name { get; } + public string EndpointName { get; } public string TriggerFunctionName { get; set; } } public class ServiceBusTriggeredEndpointConfiguration From cf58c4c82c6133433aac9d7a653a10321d55e878 Mon Sep 17 00:00:00 2001 From: Tim Bussmann Date: Mon, 9 Aug 2021 11:08:38 +0200 Subject: [PATCH 34/34] prevent potential NRE --- .../TriggerFunctionGenerator2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs index b7ff6178..bd1aa068 100644 --- a/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs +++ b/src/NServiceBus.AzureFunctions.SourceGenerator/TriggerFunctionGenerator2.cs @@ -71,7 +71,7 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) } } - bool IsNServiceBusEndpointNameAttribute(string value) => value.Equals("NServiceBus.NServiceBusTriggerFunctionAttribute"); + bool IsNServiceBusEndpointNameAttribute(string value) => value?.Equals("NServiceBus.NServiceBusTriggerFunctionAttribute") ?? false; string AttributeParameterAtPosition(int position) => context.SemanticModel.GetConstantValue(attributeSyntax.ArgumentList.Arguments[position].Expression).ToString(); int AttributeParametersCount() => attributeSyntax.ArgumentList.Arguments.Count; }