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 abstractConfigure
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 CheckWeather
constructor 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,