Picture: Microsoft

Azure Functions with MediatR and Dependency Injection

MediatR

MediatR is one of the most popular libraries in the .NET environment and describes itself as "Simple, unambitious mediator implementation in .NET". In the end, MediatR is a perfect library for in-process message handling in terms of the Mediator pattern.

Azure Functions

Azure Functions is a great, lean technology, but does not have Dependency Injection on board by default. In the meantime, however, thanks to FunctionsStartup it is possible to use this anyway.

Commands and Command Handlers

MediatR works according to the mediator pattern with the separation of the respective responsibilities; e.g. the separation into commands and command handlers. This allows maximum independence to be achieved.

In code form, for example, it looks like this:

First, we have a command that's supposed to do something. For example, we simply send something by mail.

public class EMailSendCommand : IRequest<bool>
{
    public string ToAddress { get; }
    public string ToUsername { get; }
    public IMailTemplate MailTemplate { get; }

    public EMailSendCommand(string toAddress, string toUsername, IMailTemplate mailTemplate)
    {
        ToAddress = toAddress;
        ToUsername = toUsername;
        MailTemplate = mailTemplate;
    }
}

The description of the command (in MediatR language a "Request") is that we now have certain parameters that are necessary to send the mail and the return of the command is a bool. Everybody who wants to send an e-mail needs only this command. The exact knowledge of how or with what the mail is sent is not necessary.

The counterpart to the Request is the RequestHandler: this is where the implementation takes place, which is to be executed and, if necessary, returned when a corresponding event occurs.

public class EMailSendCommandHandler : IRequestHandler<EMailSendCommand, bool>
{
    private readonly IMailProvider _mailProvider;

    public DTSensorTemperatureRangeValidationNotificationHandler(IMailProvider mailProvider)
    { 
        _mailProvider = mailProvider;
    }

    public async Task Handle(EMailSendCommand request, CancellationToken cancellationToken)
    {
        var address = request.ToAddress;

        // some code..
        await _mailProvider.SendAsync(.. mail parameters..);
        
        return true;
    }
}

MediatR and Azure Functions

In itself, Azure Functions are already a certain separation of responsibilities: Triggers decouple the exact triggering and the function itself is only the event handler. Nevertheless, it is quite practical to keep the Function very slim with the help of MediatR, so that code can be implemented independently of its runtime (functions, ASP...)

What is therefore necessary in the configuration of the FunctionStartup is the configuration of MediatR. MediatR brings the Dependency Injection capabilities in a dedicated NuGet package: MediatR.Extensions.Microsoft.DependencyInjection.

[assembly: FunctionsStartup(typeof(Startup))]
namespace FunctionsApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            if (builder != null)
            {
                // ... your config code here...

                // Analytics
                builder.Services.Configure<AnalyticsOptions>(o =>
                {
                    o.Url = Environment.GetEnvironmentVariable("ANALYTICS_SERVICE_API");
                });
                builder.Services.AddScoped<IAnalyticsService, NoiseAnalyticsService>();

                // Mail
                builder.Services.AddScoped<IMailProvider, Office365MailProvider>();

                // ... your config code here...

                // MediatR
                builder.Services.AddMediatR();
                // or builder.Services.AddMediatR(typeof(TypeOfYourExternalAssembylHere));
            }
            else
            {
                throw new ArgumentNullException(nameof(builder));
            }
        }
    }
}

Due to the basic idea of separating abstract descriptions (the Requests and the Notifications in MediatR), the real implementation is mostly that commands are located in different namespaces or even projects than the corresponding handlers. Therefore MediatR offers the possibility to search the handlers in different assemblies. However, this must be defined manually.

[FunctionName("sensor-message-processor")]
public async Task Run(
    [EventHubTrigger("events", Connection = "AZURE_EVENT_HUB_CONNECTIONSTRING")] EventData[] eventData,
    ILogger log)
{
    // we support multiple messages here
    foreach (EventData eventMessage in eventData)
    {
        DateTimeOffset receivedOn = eventMessage.SystemProperties.EnqueuedTimeUtc;

        // we try to serialize the event data
        SensorEvent sensorEvent;
        try
        {
            string requestBody = Encoding.UTF8.GetString(eventMessage.Body);
            sensorEvent = JsonConvert.DeserializeObject<SensorEvent>(requestBody);
        }
        catch (Exception e)
        {
            log.LogError(e, $"Failed to read event data from hub message.");
            return;
        }

        if (sensorEvent is null)
        {
            log.LogError($"No event data found in hub message.");
            return;
        }

        log.LogInformation("Received {@SensorEvent} from Event Hub", sensorEvent);

        try
        {
            // now use mediatr and your handler to run handle the message
            await _mediator.Publish(new ProcessSensorEventMessageCommand(sensorEvent, receivedOn)).ConfigureAwait(false);
        }
        catch (Exception e)
        {
            log.LogError(e, $"Failed to handle event data: {e.Message}");
            throw;
        }
    }
}

Conclusion

Event Processing is an integral part of modern software development and MediatR in this sense an important part of the .NET ecosystem. Also in Azure Functions, MediatR has its authority to ensure that our functions also remain clean and code can be reused in a lean manner.