What's new with NServiceBus and Azure Functions
Do you think Azure Functions are pretty great? Us too! Do you hate boilerplate code? Yeah, us too.
Have you heard of C# source generators 1 and thought they sounded pretty cool but didn’t really know how they could be useful?
In the newest version of our Azure Functions integration, we’ve used source generators to reduce the boilerplate needed to set up an NServiceBus endpoint on Azure Service Bus down to just a few lines of code.
Now, this code is all it takes to write transactionally consistent NServiceBus handlers inside of your Azure Function project.
[assembly: FunctionsStartup(typeof(Startup))]
[assembly: NServiceBusTriggerFunction("MyEndpoint", SendsAtomicWithReceive = true)]
class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder) =>
builder.UseNServiceBus();
}
From those attributes, we’ll generate an Azure Function trigger, wire it up to an Azure Service Bus queue, and manage flowing transactions around for you. All you have to do is supply the business logic. Intrigued? Let’s dive in to see what’s new with NServiceBus and Azure Functions.
🔗Automatic trigger function generation
Both NServiceBus and Azure Functions provide abstractions over receiving and handling messages from an Azure Service Bus queue. To get them working together, we need a bit of boilerplate code to create a functions trigger that passes everything needed to NServiceBus. In the 1.0 release, that looked something like this:
using System.Threading.Tasks;
using Microsoft.Azure.ServiceBus;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using NServiceBus;
class FunctionEndpointTrigger
{
readonly IFunctionEndpoint endpoint;
public FunctionEndpointTrigger(IFunctionEndpoint endpoint)
{
this.endpoint = endpoint;
}
[FunctionName("NServiceBusFunctionEndpointTrigger-ASBTriggerQueue")]
public async Task Run(
[ServiceBusTrigger(queueName: "ASBTriggerQueue")]
Message message,
ILogger logger,
ExecutionContext executionContext)
{
await endpoint.Process(message, executionContext, logger);
}
}
We didn’t like all that boilerplate, so we picked out the important details that need to be configured, and we used source generators to allow you to create an Azure Function trigger that maps to an NServiceBus endpoint with a simple attribute.
[assembly: NServiceBusTriggerFunction("ASBTriggerQueue")]
Dropping this attribute into your Azure Functions project will automatically generate the same common code shown above at compile time. Then, you can delete your custom trigger altogether. We believe that most of the code in your project should be business logic for handling messages.
You can read more about this feature in our documentation.
🔗Consistent messaging
We also used source generators to make it really easy to use the transactional processing in Azure Service Bus that gives you consistency between your incoming and outgoing messages.
Imagine a message handler that looks like this:
class ProcessOrderMessageHandler : IHandleMessages<ProcessOrder>
{
public async Task Handle(ProcessOrder message, MessageHandlerContext context)
{
await context.Send(new BillOrder { OrderId = message.OrderId });
await context.Send(new CreateShippingLabel { OrderId = message.OrderId });
await context.Publish(new OrderAccepted { OrderId = message.OrderId });
}
}
When everything is working, this handler is fine. A ProcessOrder
message comes in, and three messages are produced: a BillOrder
command, a CreateShippingLabel
command, and an OrderAccepted
event.
But what happens if one of those messages fails to be sent? What if BillOrder
and CreateShippingLabel
are sent, but something goes wrong, and the OrderAccepted
event cannot be published? This can be caused by anything from a missing event topic to a momentary network glitch.
If left unchecked, this situation will result in duplicate BillOrder
and CreateShippingLabel
messages being sent each time the ProcessOrder
handler is retried.
We definitely do not want to bill the customer multiple times nor ship them multiple orders. What we want is for the entire operation to succeed or fail atomically. Either all three outgoing messages are produced, or none of them are. If they are produced, then the incoming message should be marked as complete.
Getting this right is not always easy. You need to make sure the incoming message and all of the outgoing messages use the same transaction, and you need to ensure that the message will not be auto-completed by the incoming Service Bus Trigger. Getting it wrong can lead to some subtle bugs that are difficult to detect in a production environment.
So, we made it easy to get it right with just one line of code. If you want to enable transactional consistency between incoming and outgoing messages in your Azure Function NServiceBus endpoint, just tell us, and we’ll take care of the rest:
[assembly: NServiceBusTriggerFunction("MyEndpoint", SendsAtomicWithReceive = true)]
This enables the sends atomic with receive transport transaction mode for ServiceBus and integrates it correctly with the Azure Functions host. You can read more about this feature in our documentation.
🔗IConfiguration
By embracing the Microsoft IConfguration
API, we’ve made the setup of your Azure Functions endpoint even simpler.
If you have used any of the new hosting models from Microsoft, you probably are familiar with the new IConfiguration
API. This interface allows you to load configuration from files, environment variables, and other sources. This interface is available in Azure Functions as well, as shown here:
public class Startup : FunctionsStartup
{
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
FunctionsHostBuilderContext context = builder.GetContext();
var jsonConfig = Path.Combine(context.ApplicationRootPath, "appsettings.json");
builder.ConfigurationBuilder
.AddJsonFile(jsonConfig, optional: true, reloadOnChange: false)
.AddEnvironmentVariables();
}
}
The new release of NServiceBus Azure Functions embraces this configuration interface, so you can use that to configure your endpoints. This is a lot simpler:
public override void Configure(IFunctionsHostBuilder builder)
{
var configuration = builder.GetContext().Configuration;
builder.UseNServiceBus(() =>
new ServiceBusTriggeredEndpointConfiguration("NServiceBusFunctionEndpoint", configuration));
}
This will automatically look up configuration settings such as connections strings and license info directly from the configured sources. In fact, if you add a configuration variable called ENDPOINT_NAME
, you can shrink your endpoint configuration to just this:
public override void Configure(IFunctionsHostBuilder builder) =>
builder.UseNServiceBus();
Of course, if you don’t want to use IConfiguration
to configure the endpoint or would rather do things the old way, the manual configuration overloads still exist:
public override void Configure(IFunctionsHostBuilder builder)
{
var endpointConfig = new ServiceBusTriggeredEndpointConfiguration("NServiceBusFunctionEndpoint");
var transport = endpointConfig.Transport;
transport.ConnectionString("MyConnectionString");
builder.UseNServiceBus(cfg => endpointConfig);
}
🔗Summary
You hate boilerplate, and so do we. So with the power of C# source generators in our newest version of Azure Functions, you can shrink your endpoint configuration from a couple dozen lines of code (or more!) to a couple assembly-level attributes and a tiny Startup
class containing the bare essentials:
[assembly: FunctionsStartup(typeof(Startup))]
[assembly: NServiceBusTriggerFunction("MyEndpoint", SendsAtomicWithReceive = true)]
class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder) =>
builder.UseNServiceBus();
}
If only we could do that for all your code.
To check out NServiceBus on Azure Functions, check out our sample, Using NServiceBus in Azure Functions with Service Bus triggers.
A source generator is a new type of Roslyn analyzer that runs during compilation, inspects the code you're building, and produces additional source files that are compiled together with the rest of your code. Check out the blog post introducing source generators or the Microsoft source generator docs for more info.