From cc5644c04647bc7fedd2396999d27488ac0c22ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 10 Feb 2026 20:07:18 +0100 Subject: [PATCH 01/41] Spike function manifests --- .../FunctionManifest.cs | 3 +++ .../FunctionsRegistry.cs | 13 +++++++++++++ .../GeneratedCode/FunctionsRegistry.g.cs | 10 ++++++++++ 3 files changed, 26 insertions(+) create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs new file mode 100644 index 0000000..d3d7fa0 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs @@ -0,0 +1,3 @@ +namespace NServiceBus; + +record FunctionManifest(string Name, string Queue, string ConnectionName); \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs new file mode 100644 index 0000000..9fe8883 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs @@ -0,0 +1,13 @@ +namespace NServiceBus; + +static partial class FunctionsRegistry +{ + static partial void AddGeneratedFunctions(List entries); + + public static IReadOnlyList GetAll() + { + var list = new List(); + AddGeneratedFunctions(list); + return list; + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs new file mode 100644 index 0000000..652c1d9 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs @@ -0,0 +1,10 @@ +namespace NServiceBus; + +static partial class FunctionsRegistry +{ + static partial void AddGeneratedFunctions(List entries) + { + entries.Add(new("ReceiverEndpoint", "ReceiverEndpoint", "AzureWebJobsServiceBus")); + entries.Add(new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus")); + } +} \ No newline at end of file From d8b8b1fe34f1f3f4cc0eb5c725c183ba6aa9eb4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 08:51:20 +0100 Subject: [PATCH 02/41] Rebase and refactor to get transport from settings --- src/IntegrationTest/Program.cs | 5 +--- ...nctionsHostApplicationBuilderExtensions.cs | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 6524f42..234cc1c 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -33,10 +33,7 @@ builder.AddNServiceBusFunction("AnotherReceiverEndpoint", endpoint => { - endpoint.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default) - { - ConnectionName = "AnotherServiceBusConnection" - }); + endpoint.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); endpoint.EnableInstallers(); endpoint.UsePersistence(); endpoint.UseSerialization(); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index 3e9e22b..d8b91c9 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -2,10 +2,10 @@ namespace NServiceBus; using System; using AzureFunctions.AzureServiceBus; +using Configuration.AdvancedExtensibility; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; -using Settings; using Transport; public static class FunctionsHostApplicationBuilderExtensions @@ -21,14 +21,21 @@ public static void AddNServiceBusFunction( builder.Services.AddAzureClientsCore(); - builder.AddNServiceBusEndpoint(endpointName, configure); - - builder.Services.AddKeyedSingleton(endpointName, (sp, _) => + builder.AddNServiceBusEndpoint(endpointName, c => { - var settings = sp.GetRequiredKeyedService(endpointName); - var transport = settings.Get() as AzureServiceBusServerlessTransport - ?? throw new InvalidOperationException($"Endpoint '{endpointName}' must be configured with an AzureServiceBusServerlessTransport."); - return new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName)); + configure(c); + + if (!c.GetSettings().TryGet(out TransportDefinition transport)) + { + throw new InvalidOperationException("No transport has been defined."); + } + + if (transport is not AzureServiceBusServerlessTransport serverlessTransport) + { + throw new InvalidOperationException($"Endpoint '{endpointName}' must be configured with an {nameof(AzureServiceBusServerlessTransport)}."); + } + + builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(serverlessTransport, sp.GetRequiredKeyedService(endpointName))); }); } } \ No newline at end of file From 06441cc49d278b2353ce527da8aa08af0a4b03a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 09:40:46 +0100 Subject: [PATCH 03/41] Separate code path for send only --- src/IntegrationTest/Program.cs | 3 +- .../AzureServiceBusServerlessTransport.cs | 24 +++++----- ...nctionsHostApplicationBuilderExtensions.cs | 48 ++++++++++++++++--- .../MessageProcessor.cs | 8 ++++ .../PipelineInvokingMessageProcessor.cs | 9 +--- .../SendOnlyMessageProcessor.cs | 16 ------- 6 files changed, 62 insertions(+), 46 deletions(-) delete mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/SendOnlyMessageProcessor.cs diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 234cc1c..8cf65a2 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -12,10 +12,9 @@ builder.Services.AddHostedService(); -builder.AddNServiceBusFunction("SenderEndpoint", endpoint => +builder.AddSendOnlyNServiceBusEndpoint("SenderEndpoint", endpoint => { endpoint.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - endpoint.SendOnly(); endpoint.UseSerialization(); }); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs index 267f13b..c70d71f 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs @@ -19,21 +19,18 @@ public class AzureServiceBusServerlessTransport : TransportDefinition public AzureServiceBusServerlessTransport(TopicTopology topology) : base(TransportTransactionMode.ReceiveOnly, - supportsDelayedDelivery: true, - supportsPublishSubscribe: true, - supportsTTBR: true) + supportsDelayedDelivery: true, + supportsPublishSubscribe: true, + supportsTTBR: true) { - innerTransport = new AzureServiceBusTransport("TransportWillBeInitializedCorrectlyLater", topology) - { - TransportTransactionMode = TransportTransactionMode.ReceiveOnly - }; + innerTransport = new AzureServiceBusTransport("TransportWillBeInitializedCorrectlyLater", topology) { TransportTransactionMode = TransportTransactionMode.ReceiveOnly }; } protected override void ConfigureServicesCore(IServiceCollection services) => innerTransport.ConfigureServices(services); public string ConnectionName { get; set; } = DefaultServiceBusConnectionName; - internal IInternalMessageProcessor MessageProcessor { get; private set; } = null!; + internal PipelineInvokingMessageProcessor? MessageProcessor { get; private set; } public override async Task Initialize( HostSettings hostSettings, @@ -68,15 +65,16 @@ public override async Task Initialize( var isSendOnly = hostSettings.CoreSettings.GetOrDefault(SendOnlyConfigKey); - MessageProcessor = isSendOnly - ? new SendOnlyMessageProcessor() - : (IInternalMessageProcessor)serverlessTransportInfrastructure.Receivers[MainReceiverId]; + if (!isSendOnly) + { + MessageProcessor = (PipelineInvokingMessageProcessor)serverlessTransportInfrastructure.Receivers[MainReceiverId]; + } return serverlessTransportInfrastructure; } public override IReadOnlyCollection GetSupportedTransactionModes() => supportedTransactionModes; - + static AzureServiceBusTransport ConfigureTransportConnection( string connectionName, IConfiguration configuration, @@ -124,7 +122,7 @@ static AzureServiceBusTransport ConfigureTransportConnection( static extern ref TokenCredential GetTokenCredentialRef(AzureServiceBusTransport transport); const string MainReceiverId = "Main"; - const string SendOnlyConfigKey = "Endpoint.SendOnly"; + internal const string SendOnlyConfigKey = "Endpoint.SendOnly"; internal const string DefaultServiceBusConnectionName = "AzureWebJobsServiceBus"; readonly TransportTransactionMode[] supportedTransactionModes = [TransportTransactionMode.ReceiveOnly]; diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index d8b91c9..f4b1e23 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -6,6 +6,7 @@ namespace NServiceBus; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; +using Settings; using Transport; public static class FunctionsHostApplicationBuilderExtensions @@ -25,17 +26,50 @@ public static void AddNServiceBusFunction( { configure(c); - if (!c.GetSettings().TryGet(out TransportDefinition transport)) + var settings = c.GetSettings(); + if (settings.GetOrDefault(AzureServiceBusServerlessTransport.SendOnlyConfigKey)) { - throw new InvalidOperationException("No transport has been defined."); + throw new InvalidOperationException($"Functions can't be send only endpoints, use {nameof(AddSendOnlyNServiceBusEndpoint)}"); } - if (transport is not AzureServiceBusServerlessTransport serverlessTransport) - { - throw new InvalidOperationException($"Endpoint '{endpointName}' must be configured with an {nameof(AzureServiceBusServerlessTransport)}."); - } + var transport = GetTransport(settings); - builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(serverlessTransport, sp.GetRequiredKeyedService(endpointName))); + builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); }); } + + public static void AddSendOnlyNServiceBusEndpoint( + this FunctionsApplicationBuilder builder, + string endpointName, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(endpointName); + ArgumentNullException.ThrowIfNull(configure); + + builder.AddNServiceBusEndpoint(endpointName, c => + { + configure(c); + + c.SendOnly(); + + // Make sure that the correct transport is used + _ = GetTransport(c.GetSettings()); + }); + } + + static AzureServiceBusServerlessTransport GetTransport(SettingsHolder settings) + { + if (!settings.TryGet(out TransportDefinition transport)) + { + throw new InvalidOperationException($"{nameof(AzureServiceBusServerlessTransport)} needs to be configured"); + } + + if (transport is not AzureServiceBusServerlessTransport serverlessTransport) + { + throw new InvalidOperationException($"Endpoint must be configured with an {nameof(AzureServiceBusServerlessTransport)}."); + } + + return serverlessTransport; + } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs index 1e6201b..df3b719 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs @@ -13,6 +13,14 @@ public async Task Process(ServiceBusReceivedMessage message, ServiceBusMessageAc { using var _ = MultiEndpointLoggerFactory.Instance.PushName(endpointStarter.ServiceKey); await endpointStarter.GetOrStart(cancellationToken).ConfigureAwait(false); + + if (transport.MessageProcessor is null) + { + // This should never happen but we need to protect against it anyways + throw new InvalidOperationException( + $"This endpoint cannot process messages because it is configured in send-only mode."); + } + await transport.MessageProcessor.Process(message, messageActions, cancellationToken).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs index ae4f01c..0b41dfb 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs @@ -9,14 +9,7 @@ namespace NServiceBus.AzureFunctions.AzureServiceBus.Serverless.TransportWrapper using NServiceBus.Transport; using NServiceBus.Transport.AzureServiceBus; -interface IInternalMessageProcessor -{ - Task Process(ServiceBusReceivedMessage message, - ServiceBusMessageActions messageActions, - CancellationToken cancellationToken = default); -} - -class PipelineInvokingMessageProcessor(IMessageReceiver baseTransportReceiver) : IMessageReceiver, IInternalMessageProcessor +class PipelineInvokingMessageProcessor(IMessageReceiver baseTransportReceiver) : IMessageReceiver { public Task Initialize(PushRuntimeSettings limitations, OnMessage onMessage, OnError onError, CancellationToken cancellationToken = default) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/SendOnlyMessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/SendOnlyMessageProcessor.cs deleted file mode 100644 index d2f3e83..0000000 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/SendOnlyMessageProcessor.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace NServiceBus.AzureFunctions.AzureServiceBus.Serverless.TransportWrapper; - -using System; -using System.Threading; -using System.Threading.Tasks; -using Azure.Messaging.ServiceBus; -using Microsoft.Azure.Functions.Worker; - -class SendOnlyMessageProcessor : IInternalMessageProcessor -{ - public Task Process(ServiceBusReceivedMessage message, - ServiceBusMessageActions messageActions, - CancellationToken cancellationToken = default) => throw new InvalidOperationException( - $"This endpoint cannot process messages because it is configured in send-only mode. Remove the '{nameof(EndpointConfiguration)}.{nameof(EndpointConfiguration.SendOnly)}' configuration.'" - ); -} \ No newline at end of file From 91d6e01db77aaffbf2675e2d104548e5ee85e2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 09:45:52 +0100 Subject: [PATCH 04/41] Hook up the function manifest again --- ...nctionsHostApplicationBuilderExtensions.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index f4b1e23..fa83388 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -22,18 +22,32 @@ public static void AddNServiceBusFunction( builder.Services.AddAzureClientsCore(); - builder.AddNServiceBusEndpoint(endpointName, c => + builder.AddNServiceBusEndpoint(endpointName, endpointConfiguration => { - configure(c); + configure(endpointConfiguration); - var settings = c.GetSettings(); + var settings = endpointConfiguration.GetSettings(); if (settings.GetOrDefault(AzureServiceBusServerlessTransport.SendOnlyConfigKey)) { throw new InvalidOperationException($"Functions can't be send only endpoints, use {nameof(AddSendOnlyNServiceBusEndpoint)}"); } + var functionManifest = FunctionsRegistry.GetAll().SingleOrDefault(f => f.Name.Equals(endpointName, StringComparison.InvariantCultureIgnoreCase)); + + if (functionManifest is null) + { + throw new InvalidOperationException($"No function with name {endpointName} found"); + } + + if (functionManifest.Name != functionManifest.Queue) + { + endpointConfiguration.OverrideLocalAddress(functionManifest.Queue); + } + var transport = GetTransport(settings); + transport.ConnectionName = functionManifest.ConnectionName; + builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); }); } @@ -47,14 +61,14 @@ public static void AddSendOnlyNServiceBusEndpoint( ArgumentNullException.ThrowIfNull(endpointName); ArgumentNullException.ThrowIfNull(configure); - builder.AddNServiceBusEndpoint(endpointName, c => + builder.AddNServiceBusEndpoint(endpointName, endpointConfiguration => { - configure(c); + configure(endpointConfiguration); - c.SendOnly(); + endpointConfiguration.SendOnly(); // Make sure that the correct transport is used - _ = GetTransport(c.GetSettings()); + _ = GetTransport(endpointConfiguration.GetSettings()); }); } From 06b5ae089ad608f175bc4af9b8a808e2eed1786d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 09:48:55 +0100 Subject: [PATCH 05/41] Demo potential place where we need to set the connection name --- src/IntegrationTest/Program.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 8cf65a2..dfa953f 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -14,7 +14,13 @@ builder.AddSendOnlyNServiceBusEndpoint("SenderEndpoint", endpoint => { - endpoint.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) + { + //send only endpoints might need to set the connection name + ConnectionName = "AzureWebJobsServiceBus" + }; + + endpoint.UseTransport(transport); endpoint.UseSerialization(); }); From d1a16161c31173df8bc9031accecfab2f1720bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 09:50:20 +0100 Subject: [PATCH 06/41] Demo routing --- src/IntegrationTest/HttpSender.cs | 2 +- src/IntegrationTest/Program.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/IntegrationTest/HttpSender.cs b/src/IntegrationTest/HttpSender.cs index 798cdfa..76633cb 100644 --- a/src/IntegrationTest/HttpSender.cs +++ b/src/IntegrationTest/HttpSender.cs @@ -18,7 +18,7 @@ public async Task Run( _ = executionContext; // For now logger.LogInformation("C# HTTP trigger function received a request."); - await session.Send("ReceiverEndpoint", new TriggerMessage()).ConfigureAwait(false); + await session.Send(new TriggerMessage()).ConfigureAwait(false); var r = req.CreateResponse(HttpStatusCode.OK); await r.WriteStringAsync($"{nameof(TriggerMessage)} sent.") diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index dfa953f..4577506 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -20,7 +20,9 @@ ConnectionName = "AzureWebJobsServiceBus" }; - endpoint.UseTransport(transport); + var routing = endpoint.UseTransport(transport); + + routing.RouteToEndpoint(typeof(TriggerMessage), "ReceiverEndpoint"); endpoint.UseSerialization(); }); From f9c4137a384f75b44608873dad5ca96472a6be3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 10:05:00 +0100 Subject: [PATCH 07/41] Validate that all functions have been configured --- src/IntegrationTest/ReceiverEndpoint.cs | 9 ++++---- .../FunctionConfigurationValidator.cs | 21 +++++++++++++++++++ .../FunctionManifest.cs | 5 ++++- ...nctionsHostApplicationBuilderExtensions.cs | 5 +++++ .../FunctionsRegistry.cs | 13 +++++++++--- .../MessageProcessor.cs | 5 ++--- .../TransportWrapper/IMessageProcessor.cs | 3 +-- .../PipelineInvokingMessageProcessor.cs | 4 +--- 8 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs diff --git a/src/IntegrationTest/ReceiverEndpoint.cs b/src/IntegrationTest/ReceiverEndpoint.cs index f89b21f..5bc2543 100644 --- a/src/IntegrationTest/ReceiverEndpoint.cs +++ b/src/IntegrationTest/ReceiverEndpoint.cs @@ -13,9 +13,9 @@ public class ReceiverEndpoint([FromKeyedServices("ReceiverEndpoint")] IMessagePr public Task Receiver( [ServiceBusTrigger("ReceiverEndpoint", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] ServiceBusReceivedMessage message, - ServiceBusMessageActions messageActions, FunctionContext context, CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default) { - return processor.Process(message, messageActions, context, cancellationToken); + return processor.Process(message, cancellationToken); } } @@ -24,9 +24,8 @@ public class AnotherReceiverEndpoint([FromKeyedServices("AnotherReceiverEndpoint [Function("AnotherReceiverEndpoint")] public Task Receiver( [ServiceBusTrigger("AnotherReceiverEndpoint", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] - ServiceBusReceivedMessage message, - ServiceBusMessageActions messageActions, FunctionContext context, CancellationToken cancellationToken = default) + ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) { - return processor.Process(message, messageActions, context, cancellationToken); + return processor.Process(message, cancellationToken); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs new file mode 100644 index 0000000..bc6467f --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs @@ -0,0 +1,21 @@ +namespace NServiceBus; + +using Microsoft.Extensions.Hosting; + +public class FunctionConfigurationValidator : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken = default) + { + var allFunctions = FunctionsRegistry.GetAll(); + + var functionNotConfigured = allFunctions.Where(f => !f.Configured).Select(f => f.Name).ToArray(); + if (functionNotConfigured.Any()) + { + throw new InvalidOperationException($"The following functions have not been configured using {nameof(FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction)}(...): {string.Join(", ", functionNotConfigured)}"); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs index d3d7fa0..78defe1 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs @@ -1,3 +1,6 @@ namespace NServiceBus; -record FunctionManifest(string Name, string Queue, string ConnectionName); \ No newline at end of file +record FunctionManifest(string Name, string Queue, string ConnectionName) +{ + public bool Configured { get; set; } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index fa83388..dfaedc6 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -6,6 +6,7 @@ namespace NServiceBus; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Settings; using Transport; @@ -49,6 +50,10 @@ public static void AddNServiceBusFunction( transport.ConnectionName = functionManifest.ConnectionName; builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); + + functionManifest.Configured = true; + + builder.Services.AddHostedService(); }); } diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs index 9fe8883..819ada6 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs @@ -6,8 +6,15 @@ static partial class FunctionsRegistry public static IReadOnlyList GetAll() { - var list = new List(); - AddGeneratedFunctions(list); - return list; + if (allFunctions is not null) + { + return allFunctions; + } + + allFunctions = []; + AddGeneratedFunctions(allFunctions); + return allFunctions; } + + static List? allFunctions; } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs index df3b719..0516037 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs @@ -8,8 +8,7 @@ namespace NServiceBus.AzureFunctions.AzureServiceBus; public class MessageProcessor(AzureServiceBusServerlessTransport transport, EndpointStarter endpointStarter) : IMessageProcessor { - public async Task Process(ServiceBusReceivedMessage message, ServiceBusMessageActions messageActions, - FunctionContext functionContext, CancellationToken cancellationToken = default) + public async Task Process(ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) { using var _ = MultiEndpointLoggerFactory.Instance.PushName(endpointStarter.ServiceKey); await endpointStarter.GetOrStart(cancellationToken).ConfigureAwait(false); @@ -21,6 +20,6 @@ public async Task Process(ServiceBusReceivedMessage message, ServiceBusMessageAc $"This endpoint cannot process messages because it is configured in send-only mode."); } - await transport.MessageProcessor.Process(message, messageActions, cancellationToken).ConfigureAwait(false); + await transport.MessageProcessor.Process(message, cancellationToken).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs index f8ac6e5..e13c73a 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs @@ -3,8 +3,7 @@ namespace NServiceBus.AzureFunctions.AzureServiceBus; using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; -using Microsoft.Azure.Functions.Worker; public interface IMessageProcessor { - Task Process(ServiceBusReceivedMessage message, ServiceBusMessageActions messageActions, FunctionContext functionContext, CancellationToken cancellationToken = default); + Task Process(ServiceBusReceivedMessage message, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs index 0b41dfb..4c87e16 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs @@ -22,9 +22,7 @@ public Task Initialize(PushRuntimeSettings limitations, OnMessage onMessage, OnE cancellationToken) ?? Task.CompletedTask; } - public async Task Process(ServiceBusReceivedMessage message, - ServiceBusMessageActions messageActions, - CancellationToken cancellationToken = default) + public async Task Process(ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) { var messageId = message.GetMessageId(); var body = message.GetBody(); From 0ca774807a3b1f36e4b1105a5e48417344c77272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 13:03:01 +0100 Subject: [PATCH 08/41] Handle case where user has disabled the generator --- .../AzureServiceBusServerlessTransport.cs | 17 ++++------- ...nctionsHostApplicationBuilderExtensions.cs | 29 ++++++++++--------- .../FunctionsRegistry.cs | 2 ++ 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs index c70d71f..d02f2d8 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs @@ -9,22 +9,15 @@ namespace NServiceBus; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using NServiceBus.AzureFunctions.AzureServiceBus; using NServiceBus.AzureFunctions.AzureServiceBus.Serverless.TransportWrapper; using NServiceBus.Transport; -public class AzureServiceBusServerlessTransport : TransportDefinition +public class AzureServiceBusServerlessTransport(TopicTopology topology) : TransportDefinition(TransportTransactionMode.ReceiveOnly, + supportsDelayedDelivery: true, + supportsPublishSubscribe: true, + supportsTTBR: true) { - readonly AzureServiceBusTransport innerTransport; - - public AzureServiceBusServerlessTransport(TopicTopology topology) - : base(TransportTransactionMode.ReceiveOnly, - supportsDelayedDelivery: true, - supportsPublishSubscribe: true, - supportsTTBR: true) - { - innerTransport = new AzureServiceBusTransport("TransportWillBeInitializedCorrectlyLater", topology) { TransportTransactionMode = TransportTransactionMode.ReceiveOnly }; - } + readonly AzureServiceBusTransport innerTransport = new("TransportWillBeInitializedCorrectlyLater", topology) { TransportTransactionMode = TransportTransactionMode.ReceiveOnly }; protected override void ConfigureServicesCore(IServiceCollection services) => innerTransport.ConfigureServices(services); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index dfaedc6..d0b18cd 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -33,27 +33,30 @@ public static void AddNServiceBusFunction( throw new InvalidOperationException($"Functions can't be send only endpoints, use {nameof(AddSendOnlyNServiceBusEndpoint)}"); } - var functionManifest = FunctionsRegistry.GetAll().SingleOrDefault(f => f.Name.Equals(endpointName, StringComparison.InvariantCultureIgnoreCase)); + var transport = GetTransport(settings); - if (functionManifest is null) + if (FunctionsRegistry.SourceGeneratorEnabled) { - throw new InvalidOperationException($"No function with name {endpointName} found"); - } + var functionManifest = FunctionsRegistry.GetAll().SingleOrDefault(f => f.Name.Equals(endpointName, StringComparison.InvariantCultureIgnoreCase)); - if (functionManifest.Name != functionManifest.Queue) - { - endpointConfiguration.OverrideLocalAddress(functionManifest.Queue); - } + if (functionManifest is null) + { + throw new InvalidOperationException($"No function with name {endpointName} found"); + } - var transport = GetTransport(settings); + if (functionManifest.Name != functionManifest.Queue) + { + endpointConfiguration.OverrideLocalAddress(functionManifest.Queue); + } - transport.ConnectionName = functionManifest.ConnectionName; + transport.ConnectionName = functionManifest.ConnectionName; - builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); + functionManifest.Configured = true; - functionManifest.Configured = true; + builder.Services.AddHostedService(); + } - builder.Services.AddHostedService(); + builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); }); } diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs index 819ada6..7889b43 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs @@ -4,6 +4,8 @@ static partial class FunctionsRegistry { static partial void AddGeneratedFunctions(List entries); + public static bool SourceGeneratorEnabled { get; private set; } + public static IReadOnlyList GetAll() { if (allFunctions is not null) From cd1fe8e085e9c6085c4697de66510808b3401ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 13:03:27 +0100 Subject: [PATCH 09/41] Fixup --- .../GeneratedCode/FunctionsRegistry.g.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs index 652c1d9..6692167 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs @@ -6,5 +6,7 @@ static partial void AddGeneratedFunctions(List entries) { entries.Add(new("ReceiverEndpoint", "ReceiverEndpoint", "AzureWebJobsServiceBus")); entries.Add(new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus")); + + SourceGeneratorEnabled = true; } } \ No newline at end of file From de0ed25554ca9a30f2a756c571187a46368f7497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 11 Feb 2026 13:31:08 +0100 Subject: [PATCH 10/41] Cleanup --- .../AzureServiceBusServerlessTransport.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs index d02f2d8..5227916 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs @@ -17,8 +17,6 @@ public class AzureServiceBusServerlessTransport(TopicTopology topology) : Transp supportsPublishSubscribe: true, supportsTTBR: true) { - readonly AzureServiceBusTransport innerTransport = new("TransportWillBeInitializedCorrectlyLater", topology) { TransportTransactionMode = TransportTransactionMode.ReceiveOnly }; - protected override void ConfigureServicesCore(IServiceCollection services) => innerTransport.ConfigureServices(services); public string ConnectionName { get; set; } = DefaultServiceBusConnectionName; @@ -66,7 +64,7 @@ public override async Task Initialize( return serverlessTransportInfrastructure; } - public override IReadOnlyCollection GetSupportedTransactionModes() => supportedTransactionModes; + public override IReadOnlyCollection GetSupportedTransactionModes() => [TransportTransactionMode.ReceiveOnly]; static AzureServiceBusTransport ConfigureTransportConnection( string connectionName, @@ -114,9 +112,9 @@ static AzureServiceBusTransport ConfigureTransportConnection( [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] static extern ref TokenCredential GetTokenCredentialRef(AzureServiceBusTransport transport); + readonly AzureServiceBusTransport innerTransport = new("TransportWillBeInitializedCorrectlyLater", topology) { TransportTransactionMode = TransportTransactionMode.ReceiveOnly }; + const string MainReceiverId = "Main"; internal const string SendOnlyConfigKey = "Endpoint.SendOnly"; internal const string DefaultServiceBusConnectionName = "AzureWebJobsServiceBus"; - - readonly TransportTransactionMode[] supportedTransactionModes = [TransportTransactionMode.ReceiveOnly]; } \ No newline at end of file From a04e6eefd623c9492f1dbfd56ac0825aa2bc7e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 12 Feb 2026 10:12:28 +0100 Subject: [PATCH 11/41] Start adding alternative options for configuring functions --- .../GeneratedCode/NServiceBusEndpoints.cs | 7 ++++ src/IntegrationTest/Program.cs | 22 ++++++++----- .../FunctionConfigurationValidator.cs | 15 +++++---- .../FunctionManifest.cs | 2 +- ...nctionsHostApplicationBuilderExtensions.cs | 33 +++++++++---------- .../FunctionsRegistry.cs | 22 ------------- .../GeneratedCode/FunctionsRegistry.g.cs | 12 ------- 7 files changed, 45 insertions(+), 68 deletions(-) create mode 100644 src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs delete mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs delete mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs b/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs new file mode 100644 index 0000000..71ced2e --- /dev/null +++ b/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs @@ -0,0 +1,7 @@ +namespace NServiceBus; + +public static class NServiceBusEndpoints +{ + public static FunctionManifest ReceiverEndpoint = new("ReceiverEndpoint", "ReceiverEndpoint", "AzureWebJobsServiceBus"); + public static FunctionManifest AnotherEndpoint = new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus"); +} \ No newline at end of file diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 4577506..a57256f 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -26,9 +26,14 @@ endpoint.UseSerialization(); }); +//option 1: No source gen on the configuration side, name, queue and connection name needs to match the function definition builder.AddNServiceBusFunction("ReceiverEndpoint", endpoint => { - endpoint.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) { ConnectionName = "AzureWebJobsServiceBus" }; + + // if they differ this needs to be done + endpoint.OverrideLocalAddress("ReceiverEndpoint"); + endpoint.UseTransport(transport); endpoint.EnableInstallers(); endpoint.UsePersistence(); endpoint.UseSerialization(); @@ -38,16 +43,17 @@ endpoint.AddHandler(); }); -builder.AddNServiceBusFunction("AnotherReceiverEndpoint", endpoint => +//option 2: Pass in a manifest that we have source genned +builder.AddNServiceBusFunction(NServiceBusEndpoints.AnotherEndpoint, configuration => { - endpoint.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - endpoint.EnableInstallers(); - endpoint.UsePersistence(); - endpoint.UseSerialization(); + configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + configuration.EnableInstallers(); + configuration.UsePersistence(); + configuration.UseSerialization(); - endpoint.AddHandler(); + configuration.AddHandler(); }); var host = builder.Build(); -await host.RunAsync().ConfigureAwait(false); +await host.RunAsync().ConfigureAwait(false); \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs index bc6467f..0335138 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs @@ -6,13 +6,14 @@ public class FunctionConfigurationValidator : IHostedService { public Task StartAsync(CancellationToken cancellationToken = default) { - var allFunctions = FunctionsRegistry.GetAll(); - - var functionNotConfigured = allFunctions.Where(f => !f.Configured).Select(f => f.Name).ToArray(); - if (functionNotConfigured.Any()) - { - throw new InvalidOperationException($"The following functions have not been configured using {nameof(FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction)}(...): {string.Join(", ", functionNotConfigured)}"); - } + //TODO: See if we can do this in some other way +// var allFunctions = FunctionsRegistry.GetAll(); +// +// var functionNotConfigured = allFunctions.Where(f => !f.Configured).Select(f => f.Name).ToArray(); +// if (functionNotConfigured.Any()) +// { +// throw new InvalidOperationException($"The following functions have not been configured using {nameof(FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction)}(...): {string.Join(", ", functionNotConfigured)}"); +// } return Task.CompletedTask; } diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs index 78defe1..6da70c0 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs @@ -1,6 +1,6 @@ namespace NServiceBus; -record FunctionManifest(string Name, string Queue, string ConnectionName) +public record FunctionManifest(string Name, string Queue, string ConnectionName) { public bool Configured { get; set; } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index d0b18cd..6945420 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -6,7 +6,6 @@ namespace NServiceBus; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Settings; using Transport; @@ -15,14 +14,21 @@ public static class FunctionsHostApplicationBuilderExtensions public static void AddNServiceBusFunction( this FunctionsApplicationBuilder builder, string endpointName, + Action configure) => + builder.AddNServiceBusFunction(new FunctionManifest(endpointName, endpointName, AzureServiceBusServerlessTransport.DefaultServiceBusConnectionName), configure); + + public static void AddNServiceBusFunction( + this FunctionsApplicationBuilder builder, + FunctionManifest functionManifest, Action configure) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(endpointName); + ArgumentNullException.ThrowIfNull(functionManifest); ArgumentNullException.ThrowIfNull(configure); builder.Services.AddAzureClientsCore(); + var endpointName = functionManifest.Name; builder.AddNServiceBusEndpoint(endpointName, endpointConfiguration => { configure(endpointConfiguration); @@ -35,26 +41,17 @@ public static void AddNServiceBusFunction( var transport = GetTransport(settings); - if (FunctionsRegistry.SourceGeneratorEnabled) - { - var functionManifest = FunctionsRegistry.GetAll().SingleOrDefault(f => f.Name.Equals(endpointName, StringComparison.InvariantCultureIgnoreCase)); - if (functionManifest is null) - { - throw new InvalidOperationException($"No function with name {endpointName} found"); - } - - if (functionManifest.Name != functionManifest.Queue) - { - endpointConfiguration.OverrideLocalAddress(functionManifest.Queue); - } + if (functionManifest.Name != functionManifest.Queue) + { + endpointConfiguration.OverrideLocalAddress(functionManifest.Queue); + } - transport.ConnectionName = functionManifest.ConnectionName; + transport.ConnectionName = functionManifest.ConnectionName; - functionManifest.Configured = true; + functionManifest.Configured = true; - builder.Services.AddHostedService(); - } + builder.Services.AddHostedService(); builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); }); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs deleted file mode 100644 index 7889b43..0000000 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsRegistry.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace NServiceBus; - -static partial class FunctionsRegistry -{ - static partial void AddGeneratedFunctions(List entries); - - public static bool SourceGeneratorEnabled { get; private set; } - - public static IReadOnlyList GetAll() - { - if (allFunctions is not null) - { - return allFunctions; - } - - allFunctions = []; - AddGeneratedFunctions(allFunctions); - return allFunctions; - } - - static List? allFunctions; -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs deleted file mode 100644 index 6692167..0000000 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/GeneratedCode/FunctionsRegistry.g.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace NServiceBus; - -static partial class FunctionsRegistry -{ - static partial void AddGeneratedFunctions(List entries) - { - entries.Add(new("ReceiverEndpoint", "ReceiverEndpoint", "AzureWebJobsServiceBus")); - entries.Add(new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus")); - - SourceGeneratorEnabled = true; - } -} \ No newline at end of file From 2bd1d467688944a6fa752af5bbbe79a2ff86ae35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 12 Feb 2026 10:24:57 +0100 Subject: [PATCH 12/41] Add option with generics --- .../GeneratedCode/NServiceBusEndpoints.cs | 5 ++++- src/IntegrationTest/Program.cs | 22 ++++++++++++++++--- src/IntegrationTest/ReceiverEndpoint.cs | 11 ++++++++++ ...nctionsHostApplicationBuilderExtensions.cs | 8 +++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs b/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs index 71ced2e..8f92e83 100644 --- a/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs +++ b/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs @@ -4,4 +4,7 @@ public static class NServiceBusEndpoints { public static FunctionManifest ReceiverEndpoint = new("ReceiverEndpoint", "ReceiverEndpoint", "AzureWebJobsServiceBus"); public static FunctionManifest AnotherEndpoint = new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus"); -} \ No newline at end of file +} + + +public record AnotherEndpoint2() : FunctionManifest("AnotherReceiverEndpoint2", "AnotherReceiverEndpoint2", "AzureWebJobsServiceBus"); \ No newline at end of file diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index a57256f..84f9166 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -12,6 +12,7 @@ builder.Services.AddHostedService(); +// Send-only using a separate API since they are not "functions" builder.AddSendOnlyNServiceBusEndpoint("SenderEndpoint", endpoint => { var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) @@ -26,10 +27,14 @@ endpoint.UseSerialization(); }); -//option 1: No source gen on the configuration side, name, queue and connection name needs to match the function definition +//option 1: No source gen on the configuration side, user needs to use correct name, queue and connection name as the function definition builder.AddNServiceBusFunction("ReceiverEndpoint", endpoint => { - var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) { ConnectionName = "AzureWebJobsServiceBus" }; + var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) + { + //this needs to match + ConnectionName = "AzureWebJobsServiceBus" + }; // if they differ this needs to be done endpoint.OverrideLocalAddress("ReceiverEndpoint"); @@ -54,6 +59,17 @@ configuration.AddHandler(); }); +//option 3: Use a type that we have source genned +builder.AddNServiceBusFunction(configuration => +{ + configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + configuration.EnableInstallers(); + configuration.UsePersistence(); + configuration.UseSerialization(); + + configuration.AddHandler(); +}); + var host = builder.Build(); -await host.RunAsync().ConfigureAwait(false); \ No newline at end of file +await host.RunAsync().ConfigureAwait(false); diff --git a/src/IntegrationTest/ReceiverEndpoint.cs b/src/IntegrationTest/ReceiverEndpoint.cs index 5bc2543..354423f 100644 --- a/src/IntegrationTest/ReceiverEndpoint.cs +++ b/src/IntegrationTest/ReceiverEndpoint.cs @@ -28,4 +28,15 @@ public Task Receiver( { return processor.Process(message, cancellationToken); } +} + +public class AnotherReceiverEndpoint2([FromKeyedServices("AnotherReceiverEndpoint2")] IMessageProcessor processor) +{ + [Function("AnotherReceiverEndpoint2")] + public Task Receiver( + [ServiceBusTrigger("AnotherReceiverEndpoint2", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) + { + return processor.Process(message, cancellationToken); + } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index 6945420..b0ca8ee 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -11,6 +11,14 @@ namespace NServiceBus; public static class FunctionsHostApplicationBuilderExtensions { + public static void AddNServiceBusFunction( + this FunctionsApplicationBuilder builder, + Action configure) where TFunctionManifest : FunctionManifest, new() + { + var manifest = new TFunctionManifest(); + builder.AddNServiceBusFunction(manifest, configure); + } + public static void AddNServiceBusFunction( this FunctionsApplicationBuilder builder, string endpointName, From 4587313842467646f11a3baa7f43fcef90ed179d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 12 Feb 2026 10:34:38 +0100 Subject: [PATCH 13/41] Add more options --- .../GeneratedCode/NServiceBusEndpoints.cs | 16 ++++++++++++++-- src/IntegrationTest/Program.cs | 15 +++++++++++++-- src/IntegrationTest/ReceiverEndpoint.cs | 17 ++++++++++++++--- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs b/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs index 8f92e83..dae5d6c 100644 --- a/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs +++ b/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs @@ -1,10 +1,22 @@ namespace NServiceBus; +using Microsoft.Azure.Functions.Worker.Builder; + public static class NServiceBusEndpoints { public static FunctionManifest ReceiverEndpoint = new("ReceiverEndpoint", "ReceiverEndpoint", "AzureWebJobsServiceBus"); - public static FunctionManifest AnotherEndpoint = new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus"); + public static FunctionManifest AnotherReceiverEndpoint = new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus"); } -public record AnotherEndpoint2() : FunctionManifest("AnotherReceiverEndpoint2", "AnotherReceiverEndpoint2", "AzureWebJobsServiceBus"); \ No newline at end of file +public record AnotherReceiverEndpoint2() : FunctionManifest("AnotherReceiverEndpoint2", "AnotherReceiverEndpoint2", "AzureWebJobsServiceBus"); + +public record AnotherReceiverEndpoint3() : FunctionManifest("AnotherReceiverEndpoint3", "AnotherReceiverEndpoint3", "AzureWebJobsServiceBus"); + +public static class FunctionsHostApplicationBuilderExtensions +{ + public static void AddAnotherEndpoint3NServiceBusFunction( + this FunctionsApplicationBuilder builder, + Action configure) => + builder.AddNServiceBusFunction(configure); +} \ No newline at end of file diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 84f9166..fc9150b 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -49,7 +49,7 @@ }); //option 2: Pass in a manifest that we have source genned -builder.AddNServiceBusFunction(NServiceBusEndpoints.AnotherEndpoint, configuration => +builder.AddNServiceBusFunction(NServiceBusEndpoints.AnotherReceiverEndpoint, configuration => { configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); configuration.EnableInstallers(); @@ -60,7 +60,18 @@ }); //option 3: Use a type that we have source genned -builder.AddNServiceBusFunction(configuration => +builder.AddNServiceBusFunction(configuration => +{ + configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + configuration.EnableInstallers(); + configuration.UsePersistence(); + configuration.UseSerialization(); + + configuration.AddHandler(); +}); + +//option 4: Use source genned method +builder.AddAnotherEndpoint3NServiceBusFunction(configuration => { configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); configuration.EnableInstallers(); diff --git a/src/IntegrationTest/ReceiverEndpoint.cs b/src/IntegrationTest/ReceiverEndpoint.cs index 354423f..e9e504b 100644 --- a/src/IntegrationTest/ReceiverEndpoint.cs +++ b/src/IntegrationTest/ReceiverEndpoint.cs @@ -7,7 +7,7 @@ namespace IntegrationTest; using Microsoft.Extensions.DependencyInjection; using NServiceBus.AzureFunctions.AzureServiceBus; -public class ReceiverEndpoint([FromKeyedServices("ReceiverEndpoint")] IMessageProcessor processor) +public class ReceiverEndpointFunction([FromKeyedServices("ReceiverEndpoint")] IMessageProcessor processor) { [Function("ReceiverEndpoint")] public Task Receiver( @@ -19,7 +19,7 @@ public Task Receiver( } } -public class AnotherReceiverEndpoint([FromKeyedServices("AnotherReceiverEndpoint")] IMessageProcessor processor) +public class AnotherReceiverEndpointFunction([FromKeyedServices("AnotherReceiverEndpoint")] IMessageProcessor processor) { [Function("AnotherReceiverEndpoint")] public Task Receiver( @@ -30,7 +30,7 @@ public Task Receiver( } } -public class AnotherReceiverEndpoint2([FromKeyedServices("AnotherReceiverEndpoint2")] IMessageProcessor processor) +public class AnotherReceiverEndpoint2Function([FromKeyedServices("AnotherReceiverEndpoint2")] IMessageProcessor processor) { [Function("AnotherReceiverEndpoint2")] public Task Receiver( @@ -39,4 +39,15 @@ public Task Receiver( { return processor.Process(message, cancellationToken); } +} + +public class AnotherReceiverEndpoint3Function([FromKeyedServices("AnotherReceiverEndpoint3")] IMessageProcessor processor) +{ + [Function("AnotherReceiverEndpoint3")] + public Task Receiver( + [ServiceBusTrigger("AnotherReceiverEndpoint3", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) + { + return processor.Process(message, cancellationToken); + } } \ No newline at end of file From f3252bbf60ced02175f380048438078a5c6fe136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 12 Feb 2026 10:40:55 +0100 Subject: [PATCH 14/41] Add crazy idea --- src/IntegrationTest/ReceiverEndpoint.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/IntegrationTest/ReceiverEndpoint.cs b/src/IntegrationTest/ReceiverEndpoint.cs index e9e504b..b1a2b98 100644 --- a/src/IntegrationTest/ReceiverEndpoint.cs +++ b/src/IntegrationTest/ReceiverEndpoint.cs @@ -41,6 +41,7 @@ public Task Receiver( } } +//IDEA: Can we somehow use these as both the runtime hook and the manifest? Ie so that users can do; builder.AddNServiceBusFunction(c=>...) public class AnotherReceiverEndpoint3Function([FromKeyedServices("AnotherReceiverEndpoint3")] IMessageProcessor processor) { [Function("AnotherReceiverEndpoint3")] From 0954be75455fb0918e99a52afaeba4525c2136fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 12 Feb 2026 13:41:57 +0100 Subject: [PATCH 15/41] Add note about what happens when users forget to register functions --- src/IntegrationTest/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index fc9150b..e357d57 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -27,6 +27,9 @@ endpoint.UseSerialization(); }); + +//NOTE: forgetting to register a function leads to "Exception: Unable to resolve service for type 'NServiceBus.AzureFunctions.AzureServiceBus.IMessageProcessor' while attempting to activate 'IntegrationTest.AnotherReceiverEndpoint3Function'." + //option 1: No source gen on the configuration side, user needs to use correct name, queue and connection name as the function definition builder.AddNServiceBusFunction("ReceiverEndpoint", endpoint => { From 4ce8eb569f2df09440142d64b3fb079eb057ec3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 12 Feb 2026 13:46:39 +0100 Subject: [PATCH 16/41] Show service locator --- src/IntegrationTest/ReceiverEndpoint.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/IntegrationTest/ReceiverEndpoint.cs b/src/IntegrationTest/ReceiverEndpoint.cs index b1a2b98..6614228 100644 --- a/src/IntegrationTest/ReceiverEndpoint.cs +++ b/src/IntegrationTest/ReceiverEndpoint.cs @@ -42,13 +42,22 @@ public Task Receiver( } //IDEA: Can we somehow use these as both the runtime hook and the manifest? Ie so that users can do; builder.AddNServiceBusFunction(c=>...) -public class AnotherReceiverEndpoint3Function([FromKeyedServices("AnotherReceiverEndpoint3")] IMessageProcessor processor) +public class AnotherReceiverEndpoint3Function { [Function("AnotherReceiverEndpoint3")] public Task Receiver( [ServiceBusTrigger("AnotherReceiverEndpoint3", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] - ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) + ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default) { + //demo using service locator to avoid having to add a ctor + var processor = functionContext.InstanceServices.GetKeyedService("AnotherReceiverEndpoint3"); + + if (processor is null) + { + //which also allows us to throw a better exception + throw new InvalidOperationException("AnotherReceiverEndpoint3 is not configured, please add AddBlahBla to your program.cs"); + } + return processor.Process(message, cancellationToken); } } \ No newline at end of file From 315bbd77db74fef4742081db9313cd2ec0cca290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 12 Feb 2026 14:02:36 +0100 Subject: [PATCH 17/41] Bad idea, won't work since functions are per method not per class --- src/IntegrationTest/ReceiverEndpoint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/IntegrationTest/ReceiverEndpoint.cs b/src/IntegrationTest/ReceiverEndpoint.cs index 6614228..06f6b6b 100644 --- a/src/IntegrationTest/ReceiverEndpoint.cs +++ b/src/IntegrationTest/ReceiverEndpoint.cs @@ -41,7 +41,6 @@ public Task Receiver( } } -//IDEA: Can we somehow use these as both the runtime hook and the manifest? Ie so that users can do; builder.AddNServiceBusFunction(c=>...) public class AnotherReceiverEndpoint3Function { [Function("AnotherReceiverEndpoint3")] From cd71585aa917a1c59e575955bf0f78663f9e9ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Fri, 13 Feb 2026 09:47:23 +0100 Subject: [PATCH 18/41] Use approach with config colocated with the function --- src/IntegrationTest/BillingFunctions.cs | 55 +++++++++++++++ .../GeneratedCode/BillingFunctions.g.cs | 46 +++++++++++++ .../GeneratedCode/NServiceBusEndpoints.cs | 22 ------ .../NServiceBusFunctionConfiguration.g.cs | 36 ++++++++++ src/IntegrationTest/Program.cs | 67 +------------------ src/IntegrationTest/ReceiverEndpoint.cs | 62 ----------------- .../FunctionManifest.cs | 5 +- ...nctionsHostApplicationBuilderExtensions.cs | 24 +------ .../IEndpointConfiguration.cs | 6 ++ .../NServiceBusFunctionAttribute.cs | 6 ++ 10 files changed, 155 insertions(+), 174 deletions(-) create mode 100644 src/IntegrationTest/BillingFunctions.cs create mode 100644 src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs delete mode 100644 src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs create mode 100644 src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs delete mode 100644 src/IntegrationTest/ReceiverEndpoint.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs diff --git a/src/IntegrationTest/BillingFunctions.cs b/src/IntegrationTest/BillingFunctions.cs new file mode 100644 index 0000000..bf17e81 --- /dev/null +++ b/src/IntegrationTest/BillingFunctions.cs @@ -0,0 +1,55 @@ +namespace IntegrationTest; + +using Azure.Messaging.ServiceBus; +using Microsoft.Azure.Functions.Worker; + +public partial class BillingFunctions +{ + [Function("billing-api")] + [NServiceBusFunction(typeof(ApiConfig))] + public partial Task Api( + [ServiceBusTrigger("billing-api", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default); + + public class ApiConfig : IEndpointConfiguration + { + public void Configure(EndpointConfiguration configuration) + { + CommonConfig.Apply(configuration); + + configuration.AddHandler(); + configuration.AddHandler(); + } + } + + [Function("billing-backend")] + [NServiceBusFunction(typeof(BackendConfig))] + public partial Task Backend( + [ServiceBusTrigger("billing-backend", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default); + + public class BackendConfig : IEndpointConfiguration + { + public void Configure(EndpointConfiguration configuration) + { + CommonConfig.Apply(configuration); + + configuration.AddHandler(); + configuration.AddHandler(); + } + } + public class CommonConfig + { + public static void Apply(EndpointConfiguration configuration) + { + configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + configuration.EnableInstallers(); + configuration.UsePersistence(); + configuration.UseSerialization(); + } + } +} \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs b/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs new file mode 100644 index 0000000..e4768e5 --- /dev/null +++ b/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs @@ -0,0 +1,46 @@ +namespace IntegrationTest; + +using Azure.Messaging.ServiceBus; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.AzureFunctions.AzureServiceBus; + +public partial class BillingFunctions +{ + public partial Task Api( + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default) + { + //demo using service locator to avoid having to add a ctor + var processor = functionContext.InstanceServices.GetKeyedService(ApiEndpointName); + + if (processor is null) + { + //which also allows us to throw a better exception + throw new InvalidOperationException($"{ApiEndpointName} has not been registered."); + } + + return processor.Process(message, cancellationToken); + } + + public partial Task Backend( + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default) + { + //demo using service locator to avoid having to add a ctor + var processor = functionContext.InstanceServices.GetKeyedService(BackendEndpointName); + + if (processor is null) + { + //which also allows us to throw a better exception + throw new InvalidOperationException($"{BackendEndpointName} has not been registered."); + } + + return processor.Process(message, cancellationToken); + } + + const string ApiEndpointName = "billing-api"; + const string BackendEndpointName = "billing-backend"; +} \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs b/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs deleted file mode 100644 index dae5d6c..0000000 --- a/src/IntegrationTest/GeneratedCode/NServiceBusEndpoints.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace NServiceBus; - -using Microsoft.Azure.Functions.Worker.Builder; - -public static class NServiceBusEndpoints -{ - public static FunctionManifest ReceiverEndpoint = new("ReceiverEndpoint", "ReceiverEndpoint", "AzureWebJobsServiceBus"); - public static FunctionManifest AnotherReceiverEndpoint = new("AnotherReceiverEndpoint", "AnotherReceiverEndpoint", "AzureWebJobsServiceBus"); -} - - -public record AnotherReceiverEndpoint2() : FunctionManifest("AnotherReceiverEndpoint2", "AnotherReceiverEndpoint2", "AzureWebJobsServiceBus"); - -public record AnotherReceiverEndpoint3() : FunctionManifest("AnotherReceiverEndpoint3", "AnotherReceiverEndpoint3", "AzureWebJobsServiceBus"); - -public static class FunctionsHostApplicationBuilderExtensions -{ - public static void AddAnotherEndpoint3NServiceBusFunction( - this FunctionsApplicationBuilder builder, - Action configure) => - builder.AddNServiceBusFunction(configure); -} \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs new file mode 100644 index 0000000..34ec0dd --- /dev/null +++ b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs @@ -0,0 +1,36 @@ +namespace NServiceBus; + +using System; +using AzureFunctions.AzureServiceBus; +using Configuration.AdvancedExtensibility; +using IntegrationTest; +using Logging; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.DependencyInjection; +using MultiHosting; +using Settings; +using Transport; + +public static class NServiceBusFunctionsHostApplicationBuilderExtensions +{ + public static void AddNServiceBusFunctions( + this FunctionsApplicationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + LogManager.UseFactory(MultiEndpointLoggerFactory.Instance); + + builder.Services.AddHostedService(); + builder.Services.AddAzureClientsCore(); + + foreach (var manifest in AllFunctions) + { + builder.AddNServiceBusFunction(manifest, manifest.EndpointConfiguration); + } + } + + static List AllFunctions = [new BillingApi(), new BillingBackend()]; + + record BillingApi() : FunctionManifest("billing-api", "billing-api", "AzureWebJobsServiceBus", new BillingFunctions.ApiConfig()); + record BillingBackend() : FunctionManifest("billing-backend", "billing-backend", "AzureWebJobsServiceBus", new BillingFunctions.BackendConfig()); +} \ No newline at end of file diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index e357d57..600a24b 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -1,16 +1,10 @@ using IntegrationTest; using Microsoft.Azure.Functions.Worker.Builder; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using NServiceBus.Logging; -using NServiceBus.MultiHosting; var builder = FunctionsApplication.CreateBuilder(args); -// as early as possible -LogManager.UseFactory(MultiEndpointLoggerFactory.Instance); - -builder.Services.AddHostedService(); +builder.AddNServiceBusFunctions(); // Send-only using a separate API since they are not "functions" builder.AddSendOnlyNServiceBusEndpoint("SenderEndpoint", endpoint => @@ -23,67 +17,10 @@ var routing = endpoint.UseTransport(transport); - routing.RouteToEndpoint(typeof(TriggerMessage), "ReceiverEndpoint"); + routing.RouteToEndpoint(typeof(TriggerMessage), "billing-api"); endpoint.UseSerialization(); }); - -//NOTE: forgetting to register a function leads to "Exception: Unable to resolve service for type 'NServiceBus.AzureFunctions.AzureServiceBus.IMessageProcessor' while attempting to activate 'IntegrationTest.AnotherReceiverEndpoint3Function'." - -//option 1: No source gen on the configuration side, user needs to use correct name, queue and connection name as the function definition -builder.AddNServiceBusFunction("ReceiverEndpoint", endpoint => -{ - var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) - { - //this needs to match - ConnectionName = "AzureWebJobsServiceBus" - }; - - // if they differ this needs to be done - endpoint.OverrideLocalAddress("ReceiverEndpoint"); - endpoint.UseTransport(transport); - endpoint.EnableInstallers(); - endpoint.UsePersistence(); - endpoint.UseSerialization(); - - endpoint.AddHandler(); - endpoint.AddHandler(); - endpoint.AddHandler(); -}); - -//option 2: Pass in a manifest that we have source genned -builder.AddNServiceBusFunction(NServiceBusEndpoints.AnotherReceiverEndpoint, configuration => -{ - configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - configuration.EnableInstallers(); - configuration.UsePersistence(); - configuration.UseSerialization(); - - configuration.AddHandler(); -}); - -//option 3: Use a type that we have source genned -builder.AddNServiceBusFunction(configuration => -{ - configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - configuration.EnableInstallers(); - configuration.UsePersistence(); - configuration.UseSerialization(); - - configuration.AddHandler(); -}); - -//option 4: Use source genned method -builder.AddAnotherEndpoint3NServiceBusFunction(configuration => -{ - configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - configuration.EnableInstallers(); - configuration.UsePersistence(); - configuration.UseSerialization(); - - configuration.AddHandler(); -}); - var host = builder.Build(); await host.RunAsync().ConfigureAwait(false); diff --git a/src/IntegrationTest/ReceiverEndpoint.cs b/src/IntegrationTest/ReceiverEndpoint.cs deleted file mode 100644 index 06f6b6b..0000000 --- a/src/IntegrationTest/ReceiverEndpoint.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace IntegrationTest; - -using System.Threading; -using System.Threading.Tasks; -using Azure.Messaging.ServiceBus; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.DependencyInjection; -using NServiceBus.AzureFunctions.AzureServiceBus; - -public class ReceiverEndpointFunction([FromKeyedServices("ReceiverEndpoint")] IMessageProcessor processor) -{ - [Function("ReceiverEndpoint")] - public Task Receiver( - [ServiceBusTrigger("ReceiverEndpoint", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] - ServiceBusReceivedMessage message, - CancellationToken cancellationToken = default) - { - return processor.Process(message, cancellationToken); - } -} - -public class AnotherReceiverEndpointFunction([FromKeyedServices("AnotherReceiverEndpoint")] IMessageProcessor processor) -{ - [Function("AnotherReceiverEndpoint")] - public Task Receiver( - [ServiceBusTrigger("AnotherReceiverEndpoint", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] - ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) - { - return processor.Process(message, cancellationToken); - } -} - -public class AnotherReceiverEndpoint2Function([FromKeyedServices("AnotherReceiverEndpoint2")] IMessageProcessor processor) -{ - [Function("AnotherReceiverEndpoint2")] - public Task Receiver( - [ServiceBusTrigger("AnotherReceiverEndpoint2", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] - ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) - { - return processor.Process(message, cancellationToken); - } -} - -public class AnotherReceiverEndpoint3Function -{ - [Function("AnotherReceiverEndpoint3")] - public Task Receiver( - [ServiceBusTrigger("AnotherReceiverEndpoint3", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] - ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default) - { - //demo using service locator to avoid having to add a ctor - var processor = functionContext.InstanceServices.GetKeyedService("AnotherReceiverEndpoint3"); - - if (processor is null) - { - //which also allows us to throw a better exception - throw new InvalidOperationException("AnotherReceiverEndpoint3 is not configured, please add AddBlahBla to your program.cs"); - } - - return processor.Process(message, cancellationToken); - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs index 6da70c0..4bca156 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs @@ -1,6 +1,3 @@ namespace NServiceBus; -public record FunctionManifest(string Name, string Queue, string ConnectionName) -{ - public bool Configured { get; set; } -} \ No newline at end of file +public record FunctionManifest(string Name, string Queue, string ConnectionName, IEndpointConfiguration EndpointConfiguration); \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index b0ca8ee..a8982c1 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -11,35 +11,21 @@ namespace NServiceBus; public static class FunctionsHostApplicationBuilderExtensions { - public static void AddNServiceBusFunction( - this FunctionsApplicationBuilder builder, - Action configure) where TFunctionManifest : FunctionManifest, new() - { - var manifest = new TFunctionManifest(); - builder.AddNServiceBusFunction(manifest, configure); - } - - public static void AddNServiceBusFunction( - this FunctionsApplicationBuilder builder, - string endpointName, - Action configure) => - builder.AddNServiceBusFunction(new FunctionManifest(endpointName, endpointName, AzureServiceBusServerlessTransport.DefaultServiceBusConnectionName), configure); - public static void AddNServiceBusFunction( this FunctionsApplicationBuilder builder, FunctionManifest functionManifest, - Action configure) + IEndpointConfiguration configuration) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(functionManifest); - ArgumentNullException.ThrowIfNull(configure); + ArgumentNullException.ThrowIfNull(configuration); builder.Services.AddAzureClientsCore(); var endpointName = functionManifest.Name; builder.AddNServiceBusEndpoint(endpointName, endpointConfiguration => { - configure(endpointConfiguration); + configuration.Configure(endpointConfiguration); var settings = endpointConfiguration.GetSettings(); if (settings.GetOrDefault(AzureServiceBusServerlessTransport.SendOnlyConfigKey)) @@ -57,10 +43,6 @@ public static void AddNServiceBusFunction( transport.ConnectionName = functionManifest.ConnectionName; - functionManifest.Configured = true; - - builder.Services.AddHostedService(); - builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); }); } diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs new file mode 100644 index 0000000..ee3aced --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs @@ -0,0 +1,6 @@ +namespace NServiceBus; + +public interface IEndpointConfiguration +{ + void Configure(EndpointConfiguration configuration); +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs new file mode 100644 index 0000000..006790b --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs @@ -0,0 +1,6 @@ +namespace NServiceBus; + +public class NServiceBusFunctionAttribute(Type configurationType) : Attribute +{ + public Type ConfigurationType { get; } = configurationType; +} \ No newline at end of file From 9c2173324738fc98a4c433de38f0b262954ccbf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Fri, 13 Feb 2026 09:54:00 +0100 Subject: [PATCH 19/41] Cleanup --- .../FunctionConfigurationValidator.cs | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs deleted file mode 100644 index 0335138..0000000 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionConfigurationValidator.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace NServiceBus; - -using Microsoft.Extensions.Hosting; - -public class FunctionConfigurationValidator : IHostedService -{ - public Task StartAsync(CancellationToken cancellationToken = default) - { - //TODO: See if we can do this in some other way -// var allFunctions = FunctionsRegistry.GetAll(); -// -// var functionNotConfigured = allFunctions.Where(f => !f.Configured).Select(f => f.Name).ToArray(); -// if (functionNotConfigured.Any()) -// { -// throw new InvalidOperationException($"The following functions have not been configured using {nameof(FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction)}(...): {string.Join(", ", functionNotConfigured)}"); -// } - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; -} \ No newline at end of file From 204abe46f1c94d67057525345ec73cf839548258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Fri, 13 Feb 2026 10:58:22 +0100 Subject: [PATCH 20/41] More cleanup --- .../GeneratedCode/NServiceBusFunctionConfiguration.g.cs | 2 +- .../FunctionsHostApplicationBuilderExtensions.cs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs index 34ec0dd..df03957 100644 --- a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs +++ b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs @@ -25,7 +25,7 @@ public static void AddNServiceBusFunctions( foreach (var manifest in AllFunctions) { - builder.AddNServiceBusFunction(manifest, manifest.EndpointConfiguration); + builder.AddNServiceBusFunction(manifest); } } diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index a8982c1..102ee2f 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -13,19 +13,17 @@ public static class FunctionsHostApplicationBuilderExtensions { public static void AddNServiceBusFunction( this FunctionsApplicationBuilder builder, - FunctionManifest functionManifest, - IEndpointConfiguration configuration) + FunctionManifest functionManifest) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(functionManifest); - ArgumentNullException.ThrowIfNull(configuration); builder.Services.AddAzureClientsCore(); var endpointName = functionManifest.Name; builder.AddNServiceBusEndpoint(endpointName, endpointConfiguration => { - configuration.Configure(endpointConfiguration); + functionManifest.EndpointConfiguration.Configure(endpointConfiguration); var settings = endpointConfiguration.GetSettings(); if (settings.GetOrDefault(AzureServiceBusServerlessTransport.SendOnlyConfigKey)) @@ -35,7 +33,6 @@ public static void AddNServiceBusFunction( var transport = GetTransport(settings); - if (functionManifest.Name != functionManifest.Queue) { endpointConfiguration.OverrideLocalAddress(functionManifest.Queue); From 9c9aa65eccbf547ceeaba73ec3bc96cbe844f9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Fri, 13 Feb 2026 11:01:34 +0100 Subject: [PATCH 21/41] Demo diverging endpoint and queue names --- src/IntegrationTest/BillingFunctions.cs | 4 ++-- src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs | 4 ++-- .../GeneratedCode/NServiceBusFunctionConfiguration.g.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/IntegrationTest/BillingFunctions.cs b/src/IntegrationTest/BillingFunctions.cs index bf17e81..d89f956 100644 --- a/src/IntegrationTest/BillingFunctions.cs +++ b/src/IntegrationTest/BillingFunctions.cs @@ -5,7 +5,7 @@ namespace IntegrationTest; public partial class BillingFunctions { - [Function("billing-api")] + [Function("BillingApi")] [NServiceBusFunction(typeof(ApiConfig))] public partial Task Api( [ServiceBusTrigger("billing-api", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] @@ -24,7 +24,7 @@ public void Configure(EndpointConfiguration configuration) } } - [Function("billing-backend")] + [Function("BillingBackend")] [NServiceBusFunction(typeof(BackendConfig))] public partial Task Backend( [ServiceBusTrigger("billing-backend", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] diff --git a/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs b/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs index e4768e5..adf522e 100644 --- a/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs +++ b/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs @@ -41,6 +41,6 @@ public partial Task Backend( return processor.Process(message, cancellationToken); } - const string ApiEndpointName = "billing-api"; - const string BackendEndpointName = "billing-backend"; + const string ApiEndpointName = "BillingApi"; + const string BackendEndpointName = "BillingBackend"; } \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs index df03957..a50ec03 100644 --- a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs +++ b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs @@ -31,6 +31,6 @@ public static void AddNServiceBusFunctions( static List AllFunctions = [new BillingApi(), new BillingBackend()]; - record BillingApi() : FunctionManifest("billing-api", "billing-api", "AzureWebJobsServiceBus", new BillingFunctions.ApiConfig()); - record BillingBackend() : FunctionManifest("billing-backend", "billing-backend", "AzureWebJobsServiceBus", new BillingFunctions.BackendConfig()); + record BillingApi() : FunctionManifest("BillingApi", "billing-api", "AzureWebJobsServiceBus", new BillingFunctions.ApiConfig()); + record BillingBackend() : FunctionManifest("BillingBackend", "billing-backend", "AzureWebJobsServiceBus", new BillingFunctions.BackendConfig()); } \ No newline at end of file From 70a085e79ee924627afada5c9acc9a56de704253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Fri, 13 Feb 2026 11:08:48 +0100 Subject: [PATCH 22/41] Move initalizer --- .../InitializeLogger.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{IntegrationTest => NServiceBus.AzureFunctions.AzureServiceBus}/InitializeLogger.cs (100%) diff --git a/src/IntegrationTest/InitializeLogger.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/InitializeLogger.cs similarity index 100% rename from src/IntegrationTest/InitializeLogger.cs rename to src/NServiceBus.AzureFunctions.AzureServiceBus/InitializeLogger.cs From 6b4a4debcecf3b288a53af4e857259de65c6602a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Fri, 13 Feb 2026 15:48:10 +0100 Subject: [PATCH 23/41] Demo using function class for config as well --- src/IntegrationTest/BillingFunctions.cs | 11 +------ src/IntegrationTest/CommonConfig.cs | 12 ++++++++ .../NServiceBusFunctionConfiguration.g.cs | 3 +- .../GeneratedCode/Shipping.g.cs | 29 +++++++++++++++++++ src/IntegrationTest/Shipping.cs | 24 +++++++++++++++ 5 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 src/IntegrationTest/CommonConfig.cs create mode 100644 src/IntegrationTest/GeneratedCode/Shipping.g.cs create mode 100644 src/IntegrationTest/Shipping.cs diff --git a/src/IntegrationTest/BillingFunctions.cs b/src/IntegrationTest/BillingFunctions.cs index d89f956..824fa17 100644 --- a/src/IntegrationTest/BillingFunctions.cs +++ b/src/IntegrationTest/BillingFunctions.cs @@ -1,6 +1,7 @@ namespace IntegrationTest; using Azure.Messaging.ServiceBus; +using ITOps; using Microsoft.Azure.Functions.Worker; public partial class BillingFunctions @@ -42,14 +43,4 @@ public void Configure(EndpointConfiguration configuration) configuration.AddHandler(); } } - public class CommonConfig - { - public static void Apply(EndpointConfiguration configuration) - { - configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - configuration.EnableInstallers(); - configuration.UsePersistence(); - configuration.UseSerialization(); - } - } } \ No newline at end of file diff --git a/src/IntegrationTest/CommonConfig.cs b/src/IntegrationTest/CommonConfig.cs new file mode 100644 index 0000000..549b7d3 --- /dev/null +++ b/src/IntegrationTest/CommonConfig.cs @@ -0,0 +1,12 @@ +namespace ITOps; + +public class CommonConfig +{ + public static void Apply(EndpointConfiguration configuration) + { + configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + configuration.EnableInstallers(); + configuration.UsePersistence(); + configuration.UseSerialization(); + } +} \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs index a50ec03..6b8fb8d 100644 --- a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs +++ b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs @@ -29,8 +29,9 @@ public static void AddNServiceBusFunctions( } } - static List AllFunctions = [new BillingApi(), new BillingBackend()]; + static List AllFunctions = [new BillingApi(), new BillingBackend(), new Shipping()]; record BillingApi() : FunctionManifest("BillingApi", "billing-api", "AzureWebJobsServiceBus", new BillingFunctions.ApiConfig()); record BillingBackend() : FunctionManifest("BillingBackend", "billing-backend", "AzureWebJobsServiceBus", new BillingFunctions.BackendConfig()); + record Shipping() : FunctionManifest("Shipping", "shipping", "AzureWebJobsServiceBus", new IntegrationTest.Shipping()); } \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/Shipping.g.cs b/src/IntegrationTest/GeneratedCode/Shipping.g.cs new file mode 100644 index 0000000..2a080af --- /dev/null +++ b/src/IntegrationTest/GeneratedCode/Shipping.g.cs @@ -0,0 +1,29 @@ +namespace IntegrationTest; + +using Azure.Messaging.ServiceBus; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.AzureFunctions.AzureServiceBus; + +public partial class Shipping +{ + public partial Task Invoke( + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default) + { + //demo using service locator to avoid having to add a ctor + var processor = functionContext.InstanceServices.GetKeyedService(EndpointName); + + if (processor is null) + { + //which also allows us to throw a better exception + throw new InvalidOperationException($"{EndpointName} has not been registered."); + } + + return processor.Process(message, cancellationToken); + } + + + const string EndpointName = "Shipping"; +} \ No newline at end of file diff --git a/src/IntegrationTest/Shipping.cs b/src/IntegrationTest/Shipping.cs new file mode 100644 index 0000000..97cfbe3 --- /dev/null +++ b/src/IntegrationTest/Shipping.cs @@ -0,0 +1,24 @@ +namespace IntegrationTest; + +using Azure.Messaging.ServiceBus; +using ITOps; +using Microsoft.Azure.Functions.Worker; + +public partial class Shipping : IEndpointConfiguration +{ + [Function("Shipping")] + [NServiceBusFunction(typeof(Shipping))] //we could default to containing type? + public partial Task Invoke( + [ServiceBusTrigger("shipping", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default); + + + public void Configure(EndpointConfiguration configuration) + { + CommonConfig.Apply(configuration); + + configuration.AddHandler(); + } +} \ No newline at end of file From efe53fa749eadd589adbf17883257e2cd845dc49 Mon Sep 17 00:00:00 2001 From: Matt Mercurio Date: Fri, 13 Feb 2026 14:03:29 -0600 Subject: [PATCH 24/41] Spike v3: Unification of approaches. Separate User libraries in manual testing. --- .../BillingFunctions.cs | 16 +- .../Handlers/ProcessPaymentHandler.cs | 17 ++ .../IntegrationTest.Billing.csproj | 21 ++ .../Handlers/AcceptOrderHandler.cs | 17 ++ .../IntegrationTest.Sales.csproj | 21 ++ src/IntegrationTest.Sales/SalesEndpoint.cs | 22 ++ .../CommonEndpointConfig.cs} | 4 +- .../IntegrationTest.Shared.csproj | 11 + src/IntegrationTest.Shared/OrderSubmitted.cs | 5 + src/IntegrationTest.Shared/PaymentCleared.cs | 5 + src/IntegrationTest.Shared/SubmitOrder.cs | 5 + .../Handlers/ShipOrderHandler.cs | 17 ++ .../IntegrationTest.Shipping.csproj | 21 ++ .../ShippingEndpoint.cs | 22 ++ .../GeneratedCode/BillingFunctions.g.cs | 46 --- .../NServiceBusFunctionConfiguration.g.cs | 37 --- .../GeneratedCode/Shipping.g.cs | 29 -- src/IntegrationTest/HttpSender.cs | 5 +- src/IntegrationTest/IntegrationTest.csproj | 8 + src/IntegrationTest/Program.cs | 4 +- src/IntegrationTest/Shipping.cs | 24 -- src/IntegrationTest/SomeEvent.cs | 5 - .../SomeEventMessageHandler.cs | 18 -- src/IntegrationTest/SomeOtherMessage.cs | 5 - .../SomeOtherMessageHandler.cs | 18 -- src/IntegrationTest/TriggerMessage.cs | 5 - src/IntegrationTest/TriggerMessageHandler.cs | 16 -- .../Class1.cs | 7 - .../CompilationAssemblyDetails.cs | 32 +++ .../DiagnosticIds.cs | 35 +++ .../FunctionCompositionGenerator.cs | 193 +++++++++++++ .../FunctionEndpointAnalyzer.cs | 103 +++++++ .../FunctionEndpointGenerator.cs | 270 ++++++++++++++++++ ...NServiceBus.AzureFunctions.Analyzer.csproj | 3 +- ...GeneratedFunctionRegistrationsAttribute.cs | 7 + ...eBus.AzureFunctions.AzureServiceBus.csproj | 3 + .../NServiceBusFunctionsInfrastructure.cs | 19 ++ ...NServiceBusFunctionsSourceGenExtensions.cs | 10 + src/NServiceBus.AzureFunctions.slnx | 4 + 39 files changed, 882 insertions(+), 228 deletions(-) rename src/{IntegrationTest => IntegrationTest.Billing}/BillingFunctions.cs (74%) create mode 100644 src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs create mode 100644 src/IntegrationTest.Billing/IntegrationTest.Billing.csproj create mode 100644 src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs create mode 100644 src/IntegrationTest.Sales/IntegrationTest.Sales.csproj create mode 100644 src/IntegrationTest.Sales/SalesEndpoint.cs rename src/{IntegrationTest/CommonConfig.cs => IntegrationTest.Shared/CommonEndpointConfig.cs} (82%) create mode 100644 src/IntegrationTest.Shared/IntegrationTest.Shared.csproj create mode 100644 src/IntegrationTest.Shared/OrderSubmitted.cs create mode 100644 src/IntegrationTest.Shared/PaymentCleared.cs create mode 100644 src/IntegrationTest.Shared/SubmitOrder.cs create mode 100644 src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs create mode 100644 src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj create mode 100644 src/IntegrationTest.Shipping/ShippingEndpoint.cs delete mode 100644 src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs delete mode 100644 src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs delete mode 100644 src/IntegrationTest/GeneratedCode/Shipping.g.cs delete mode 100644 src/IntegrationTest/Shipping.cs delete mode 100644 src/IntegrationTest/SomeEvent.cs delete mode 100644 src/IntegrationTest/SomeEventMessageHandler.cs delete mode 100644 src/IntegrationTest/SomeOtherMessage.cs delete mode 100644 src/IntegrationTest/SomeOtherMessageHandler.cs delete mode 100644 src/IntegrationTest/TriggerMessage.cs delete mode 100644 src/IntegrationTest/TriggerMessageHandler.cs delete mode 100644 src/NServiceBus.AzureFunctions.Analyzer/Class1.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointAnalyzer.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/AutoGeneratedFunctionRegistrationsAttribute.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsInfrastructure.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsSourceGenExtensions.cs diff --git a/src/IntegrationTest/BillingFunctions.cs b/src/IntegrationTest.Billing/BillingFunctions.cs similarity index 74% rename from src/IntegrationTest/BillingFunctions.cs rename to src/IntegrationTest.Billing/BillingFunctions.cs index 824fa17..eae7186 100644 --- a/src/IntegrationTest/BillingFunctions.cs +++ b/src/IntegrationTest.Billing/BillingFunctions.cs @@ -1,7 +1,7 @@ -namespace IntegrationTest; +namespace IntegrationTest.Billing; using Azure.Messaging.ServiceBus; -using ITOps; +using IntegrationTest.Shared; using Microsoft.Azure.Functions.Worker; public partial class BillingFunctions @@ -18,10 +18,8 @@ public class ApiConfig : IEndpointConfiguration { public void Configure(EndpointConfiguration configuration) { - CommonConfig.Apply(configuration); - - configuration.AddHandler(); - configuration.AddHandler(); + CommonEndpointConfig.Apply(configuration); + configuration.AddHandler(); } } @@ -37,10 +35,8 @@ public class BackendConfig : IEndpointConfiguration { public void Configure(EndpointConfiguration configuration) { - CommonConfig.Apply(configuration); - - configuration.AddHandler(); - configuration.AddHandler(); + CommonEndpointConfig.Apply(configuration); + // different handlers for the backend queue } } } \ No newline at end of file diff --git a/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs b/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs new file mode 100644 index 0000000..fae28d9 --- /dev/null +++ b/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs @@ -0,0 +1,17 @@ +namespace IntegrationTest.Billing.Handlers; + +using IntegrationTest.Shared; +using NServiceBus; +using NServiceBus.Logging; + +public class ProcessPaymentHandler : IHandleMessages +{ + static readonly ILog Log = LogManager.GetLogger(); + + public async Task Handle(OrderSubmitted message, IMessageHandlerContext context) + { + Log.Warn($"Handling {nameof(OrderSubmitted)} in {nameof(ProcessPaymentHandler)}"); + + await context.Publish(new PaymentCleared()).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/IntegrationTest.Billing/IntegrationTest.Billing.csproj b/src/IntegrationTest.Billing/IntegrationTest.Billing.csproj new file mode 100644 index 0000000..a750c51 --- /dev/null +++ b/src/IntegrationTest.Billing/IntegrationTest.Billing.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + $(InterceptorsPreviewNamespaces);NServiceBus + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs b/src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs new file mode 100644 index 0000000..d49688d --- /dev/null +++ b/src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs @@ -0,0 +1,17 @@ +namespace IntegrationTest.Sales.Handlers; + +using IntegrationTest.Shared; +using NServiceBus; +using NServiceBus.Logging; + +public class AcceptOrderHandler : IHandleMessages +{ + static readonly ILog Log = LogManager.GetLogger(); + + public async Task Handle(SubmitOrder message, IMessageHandlerContext context) + { + Log.Warn($"Handling {nameof(SubmitOrder)} in {nameof(AcceptOrderHandler)}"); + + await context.Publish(new OrderSubmitted()).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj b/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj new file mode 100644 index 0000000..a750c51 --- /dev/null +++ b/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + $(InterceptorsPreviewNamespaces);NServiceBus + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/IntegrationTest.Sales/SalesEndpoint.cs b/src/IntegrationTest.Sales/SalesEndpoint.cs new file mode 100644 index 0000000..c3cf9d7 --- /dev/null +++ b/src/IntegrationTest.Sales/SalesEndpoint.cs @@ -0,0 +1,22 @@ +namespace IntegrationTest.Sales; + +using Azure.Messaging.ServiceBus; +using IntegrationTest.Shared; +using Microsoft.Azure.Functions.Worker; + +[NServiceBusFunction(typeof(SalesEndpoint))] +public partial class SalesEndpoint : IEndpointConfiguration +{ + [Function("Sales")] + public partial Task ProcessMessage( + [ServiceBusTrigger("sales", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default); + + public void Configure(EndpointConfiguration endpoint) + { + CommonEndpointConfig.Apply(endpoint); + endpoint.AddHandler(); + } +} \ No newline at end of file diff --git a/src/IntegrationTest/CommonConfig.cs b/src/IntegrationTest.Shared/CommonEndpointConfig.cs similarity index 82% rename from src/IntegrationTest/CommonConfig.cs rename to src/IntegrationTest.Shared/CommonEndpointConfig.cs index 549b7d3..6e9d68d 100644 --- a/src/IntegrationTest/CommonConfig.cs +++ b/src/IntegrationTest.Shared/CommonEndpointConfig.cs @@ -1,6 +1,6 @@ -namespace ITOps; +namespace IntegrationTest.Shared; -public class CommonConfig +public static class CommonEndpointConfig { public static void Apply(EndpointConfiguration configuration) { diff --git a/src/IntegrationTest.Shared/IntegrationTest.Shared.csproj b/src/IntegrationTest.Shared/IntegrationTest.Shared.csproj new file mode 100644 index 0000000..9c30893 --- /dev/null +++ b/src/IntegrationTest.Shared/IntegrationTest.Shared.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + + + + + + + \ No newline at end of file diff --git a/src/IntegrationTest.Shared/OrderSubmitted.cs b/src/IntegrationTest.Shared/OrderSubmitted.cs new file mode 100644 index 0000000..db37cce --- /dev/null +++ b/src/IntegrationTest.Shared/OrderSubmitted.cs @@ -0,0 +1,5 @@ +namespace IntegrationTest.Shared; + +using NServiceBus; + +public class OrderSubmitted : IEvent; \ No newline at end of file diff --git a/src/IntegrationTest.Shared/PaymentCleared.cs b/src/IntegrationTest.Shared/PaymentCleared.cs new file mode 100644 index 0000000..a213e26 --- /dev/null +++ b/src/IntegrationTest.Shared/PaymentCleared.cs @@ -0,0 +1,5 @@ +namespace IntegrationTest.Shared; + +using NServiceBus; + +public class PaymentCleared : IEvent; \ No newline at end of file diff --git a/src/IntegrationTest.Shared/SubmitOrder.cs b/src/IntegrationTest.Shared/SubmitOrder.cs new file mode 100644 index 0000000..32c2ddc --- /dev/null +++ b/src/IntegrationTest.Shared/SubmitOrder.cs @@ -0,0 +1,5 @@ +namespace IntegrationTest.Shared; + +using NServiceBus; + +public class SubmitOrder : ICommand; \ No newline at end of file diff --git a/src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs b/src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs new file mode 100644 index 0000000..7c1422e --- /dev/null +++ b/src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs @@ -0,0 +1,17 @@ +namespace IntegrationTest.Shipping.Handlers; + +using IntegrationTest.Shared; +using NServiceBus; +using NServiceBus.Logging; + +public class ShipOrderHandler : IHandleMessages +{ + static readonly ILog Log = LogManager.GetLogger(); + + public Task Handle(PaymentCleared message, IMessageHandlerContext context) + { + Log.Warn($"Handling {nameof(PaymentCleared)} in {nameof(ShipOrderHandler)}"); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj b/src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj new file mode 100644 index 0000000..a750c51 --- /dev/null +++ b/src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + $(InterceptorsPreviewNamespaces);NServiceBus + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/IntegrationTest.Shipping/ShippingEndpoint.cs b/src/IntegrationTest.Shipping/ShippingEndpoint.cs new file mode 100644 index 0000000..c8e1b20 --- /dev/null +++ b/src/IntegrationTest.Shipping/ShippingEndpoint.cs @@ -0,0 +1,22 @@ +namespace IntegrationTest.Shipping; + +using Azure.Messaging.ServiceBus; +using IntegrationTest.Shared; +using Microsoft.Azure.Functions.Worker; + +[NServiceBusFunction(typeof(ShippingEndpoint))] +public partial class ShippingEndpoint : IEndpointConfiguration +{ + [Function("Shipping")] + public partial Task ProcessMessage( + [ServiceBusTrigger("shipping", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + ServiceBusReceivedMessage message, + FunctionContext functionContext, + CancellationToken cancellationToken = default); + + public void Configure(EndpointConfiguration endpoint) + { + CommonEndpointConfig.Apply(endpoint); + endpoint.AddHandler(); + } +} \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs b/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs deleted file mode 100644 index adf522e..0000000 --- a/src/IntegrationTest/GeneratedCode/BillingFunctions.g.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace IntegrationTest; - -using Azure.Messaging.ServiceBus; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.DependencyInjection; -using NServiceBus.AzureFunctions.AzureServiceBus; - -public partial class BillingFunctions -{ - public partial Task Api( - ServiceBusReceivedMessage message, - FunctionContext functionContext, - CancellationToken cancellationToken = default) - { - //demo using service locator to avoid having to add a ctor - var processor = functionContext.InstanceServices.GetKeyedService(ApiEndpointName); - - if (processor is null) - { - //which also allows us to throw a better exception - throw new InvalidOperationException($"{ApiEndpointName} has not been registered."); - } - - return processor.Process(message, cancellationToken); - } - - public partial Task Backend( - ServiceBusReceivedMessage message, - FunctionContext functionContext, - CancellationToken cancellationToken = default) - { - //demo using service locator to avoid having to add a ctor - var processor = functionContext.InstanceServices.GetKeyedService(BackendEndpointName); - - if (processor is null) - { - //which also allows us to throw a better exception - throw new InvalidOperationException($"{BackendEndpointName} has not been registered."); - } - - return processor.Process(message, cancellationToken); - } - - const string ApiEndpointName = "BillingApi"; - const string BackendEndpointName = "BillingBackend"; -} \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs b/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs deleted file mode 100644 index 6b8fb8d..0000000 --- a/src/IntegrationTest/GeneratedCode/NServiceBusFunctionConfiguration.g.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace NServiceBus; - -using System; -using AzureFunctions.AzureServiceBus; -using Configuration.AdvancedExtensibility; -using IntegrationTest; -using Logging; -using Microsoft.Azure.Functions.Worker.Builder; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.DependencyInjection; -using MultiHosting; -using Settings; -using Transport; - -public static class NServiceBusFunctionsHostApplicationBuilderExtensions -{ - public static void AddNServiceBusFunctions( - this FunctionsApplicationBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - LogManager.UseFactory(MultiEndpointLoggerFactory.Instance); - - builder.Services.AddHostedService(); - builder.Services.AddAzureClientsCore(); - - foreach (var manifest in AllFunctions) - { - builder.AddNServiceBusFunction(manifest); - } - } - - static List AllFunctions = [new BillingApi(), new BillingBackend(), new Shipping()]; - - record BillingApi() : FunctionManifest("BillingApi", "billing-api", "AzureWebJobsServiceBus", new BillingFunctions.ApiConfig()); - record BillingBackend() : FunctionManifest("BillingBackend", "billing-backend", "AzureWebJobsServiceBus", new BillingFunctions.BackendConfig()); - record Shipping() : FunctionManifest("Shipping", "shipping", "AzureWebJobsServiceBus", new IntegrationTest.Shipping()); -} \ No newline at end of file diff --git a/src/IntegrationTest/GeneratedCode/Shipping.g.cs b/src/IntegrationTest/GeneratedCode/Shipping.g.cs deleted file mode 100644 index 2a080af..0000000 --- a/src/IntegrationTest/GeneratedCode/Shipping.g.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace IntegrationTest; - -using Azure.Messaging.ServiceBus; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.DependencyInjection; -using NServiceBus.AzureFunctions.AzureServiceBus; - -public partial class Shipping -{ - public partial Task Invoke( - ServiceBusReceivedMessage message, - FunctionContext functionContext, - CancellationToken cancellationToken = default) - { - //demo using service locator to avoid having to add a ctor - var processor = functionContext.InstanceServices.GetKeyedService(EndpointName); - - if (processor is null) - { - //which also allows us to throw a better exception - throw new InvalidOperationException($"{EndpointName} has not been registered."); - } - - return processor.Process(message, cancellationToken); - } - - - const string EndpointName = "Shipping"; -} \ No newline at end of file diff --git a/src/IntegrationTest/HttpSender.cs b/src/IntegrationTest/HttpSender.cs index 76633cb..35ddec3 100644 --- a/src/IntegrationTest/HttpSender.cs +++ b/src/IntegrationTest/HttpSender.cs @@ -2,6 +2,7 @@ namespace IntegrationTest; using System.Net; using System.Threading.Tasks; +using IntegrationTest.Shared; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.DependencyInjection; @@ -18,10 +19,10 @@ public async Task Run( _ = executionContext; // For now logger.LogInformation("C# HTTP trigger function received a request."); - await session.Send(new TriggerMessage()).ConfigureAwait(false); + await session.Send(new SubmitOrder()).ConfigureAwait(false); var r = req.CreateResponse(HttpStatusCode.OK); - await r.WriteStringAsync($"{nameof(TriggerMessage)} sent.") + await r.WriteStringAsync($"{nameof(SubmitOrder)} sent.") .ConfigureAwait(false); return r; } diff --git a/src/IntegrationTest/IntegrationTest.csproj b/src/IntegrationTest/IntegrationTest.csproj index 31e7039..9bc4e35 100644 --- a/src/IntegrationTest/IntegrationTest.csproj +++ b/src/IntegrationTest/IntegrationTest.csproj @@ -4,12 +4,20 @@ net10.0 v4 Exe + $(InterceptorsPreviewNamespaces);NServiceBus true $(BaseIntermediateOutputPath)\GeneratedFiles + + + + + diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 600a24b..67ed58f 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -1,4 +1,4 @@ -using IntegrationTest; +using IntegrationTest.Shared; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Hosting; @@ -17,7 +17,7 @@ var routing = endpoint.UseTransport(transport); - routing.RouteToEndpoint(typeof(TriggerMessage), "billing-api"); + routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); endpoint.UseSerialization(); }); diff --git a/src/IntegrationTest/Shipping.cs b/src/IntegrationTest/Shipping.cs deleted file mode 100644 index 97cfbe3..0000000 --- a/src/IntegrationTest/Shipping.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace IntegrationTest; - -using Azure.Messaging.ServiceBus; -using ITOps; -using Microsoft.Azure.Functions.Worker; - -public partial class Shipping : IEndpointConfiguration -{ - [Function("Shipping")] - [NServiceBusFunction(typeof(Shipping))] //we could default to containing type? - public partial Task Invoke( - [ServiceBusTrigger("shipping", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] - ServiceBusReceivedMessage message, - FunctionContext functionContext, - CancellationToken cancellationToken = default); - - - public void Configure(EndpointConfiguration configuration) - { - CommonConfig.Apply(configuration); - - configuration.AddHandler(); - } -} \ No newline at end of file diff --git a/src/IntegrationTest/SomeEvent.cs b/src/IntegrationTest/SomeEvent.cs deleted file mode 100644 index c97351b..0000000 --- a/src/IntegrationTest/SomeEvent.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace IntegrationTest; - -using NServiceBus; - -public class SomeEvent : IEvent; \ No newline at end of file diff --git a/src/IntegrationTest/SomeEventMessageHandler.cs b/src/IntegrationTest/SomeEventMessageHandler.cs deleted file mode 100644 index e39a58b..0000000 --- a/src/IntegrationTest/SomeEventMessageHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace IntegrationTest; - -using System.Threading.Tasks; -using NServiceBus; -using NServiceBus.Logging; - -public class SomeEventMessageHandler : IHandleMessages -{ - // using static logger here deliberately - static readonly ILog Log = LogManager.GetLogger(); - - public Task Handle(SomeEvent message, IMessageHandlerContext context) - { - Log.Warn($"Handling {nameof(SomeEvent)} in {nameof(SomeEventMessageHandler)}"); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/IntegrationTest/SomeOtherMessage.cs b/src/IntegrationTest/SomeOtherMessage.cs deleted file mode 100644 index 7c0e3c1..0000000 --- a/src/IntegrationTest/SomeOtherMessage.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace IntegrationTest; - -using NServiceBus; - -public class SomeOtherMessage : ICommand; \ No newline at end of file diff --git a/src/IntegrationTest/SomeOtherMessageHandler.cs b/src/IntegrationTest/SomeOtherMessageHandler.cs deleted file mode 100644 index 317e652..0000000 --- a/src/IntegrationTest/SomeOtherMessageHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace IntegrationTest; - -using System.Threading.Tasks; -using NServiceBus; -using NServiceBus.Logging; - -public class SomeOtherMessageHandler : IHandleMessages -{ - // using static logger here deliberately - static readonly ILog Log = LogManager.GetLogger(); - - public Task Handle(SomeOtherMessage message, IMessageHandlerContext context) - { - Log.Warn($"Handling {nameof(SomeOtherMessage)} in {nameof(SomeOtherMessageHandler)}"); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/IntegrationTest/TriggerMessage.cs b/src/IntegrationTest/TriggerMessage.cs deleted file mode 100644 index 708f602..0000000 --- a/src/IntegrationTest/TriggerMessage.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace IntegrationTest; - -using NServiceBus; - -public class TriggerMessage : IMessage; \ No newline at end of file diff --git a/src/IntegrationTest/TriggerMessageHandler.cs b/src/IntegrationTest/TriggerMessageHandler.cs deleted file mode 100644 index d3a77b3..0000000 --- a/src/IntegrationTest/TriggerMessageHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace IntegrationTest; - -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using NServiceBus; - -public class TriggerMessageHandler(ILogger logger) : IHandleMessages -{ - public async Task Handle(TriggerMessage message, IMessageHandlerContext context) - { - logger.LogWarning($"Handling {nameof(TriggerMessage)} in {nameof(TriggerMessageHandler)}"); - - await context.SendLocal(new SomeOtherMessage()).ConfigureAwait(false); - await context.Publish(new SomeEvent()).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/Class1.cs b/src/NServiceBus.AzureFunctions.Analyzer/Class1.cs deleted file mode 100644 index 43a4f26..0000000 --- a/src/NServiceBus.AzureFunctions.Analyzer/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NServiceBus.AzureFunctions.Analyzers -{ - public class Class1 - { - - } -} diff --git a/src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs b/src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs new file mode 100644 index 0000000..6d14cbd --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs @@ -0,0 +1,32 @@ +#nullable enable +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Security.Cryptography; +using System.Text; +using Microsoft.CodeAnalysis; + +record struct CompilationAssemblyDetails(string SimpleName, string Identity) +{ + public static CompilationAssemblyDetails FromAssembly(IAssemblySymbol assembly) => new(assembly.Name, assembly.Identity.GetDisplayName()); + + const string NamePrefix = "GeneratedFunctionRegistrations_"; + const int HashBytesToUse = 10; + + public readonly string ToGenerationClassName() + { + var sb = new StringBuilder(NamePrefix, NamePrefix.Length + SimpleName.Length + 1 + (HashBytesToUse * 2)) + .Append(SimpleName.Replace('.', '_')) + .Append('_'); + + using var sha = SHA256.Create(); + + var identityBytes = Encoding.UTF8.GetBytes(Identity); + var hashBytes = sha.ComputeHash(identityBytes); + for (var i = 0; i < HashBytesToUse; i++) + { + _ = sb.Append(hashBytes[i].ToString("x2")); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs new file mode 100644 index 0000000..4681c44 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs @@ -0,0 +1,35 @@ +#nullable enable +namespace NServiceBus.AzureFunctions.Analyzer; + +using Microsoft.CodeAnalysis; + +static class DiagnosticIds +{ + public const string ClassMustBePartial = "NSBFUNC001"; + public const string ShouldNotImplementIHandleMessages = "NSBFUNC002"; + public const string MethodMustBePartial = "NSBFUNC003"; + + public static readonly DiagnosticDescriptor ClassMustBePartialDescriptor = new( + id: ClassMustBePartial, + title: "Class containing [NServiceBusFunction] must be partial", + messageFormat: "Class '{0}' must be declared as partial to use [NServiceBusFunction]", + category: "NServiceBus.AzureFunctions", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor ShouldNotImplementIHandleMessagesDescriptor = new( + id: ShouldNotImplementIHandleMessages, + title: "Function class should not implement IHandleMessages", + messageFormat: "Class '{0}' should not implement IHandleMessages; message handlers should be registered separately via IEndpointConfiguration", + category: "NServiceBus.AzureFunctions", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor MethodMustBePartialDescriptor = new( + id: MethodMustBePartial, + title: "Method with [NServiceBusFunction] must be partial", + messageFormat: "Method '{0}' must be declared as partial to use [NServiceBusFunction]", + category: "NServiceBus.AzureFunctions", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs new file mode 100644 index 0000000..541bea1 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs @@ -0,0 +1,193 @@ +#nullable enable +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +[Generator] +public sealed class FunctionCompositionGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var methodsToIntercept = context.SyntaxProvider + .CreateSyntaxProvider(SyntaxLooksLikeAddNServiceBusFunctions, TransformToInterceptor) + .Where(candidate => candidate is not null); + + var registrationTypesFromCompilation = context.CompilationProvider.SelectMany((compilation, ct) => + { + var attributeType = compilation.GetTypeByMetadataName(MarkerAttributeName); + if (attributeType is null) + { + return ImmutableArray.Empty; + } + + return GetAutoGeneratedRegistrationTypes(compilation.GlobalNamespace, attributeType, ct) + .ToImmutableArray(); + }); + + var collectedRegistrationTypes = registrationTypesFromCompilation.Collect(); + var collectedInterceptorLocations = methodsToIntercept.Collect(); + + var currentAssemblyRegistrationTypeByConvention = context.CompilationProvider + .Select((compilation, _) => CompilationAssemblyDetails.FromAssembly(compilation.Assembly)) + .Select((assemblyInfo, _) => assemblyInfo.ToGenerationClassName()) + .Select((className, _) => new GeneratedRegistrationClass($"NServiceBus.Generated.{className}")); + + var allDataForInterceptor = collectedRegistrationTypes + .Combine(collectedInterceptorLocations) + .Combine(currentAssemblyRegistrationTypeByConvention) + .Select((tuple, _) => + { + var ((regClasses, interceptorLocations), currentAssemblyClassName) = tuple; + + if (!interceptorLocations.Any()) + { + return default; + } + + var allClasses = regClasses + .Concat([currentAssemblyClassName]) + .Distinct() + .ToImmutableArray(); + + return new InterceptorData(allClasses, interceptorLocations); + }); + + context.RegisterSourceOutput(allDataForInterceptor, GenerateInterceptorCode); + } + + static bool SyntaxLooksLikeAddNServiceBusFunctions(SyntaxNode node, CancellationToken cancellationToken) => + node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: MethodName } }; + + static InterceptDetails? TransformToInterceptor(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + if (context.Node is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax } invocation) + { + return null; + } + + if (context.SemanticModel.GetOperation(invocation, cancellationToken) is not IInvocationOperation operation) + { + return null; + } + + if (operation.TargetMethod is not + { + Name: MethodName, + ContainingType: + { + Name: ExtensionClassName, + ContainingNamespace: { Name: "NServiceBus", ContainingNamespace.IsGlobalNamespace: true } + } + }) + { + return null; + } + + if (context.SemanticModel.GetInterceptableLocation(invocation, cancellationToken) is not { } location) + { + return null; + } + + return new InterceptDetails(location); + } + + static IEnumerable GetAutoGeneratedRegistrationTypes( + INamespaceSymbol ns, INamedTypeSymbol attributeType, CancellationToken cancellationToken) + { + foreach (var type in ns.GetTypeMembers()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType))) + { + yield return new GeneratedRegistrationClass(type.ToDisplayString()); + } + } + + foreach (var childNamespace in ns.GetNamespaceMembers()) + { + foreach (var reg in GetAutoGeneratedRegistrationTypes(childNamespace, attributeType, cancellationToken)) + { + yield return reg; + } + } + } + + static void GenerateInterceptorCode(SourceProductionContext spc, InterceptorData data) + { + if (data == default) + { + return; + } + + var (regClasses, interceptLocations) = data; + + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + sb.AppendLine("namespace System.Runtime.CompilerServices"); + sb.AppendLine("{"); + sb.AppendLine(" [global::System.Diagnostics.Conditional(\"DEBUG\")]"); + sb.AppendLine(" [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]"); + sb.AppendLine(" sealed file class InterceptsLocationAttribute : global::System.Attribute"); + sb.AppendLine(" {"); + sb.AppendLine(" public InterceptsLocationAttribute(int version, string data)"); + sb.AppendLine(" {"); + sb.AppendLine(" _ = version;"); + sb.AppendLine(" _ = data;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + sb.AppendLine(); + sb.AppendLine("namespace NServiceBus"); + sb.AppendLine("{"); + sb.AppendLine(" static file class NServiceBusFunctionsInterceptors"); + sb.AppendLine(" {"); + + foreach (var location in interceptLocations) + { + if (location is not null) + { + sb.AppendLine($" [global::System.Runtime.CompilerServices.InterceptsLocation({location.Value.Location.Version}, \"{location.Value.Location.Data}\")] // {location.Value.Location.GetDisplayLocation()}"); + } + } + + sb.AppendLine(" public static void AddNServiceBusFunctions("); + sb.AppendLine(" this global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder)"); + sb.AppendLine(" {"); + sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(builder);"); + sb.AppendLine(" global::NServiceBus.NServiceBusFunctionsInfrastructure.Initialize(builder);"); + sb.AppendLine(); + + foreach (var regClass in regClasses) + { + sb.AppendLine($" foreach (var m in global::{regClass.FullClassName}.GetFunctionManifests())"); + sb.AppendLine($" global::NServiceBus.FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction(builder, m);"); + } + + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + + spc.AddSource("Interception.g.cs", sb.ToString()); + } + + record struct GeneratedRegistrationClass(string FullClassName); + record struct InterceptDetails(InterceptableLocation Location); + record struct InterceptorData( + ImmutableArray RegistrationClasses, + ImmutableArray Locations); + + const string MarkerAttributeName = "NServiceBus.AutoGeneratedFunctionRegistrationsAttribute"; + const string ExtensionClassName = "NServiceBusFunctionsSourceGenExtensions"; + const string MethodName = "AddNServiceBusFunctions"; +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointAnalyzer.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointAnalyzer.cs new file mode 100644 index 0000000..2adf0ce --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointAnalyzer.cs @@ -0,0 +1,103 @@ +#nullable enable +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class FunctionEndpointAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create( + DiagnosticIds.ClassMustBePartialDescriptor, + DiagnosticIds.ShouldNotImplementIHandleMessagesDescriptor, + DiagnosticIds.MethodMustBePartialDescriptor); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + } + + static void AnalyzeNamedType(SymbolAnalysisContext context) + { + var type = (INamedTypeSymbol)context.Symbol; + + var hasAttribute = type.GetAttributes() + .Any(a => a.AttributeClass?.ToDisplayString() == "NServiceBus.NServiceBusFunctionAttribute"); + + if (!hasAttribute) + { + return; + } + + // NSBFUNC001: Class must be partial + var declarations = type.DeclaringSyntaxReferences; + var isPartial = declarations.Any(r => + r.GetSyntax(context.CancellationToken) is ClassDeclarationSyntax classDecl && + classDecl.Modifiers.Any(SyntaxKind.PartialKeyword)); + + if (!isPartial) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticIds.ClassMustBePartialDescriptor, + type.Locations.FirstOrDefault(), + type.Name)); + } + + // NSBFUNC002: Should not implement IHandleMessages + var implementsIHandleMessages = type.AllInterfaces + .Any(i => i.OriginalDefinition.ToDisplayString() == "NServiceBus.IHandleMessages"); + + if (implementsIHandleMessages) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticIds.ShouldNotImplementIHandleMessagesDescriptor, + type.Locations.FirstOrDefault(), + type.Name)); + } + } + + static void AnalyzeMethod(SymbolAnalysisContext context) + { + var method = (IMethodSymbol)context.Symbol; + + var hasAttribute = method.GetAttributes() + .Any(a => a.AttributeClass?.ToDisplayString() == "NServiceBus.NServiceBusFunctionAttribute"); + + if (!hasAttribute) + { + return; + } + + // NSBFUNC003: Method must be partial + if (!method.IsPartialDefinition) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticIds.MethodMustBePartialDescriptor, + method.Locations.FirstOrDefault(), + method.Name)); + } + + // NSBFUNC001: Containing class must be partial (when attribute is on method) + var containingType = method.ContainingType; + var declarations = containingType.DeclaringSyntaxReferences; + var isPartial = declarations.Any(r => + r.GetSyntax(context.CancellationToken) is ClassDeclarationSyntax classDecl && + classDecl.Modifiers.Any(SyntaxKind.PartialKeyword)); + + if (!isPartial) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticIds.ClassMustBePartialDescriptor, + containingType.Locations.FirstOrDefault(), + containingType.Name)); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs new file mode 100644 index 0000000..47e654b --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -0,0 +1,270 @@ +#nullable enable +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +[Generator] +public sealed class FunctionEndpointGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var functionInfos = context.SyntaxProvider + .ForAttributeWithMetadataName( + "NServiceBus.NServiceBusFunctionAttribute", + predicate: static (node, _) => node is ClassDeclarationSyntax or MethodDeclarationSyntax, + transform: static (ctx, ct) => ExtractFunctionInfos(ctx, ct)) + .SelectMany(static (infos, _) => infos); + + var assemblyClassName = context.CompilationProvider + .Select(static (c, _) => CompilationAssemblyDetails.FromAssembly(c.Assembly).ToGenerationClassName()); + + var combined = functionInfos.Collect().Combine(assemblyClassName); + + context.RegisterSourceOutput(combined, static (spc, data) => GenerateSource(spc, data.Left, data.Right)); + } + + static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + if (context.Attributes.Length == 0) + { + return ImmutableArray.Empty; + } + + var attr = context.Attributes[0]; + if (attr.ConstructorArguments.Length == 0 || attr.ConstructorArguments[0].Value is not INamedTypeSymbol configType) + { + return ImmutableArray.Empty; + } + + var configTypeFullName = configType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + if (context.TargetSymbol is INamedTypeSymbol classSymbol) + { + var results = ImmutableArray.CreateBuilder(); + foreach (var member in classSymbol.GetMembers()) + { + cancellationToken.ThrowIfCancellationRequested(); + if (member is IMethodSymbol method) + { + var info = TryExtractFromMethod(method, configTypeFullName); + if (info is not null) + { + results.Add(info.Value); + } + } + } + return results.ToImmutable(); + } + + if (context.TargetSymbol is IMethodSymbol methodSymbol) + { + var info = TryExtractFromMethod(methodSymbol, configTypeFullName); + return info is not null + ? ImmutableArray.Create(info.Value) + : ImmutableArray.Empty; + } + + return ImmutableArray.Empty; + } + + static FunctionInfo? TryExtractFromMethod(IMethodSymbol method, string configTypeFullName) + { + var functionAttr = method.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "Microsoft.Azure.Functions.Worker.FunctionAttribute"); + if (functionAttr is null || functionAttr.ConstructorArguments.Length == 0) + { + return null; + } + + if (functionAttr.ConstructorArguments[0].Value is not string functionName) + { + return null; + } + + string? queueName = null; + string? connectionName = null; + string? messageParamName = null; + string? functionContextParamName = null; + string? cancellationTokenParamName = null; + + var paramList = new StringBuilder(); + + for (int i = 0; i < method.Parameters.Length; i++) + { + var param = method.Parameters[i]; + if (i > 0) + { + paramList.Append(",\n "); + } + + var paramTypeFqn = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + paramList.Append(paramTypeFqn).Append(' ').Append(param.Name); + + foreach (var pAttr in param.GetAttributes()) + { + if (pAttr.AttributeClass?.ToDisplayString() == "Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute") + { + if (pAttr.ConstructorArguments.Length > 0) + { + queueName = pAttr.ConstructorArguments[0].Value as string; + } + foreach (var namedArg in pAttr.NamedArguments) + { + if (namedArg.Key == "Connection") + { + connectionName = namedArg.Value.Value as string; + } + } + messageParamName = param.Name; + } + } + + if (param.Type.ToDisplayString() == "Microsoft.Azure.Functions.Worker.FunctionContext") + { + functionContextParamName = param.Name; + } + + if (param.Type.ToDisplayString() == "System.Threading.CancellationToken") + { + cancellationTokenParamName = param.Name; + } + } + + if (queueName is null || functionContextParamName is null || messageParamName is null) + { + return null; + } + + connectionName ??= ""; + cancellationTokenParamName ??= "cancellationToken"; + + var containingType = method.ContainingType; + var ns = containingType.ContainingNamespace.ToDisplayString(); + var className = containingType.Name; + var returnType = method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + var accessibility = method.DeclaredAccessibility switch + { + Accessibility.NotApplicable => "public", + Accessibility.Private => "private", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + Accessibility.ProtectedOrInternal => "protected internal", + Accessibility.Public => "public", + _ => "public", + }; + + return new FunctionInfo( + ns, className, accessibility, method.Name, returnType, + paramList.ToString(), messageParamName, functionContextParamName, + cancellationTokenParamName, functionName, queueName, connectionName, + configTypeFullName); + } + + static void GenerateSource(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) + { + if (functions.Length > 0) + { + GenerateMethodBodies(spc, functions); + } + + // Always generate the registration class so the composition generator + // can discover it by convention for same-project scenarios + GenerateRegistration(spc, functions, assemblyClassName); + } + + static void GenerateMethodBodies(SourceProductionContext spc, ImmutableArray functions) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + + var groups = functions.GroupBy(f => (f.ContainingNamespace, f.ContainingClassName)); + + foreach (var group in groups) + { + sb.AppendLine(); + sb.AppendLine($"namespace {group.Key.ContainingNamespace}"); + sb.AppendLine("{"); + sb.AppendLine($" public partial class {group.Key.ContainingClassName}"); + sb.AppendLine(" {"); + + bool first = true; + foreach (var func in group) + { + if (!first) + { + sb.AppendLine(); + } + first = false; + + sb.AppendLine($" {func.Accessibility} partial {func.ReturnType} {func.MethodName}("); + sb.AppendLine($" {func.ParameterList})"); + sb.AppendLine(" {"); + sb.AppendLine($" var processor = {func.FunctionContextParamName}.InstanceServices"); + sb.AppendLine($" .GetKeyedService(\"{func.FunctionName}\");"); + sb.AppendLine(" if (processor is null)"); + sb.AppendLine(" {"); + sb.AppendLine($" throw new global::System.InvalidOperationException(\"{func.FunctionName} has not been registered.\");"); + sb.AppendLine(" }"); + sb.AppendLine($" return processor.Process({func.MessageParamName}, {func.CancellationTokenParamName});"); + sb.AppendLine(" }"); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + } + + spc.AddSource("FunctionMethodBodies.g.cs", sb.ToString()); + } + + static void GenerateRegistration(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("namespace NServiceBus.Generated"); + sb.AppendLine("{"); + sb.AppendLine(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + sb.AppendLine(" [global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute]"); + sb.AppendLine($" public static class {assemblyClassName}"); + sb.AppendLine(" {"); + sb.AppendLine(" public static global::System.Collections.Generic.IEnumerable"); + sb.AppendLine(" GetFunctionManifests()"); + sb.AppendLine(" {"); + + foreach (var func in functions) + { + sb.AppendLine($" yield return new global::NServiceBus.FunctionManifest("); + sb.AppendLine($" \"{func.FunctionName}\", \"{func.QueueName}\", \"{func.ConnectionName}\","); + sb.AppendLine($" new {func.ConfigTypeFullName}());"); + } + + sb.AppendLine(" yield break;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + + spc.AddSource("FunctionRegistration.g.cs", sb.ToString()); + } + + record struct FunctionInfo( + string ContainingNamespace, + string ContainingClassName, + string Accessibility, + string MethodName, + string ReturnType, + string ParameterList, + string MessageParamName, + string FunctionContextParamName, + string CancellationTokenParamName, + string FunctionName, + string QueueName, + string ConnectionName, + string ConfigTypeFullName); +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.csproj b/src/NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.csproj index 469266e..9703da8 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.csproj +++ b/src/NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.csproj @@ -12,8 +12,7 @@ - - + diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AutoGeneratedFunctionRegistrationsAttribute.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AutoGeneratedFunctionRegistrationsAttribute.cs new file mode 100644 index 0000000..f268fd1 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AutoGeneratedFunctionRegistrationsAttribute.cs @@ -0,0 +1,7 @@ +namespace NServiceBus; + +using System.ComponentModel; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class AutoGeneratedFunctionRegistrationsAttribute : Attribute { } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBus.AzureFunctions.AzureServiceBus.csproj b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBus.AzureFunctions.AzureServiceBus.csproj index c391d43..30e6569 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBus.AzureFunctions.AzureServiceBus.csproj +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBus.AzureFunctions.AzureServiceBus.csproj @@ -16,6 +16,9 @@ + diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsInfrastructure.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsInfrastructure.cs new file mode 100644 index 0000000..3200160 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsInfrastructure.cs @@ -0,0 +1,19 @@ +namespace NServiceBus; + +using System.ComponentModel; +using Logging; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.DependencyInjection; +using MultiHosting; + +[EditorBrowsable(EditorBrowsableState.Never)] +public static class NServiceBusFunctionsInfrastructure +{ + public static void Initialize(FunctionsApplicationBuilder builder) + { + LogManager.UseFactory(MultiEndpointLoggerFactory.Instance); + builder.Services.AddHostedService(); + builder.Services.AddAzureClientsCore(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsSourceGenExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsSourceGenExtensions.cs new file mode 100644 index 0000000..3b2f4c9 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionsSourceGenExtensions.cs @@ -0,0 +1,10 @@ +namespace NServiceBus; + +using Microsoft.Azure.Functions.Worker.Builder; + +public static class NServiceBusFunctionsSourceGenExtensions +{ + public static void AddNServiceBusFunctions(this FunctionsApplicationBuilder builder) + => throw new NotImplementedException( + "Source generators must be enabled to use AddNServiceBusFunctions."); +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.slnx b/src/NServiceBus.AzureFunctions.slnx index 49cadcd..b657399 100644 --- a/src/NServiceBus.AzureFunctions.slnx +++ b/src/NServiceBus.AzureFunctions.slnx @@ -9,6 +9,10 @@ + + + + From 1416ee52373d9fff72f8dfc40c1ed53c99041ce9 Mon Sep 17 00:00:00 2001 From: Matt Mercurio Date: Fri, 13 Feb 2026 15:09:28 -0600 Subject: [PATCH 25/41] Ceremony reduction. Support parameterless [NServiceBusFunction] --- src/IntegrationTest.Billing/BillingFunctions.cs | 1 + src/IntegrationTest.Sales/SalesEndpoint.cs | 11 ++++++----- .../EndpointConfigBase.cs | 12 ++++++++++++ .../ShippingEndpoint.cs | 2 +- .../FunctionEndpointGenerator.cs | 17 +++++++++++++---- .../NServiceBusFunctionAttribute.cs | 12 ++++++++++-- 6 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 src/IntegrationTest.Shared/EndpointConfigBase.cs diff --git a/src/IntegrationTest.Billing/BillingFunctions.cs b/src/IntegrationTest.Billing/BillingFunctions.cs index eae7186..c192fe6 100644 --- a/src/IntegrationTest.Billing/BillingFunctions.cs +++ b/src/IntegrationTest.Billing/BillingFunctions.cs @@ -4,6 +4,7 @@ namespace IntegrationTest.Billing; using IntegrationTest.Shared; using Microsoft.Azure.Functions.Worker; +// Pattern for multiple endpoints in one class with separate configs public partial class BillingFunctions { [Function("BillingApi")] diff --git a/src/IntegrationTest.Sales/SalesEndpoint.cs b/src/IntegrationTest.Sales/SalesEndpoint.cs index c3cf9d7..b377091 100644 --- a/src/IntegrationTest.Sales/SalesEndpoint.cs +++ b/src/IntegrationTest.Sales/SalesEndpoint.cs @@ -4,8 +4,9 @@ namespace IntegrationTest.Sales; using IntegrationTest.Shared; using Microsoft.Azure.Functions.Worker; -[NServiceBusFunction(typeof(SalesEndpoint))] -public partial class SalesEndpoint : IEndpointConfiguration +// Cleanest pattern for single-function endpoints +[NServiceBusFunction] +public partial class SalesEndpoint : EndpointConfigBase { [Function("Sales")] public partial Task ProcessMessage( @@ -14,9 +15,9 @@ public partial Task ProcessMessage( FunctionContext functionContext, CancellationToken cancellationToken = default); - public void Configure(EndpointConfiguration endpoint) + public override void Configure(EndpointConfiguration configuration) { - CommonEndpointConfig.Apply(endpoint); - endpoint.AddHandler(); + base.Configure(configuration); + configuration.AddHandler(); } } \ No newline at end of file diff --git a/src/IntegrationTest.Shared/EndpointConfigBase.cs b/src/IntegrationTest.Shared/EndpointConfigBase.cs new file mode 100644 index 0000000..131f870 --- /dev/null +++ b/src/IntegrationTest.Shared/EndpointConfigBase.cs @@ -0,0 +1,12 @@ +namespace IntegrationTest.Shared; + +public abstract class EndpointConfigBase : IEndpointConfiguration +{ + public virtual void Configure(EndpointConfiguration configuration) + { + configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); + configuration.EnableInstallers(); + configuration.UsePersistence(); + configuration.UseSerialization(); + } +} \ No newline at end of file diff --git a/src/IntegrationTest.Shipping/ShippingEndpoint.cs b/src/IntegrationTest.Shipping/ShippingEndpoint.cs index c8e1b20..2cf5493 100644 --- a/src/IntegrationTest.Shipping/ShippingEndpoint.cs +++ b/src/IntegrationTest.Shipping/ShippingEndpoint.cs @@ -4,7 +4,7 @@ namespace IntegrationTest.Shipping; using IntegrationTest.Shared; using Microsoft.Azure.Functions.Worker; -[NServiceBusFunction(typeof(ShippingEndpoint))] +[NServiceBusFunction] public partial class ShippingEndpoint : IEndpointConfiguration { [Function("Shipping")] diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 47e654b..72badc5 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -36,15 +36,20 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta } var attr = context.Attributes[0]; - if (attr.ConstructorArguments.Length == 0 || attr.ConstructorArguments[0].Value is not INamedTypeSymbol configType) + + // Extract explicit config type from typeof() argument, if provided + INamedTypeSymbol? explicitConfigType = null; + if (attr.ConstructorArguments.Length > 0) { - return ImmutableArray.Empty; + explicitConfigType = attr.ConstructorArguments[0].Value as INamedTypeSymbol; } - var configTypeFullName = configType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - if (context.TargetSymbol is INamedTypeSymbol classSymbol) { + // Class-level: infer config type from the class itself when no typeof() + var configType = explicitConfigType ?? classSymbol; + var configTypeFullName = configType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var results = ImmutableArray.CreateBuilder(); foreach (var member in classSymbol.GetMembers()) { @@ -63,6 +68,10 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta if (context.TargetSymbol is IMethodSymbol methodSymbol) { + // Method-level: infer config type from the containing class when no typeof() + var configType = explicitConfigType ?? methodSymbol.ContainingType; + var configTypeFullName = configType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var info = TryExtractFromMethod(methodSymbol, configTypeFullName); return info is not null ? ImmutableArray.Create(info.Value) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs index 006790b..52db328 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs @@ -1,6 +1,14 @@ namespace NServiceBus; -public class NServiceBusFunctionAttribute(Type configurationType) : Attribute +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public class NServiceBusFunctionAttribute : Attribute { - public Type ConfigurationType { get; } = configurationType; + public NServiceBusFunctionAttribute() { } + + public NServiceBusFunctionAttribute(Type configurationType) + { + ConfigurationType = configurationType; + } + + public Type? ConfigurationType { get; } } \ No newline at end of file From 9b5f1c40ad83076013c12d285d93cb48cdce0c06 Mon Sep 17 00:00:00 2001 From: Matt Mercurio Date: Fri, 13 Feb 2026 15:35:23 -0600 Subject: [PATCH 26/41] NServiceBusSendOnlyEndpoint attribute --- src/IntegrationTest/Program.cs | 16 ----- src/IntegrationTest/SenderEndpoint.cs | 20 +++++++ .../FunctionCompositionGenerator.cs | 8 +++ .../FunctionEndpointGenerator.cs | 60 +++++++++++++++++-- .../NServiceBusSendOnlyEndpointAttribute.cs | 14 +++++ .../SendOnlyManifest.cs | 3 + 6 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 src/IntegrationTest/SenderEndpoint.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs create mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 67ed58f..f5b411e 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -1,4 +1,3 @@ -using IntegrationTest.Shared; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Hosting; @@ -6,21 +5,6 @@ builder.AddNServiceBusFunctions(); -// Send-only using a separate API since they are not "functions" -builder.AddSendOnlyNServiceBusEndpoint("SenderEndpoint", endpoint => -{ - var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) - { - //send only endpoints might need to set the connection name - ConnectionName = "AzureWebJobsServiceBus" - }; - - var routing = endpoint.UseTransport(transport); - - routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); - endpoint.UseSerialization(); -}); - var host = builder.Build(); await host.RunAsync().ConfigureAwait(false); diff --git a/src/IntegrationTest/SenderEndpoint.cs b/src/IntegrationTest/SenderEndpoint.cs new file mode 100644 index 0000000..99ea832 --- /dev/null +++ b/src/IntegrationTest/SenderEndpoint.cs @@ -0,0 +1,20 @@ +namespace IntegrationTest; + +using IntegrationTest.Shared; + +[NServiceBusSendOnlyEndpoint] +class SenderEndpoint : IEndpointConfiguration +{ + public void Configure(EndpointConfiguration configuration) + { + var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) + { + ConnectionName = "AzureWebJobsServiceBus" + }; + + var routing = configuration.UseTransport(transport); + + routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); + configuration.UseSerialization(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs index 541bea1..f1ecde2 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs @@ -174,6 +174,14 @@ static void GenerateInterceptorCode(SourceProductionContext spc, InterceptorData sb.AppendLine($" global::NServiceBus.FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction(builder, m);"); } + sb.AppendLine(); + + foreach (var regClass in regClasses) + { + sb.AppendLine($" foreach (var m in global::{regClass.FullClassName}.GetSendOnlyManifests())"); + sb.AppendLine($" global::NServiceBus.FunctionsHostApplicationBuilderExtensions.AddSendOnlyNServiceBusEndpoint(builder, m.Name, m.EndpointConfiguration.Configure);"); + } + sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine("}"); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 72badc5..79037ef 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -20,12 +20,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: static (ctx, ct) => ExtractFunctionInfos(ctx, ct)) .SelectMany(static (infos, _) => infos); + var sendOnlyInfos = context.SyntaxProvider + .ForAttributeWithMetadataName( + "NServiceBus.NServiceBusSendOnlyEndpointAttribute", + predicate: static (node, _) => node is ClassDeclarationSyntax, + transform: static (ctx, _) => ExtractSendOnlyInfo(ctx)) + .Where(static info => info is not null) + .Select(static (info, _) => info!.Value); + var assemblyClassName = context.CompilationProvider .Select(static (c, _) => CompilationAssemblyDetails.FromAssembly(c.Assembly).ToGenerationClassName()); - var combined = functionInfos.Collect().Combine(assemblyClassName); + var combined = functionInfos.Collect() + .Combine(sendOnlyInfos.Collect()) + .Combine(assemblyClassName); - context.RegisterSourceOutput(combined, static (spc, data) => GenerateSource(spc, data.Left, data.Right)); + context.RegisterSourceOutput(combined, static (spc, data) => + GenerateSource(spc, data.Left.Left, data.Left.Right, data.Right)); } static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) @@ -81,6 +92,28 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta return ImmutableArray.Empty; } + static SendOnlyEndpointInfo? ExtractSendOnlyInfo(GeneratorAttributeSyntaxContext context) + { + if (context.TargetSymbol is not INamedTypeSymbol classSymbol) + { + return null; + } + + var attr = context.Attributes[0]; + + string? endpointName = null; + if (attr.ConstructorArguments.Length > 0) + { + endpointName = attr.ConstructorArguments[0].Value as string; + } + + endpointName ??= classSymbol.Name; + + var configTypeFullName = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + return new SendOnlyEndpointInfo(endpointName, configTypeFullName); + } + static FunctionInfo? TryExtractFromMethod(IMethodSymbol method, string configTypeFullName) { var functionAttr = method.GetAttributes() @@ -176,7 +209,7 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta configTypeFullName); } - static void GenerateSource(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) + static void GenerateSource(SourceProductionContext spc, ImmutableArray functions, ImmutableArray sendOnlyEndpoints, string assemblyClassName) { if (functions.Length > 0) { @@ -185,7 +218,7 @@ static void GenerateSource(SourceProductionContext spc, ImmutableArray functions) @@ -233,7 +266,7 @@ static void GenerateMethodBodies(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) + static void GenerateRegistration(SourceProductionContext spc, ImmutableArray functions, ImmutableArray sendOnlyEndpoints, string assemblyClassName) { var sb = new StringBuilder(); sb.AppendLine("// "); @@ -254,6 +287,19 @@ static void GenerateRegistration(SourceProductionContext spc, ImmutableArray"); + sb.AppendLine(" GetSendOnlyManifests()"); + sb.AppendLine(" {"); + + foreach (var endpoint in sendOnlyEndpoints) + { + sb.AppendLine($" yield return new global::NServiceBus.SendOnlyManifest("); + sb.AppendLine($" \"{endpoint.EndpointName}\", new {endpoint.ConfigTypeFullName}());"); + } + sb.AppendLine(" yield break;"); sb.AppendLine(" }"); sb.AppendLine(" }"); @@ -276,4 +322,8 @@ record struct FunctionInfo( string QueueName, string ConnectionName, string ConfigTypeFullName); + + record struct SendOnlyEndpointInfo( + string EndpointName, + string ConfigTypeFullName); } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs new file mode 100644 index 0000000..e16a5b6 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs @@ -0,0 +1,14 @@ +namespace NServiceBus; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class NServiceBusSendOnlyEndpointAttribute : Attribute +{ + public NServiceBusSendOnlyEndpointAttribute() { } + + public NServiceBusSendOnlyEndpointAttribute(string endpointName) + { + EndpointName = endpointName; + } + + public string? EndpointName { get; } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs new file mode 100644 index 0000000..a2dc89f --- /dev/null +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs @@ -0,0 +1,3 @@ +namespace NServiceBus; + +public record SendOnlyManifest(string Name, IEndpointConfiguration EndpointConfiguration); \ No newline at end of file From 8e585fd95c7efa9d6db4540222106135f67343fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Sat, 14 Feb 2026 07:28:02 +0100 Subject: [PATCH 27/41] Emit generated files --- src/IntegrationTest.Billing/IntegrationTest.Billing.csproj | 2 ++ src/IntegrationTest.Sales/IntegrationTest.Sales.csproj | 2 ++ src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/IntegrationTest.Billing/IntegrationTest.Billing.csproj b/src/IntegrationTest.Billing/IntegrationTest.Billing.csproj index a750c51..b5b139c 100644 --- a/src/IntegrationTest.Billing/IntegrationTest.Billing.csproj +++ b/src/IntegrationTest.Billing/IntegrationTest.Billing.csproj @@ -3,6 +3,8 @@ net10.0 $(InterceptorsPreviewNamespaces);NServiceBus + true + $(BaseIntermediateOutputPath)\GeneratedFiles diff --git a/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj b/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj index a750c51..b5b139c 100644 --- a/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj +++ b/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj @@ -3,6 +3,8 @@ net10.0 $(InterceptorsPreviewNamespaces);NServiceBus + true + $(BaseIntermediateOutputPath)\GeneratedFiles diff --git a/src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj b/src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj index a750c51..b5b139c 100644 --- a/src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj +++ b/src/IntegrationTest.Shipping/IntegrationTest.Shipping.csproj @@ -3,6 +3,8 @@ net10.0 $(InterceptorsPreviewNamespaces);NServiceBus + true + $(BaseIntermediateOutputPath)\GeneratedFiles From 3d7e9ca81481d365cc2c9cb00dd319b40c9708ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Sat, 14 Feb 2026 07:29:33 +0100 Subject: [PATCH 28/41] Consolidate config --- src/IntegrationTest.Shared/EndpointConfigBase.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/IntegrationTest.Shared/EndpointConfigBase.cs b/src/IntegrationTest.Shared/EndpointConfigBase.cs index 131f870..970af5b 100644 --- a/src/IntegrationTest.Shared/EndpointConfigBase.cs +++ b/src/IntegrationTest.Shared/EndpointConfigBase.cs @@ -2,11 +2,5 @@ namespace IntegrationTest.Shared; public abstract class EndpointConfigBase : IEndpointConfiguration { - public virtual void Configure(EndpointConfiguration configuration) - { - configuration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - configuration.EnableInstallers(); - configuration.UsePersistence(); - configuration.UseSerialization(); - } + public virtual void Configure(EndpointConfiguration configuration) => CommonEndpointConfig.Apply(configuration); } \ No newline at end of file From 4b36e3b45eb32672df2ef6eeb26d62bbe5df0f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Sat, 14 Feb 2026 07:42:34 +0100 Subject: [PATCH 29/41] Merge sendonly config into http sender --- src/IntegrationTest/HttpSender.cs | 17 ++++++++++++++++ src/IntegrationTest/SenderEndpoint.cs | 20 ------------------- .../FunctionEndpointGenerator.cs | 15 +++++++++++++- .../NServiceBusSendOnlyEndpointAttribute.cs | 11 +++------- 4 files changed, 34 insertions(+), 29 deletions(-) delete mode 100644 src/IntegrationTest/SenderEndpoint.cs diff --git a/src/IntegrationTest/HttpSender.cs b/src/IntegrationTest/HttpSender.cs index 35ddec3..e6f5773 100644 --- a/src/IntegrationTest/HttpSender.cs +++ b/src/IntegrationTest/HttpSender.cs @@ -9,6 +9,7 @@ namespace IntegrationTest; using Microsoft.Extensions.Logging; using NServiceBus; +[NServiceBusSendOnlyEndpoint(endpointName: "SenderEndpoint", configurationType: typeof(Config))] class HttpSender([FromKeyedServices("SenderEndpoint")] IMessageSession session, ILogger logger) { [Function("HttpSenderV4")] @@ -26,4 +27,20 @@ await r.WriteStringAsync($"{nameof(SubmitOrder)} sent.") .ConfigureAwait(false); return r; } + + public class Config : IEndpointConfiguration + { + public void Configure(EndpointConfiguration configuration) + { + var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) + { + ConnectionName = "AzureWebJobsServiceBus" + }; + + var routing = configuration.UseTransport(transport); + + routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); + configuration.UseSerialization(); + } + } } \ No newline at end of file diff --git a/src/IntegrationTest/SenderEndpoint.cs b/src/IntegrationTest/SenderEndpoint.cs deleted file mode 100644 index 99ea832..0000000 --- a/src/IntegrationTest/SenderEndpoint.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace IntegrationTest; - -using IntegrationTest.Shared; - -[NServiceBusSendOnlyEndpoint] -class SenderEndpoint : IEndpointConfiguration -{ - public void Configure(EndpointConfiguration configuration) - { - var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) - { - ConnectionName = "AzureWebJobsServiceBus" - }; - - var routing = configuration.UseTransport(transport); - - routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); - configuration.UseSerialization(); - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 79037ef..3f877de 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -74,6 +74,7 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta } } } + return results.ToImmutable(); } @@ -107,9 +108,18 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta endpointName = attr.ConstructorArguments[0].Value as string; } + // Extract explicit config type from typeof() argument, if provided + INamedTypeSymbol? explicitConfigType = null; + if (attr.ConstructorArguments.Length > 1) + { + explicitConfigType = attr.ConstructorArguments[1].Value as INamedTypeSymbol; + } + endpointName ??= classSymbol.Name; - var configTypeFullName = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var configType = explicitConfigType ?? classSymbol; + + var configTypeFullName = configType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return new SendOnlyEndpointInfo(endpointName, configTypeFullName); } @@ -155,6 +165,7 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta { queueName = pAttr.ConstructorArguments[0].Value as string; } + foreach (var namedArg in pAttr.NamedArguments) { if (namedArg.Key == "Connection") @@ -162,6 +173,7 @@ static ImmutableArray ExtractFunctionInfos(GeneratorAttributeSynta connectionName = namedArg.Value.Value as string; } } + messageParamName = param.Name; } } @@ -244,6 +256,7 @@ static void GenerateMethodBodies(SourceProductionContext spc, ImmutableArray Date: Sat, 14 Feb 2026 07:48:18 +0100 Subject: [PATCH 30/41] Make the http sender into the sales api endpoint --- .../IntegrationTest.Sales.csproj | 1 + .../SalesApi.cs} | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) rename src/{IntegrationTest/HttpSender.cs => IntegrationTest.Sales/SalesApi.cs} (69%) diff --git a/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj b/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj index b5b139c..a8dd877 100644 --- a/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj +++ b/src/IntegrationTest.Sales/IntegrationTest.Sales.csproj @@ -17,6 +17,7 @@ + diff --git a/src/IntegrationTest/HttpSender.cs b/src/IntegrationTest.Sales/SalesApi.cs similarity index 69% rename from src/IntegrationTest/HttpSender.cs rename to src/IntegrationTest.Sales/SalesApi.cs index e6f5773..a30ea9c 100644 --- a/src/IntegrationTest/HttpSender.cs +++ b/src/IntegrationTest.Sales/SalesApi.cs @@ -1,4 +1,4 @@ -namespace IntegrationTest; +namespace IntegrationTest.Sales; using System.Net; using System.Threading.Tasks; @@ -9,22 +9,22 @@ namespace IntegrationTest; using Microsoft.Extensions.Logging; using NServiceBus; -[NServiceBusSendOnlyEndpoint(endpointName: "SenderEndpoint", configurationType: typeof(Config))] -class HttpSender([FromKeyedServices("SenderEndpoint")] IMessageSession session, ILogger logger) +[NServiceBusSendOnlyEndpoint(configurationType: typeof(Config))] +class SalesApi([FromKeyedServices("SalesApi")] IMessageSession session, ILogger logger) { - [Function("HttpSenderV4")] + [Function("SalesApi")] public async Task Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData req, - FunctionContext executionContext) + FunctionContext executionContext, + CancellationToken cancellationToken) { _ = executionContext; // For now logger.LogInformation("C# HTTP trigger function received a request."); - await session.Send(new SubmitOrder()).ConfigureAwait(false); + await session.Send(new SubmitOrder(), cancellationToken).ConfigureAwait(false); var r = req.CreateResponse(HttpStatusCode.OK); - await r.WriteStringAsync($"{nameof(SubmitOrder)} sent.") - .ConfigureAwait(false); + await r.WriteStringAsync($"{nameof(SubmitOrder)} sent.", cancellationToken).ConfigureAwait(false); return r; } From 75ab4acac57aa6daf94bbf9d67cfbedc85b3e17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 16 Feb 2026 09:11:01 +0100 Subject: [PATCH 31/41] Make sure to start nsb endpoints earlier to make sure that installers run befor function triggers starts to process messages --- .../NServiceBusHostedService.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/NServiceBus.MultiHosting/NServiceBusHostedService.cs b/src/NServiceBus.MultiHosting/NServiceBusHostedService.cs index 7451a29..66f6bb0 100644 --- a/src/NServiceBus.MultiHosting/NServiceBusHostedService.cs +++ b/src/NServiceBus.MultiHosting/NServiceBusHostedService.cs @@ -5,15 +5,22 @@ namespace NServiceBus.MultiHosting; /// /// Hosted service that starts NServiceBus endpoints in multi-endpoint scenarios. /// -public sealed class NServiceBusHostedService(IEndpointStarter endpointStarter) : IHostedService, IAsyncDisposable +public sealed class NServiceBusHostedService(IEndpointStarter endpointStarter) : IHostedLifecycleService, IAsyncDisposable { - public async Task StartAsync(CancellationToken cancellationToken = default) - => await endpointStarter.GetOrStart(cancellationToken).ConfigureAwait(false); + public async Task StartingAsync(CancellationToken cancellationToken) => await endpointStarter.GetOrStart(cancellationToken).ConfigureAwait(false); - public Task StopAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; public async ValueTask DisposeAsync() { await endpointStarter.DisposeAsync().ConfigureAwait(false); } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } From c780ea07464215f7114cae972fda56f55397b581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 16 Feb 2026 09:39:42 +0100 Subject: [PATCH 32/41] Enable logging --- .../Handlers/ProcessPaymentHandler.cs | 8 +++----- src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs | 8 +++----- src/IntegrationTest.Sales/SalesApi.cs | 2 -- src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs | 8 +++----- src/IntegrationTest/Program.cs | 4 ++++ 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs b/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs index fae28d9..ce90a64 100644 --- a/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs +++ b/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs @@ -1,16 +1,14 @@ namespace IntegrationTest.Billing.Handlers; using IntegrationTest.Shared; +using Microsoft.Extensions.Logging; using NServiceBus; -using NServiceBus.Logging; -public class ProcessPaymentHandler : IHandleMessages +public class ProcessPaymentHandler(ILogger logger): IHandleMessages { - static readonly ILog Log = LogManager.GetLogger(); - public async Task Handle(OrderSubmitted message, IMessageHandlerContext context) { - Log.Warn($"Handling {nameof(OrderSubmitted)} in {nameof(ProcessPaymentHandler)}"); + logger.LogWarning($"Handling {nameof(OrderSubmitted)} in {nameof(ProcessPaymentHandler)}"); await context.Publish(new PaymentCleared()).ConfigureAwait(false); } diff --git a/src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs b/src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs index d49688d..43b2811 100644 --- a/src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs +++ b/src/IntegrationTest.Sales/Handlers/AcceptOrderHandler.cs @@ -1,16 +1,14 @@ namespace IntegrationTest.Sales.Handlers; using IntegrationTest.Shared; +using Microsoft.Extensions.Logging; using NServiceBus; -using NServiceBus.Logging; -public class AcceptOrderHandler : IHandleMessages +public class AcceptOrderHandler(ILogger logger) : IHandleMessages { - static readonly ILog Log = LogManager.GetLogger(); - public async Task Handle(SubmitOrder message, IMessageHandlerContext context) { - Log.Warn($"Handling {nameof(SubmitOrder)} in {nameof(AcceptOrderHandler)}"); + logger.LogWarning($"Handling {nameof(SubmitOrder)} in {nameof(AcceptOrderHandler)}"); await context.Publish(new OrderSubmitted()).ConfigureAwait(false); } diff --git a/src/IntegrationTest.Sales/SalesApi.cs b/src/IntegrationTest.Sales/SalesApi.cs index a30ea9c..fb2fae4 100644 --- a/src/IntegrationTest.Sales/SalesApi.cs +++ b/src/IntegrationTest.Sales/SalesApi.cs @@ -15,10 +15,8 @@ class SalesApi([FromKeyedServices("SalesApi")] IMessageSession session, ILogger< [Function("SalesApi")] public async Task Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData req, - FunctionContext executionContext, CancellationToken cancellationToken) { - _ = executionContext; // For now logger.LogInformation("C# HTTP trigger function received a request."); await session.Send(new SubmitOrder(), cancellationToken).ConfigureAwait(false); diff --git a/src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs b/src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs index 7c1422e..18ac333 100644 --- a/src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs +++ b/src/IntegrationTest.Shipping/Handlers/ShipOrderHandler.cs @@ -1,16 +1,14 @@ namespace IntegrationTest.Shipping.Handlers; using IntegrationTest.Shared; +using Microsoft.Extensions.Logging; using NServiceBus; -using NServiceBus.Logging; -public class ShipOrderHandler : IHandleMessages +public class ShipOrderHandler(ILogger logger) : IHandleMessages { - static readonly ILog Log = LogManager.GetLogger(); - public Task Handle(PaymentCleared message, IMessageHandlerContext context) { - Log.Warn($"Handling {nameof(PaymentCleared)} in {nameof(ShipOrderHandler)}"); + logger.LogWarning($"Handling {nameof(PaymentCleared)} in {nameof(ShipOrderHandler)}"); return Task.CompletedTask; } diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index f5b411e..b8fa7ba 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -1,8 +1,12 @@ using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; var builder = FunctionsApplication.CreateBuilder(args); +builder.Logging.ClearProviders(); +builder.Logging.AddSimpleConsole(options =>options.IncludeScopes = true); +builder.Logging.SetMinimumLevel(LogLevel.Information); builder.AddNServiceBusFunctions(); var host = builder.Build(); From 6cd55b594bb738df1cbde170ce1601ac9ad33539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 16 Feb 2026 20:34:11 +0100 Subject: [PATCH 33/41] remove http ref --- src/IntegrationTest/IntegrationTest.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/IntegrationTest/IntegrationTest.csproj b/src/IntegrationTest/IntegrationTest.csproj index 9bc4e35..284a41b 100644 --- a/src/IntegrationTest/IntegrationTest.csproj +++ b/src/IntegrationTest/IntegrationTest.csproj @@ -22,7 +22,6 @@ - From 01686b78ab9743b452c0ac9127f1e168c564aab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 17 Feb 2026 08:49:17 +0100 Subject: [PATCH 34/41] Make sure we have a logging scope with the endpoint name present when invoction the pipeline --- src/IntegrationTest/Program.cs | 9 +++++++-- .../FunctionEndpointGenerator.cs | 2 +- .../MessageProcessor.cs | 13 ++++++++++--- .../TransportWrapper/IMessageProcessor.cs | 4 +++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index b8fa7ba..6f05e1a 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -5,10 +6,14 @@ var builder = FunctionsApplication.CreateBuilder(args); builder.Logging.ClearProviders(); -builder.Logging.AddSimpleConsole(options =>options.IncludeScopes = true); +builder.Logging.AddJsonConsole(o => +{ + o.IncludeScopes = true; + o.JsonWriterOptions = new JsonWriterOptions { Indented = true }; +}); builder.Logging.SetMinimumLevel(LogLevel.Information); builder.AddNServiceBusFunctions(); var host = builder.Build(); -await host.RunAsync().ConfigureAwait(false); +await host.RunAsync().ConfigureAwait(false); \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 3f877de..cc1e4c9 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -268,7 +268,7 @@ static void GenerateMethodBodies(SourceProductionContext spc, ImmutableArray { ["Endpoint"] = endpointName }); + using var _ = MultiEndpointLoggerFactory.Instance.PushName(endpointStarter.ServiceKey); + await endpointStarter.GetOrStart(cancellationToken).ConfigureAwait(false); if (transport.MessageProcessor is null) { // This should never happen but we need to protect against it anyways - throw new InvalidOperationException( - $"This endpoint cannot process messages because it is configured in send-only mode."); + throw new InvalidOperationException($"Endpoint {endpointName} cannot process messages because it is configured in send-only mode."); } await transport.MessageProcessor.Process(message, cancellationToken).ConfigureAwait(false); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs index e13c73a..3774bc2 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs @@ -3,7 +3,9 @@ namespace NServiceBus.AzureFunctions.AzureServiceBus; using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; +using Microsoft.Azure.Functions.Worker; + public interface IMessageProcessor { - Task Process(ServiceBusReceivedMessage message, CancellationToken cancellationToken = default); + Task Process(ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default); } \ No newline at end of file From e85a6f3bea893cee95d54520efbb1f217233d154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 17 Feb 2026 12:53:29 +0100 Subject: [PATCH 35/41] Get rid of unneded abstraction --- .../FunctionEndpointGenerator.cs | 2 +- .../FunctionsHostApplicationBuilderExtensions.cs | 2 +- .../MessageProcessor.cs | 2 +- .../Serverless/TransportWrapper/IMessageProcessor.cs | 11 ----------- 4 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index cc1e4c9..6ebabbd 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -263,7 +263,7 @@ static void GenerateMethodBodies(SourceProductionContext spc, ImmutableArray(\"{func.FunctionName}\");"); + sb.AppendLine($" .GetKeyedService(\"{func.FunctionName}\");"); sb.AppendLine(" if (processor is null)"); sb.AppendLine(" {"); sb.AppendLine($" throw new global::System.InvalidOperationException(\"{func.FunctionName} has not been registered.\");"); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index 102ee2f..da1a770 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -40,7 +40,7 @@ public static void AddNServiceBusFunction( transport.ConnectionName = functionManifest.ConnectionName; - builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); + builder.Services.AddKeyedSingleton(endpointName, (sp, _) => new MessageProcessor(transport, sp.GetRequiredKeyedService(endpointName))); }); } diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs index 11d2ff7..c164163 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/MessageProcessor.cs @@ -6,7 +6,7 @@ namespace NServiceBus.AzureFunctions.AzureServiceBus; using Microsoft.Azure.Functions.Worker; using NServiceBus.MultiHosting; -public class MessageProcessor(AzureServiceBusServerlessTransport transport, EndpointStarter endpointStarter) : IMessageProcessor +public class MessageProcessor(AzureServiceBusServerlessTransport transport, EndpointStarter endpointStarter) { public async Task Process(ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default) { diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs deleted file mode 100644 index 3774bc2..0000000 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/IMessageProcessor.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace NServiceBus.AzureFunctions.AzureServiceBus; - -using System.Threading; -using System.Threading.Tasks; -using Azure.Messaging.ServiceBus; -using Microsoft.Azure.Functions.Worker; - -public interface IMessageProcessor -{ - Task Process(ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default); -} \ No newline at end of file From 9671f13f304a8f8ff68fa00951912fc2978fe2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 17 Feb 2026 12:55:19 +0100 Subject: [PATCH 36/41] Cleanup --- .../TransportWrapper/PipelineInvokingMessageProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs index 4c87e16..ad95716 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/Serverless/TransportWrapper/PipelineInvokingMessageProcessor.cs @@ -4,7 +4,6 @@ namespace NServiceBus.AzureFunctions.AzureServiceBus.Serverless.TransportWrapper using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; -using Microsoft.Azure.Functions.Worker; using NServiceBus.Extensibility; using NServiceBus.Transport; using NServiceBus.Transport.AzureServiceBus; From 1680a4c6580bf9d15e958215490ed69529247211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 18 Feb 2026 13:23:09 +0100 Subject: [PATCH 37/41] Skip the interface and go with conventional method names instead --- .../BillingFunctions.cs | 26 +++++++------------ src/IntegrationTest.Sales/SalesApi.cs | 23 +++++++--------- src/IntegrationTest.Sales/SalesEndpoint.cs | 8 +++--- .../EndpointConfigBase.cs | 6 ----- .../ShippingEndpoint.cs | 4 +-- .../FunctionCompositionGenerator.cs | 2 +- .../FunctionEndpointGenerator.cs | 4 +-- .../FunctionManifest.cs | 2 +- ...nctionsHostApplicationBuilderExtensions.cs | 2 +- .../IEndpointConfiguration.cs | 6 ----- .../NServiceBusSendOnlyEndpointAttribute.cs | 4 +-- .../SendOnlyManifest.cs | 2 +- 12 files changed, 32 insertions(+), 57 deletions(-) delete mode 100644 src/IntegrationTest.Shared/EndpointConfigBase.cs delete mode 100644 src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs diff --git a/src/IntegrationTest.Billing/BillingFunctions.cs b/src/IntegrationTest.Billing/BillingFunctions.cs index c192fe6..a35c97f 100644 --- a/src/IntegrationTest.Billing/BillingFunctions.cs +++ b/src/IntegrationTest.Billing/BillingFunctions.cs @@ -8,36 +8,30 @@ namespace IntegrationTest.Billing; public partial class BillingFunctions { [Function("BillingApi")] - [NServiceBusFunction(typeof(ApiConfig))] - public partial Task Api( + [NServiceBusFunction] + public partial Task BillingApi( [ServiceBusTrigger("billing-api", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default); - public class ApiConfig : IEndpointConfiguration + public static void ConfigureBillingApi(EndpointConfiguration configuration) { - public void Configure(EndpointConfiguration configuration) - { - CommonEndpointConfig.Apply(configuration); - configuration.AddHandler(); - } + CommonEndpointConfig.Apply(configuration); + configuration.AddHandler(); } [Function("BillingBackend")] - [NServiceBusFunction(typeof(BackendConfig))] - public partial Task Backend( + [NServiceBusFunction] + public partial Task BillingBackend( [ServiceBusTrigger("billing-backend", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default); - public class BackendConfig : IEndpointConfiguration + public static void ConfigureBillingBackend(EndpointConfiguration configuration) { - public void Configure(EndpointConfiguration configuration) - { - CommonEndpointConfig.Apply(configuration); - // different handlers for the backend queue - } + CommonEndpointConfig.Apply(configuration); + // different handlers for the backend queue } } \ No newline at end of file diff --git a/src/IntegrationTest.Sales/SalesApi.cs b/src/IntegrationTest.Sales/SalesApi.cs index fb2fae4..201f989 100644 --- a/src/IntegrationTest.Sales/SalesApi.cs +++ b/src/IntegrationTest.Sales/SalesApi.cs @@ -9,12 +9,13 @@ namespace IntegrationTest.Sales; using Microsoft.Extensions.Logging; using NServiceBus; -[NServiceBusSendOnlyEndpoint(configurationType: typeof(Config))] +[NServiceBusSendOnlyEndpoint] class SalesApi([FromKeyedServices("SalesApi")] IMessageSession session, ILogger logger) { [Function("SalesApi")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData req, + public async Task Api( + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] + HttpRequestData req, CancellationToken cancellationToken) { logger.LogInformation("C# HTTP trigger function received a request."); @@ -26,19 +27,13 @@ public async Task Run( return r; } - public class Config : IEndpointConfiguration + public static void ConfigureSalesApi(EndpointConfiguration configuration) { - public void Configure(EndpointConfiguration configuration) - { - var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) - { - ConnectionName = "AzureWebJobsServiceBus" - }; + var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) { ConnectionName = "AzureWebJobsServiceBus" }; - var routing = configuration.UseTransport(transport); + var routing = configuration.UseTransport(transport); - routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); - configuration.UseSerialization(); - } + routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); + configuration.UseSerialization(); } } \ No newline at end of file diff --git a/src/IntegrationTest.Sales/SalesEndpoint.cs b/src/IntegrationTest.Sales/SalesEndpoint.cs index b377091..a2a230b 100644 --- a/src/IntegrationTest.Sales/SalesEndpoint.cs +++ b/src/IntegrationTest.Sales/SalesEndpoint.cs @@ -6,18 +6,18 @@ namespace IntegrationTest.Sales; // Cleanest pattern for single-function endpoints [NServiceBusFunction] -public partial class SalesEndpoint : EndpointConfigBase +public partial class SalesEndpoint { [Function("Sales")] - public partial Task ProcessMessage( + public partial Task Sales( [ServiceBusTrigger("sales", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default); - public override void Configure(EndpointConfiguration configuration) + public static void ConfigureSales(EndpointConfiguration configuration) { - base.Configure(configuration); + CommonEndpointConfig.Apply(configuration); configuration.AddHandler(); } } \ No newline at end of file diff --git a/src/IntegrationTest.Shared/EndpointConfigBase.cs b/src/IntegrationTest.Shared/EndpointConfigBase.cs deleted file mode 100644 index 970af5b..0000000 --- a/src/IntegrationTest.Shared/EndpointConfigBase.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace IntegrationTest.Shared; - -public abstract class EndpointConfigBase : IEndpointConfiguration -{ - public virtual void Configure(EndpointConfiguration configuration) => CommonEndpointConfig.Apply(configuration); -} \ No newline at end of file diff --git a/src/IntegrationTest.Shipping/ShippingEndpoint.cs b/src/IntegrationTest.Shipping/ShippingEndpoint.cs index 2cf5493..6fd92d1 100644 --- a/src/IntegrationTest.Shipping/ShippingEndpoint.cs +++ b/src/IntegrationTest.Shipping/ShippingEndpoint.cs @@ -5,7 +5,7 @@ namespace IntegrationTest.Shipping; using Microsoft.Azure.Functions.Worker; [NServiceBusFunction] -public partial class ShippingEndpoint : IEndpointConfiguration +public partial class ShippingEndpoint { [Function("Shipping")] public partial Task ProcessMessage( @@ -14,7 +14,7 @@ public partial Task ProcessMessage( FunctionContext functionContext, CancellationToken cancellationToken = default); - public void Configure(EndpointConfiguration endpoint) + public static void Configure(EndpointConfiguration endpoint) { CommonEndpointConfig.Apply(endpoint); endpoint.AddHandler(); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs index f1ecde2..560c8cf 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs @@ -179,7 +179,7 @@ static void GenerateInterceptorCode(SourceProductionContext spc, InterceptorData foreach (var regClass in regClasses) { sb.AppendLine($" foreach (var m in global::{regClass.FullClassName}.GetSendOnlyManifests())"); - sb.AppendLine($" global::NServiceBus.FunctionsHostApplicationBuilderExtensions.AddSendOnlyNServiceBusEndpoint(builder, m.Name, m.EndpointConfiguration.Configure);"); + sb.AppendLine($" global::NServiceBus.FunctionsHostApplicationBuilderExtensions.AddSendOnlyNServiceBusEndpoint(builder, m.Name, m.EndpointConfiguration);"); } sb.AppendLine(" }"); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 6ebabbd..7db7680 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -297,7 +297,7 @@ static void GenerateRegistration(SourceProductionContext spc, ImmutableArray{func.ConfigTypeFullName}.Configure{func.FunctionName}(ec));"); } sb.AppendLine(" yield break;"); @@ -310,7 +310,7 @@ static void GenerateRegistration(SourceProductionContext spc, ImmutableArray{endpoint.ConfigTypeFullName}.Configure{endpoint.EndpointName}(ec));"); } sb.AppendLine(" yield break;"); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs index 4bca156..a951618 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionManifest.cs @@ -1,3 +1,3 @@ namespace NServiceBus; -public record FunctionManifest(string Name, string Queue, string ConnectionName, IEndpointConfiguration EndpointConfiguration); \ No newline at end of file +public record FunctionManifest(string Name, string Queue, string ConnectionName, Action EndpointConfiguration); \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs index da1a770..053925d 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/FunctionsHostApplicationBuilderExtensions.cs @@ -23,7 +23,7 @@ public static void AddNServiceBusFunction( var endpointName = functionManifest.Name; builder.AddNServiceBusEndpoint(endpointName, endpointConfiguration => { - functionManifest.EndpointConfiguration.Configure(endpointConfiguration); + functionManifest.EndpointConfiguration(endpointConfiguration); var settings = endpointConfiguration.GetSettings(); if (settings.GetOrDefault(AzureServiceBusServerlessTransport.SendOnlyConfigKey)) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs deleted file mode 100644 index ee3aced..0000000 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/IEndpointConfiguration.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NServiceBus; - -public interface IEndpointConfiguration -{ - void Configure(EndpointConfiguration configuration); -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs index 19f19fc..8ed54df 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusSendOnlyEndpointAttribute.cs @@ -1,9 +1,7 @@ namespace NServiceBus; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class NServiceBusSendOnlyEndpointAttribute(string? endpointName = null, Type? configurationType = null) : Attribute +public class NServiceBusSendOnlyEndpointAttribute(string? endpointName = null) : Attribute { public string? EndpointName { get; } = endpointName; - - public Type? ConfigurationType { get; } = configurationType; } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs index a2dc89f..d364457 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/SendOnlyManifest.cs @@ -1,3 +1,3 @@ namespace NServiceBus; -public record SendOnlyManifest(string Name, IEndpointConfiguration EndpointConfiguration); \ No newline at end of file +public record SendOnlyManifest(string Name, Action EndpointConfiguration); \ No newline at end of file From 22daf947814cb79f33c46a61d9993c729c3410ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 18 Feb 2026 13:27:04 +0100 Subject: [PATCH 38/41] Fixup --- src/IntegrationTest.Shipping/ShippingEndpoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IntegrationTest.Shipping/ShippingEndpoint.cs b/src/IntegrationTest.Shipping/ShippingEndpoint.cs index 6fd92d1..e259c64 100644 --- a/src/IntegrationTest.Shipping/ShippingEndpoint.cs +++ b/src/IntegrationTest.Shipping/ShippingEndpoint.cs @@ -14,7 +14,7 @@ public partial Task ProcessMessage( FunctionContext functionContext, CancellationToken cancellationToken = default); - public static void Configure(EndpointConfiguration endpoint) + public static void ConfigureShipping(EndpointConfiguration endpoint) { CommonEndpointConfig.Apply(endpoint); endpoint.AddHandler(); From 51afc5079f5dd4188cb9cde83ecb362faa2bccf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 18 Feb 2026 13:28:48 +0100 Subject: [PATCH 39/41] Cleanup --- .../Handlers/ProcessPaymentHandler.cs | 2 +- src/IntegrationTest/Program.cs | 2 +- .../NServiceBusFunctionAttribute.cs | 12 +----------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs b/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs index ce90a64..dc7ff98 100644 --- a/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs +++ b/src/IntegrationTest.Billing/Handlers/ProcessPaymentHandler.cs @@ -4,7 +4,7 @@ namespace IntegrationTest.Billing.Handlers; using Microsoft.Extensions.Logging; using NServiceBus; -public class ProcessPaymentHandler(ILogger logger): IHandleMessages +public class ProcessPaymentHandler(ILogger logger) : IHandleMessages { public async Task Handle(OrderSubmitted message, IMessageHandlerContext context) { diff --git a/src/IntegrationTest/Program.cs b/src/IntegrationTest/Program.cs index 6f05e1a..0ed9071 100644 --- a/src/IntegrationTest/Program.cs +++ b/src/IntegrationTest/Program.cs @@ -16,4 +16,4 @@ var host = builder.Build(); -await host.RunAsync().ConfigureAwait(false); \ No newline at end of file +await host.RunAsync().ConfigureAwait(false); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs index 52db328..e78586b 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/NServiceBusFunctionAttribute.cs @@ -1,14 +1,4 @@ namespace NServiceBus; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public class NServiceBusFunctionAttribute : Attribute -{ - public NServiceBusFunctionAttribute() { } - - public NServiceBusFunctionAttribute(Type configurationType) - { - ConfigurationType = configurationType; - } - - public Type? ConfigurationType { get; } -} \ No newline at end of file +public class NServiceBusFunctionAttribute : Attribute; \ No newline at end of file From e5f34c88a5d5678767347e0cb02eca6a6ed25dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 18 Feb 2026 13:31:25 +0100 Subject: [PATCH 40/41] Use nameof --- src/IntegrationTest.Shipping/ShippingEndpoint.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IntegrationTest.Shipping/ShippingEndpoint.cs b/src/IntegrationTest.Shipping/ShippingEndpoint.cs index e259c64..92ea9db 100644 --- a/src/IntegrationTest.Shipping/ShippingEndpoint.cs +++ b/src/IntegrationTest.Shipping/ShippingEndpoint.cs @@ -7,8 +7,8 @@ namespace IntegrationTest.Shipping; [NServiceBusFunction] public partial class ShippingEndpoint { - [Function("Shipping")] - public partial Task ProcessMessage( + [Function(nameof(Shipping))] + public partial Task Shipping( [ServiceBusTrigger("shipping", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] ServiceBusReceivedMessage message, FunctionContext functionContext, From 83b0fbae5425438e00a7ffd3fc5418236710a5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 18 Feb 2026 13:33:17 +0100 Subject: [PATCH 41/41] Demo use of default connection --- src/IntegrationTest.Shipping/ShippingEndpoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IntegrationTest.Shipping/ShippingEndpoint.cs b/src/IntegrationTest.Shipping/ShippingEndpoint.cs index 92ea9db..25d66b0 100644 --- a/src/IntegrationTest.Shipping/ShippingEndpoint.cs +++ b/src/IntegrationTest.Shipping/ShippingEndpoint.cs @@ -9,7 +9,7 @@ public partial class ShippingEndpoint { [Function(nameof(Shipping))] public partial Task Shipping( - [ServiceBusTrigger("shipping", Connection = "AzureWebJobsServiceBus", AutoCompleteMessages = true)] + [ServiceBusTrigger("shipping", AutoCompleteMessages = true)] ServiceBusReceivedMessage message, FunctionContext functionContext, CancellationToken cancellationToken = default);