Dependency Injection in Azure Functions


6 min read

Dependency Injection in Azure Functions

A little while ago Microsoft finally added support for dependency injection in their Azure Function product. Now, there are some subtle but important differences, but for the most part the experience is similar to what you're used to in ASPNET Core.

Microsoft's official documentation, found here, is as usual lacking in real-world examples. Thus, the purpose of this article is to expose some real world use cases to the wider public. We're going to build an Azure Function which listens to a Service Bus and writes to an Azure Search index.

Before we started, a brief introduction.

Azure Function Primer

Azure Functions are supposed to be single-purpose pieces of code which are trigger when an action happens. These actions are in fact called triggers. Once your code is trigger, it can do anything you want it to, but most often you'll want to do some kind of processing on the input from the trigger and spit out the result into a 'binding'. Bindings are essentially output buckets you can deposit the result of your function into. The key is to understand that triggers are mandatory but bindings are optional.

Microsoft's own documentation on triggers and bindings is pretty good, so if you're new to Azure Functions, I recommend giving that a read.

In our case, we'll be using a Service Bus trigger, but since there is no Azure Search index binding, we'll have to write to the index manually.

Not having a binding you need is a pretty common scenario, because Microsoft doesn't really support anything outside of their own ecosystem and even within the ecosystem getting them to build bindings you need is a tall order. How many years have we been waiting for Azure File Storage bindings?

Right, so now that we know what we're building, let's dive into the code.

Creating the Function

Go ahead and spin up a new Azure Function in Visual Studio. If you want, you can choose the Service Bus trigger while scaffolding -- that will make the process a tad bit faster. Choose whatever default storage account you use to store your functions (or Storage Emulator), make sure you've selected at least version 2 of Azure Functions (though as of this writing, 3 is the latest one so I'll be using that).

You already have a Function1.cs file which contains the default function code. There are also host.json, local.settings.json and possibly a .gitignore. Let's add a couple more files:

  • Startup.cs
  • ConfigBuilder.cs

Startup is going to contain our dependency injection code and ConfigBuilder will pull config files from the filesystem.

We're also going to need to add a Nuget package Microsoft.Azure.Functions.Extensions.

Inside Startup.cs class, add the following code:

[assembly: FunctionsStartup(typeof(FancyFunction.Startup))]
namespace FancyFunction
    public class Startup : FunctionsStartup
        public Startup() { }

        public override void Configure(IFunctionsHostBuilder builder)
            // 1. Build Config

            // 2. Add Config as a Singleton

            // 3. Add logging

            // 4. Add the search service

First, we have to add an assembly reference attribute at the top of the namespace and specify the Startup class as the entrypoint. Then, we need to inherit from the FunctionsStartup base class and add an empty constructor. Lastly, implement the required Configure override.

We have our steps defined in the comments there, so let's start with building the config. We can add appsettings.json via Microsoft's IConfiguration interface. Inside ConfigBuilder.cs add the following code:

namespace FancyFunction
    public static class ConfigBuilder
        public static IConfiguration BuildConfiguration(string rootDir = null)
            // We're allowing specifying a custom root directory
            if (string.IsNullOrEmpty(rootDir))
                // Theoretically this should exist in Azure Function apps
                var localRoot = Environment.GetEnvironmentVariable("AzureWebJobsScriptRoot");
                // But if it doesnt, then this will
                var azureRoot = $"{Environment.GetEnvironmentVariable("HOME")}/site/wwwroot";

                rootDir = localRoot ?? azureRoot;

            // Grab the environment setting to use below
            var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";

            // Create and retun the config
            return new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: true)
                .AddJsonFile($"appsettings.{environment}.json", optional: true)
                // Add any more sources you need here

Resolve any usings missing. The code is explained in the comments.

Inside Startup.cs, let's do steps 1 and 2:

// 1. Build Config
var config = ConfigBuilder.BuildConfiguration();

// 2. Add Config as a Singleton

Next, logging. Simple:

// 3. Add logging

This will add the default loggers, one of which is Console. This step is not strictly necessary if you intend on using just the Console, since functions are injected ILogger out of the box, but if you plan on using any 3rd party or maybe your own logger, you can do so here by passing in a parameter:

builder.Services.AddLogging(cfg => {
    // your code here

We will inject this and other services via Dependency Injection into the function soon.

Lastly, let's add the Azure Search Service. We need to create the search service first, then add it as a Singleton. Go ahead and create a new class: AzureSearchService.cs. Inside, create an interface and the class which inherits from it:

public interface IAzureSearchService {
    Task<bool> MergeDocument<T>(T entity, string index);

public class AzureSearchService : IAzureSearchService {
    public SearchServiceClient Client;
    public AzureSearchService(SearchServiceClient adminClient) {
        Client = adminClient;
    public Task<bool> MergeDocument<T>(T entity, string index) {
        // ... code to merge a message from SB into Search Index

We're not going to build out the search client in this post, since the focus here is on Azure Functions. You can think of this service as a standard DI service you'd write in a typical .NET Core application. The interface is there to make it testable.

You may have to install Microsoft's latest Azure Search Nuget package. As of this writing, it's Microsoft.Azure.Search version 10.1.0, but knowing Microsoft, by the time you read this, it'll be deprecated...

Now that you have the Azure Search Service class, you can add it to Startup.cs:

// 4. Add the search service
builder.Services.AddSingleton<IAzureSearchService>((provider) =>
    // Create the client first using config values from appsettings.json (or alternative source)
    var adminClient = new SearchServiceClient(config["SearchServiceName"], new SearchCredentials(config["SearchServiceAdminKey"]));
    // Return the implementation of the class with this adminClient.
    return new AzureSearchService(adminClient);

We're creating a singleton here, because the Search Client is reusable and does not need to instantiate every time a request is made.

Using DI in the Function

Finally, we can get to writing our function! Inside Function1.cs (or, if you renamed it, whatever the new file name is) make the following changes:

  1. Remove static from the class definition.
    public class Function1
  2. Add a constructor

    public class Function1
     private IAzureSearchService _searchService;
     private IConfiguration _config;
     private ILogger<Function1> _logger;
     public Function1(IAzureSearchService azureSearchService, IConfiguration configuration, ILogger<Function1> logger)
         _searchService = azureSearchService;
         _config = configuration;
         _logger = logger;
     // ... Your function code here
  3. Write out the function just as you would a typical controller endpoint. You'll have access to anything you injected in the constructor and reassigned out of its scope. Here's an example:

    public async Task Run([ServiceBusTrigger("fancy-topic", "fancy-subscription", Connection = "ConnectionStringDefinedInEnvironment")]Message message)
     // Get the body of the message
     var body = Encoding.UTF8.GetString(message.Body);
     // Get user defined properties (UserProperties) is just a dictionary
     message.UserProperties.TryGetValue("customProperty", out object customProperty);
     // Deserialize the body
     var msgObject = JsonSerializer.Deserialize<FancyMessage>(body);
     // Use the search service
     await _searchService.MergeDocument<FancySearchIndexModel>(new FancySearchIndexModel
         /* Reassign properties from message to search model here */
     }, "FancyIndex");

    This is it!

The entire process basically goes as follows:

  1. Create Startup.cs
  2. Add some attributes to specify that we're using a Functions-specific Startup class
  3. Add services to the DI container
  4. Inject services into the constructor of the function

Thanks for reading!

Did you find this article valuable?

Support Paul K by becoming a sponsor. Any amount is appreciated!