Azure Functions Dependency Injection – The forgotten greatness

Had a quite big Azure functions project that did not feel right. It had grown quite big, multiple developers and many functions of semi-complex nature. Although we had divided the code into class libraries with different purposes, in order to get logging and configuration in the repository classes we sent references back and forward between classes. Also there were places where HTTPClient was used incorrectly and instanced in several places. In fact, there was a lot of in code instancing of different types in different places. and instanced other objects as needed in multiple places. I really missed the Dependency Injection from other templates/solutions. But the truth is that adding DI in Azure function solutions is really simple but not widely used and documented.

The “bad” template

If you start a new Azure Function project in Visual Studio you will get a template based on static classes. To get configuration values you need to write your own by writing code for ConfigurationBuilder, you will have to write custom code for maintaining HTTPClient efficiently as well as initiating other classes as you need.

public static class Function1
{
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");

While this is perfectly fine for smaller simpler functions, our function solution had just too many instantiations, too many places where we needed HTTPClient, too many classes that needed to log or access the application settings and it finally grew out of control.

Note the ILogger instance that is sent to the function by default. We will discuss ILogger later.

The good news

The good thing is that Dependency Injection has been available for Azure Functions since Azure Functions V2 and that it fixes just about all the issues I had with out solution by simplifying instantiation and making it easy to access shared functionality without sending class references back and forth and standardizing instantiation.

Set up directly – not afterwards

I did however experience that converting/refactoring a big function solution to support dependency injection was a bit inefficient. Once I started it pretty much had me unable to start the solution until I had fixed the complete code base. And when I refactor code, I usually prefer with changing smaller units and not using the big bang conversion. What if something would have prevented DI from running my solution? In that case I wouldn’t have found it until everything was converted and would have wasted a lot of time. I did also run into some strange function startup error messages after refactoring that I could not really pinpoint to what was the cause – which made error searching frustrating. If one only change a little at the time it is easier to know that the last change broke the functionality. So from now on I will set up DI the first thing when I create new Azure Function projects.

It is also worth noting that the DI implementation is extremely sensitive to a faulty version of any assembly which can give the strangest of errors. One of my class libraries had too high version of Microsoft.Extensions.Logging.Abstractions but indicated that I had problems with IConfiguration.

[2/11/2020 8:26:58 AM] An unhandled host error has occurred.
[2/11/2020 8:26:58 AM] Microsoft.Extensions.DependencyInjection.Abstractions: Unable to resolve service for type 'Microsoft.Extensions.Configuration.IConfiguration' while attempting to activate 'Infrastructure.Workorders.SendWorkOrdersToContractor'.

It would probably have been easier if all libraries were the latest version but as this was 2.2 it caused me a lot of headaches. In fact the .Net Core 3.1 version of the final solution is basically the same as the 2.2 version except from updated the packages.

Theory into test

As stated above, our Azure Functions project was a .Net Core 2.2 based I will show detailed steps on how to fix a project in this version of Azure Functions and .Net Core but the same can be done with minor differences for .Net Core 3.x instead. For pedagogical reasons I will however start from a newly created Azure Function project to use as little code as possible and to focus on setting everything up rather than using a real-life example. You can find a download version of the same solution but for .Net Core 3.1 at the end of the post as you would not likely create new .Net Core 2.2 Function project today.

Create the project – setting up the project to convert

CretateAzureFunction
Create Azure Function V2.0 project

Note that it will by default build for .Net Core 2.1 so for simplicity I just update it to .Net Core 2.2

ChangeToNetCore22
Change to .Net Core 2.2

Add dependency injection support to the function project

Add the following references to your project

The first one will enable Dependency Injection. The second will allow us to use the HTTPClient in a safe way in the project.

<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />

Initialize Dependency Injection

In your Azure Function project, add a new class and name it Startup(.cs) or similar (the name does not really matter but follows the .Net Core Web template naming standard.

Add Startup.cs

We now need to fix the class a bit.

  • (Line 1) Before the namespace lets add an assembly tag [assembly: FunctionsStartup(typeof(DIAzureFunction.Startup))]. Replace the namespece with your own of course.
  • (Line 4) Change the startup class to public and inherit from FunctionStartup. public class Startup : FunctionsStartup
  • (Line 6) Override the ConfigureMethod and add the needed services. For now lets just add HTTPClientFactory support (as we will need that later)

Note that Configuration and Logging* is added by default so you don’t have to do anything special to get that to work as long as you work with Application Insights. Other logging might require some additional configuration changes.

[assembly: FunctionsStartup(typeof(DIAzureFunction.Startup))]
namespace DIAzureFunction
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
        }
    }
}

Fixing the generated function code

Before modifications

public static class MyWebCallingFunction
{
[FunctionName("MyWebCallingFunction")]
public static async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
return name != null
? (ActionResult)new OkObjectResult($"Hello, {name}")
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}

Now we need to do some changes to adjust the function in Function1 to use dependency injection.

  • First I changed the name of the function, file and class to MyWebCallingFunction.
  • Secondly to use dependency injection we need to change the implementation a bit. We have to remove the static implementation and replace it with a standard class (Line 1).
  • After the class declaration I declare some variables to hold the injected classes
  • Dependency injection works by injecting the predefined instances for a specific Interface to the Class Constructor. So on line 6 I add a constructor and specify what objects I need in this class. Logging and Configuration comes predefined though you can tweak the implementation if needed in the Startup code described earlier. The constructor does nothing more than to store the injected instances in the private variables.
  • I Change the function from static to “normal” and remove the ILogger parameter on line 14.
  • The method is going to be expanded later, but for not I just simulate a call and return a value from the configuration. Application Insights logging is supported from the beginning but for now I will wait to set it up.

Add a couple of settings

Lets add a couple of settings to the local.settings.json file.

  • A sample setting to just be able to test the IConfiguration interface that just returns a value (MyCustomValue)
  • A URI to the Microsoft home page (MicrosoftHomePage) (for use later)
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "MyCustomValue": "25",
        "MicrosoftHomePage": "https://www.microsoft.com"
    }
}

After modifications

public class MyWebCallingFunction
    {
        private readonly ILogger<MyWebCallingFunction> _Logger;
        private readonly IConfiguration _Configuration;

        public MyWebCallingFunction(ILogger<MyWebCallingFunction> logger, IConfiguration config)
        {
            _Logger = logger;
            _Configuration = config;
        }

        [FunctionName("MyWebCallingFunction")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
        {
            _Logger.LogInformation($"Starting function in Function Proj");
            _Logger.LogInformation($"Value from config = {_Configuration["MyCustomValue"]}");

            //Just return a value from Condig
            var returnValue = await Task.FromResult(_Configuration["MyCustomValue"]); 

            return (ActionResult)new OkObjectResult(returnValue);
        }
    }

Change for nothing?

So now we actually changed the project to Dependency Injection. But why? What have we gained? Well not much. But as I indicated earlier our function was decently well thought-through. We had a combination of repositories that interacted with different components such as a Service Bus, Dynamics 365, legacy systems etc. These were put in a class libraries and the functions were did mostly call these repositories and held very little code in the function itself to encourage reuse of the repositories. So lets simulate that.

The repositories may in their turn may need helper libraries, access to configuration and logging and you may see the initial problem a little clearer.

So I am going to add two new class libraries.

  • An interface class-library where I can define the different interfaces in the solution
  • A Repository Class in another library where I implement the actual functionality of the Repository that accesses a particular data store.

In my example I will make a web call to http://www.microsoft.com from the repository, I will read the Uri from the configuration settings and I log to application insights.

Add the interface project

Go to the solution and add a new project of type Class Library

Add an interface project

Change the build target to .Net Core 2.2 and after that add an IMicrosoft interface to be able to get the home page data

Add an IMicrosoftInterface
namespace Interfaces
{
    public interface IMicrosoftRepository
    {
        Task<string> GetHomePageData();
    }
}

Implement IMicrosoftRepository

Again – add a new ClassLibrary. Name it Microsoft. Change Target runtime to .Net Core 2.2

Rename Class1 to MicrosoftRepository and implement IMicrosoftRepository

A couple of quick fixes later you have added the reference to the interface project and have implemented the Interface (see below)

namespace Microsoft
{
    public class MicrosoftRepository : IMicrosoftRepository
    {
        public Task<string> GetHomePageData()
        {
            throw new NotImplementedException();
        }
    }
}

Now lets improve this function to a more realistic DI implementation.

  • Add the needed dependencies to HTTPClientFactory, ILogging, IConfiguration
  • Call the Microsoft Web page.
  • Log something from the class library
  • Read the Configuration settings to get the URI to the Microsoft home page from the Repository

Hints for the repository:

 <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
  </ItemGroup>

With that in mind. Here is my implementation of the MicrosoftRepositoty Class.

    public class MicrosoftRepository : IMicrosoftRepository
    {
        private readonly ILogger<MicrosoftRepository> _Logger;
        private readonly IConfiguration _Configuration;
        private readonly HttpClient _HttpClient;
               
        public MicrosoftRepository(IConfiguration config, IHttpClientFactory httpclientFactory, ILogger<MicrosoftRepository> logger)
        {
            _Configuration = config;
            _Logger = logger;
            _HttpClient = httpclientFactory.CreateClient();
        }

        public async Task<string> GetHomePageData()
        {
            HttpRequestMessage request =
               new HttpRequestMessage(
                   HttpMethod.Get,
                    $"{_Configuration["MicrosoftHomePage"]}");

            //Set the access token
            _Logger.LogInformation($"Gonna call {(_Configuration["MicrosoftHomePage"])} from MicrosoftRepository");   
            HttpResponseMessage response = await _HttpClient.SendAsync(request);
            if (response.IsSuccessStatusCode)
                return await response.Content.ReadAsStringAsync();
            else
            {
                _Logger.LogError($"Error {response.StatusCode} in MicrosoftRepository");
                return null;
            }
        }
    }

Note the _HttpClient = httpclientFactory.CreateClient() row for handling the HttpClient in a non exhausting manner.

Modify the Startup.cs file

If you are familiar with the standard Dependency Injection of .Net Core you understand what we need tp do in the Startup.cs file. If you are uncertain you will need to change as of below (line 9). You can add your services in different scopes (Singleton, Scoped and Transient) with slightly different behavior. Explaining these in detail are outside the scope of this post but are well documented in Microsofts official documentation.

[assembly: FunctionsStartup(typeof(DIAzureFunction.Startup))]
namespace DIAzureFunction
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
            builder.Services.AddScoped<IMicrosoftRepository, MicrosoftRepository>();
        }
    }
}

What about Durable Functions?

Before we test the solution you may wonder if this works for Durable Functions also. And it absolutely does. It you like putting the activities in files separate from the triggers and/or the orchestration (see sample below) – dependency injection will help you maintain instances of logging and configuration and other shared, needed classes. You can also follow the same principle for the orchestrator and the triggers if you want to.

Below follows an ActivityTrigger implementing the same call as the function earlier.

    public class WebCallingActivities
    {
        private readonly ILogger<MyWebCallingFunction> _Logger;
        private readonly IMicrosoftRepository _Microsoft;

        public WebCallingActivities(ILogger<MyWebCallingFunction> logger, IConfiguration config, IMicrosoftRepository microsoft)
        {
            _Logger = logger;
            _Microsoft = microsoft;
        }

        [FunctionName("A-MyDurableWebCallingFunction")]
        public async Task<string> MyDurableWebCallingFunctionActivity([ActivityTrigger] string name)
        {
            _Logger.LogInformation($"Log from activity");
            return await _Microsoft.GetHomePageData();
        }
    }

Before we test – change the host file

There are many people having problem with getting the ILogger functionality to work. I also had issues with this but so far I find that I always seem to get it to work if I add some logging configuration in the hosts file.

{
  "version": "2.0",
  "logging": {
    "fileLoggingMode": "debugOnly",
    "logLevel": {
      "default": "Information"
    }
  }
}

Put the solution to the test

So lets deploy this to Azure. The expected result is to generate the same result as in test but to make sure that logging and configuration works as intended.

Deploy to Azure

Publish the application

Start the deployment

Configure your application and enter the parameters

Deploy to Azure

After deployment, add the parameters to our custom settings in Azure

{
    "name": "MicrosoftHomePage",
    "value": "https://www.microsoft.com",
    "slotSetting": false
  },
  {
    "name": "MyCustomValue",
    "value": "28",
    "slotSetting": false
  }

Test the application

Enable Application Insights

To get the proper logging we need to enable application insights. In this case I will create a new instance.

Enable Application Insights

Test the function

Lets call the function.

Initiate the tests from Azure

The test results should then show that you can

  • Read the configuration also in the repository
  • Use the HTTPClient from HTTPClientFactory in the repository
  • Log in the repository
Logs from application insights

Conclusion

We have now implemented an approach where we can reuse configuration, logging and distribute business logic between different class libraries in a dependency injection enabled project.

We see that the extra effort in the setting up of this forces us to change some default configuration but gives us all the advantages of dependency injection and reuse of functionality without having to pass object references in our function.

Download sample project

There is actually really little difference in these versions. The .Net Core version basically just compiles to newer .Net Core and references later versions of the packages used.

.Net Core 2.2

Download Zip

.Net Core 3.1

Download Zip

References

Microsoft doc: https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection

ILogger problematics: https://github.com/Azure/azure-functions-host/issues/4425

Leave a comment