This sample shows how to host NServiceBus within an Azure Function, in this case, a function triggered by an incoming Service Bus message. This enables hosting message handlers in Azure Functions, gaining the abstraction of message handlers implemented using IHandleMessages
and also taking advantage of NServiceBus's extensible message processing pipeline.
The sample demonstrates two configuration approaches that achieve the same outcome:
- Integrating with the
IFunctionHostBuilder
, including the host managed DI container. - Configuring the endpoint inside the trigger class as a static field.
When hosting NServiceBus within Azure Functions, each Function (as identified by the [FunctionName]
attribute) hosts an NServiceBus endpoint that is capable of processing different message types.
The Azure Functions SDK enforces certain constraints that are also applied to NServiceBus endpoints. Review these constraints before running the sample.
Prerequisites
Unlike a traditional NServiceBus endpoint, an endpoint hosted in Azure Functions cannot create its own input queue. In this sample, that queue name is ASBTriggerQueue
.
To create the queue with the Azure CLI, execute the following Azure CLI command:
az servicebus queue create --name ASBTriggerQueue --namespace-name <asb-namespace-to-use> --resource-group <resource-group-containing-namespace>
To use the sample, a valid Service Bus connection string must be provided in the local.
file.
Sample structure
The sample contains the following projects:
AzureFunctions.
- UsingASBTrigger. FunctionsHostBuilder IFunctionHostBuilder
approach to host the NServiceBus endpointAzureFunctions.
- Using static approach to host the NServiceBus endpointASBTrigger. Static
AzureFunctions.ASBTrigger.FunctionsHostBuilder
and AzureFunctions.ASBTrigger.Static
are both using the same trigger queue and should not be executed simultaneously.Running the sample
Each Functions project contains two functions:
- Service Bus-triggered function.
- HTTP-triggered function.
Running the sample will launch the Azure Functions runtime window.
To try the Azure Function:
- Open a browser and navigate to
http:/
. The port number might be different and will be indicated when the function project is started./ localhost:7071/ api/ HttpSender - The queue-triggered function will receive the
TriggerMessage
and process it with NServiceBus. - The NServiceBus message handler for
TriggerMessage
sends aFollowUpMessage
. - The queue-triggered function will receive the
FollowUpMessage
and process it with NServiceBus.
Code walk-through
IFunctionHostBuilder
approach
The NServiceBus endpoint configured using IFunctionHostBuilder
is using the convention and is wired using Startup
class like this:
[assembly: FunctionsStartup(typeof(Startup))]
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
var services = builder.Services;
// register custom service in the container
services.AddSingleton(_ =>
{
var configurationRoot = builder.GetContext().Configuration;
var customComponentInitializationValue = configurationRoot.GetValue<string>("CustomComponentValue");
return new CustomComponent(customComponentInitializationValue);
});
builder.UseNServiceBus(() =>
{
var configuration = new ServiceBusTriggeredEndpointConfiguration(AzureServiceBusTriggerFunction.EndpointName);
// optional: log startup diagnostics using Functions provided logger
configuration.LogDiagnostics();
return configuration;
});
}
}
IFunctionEndpoint
is then injected into the function class:
public AzureServiceBusTriggerFunction(IFunctionEndpoint endpoint)
{
this.endpoint = endpoint;
}
And is invoked in the following manner:
[FunctionName(EndpointName)]
public async Task Run(
[ServiceBusTrigger(queueName: EndpointName)]
Message message,
ILogger logger,
ExecutionContext executionContext)
{
await endpoint.Process(message, executionContext, logger);
}
Static approach
The static NServiceBus endpoint must be configured using details that come from the Azure Functions ExecutionContext
. Since that is not available until a message is handled by the function, the NServiceBus endpoint instance is deferred until the first message is processed, using a lambda expression like this:
private static readonly IFunctionEndpoint endpoint = new FunctionEndpoint(executionContext =>
{
// endpoint name, and connection strings are automatically derived from FunctionName and ServiceBusTrigger attributes
var configuration = ServiceBusTriggeredEndpointConfiguration.FromAttributes();
// optional: log startup diagnostics using Functions provided logger
configuration.LogDiagnostics();
// register custom service in the container
configuration.AdvancedConfiguration.RegisterComponents(r => r.ConfigureComponent(() =>
{
var customComponentInitializationValue = Environment.GetEnvironmentVariable("CustomComponentValue");
return new CustomComponent(customComponentInitializationValue);
}, DependencyLifecycle.SingleInstance));
return configuration;
});
The same class defines the Azure Function which makes up the hosting for the NServiceBus endpoint. The Function hands off processing of the message to NServiceBus:
[FunctionName(EndpointName)]
public static async Task Run(
[ServiceBusTrigger(queueName: EndpointName)]
Message message,
ILogger logger,
ExecutionContext executionContext)
{
await endpoint.Process(message, executionContext, logger);
}
Meanwhile, the message handlers for TriggerMessage
and FollowUpMessage
, also hosted within the Azure Functions project, are normal NServiceBus message handlers, which are also capable of sending messages themselves.
Handlers
Both approaches use the same message handlers, with a CustomDependency
passed in.
public class TriggerMessageHandler : IHandleMessages<TriggerMessage>
{
static readonly ILog Log = LogManager.GetLogger<TriggerMessageHandler>();
readonly CustomComponent customComponent;
public TriggerMessageHandler(CustomComponent customComponent)
{
this.customComponent = customComponent;
}
public Task Handle(TriggerMessage message, IMessageHandlerContext context)
{
Log.Warn($"Handling {nameof(TriggerMessage)} in {nameof(TriggerMessageHandler)}");
Log.Warn($"Custom component returned: {customComponent.GetValue()}");
return context.SendLocal(new FollowupMessage());
}
}
public class FollowupMessageHandler : IHandleMessages<FollowupMessage>
{
static readonly ILog Log = LogManager.GetLogger<FollowupMessageHandler>();
readonly CustomComponent customComponent;
public FollowupMessageHandler(CustomComponent customComponent)
{
this.customComponent = customComponent;
}
public Task Handle(FollowupMessage message, IMessageHandlerContext context)
{
Log.Warn($"Handling {nameof(FollowupMessage)} in {nameof(FollowupMessageHandler)}.");
Log.Warn($"Custom component returned: {customComponent.GetValue()}");
return Task.CompletedTask;
}
}