Azure Functions with Configuration and Dependency Injection

Azure Functions on PureSourceCode.com

In the last couple of weeks, I wrote a lot about Azure Functions because companies like this new approach. First, I want to know how add in a Azure Function dependency injection and read the configuration from a file.

I want to use .NET Core 3.x for my function and store some settings in a configuration file. First problem is where this configuration file is saved, on my machine and on Azure.

Then, I have to inject this configuration and other dependencies in some way to the function. We have to write a bit of code now.

Dependency Injection

What I want to do is pretty easy: create a function to return a value from the configuration. Just to complicated the example a bit more, I want to inject an HttpClient so I can add some logs in there.

I want to made a call to retrieve weather information by instantiating an HttpClient then making a call to a REST endpoint, returning the result.

When I consider how we to test this functionality I immediately run into a problem in that we can’t mock this HttpClient so I can test properly my function.

Assuming our Function class is named CheckWeather, lets start off my creating a constructor that accepts some interfaces to be passed in:

public CheckWeather(IHttpClientFactory httpClientFactory, IConfiguration config)
{
    _client = httpClientFactory.CreateClient();
    _config = config;
    _apiKey = _config["WeatherApiKey"];
    _stockPriceUrl = $"https://www.weather.co/forecast?symbol={symbol}&apiKey={_apiKey}";
}

In the method above, we accept an IHttpClientFactory and IConfiguration. The body of the method is not relevant for this example.

In a .Net or .Net Core application we’d usually have a startup class where dependencies are registered in a container and accessible when required.

In Azure Functions, we can do the same thing. Create a ‘Startup.cs` class (technically it could be called whatever you like):

[assembly: FunctionsStartup(typeof(Weather.Startup))]
namespace Weather
{
    internal class Startup: FunctionsStartup
    {
        public Startup()
        {

        }

        public override void Configure(IFunctionsHostBuilder builder)
        {
            ...
        }
    }
}

Three things to notice here are:

  • The first line which introduces this as a FunctionStartup class
  • Implementation of the abstract FunctionsStartup which provides an abstract Configure method that we override; and
  • The implementation override void Configure(IFunctionsHostBuilder builder)where IFunctionHostBuilder is automatically injected in by the framework

Registering the dependencies is now simple within the Configure method:

builder.Services.AddHttpClient();
builder.Services.AddSingleton(configuration);

So, CheckWeather(IHttpClientFactory httpClientFactory, IConfiguration config) will now have the correct dependencies injected.

Configuration

When you initially create your function, you’re provided with a local.settings.json file which you can use to store whatever values you like. This is useful when running in a development machine where you may not want to create environment variables that means switching out of you IDE.

You may also want to check some of these setting’s into source control.

In addition to this, you want to ensure that in a production environment these settings is read out of environment variables.

In other words, when developing you’d like to read keys from some sort of local file but also environment variables when that exists.

var configBuilder = new ConfigurationBuilder()
                .AddEnvironmentVariables()
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true);
IConfiguration configuration = configBuilder.Build();

Running this locally will fail with an error indicating that the settings (from the local file) cant be found. The reason for this is because it doesn’t know where to look for it. Ordinarily, we’d provide this information to the configBuilder through the ExecutionContext:

public static async Task<IActionResult> Run([HttpTrigger(..., *ExecutionContext context*)
{
    var config = new ConfigurationBuilder()
        .SetBasePath(context.FunctionAppDirectory)
        ...
}

This ExecutionContext is available to our Run function and will be automatically injected however, the same is not true of the Startup class we’ve introduced and the framework will not inject it into our Configure method.

One workaround for this issue is to introduce:

var localRoot = Environment.GetEnvironmentVariable("AzureWebJobsScriptRoot");
var azureRoot = $"{Environment.GetEnvironmentVariable("HOME")}/site/wwwroot";

var actualRoot = localRoot ?? azureRoot;

var configBuilder = new ConfigurationBuilder()
    .SetBasePath(actualRoot)

The complete Startup class now looks like this:

internal class Startup: FunctionsStartup
{
    public Startup()
    {

    }


    public override void Configure(IFunctionsHostBuilder builder)
    {
        var localRoot = Environment.GetEnvironmentVariable("AzureWebJobsScriptRoot");
        var azureRoot = $"{Environment.GetEnvironmentVariable("HOME")}/site/wwwroot";

        var actualRoot = localRoot ?? azureRoot;

        var configBuilder = new ConfigurationBuilder()
            .SetBasePath(actualRoot)
            .AddEnvironmentVariables()
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true);
        IConfiguration configuration = configBuilder.Build();


        builder.Services.AddHttpClient();
        builder.Services.AddSingleton(configuration);
    }
}

And our CheckWeatherconstructor will now work by accepting these injected parameters:

public CheckWeather(IHttpClientFactory httpClientFactory, IConfiguration config)
{
    ...
}

Finally, I have the Azure Functions with configuration and dependency injection I wanted. For more information there is A Microsoft documentation,

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.