Azure Functions Built-In Dependency Injection

Posted by

There were so many announcements made a few weeks ago at //build/ that it’s hard to keep track of it all. Hidden under all the major unveilings was the news that a mechanism for handling dependency injection in Azure Functions is now available.

This post walks you through how to use the new DI module. Along the way, I’ll look at why this feature is helpful in developing reliable functions.

The Basics

The easiest way to explain how to use something is by way of an example. To that end, I’ve created a timer-trigger function that retrieves scheduled flights for a fictional airline and validates that each flight’s details are correct. Nothing fancy, but we’ll avoid getting bogged down into too many details.

The function depends on two services, each represented by a class in the same project as the function trigger:

  • A data store that retrieves all scheduled flights.
  • A warning generator that sends an email when a flight has invalid details.

Before dependency injection was available, I would have declared both classes as static objects in the function trigger:

public class Validator
{
    private static FlightStore _store = new FlightStore();
    private static WarningGenerator _warnings = new WarningGenerator();

    [return: Queue("validationscompleted")]
    [FunctionName("Validator")]
    public async Task<ValidationsComplete&gt; Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, ILogger log)
    {
        var flights = await _store.Get();
        ...
    }

But the newly released dependency injection module provides more flexibility, with little additional work. Let’s look at how to convert this basic function to use DI instead.

The first step is to add the Microsoft.AzureFunctions.Extensions package from NuGet. It provides all the needed plumbing to use the DI primitives. I also updated to the latest Microsoft.Sdk.Functions package, 1.0.27.

Next, I extracted interfaces for both of my classes. The interfaces allow the code to depend on an abstraction rather than a concrete implementation, but just as interestingly, it allows for proper testing of the function trigger’s business logic.

public interface IFlightStore
{
    Task<ImmutableList<Flight&gt;&gt; Get();
    Task Add(Flight f);
}

public interface IWarningGenerator
{
    Task Send();
}

The Azure Functions DI module is powered by the same engine as ASP.NET Core’s. The services are configured in a Startup class that inherits from FunctionsStartup. Within the Configure method, I defined all the required bindings:

[assembly: FunctionsStartup(typeof(Flights.Startup))]

namespace Flights
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddSingleton<IFlightStore, FlightStore&gt;();
            builder.Services.AddSingleton<IWarningGenerator, EmailWarningGenerator&gt;();
        }
    }
}

I injected both services as singleton objects, meaning that every execution of the function will receive the same instance of the given service. You can find out more about service lifetimes over at Microsoft Docs.

The final step is to add a constructor to the function trigger class. The dependency injection module will then resolve and inject the two services via the contructor at runtime.

[StorageAccount("AzureWebJobsStorage")]
public class Validator
{
    private readonly IFlightStore _store;
    private readonly IWarningGenerator _warnings;
    public Validator(IFlightStore store, IWarningGenerator warnings)
    {
        _store = store;
        _warnings = warnings;
    }

    [return: Queue("validationscompleted")]
    [FunctionName("Validator")]
    public async Task<ValidationsComplete&gt; Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, ILogger log)
    {
        var flights = await _store.Get();

        if (flights.IsEmpty)
        {
            log.LogInformation($"No flights scheduled.");
        }

        foreach (var flight in flights)
        {
            if (flight.Revised <= flight.Scheduled)
            {
                _warnings.Send();        
            }
            log.LogInformation($"Flight {flight.Id} is valid.");
        }

        return new ValidationsComplete() {FlightIds = flights.Select(f =&gt; f.Id).ToList() , Succesful = true  } ;
    }
}

I followed the usual pattern of assigning the injected objects to private read-only fields within the function trigger. This way, both services are initialized when the Run method is invoked by the functions host.

At this point, I ran the local functions host to see if everything was working as expected. Sure enough, the services were injected, and the function was able to do its work.

Testability

It was very difficult to test function triggers before the release of dependency injection. You couldn’t easily stub objects, so mocking was near-impossible.

But now, it’s just like testing any other C# class. Here are the steps I took to write the test class shown below:

  • Created a test project that references the project with the function trigger.
  • Added a dependency to Moq via NuGet.
  • Declared, created, and injected mock objects into the constructor of the function trigger class.
  • Set expectations on the mock objects to test business logic.

The end result is a class that looks very much like any test class you’d find in an ASP.NET Core project, or any other project for that matter.

    [TestClass]
    public class ValidatorTests
    {
        private Validator _sut;
        private Mock<IFlightStore&gt; _mockStore;
        private Mock<ILogger&gt; _mockLogger;
        private Mock<IWarningGenerator&gt; _mockWarnings;

        [TestInitialize]
        public void Init()
        {
            _mockStore = new Mock<IFlightStore&gt;();
            _mockLogger = new Mock<ILogger&gt;();
            _mockWarnings = new Mock<IWarningGenerator&gt;();
            _sut = new Validator(_mockStore.Object, _mockWarnings.Object);
        }

        [TestMethod]
        public void GivenRevisedTime_EarlierThanScheduled_LogWarning()
        {
            var mockTime = new Mock<TimerSchedule&gt;();
            var timer = new TimerInfo(mockTime.Object, new ScheduleStatus(), true);
            _mockStore
                .Setup(f =&gt; f.Get())
                .Returns(Task.FromResult(MockFlights()));

            _mockWarnings
                .Setup(w =&gt; w.Send());

            _sut.Run(timer, _mockLogger.Object);

            _mockWarnings
                .Verify(w =&gt; w.Send());
        }

        private ImmutableList<Flight&gt; MockFlights()
        {
            return new List<Flight&gt;()
            {
                 new Flight(
                     1, 
                     DateTimeOffset.UtcNow.ToString(), 
                     DateTimeOffset.UtcNow.ToString(),
                     "Airbus A380",
                     1000,
                     999)
            }.ToImmutableList();
        }
    }

Notes from the Field

The dependency injection package seems to only work for functions using the V2 runtime. I’d hoped that the V1 runtime would also be supported, but I was unable to get it working. It’s not entirely surprising, since active development seems to be focused entirely on the newer V2 runtime.

Although the dependency injection works much like it does in ASP.NET Core, it does not seem to support the HttpClientFactory extensions that are available on the ASP.NET Core side. This could be a useful improvement since HttpClients are frequently needed in functions, and are just as frequently misused.

A Big Step Forward

The release of the dependency injection package removes one of the few remaining barriers to make Azure Functions a mainstream approach for writing APIs and background workers. Kudos to the team for developing such an awesome feature!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s