Posted in:

One of the questions I frequently get asked by people who watch my Durable Functions Fundamentals Pluralsight course is whether you can use dependency injection with Durable Functions (as my demo app uses static methods). The answer is yes, and it's quite simple although there are a couple of considerations about logging that are worth pointing out.

In this post I'll give a quick overview of the main steps, and you can get more details on the official docs site if you'd like to dive further into the topic of dependency injection in Azure Functions.

UPDATE: I should point out that in this post I am using the in-process Azure Functions programming model using .NET Core 3.1, rather than the out-of-process .NET 5 model, because that does not support Durable Functions. In the future, the out-of-process model is going to support Durable Functions, but the out-of-process model uses a different approach for setting up dependency injection, so this tutorial does not apply for .NET 5 Azure Function apps.

Step 1 - Add NuGet references

First of all, if you've already created your Azure Function app, you need to add two NuGet references to your csproj file. These are Microsoft.Azure.Functions.Extensions and Microsoft.Extensions.DependencyInjection.

<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />

Step 2 - Register Services at Startup

The next step is to create a Startup class derived from FunctionsStartup and override the Configure method. In here you can set up whatever dependencies you need with AddSingleton or AddTransient.

The example I show below also calls AddHttpClient to register IHttpClientFactory. And you could even register a custom logger provider here if you need that.

Note that we also need to add an assembly level attribute of FunctionsStartup that points to the custom Startup class.

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

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

namespace DurableFunctionApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient(); // registers IHttpClientFactory
            builder.Services.AddSingleton<IGreeter>(_ => new Greeter());
        }
    }
}

Step 3 - Injecting Dependencies

Injecting dependencies is very simple. Instead of defining functions as static methods on a static class, just create a regular class with a constructor that takes the dependencies and stores them as class members. These can then be used in the functions themselves which are now instance methods.

In this example I'm using the injected IGreeter in an activity function. You can use dependencies in orchestrator functions as well, but remember that the strict rules of orchestrator functions must still be adhered to.

public class MyOrchestration
{
    private readonly IGreeter greeter;

    public MyOrchestration(IGreeter greeter)
    {
        this.greeter = greeter;
    }

    [FunctionName("MyOrchestration_Hello")]
    public string SayHello([ActivityTrigger] string name, ILogger log)
    {
        log.LogInformation($"Saying hello to {name}.");
        return greeter.Greet(name);
    }

And that's all there is to it. Although I did say I'd mention a few gotchas with logging.

Gotchas - injecting loggers

When you set up dependency injection in an Azure Functions project, you might be tempted to attempt to inject an ILogger using the class constructor, rather than having an ILogger as a parameter on every function. If you do this you'll run into a few problems.

First, you can't inject the generic ILogger - that doesn't get registered by default. Instead you have to inject an ILogger<T> - so ILogger<MyFunctions> for example.

public class MyFunctions
{
    private readonly ILogger<MyFunctions> logger;
    private readonly IGreeter greeter;

    public MyFunctions(ILogger<MyFunctions> logger, IGreeter greeter)
    {
        this.logger = logger;
        this.greeter = greeter;
    }

Second, the logs that you write to that ILogger<T> will get filtered out unless you update your host.json file to include logs for your namespace. In this example we're turning logging on for the MyDurableFunctionApp namespace.

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingExcludedTypes": "Request",
      "samplingSettings": {
        "isEnabled": true
      }
    },
    "logLevel": {
      "MyDurableFunctionApp": "Information"
    }
  }
}

And third, when you use ILogger in an orchestrator function, the best practice is to use CreateReplaySafeLogger on the IDurableOrchestrationContext. UPDATE - I initially thought that this doesn't work with an ILogger<T> but I was mistaken. The code snippet below shows how to create a replay safe logger from the injected logger.

private readonly ILogger<MyFunctions> injectedLogger; // set up in the constructor

[FunctionName("MyOrchestration")]
public static async Task<List<string>> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var outputs = new List<string>();
    var log = context.CreateReplaySafeLogger(injectedLogger);

    log.LogInformation("about to start orchestration...");

It may be that there are some other ways round these issues, so do let me know in the comments.

Want to learn more about how easy it is to get up and running with Durable Functions? Be sure to check out my Pluralsight course Azure Durable Functions Fundamentals.

Comments

Comment by Jason

Hello, I have a Startup.cs class just like you created. I am running SonarQube and I am getting security hotspot on builder.Services.AddLogging();
The SonarQube message says: Make sure that this logger's configuration is safe.
Have you got any idea how I could resolve this?

Jason