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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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")]