Dependency Injections on Azure Functions is not that quite intuitive. I've written many blog posts about dependency management on Azure Functions to improve testability and this was my latest one. However, they are mostly about V1, which supports .NET Framework. Azure Functions V2 is now on public preview and I'm going to write another post for DI on Azure Functions V2, by taking advantage of the simple dependency injection feature that ASP.NET Core provides out-of-the-box.
The Problem
Due to the static
nature of Azure Function triggers, it's not that easy to manage dependencies. If we can inject an IoC container itself, when an Azure Function instance is being loaded, this will be ideal. I am pretty sure that Azure Functions Team at Microsoft currently works hard to make this happen. In the meantime, we need to find out a workaround. One of the easiest and most popular workarounds is to use a static
property on each trigger. This static
property is basically an instance of an IoC container. Once the property gets instantiated, each function trigger resolves dependencies within the function.
The Workaround – IoC Container from ASP.NET Core
When an ASP.NET Core application is up and running, it bootstraps all dependencies at first within the StartUp
class using the IServiceCollection
instance. This instance also has some DI functions like AddTransient()
, AddScoped()
, and AddSingleton()
. As Azure Functions V2 comes with ASP.NET Core, we can directly make use of this feature. The only difference is that, in Azure Functions, we have to bootstrap dependencies by ourselves. Let's have a look.
The source code used in this post can be found here.
Scenario
I am asked to write an Azure Function code, given a username or organisation name on GitHub, to return the list of repositories. Let's simplify the user story here:
AS a user, when I give a GitHub username or organisation name, I WANT TO see the list of GitHub repositories
In order to achieve this user story, I need to write an HTTP trigger function to send an HTTP request to a GitHub API. OK, first things first.
HTTP Trigger Function
This is the HTTP trigger function, with all dependencies inside.
public static class CoreGitHubRepositoriesHttpTrigger | |
{ | |
[FunctionName("CoreGitHubRepositoriesHttpTrigger")] | |
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = "core/repositories")]HttpRequest req, ILogger log) | |
{ | |
var type = this._req.Query["type"]; | |
var name = this._req.Query["name"]; | |
var requestUrl = $"https://api.github.com/{type}/{name}/repos"; | |
using (var httpClient = new HttpClient()) | |
using (var message = await httpClient.GetAsync(requestUrl).ConfigureAwait(false)) | |
{ | |
var result = await message.Content.ReadAsStringAsync().ConfigureAwait(false); | |
var res = JsonConvert.DeserializeObject<object>(result); | |
return new OkObjectResult(res); | |
} | |
} | |
} |
No good. I am not happy with that because the HttpClient
should be injected from outside and there are a few things to be injected outside. This needs to be refactored. Let's change it. First of all, I need to add a static property of IServiceProvider
to the trigger, which acts as an IoC container. By the way, the IServiceProvider
should be instantiated by IServiceCollection
. Therefore, it's a good idea to create a ContainerBuilder
class to build it.
Container Builder
This is the interface design. It accepts a module from outside, RegisterModule()
, which contains all dependencies then build a container, Build()
, to return IServiceProvider
.
public interface IContainerBuilder | |
{ | |
IContainerBuilder RegisterModule(IModule module = null); | |
IServiceProvider Build(); | |
} |
Therefore, its implementation creates a new IServiceCollection
instance, loads all dependencies from the IModule
, then builds IServiceProvider
instance, like below:
public class ContainerBuilder : IContainerBuilder | |
{ | |
private readonly IServiceCollection _services; | |
public ContainerBuilder() | |
{ | |
this._services = new ServiceCollection(); | |
} | |
public IContainerBuilder RegisterModule(IModule module = null) | |
{ | |
if (module == null) | |
{ | |
module = new Module(); | |
} | |
module.Load(this._services); | |
return this; | |
} | |
public IServiceProvider Build() | |
{ | |
var provider = this._services.BuildServiceProvider(); | |
return provider; | |
} | |
} |
Module
Then, how does the IModule
works? From one trigger to another, they don't have the same dependencies at all. In order to keep the collection of dependencies as light as possible, modularising dependencies is a better practice. Let's have a look. The IModule
interface defines one method, Load()
and it takes one parameter of IServiceCollection
.
public interface IModule | |
{ | |
void Load(IServiceCollection services); | |
} |
The Module
class implements IModule
and does nothing but works as a placeholder. This is a sort of base module which can be used, in case there is no suitable module found.
public class Module : IModule | |
{ | |
public virtual void Load(IServiceCollection services) | |
{ | |
return; | |
} | |
} |
Another implementation of IModule
is CoreAppModule
, which loads an instance of HttpClient
as a singleton. The reason why it should be registered as a singleton can be found here.
public class CoreAppModule : Module | |
{ | |
public override void Load(IServiceCollection services) | |
{ | |
services.AddSingleton<HttpClient>(); | |
} | |
} |
I've created the ContainerBuilder
class above and it's ready to play. Let's refactor the existing trigger.
HTTP Trigger Function – Refactored #1
The trigger function now needs to have a static property of IServiceProvider
like:
public static class CoreGitHubRepositoriesHttpTrigger | |
{ | |
public static IServiceProvider Container = new ContainerBuilder() | |
.RegisterModule(new CoreAppModule()) | |
.Build(); | |
[FunctionName("CoreGitHubRepositoriesHttpTrigger")] | |
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = "core/repositories")]HttpRequest req, ILogger log) | |
{ | |
var type = this._req.Query["type"]; | |
var name = this._req.Query["name"]; | |
var requestUrl = $"https://api.github.com/{type}/{name}/repos"; | |
var httpClient = Container.GetService<HttpClient>(); | |
using (var message = await httpClient.GetAsync(requestUrl).ConfigureAwait(false)) | |
{ | |
var result = await message.Content.ReadAsStringAsync().ConfigureAwait(false); | |
var res = JsonConvert.DeserializeObject<object>(result); | |
return new OkObjectResult(res); | |
} | |
} | |
} |
Now, I can inject HttpClient
instance into the trigger, which has become more testable. But I'm still not happy with the result. Why? Let's see the requestUrl
variable. It's hard-coded. What if the endpoint URL is changed for some reason? It should be configurable by reading from either an environment variable or a separate settings files like appsettings.json
which is a similar way to how an ASP.NET Core application does.
Configurations
I have a config.json
file that looks like:
{ | |
"github": { | |
"baseUrl": "https://api.github.com/", | |
"endpoints": { | |
"repositories": "{0}/{1}/repos" | |
} | |
} | |
} |
Its corresponding POCO class looks like:
public class GitHub | |
{ | |
public string BaseUrl { get; set; } | |
public Endpoints Endpoints { get; set; } | |
} | |
public class Endpoints | |
{ | |
public string Repositories { get; set; } | |
} |
Module – Refactored #1
ASP.NET Core supports a configuration builder OOTB, so I can just use it in the module class.
public class CoreAppModule : Module | |
{ | |
public override void Load(IServiceCollection services) | |
{ | |
var config = new ConfigurationBuilder() | |
.SetBasePath(Directory.GetCurrentDirectory()) | |
.AddJsonFile("config.json") | |
.Build(); | |
var github = config.Get<GitHub>("github"); | |
services.AddSingleton(github); | |
services.AddSingleton<HttpClient>(); | |
} | |
} |
In this example, I just use the AddJsonFile()
method, but other methods like AddXmlFile()
or AddEnvironmentVariables()
can be used depending on your preferences. Even I can use YAML file for configuration settings. Now, the GitHub endpoint URL is all configurable.
HTTP Trigger Function – Refactored #2
With this in mind, let's do another refactoring and this is the result:
public static class CoreGitHubRepositoriesHttpTrigger | |
{ | |
public static IServiceProvider Container = new ContainerBuilder() | |
.RegisterModule(new CoreAppModule()) | |
.Build(); | |
[FunctionName("CoreGitHubRepositoriesHttpTrigger")] | |
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = "core/repositories")]HttpRequest req, ILogger log) | |
{ | |
var type = this._req.Query["type"]; | |
var name = this._req.Query["name"]; | |
var github = Container.GetService<GitHub>(); | |
var httpClient = Container.GetService<HttpClient>(); | |
var requestUrl = $"{github.BaseUrl}{string.Format(github.Endpoints.Repositories, type, name)}"; | |
using (var message = await httpClient.GetAsync(requestUrl).ConfigureAwait(false)) | |
{ | |
var result = await message.Content.ReadAsStringAsync().ConfigureAwait(false); | |
var res = JsonConvert.DeserializeObject<object>(result); | |
return new OkObjectResult(res); | |
} | |
} | |
} |
For now it's a sort of working code with full testability. Therefore, the test code for this function might look like:
[TestClass] | |
public class CoreGitHubRepositoriesHttpTriggerTests | |
{ | |
[TestMethod] | |
public async Task Given_TypeAndName_Run_Should_Return_Result() | |
{ | |
// Arrange | |
var github = new GitHub() { BaseUrl = "https://localhost", Endpoints = new Endpoints() { Repositories = "repositories" } }; | |
... | |
var httpclient = new HttpClient(handler); | |
var container = new Mock<IServiceProvider>(); | |
container.Setup(p => p.GetService(typeof(GitHub))).Returns(github); | |
container.Setup(p => p.GetService(typeof(HttpClient))).Returns(httpclient); | |
// Inject the mocked IServiceProvider instance. | |
CoreGitHubRepositoriesHttpTrigger.Container = container.Object; | |
var query = new FakeQueryCollection(); | |
... | |
var req = new Mock<HttpRequest>(); | |
req.SetupGet(p => p.Query).Returns(query); | |
var log = new Mock<ILogger>(); | |
// Action | |
// Inject the mocked parameters | |
var response = await CoreGitHubRepositoriesHttpTrigger.Run(req.Object, log.Object).ConfigureAwait(false); | |
// Assert | |
response.Should().BeOfType<OkObjectResult>(); | |
(response as OkObjectResult).Value.Should().Be(result); | |
} | |
} |
As you can see above, the static property has got a mocked instance, and the function parameters also have received the mocked ones for testing. This is how dependency injection approach is used for Azure Function triggers.
Service Locator
However, this approach still imposes an issue – Service Provider Pattern, which is known as an anti-pattern. In the function trigger code refactored above, I explicitly resolved two instances.
var github = Container.GetService<GitHub>(); | |
var httpClient = Container.GetService<HttpClient>(); |
From the caller's point of view, the function trigger in this case, it's not necessary to know which dependencies I need to resolve, but just run them. With this point, the function trigger needs more refactoring to hide dependency resolutions. This is also a good practice for encapsulation of features that should be hidden.
Function Factory
Let's have a look at the interface design of IFunctionFactory
.
public interface IFunctionFactory | |
{ | |
TFunction Create<TFunction>(ILogger log) where TFunction : IFunction; | |
} |
It returns a function implementing the IFunction
interface, which is also registered into the IoC container. What does IFunction
do? All logics in the function trigger move into there. For example,
public interface IFunction | |
{ | |
Task<TOutput> InvokeAsync<TInput, TOutput>(TInput input); | |
} | |
public interface IGitHubRepositoriesFunction : IFunction | |
{ | |
} | |
public class CoreGitHubRepositoriesFunction : IGitHubRepositoriesFunction | |
{ | |
private readonly GitHub _github; | |
private readonly HttpClient _httpClient; | |
public CoreGitHubRepositoriesFunction(GitHub github, HttpClient httpClient) | |
{ | |
this._github = github; | |
this._httpClient = httpClient; | |
} | |
public async Task<TOutput> InvokeAsync<TInput, TOutput>(TInput input) | |
{ | |
var req = input as HttpRequest; | |
var type = req.Query["type"]; | |
var name = req.Query["name"]; | |
var requestUrl = $"{this._github.BaseUrl}{string.Format(this._github.Endpoints.Repositories, type, name)}"; | |
using (var message = await this._httpClient.GetAsync(requestUrl).ConfigureAwait(false)) | |
{ | |
var result = await message.Content.ReadAsStringAsync().ConfigureAwait(false); | |
var res = JsonConvert.DeserializeObject<object>(result); | |
return (TOutput)res; | |
} | |
} | |
} |
As you can see, all the logics resided in the function trigger has moved into the CoreGitHubRepositoriesFunction
class. Now the implementation of the IFunctionFactory
might look like:
public class CoreFunctionFactory : IFunctionFactory | |
{ | |
private readonly IServiceProvider _container; | |
public CoreFunctionFactory(IModule module = null) | |
{ | |
this._container = new ContainerBuilder() | |
.RegisterModule(module) | |
.Build(); | |
} | |
public TFunction Create<TFunction>(ILogger log) | |
where TFunction : IFunction | |
{ | |
// Resolve the function instance directly from the container. | |
var function = this._container.GetService<TFunction>(); | |
function.Log = log; | |
return function; | |
} | |
} |
This factory class firstly loads dependencies, then resolves a function with the given type when it's called.
Module – Refactored #2
Now, I need to update the CoreAppModule
class to register the IFunction
instance.
public class CoreAppModule : Module | |
{ | |
public override void Load(IServiceCollection services) | |
{ | |
var config = new ConfigurationBuilder() | |
.SetBasePath(Directory.GetCurrentDirectory()) | |
.AddJsonFile("config.json") | |
.Build(); | |
var github = config.Get<GitHub>("github"); | |
services.AddSingleton(github); | |
services.AddSingleton<HttpClient>(); | |
services.AddTransient<IGitHubRepositoriesFunction, CoreGitHubRepositoriesFunction>(); | |
} | |
} |
By doing so, all necessary dependencies have been registered into the IoC container.
HTTP Trigger Function – Refactored #3
With these changes, let's refactor the function trigger again. Instead of directly using the IServiceProvider
as a static property, it uses the IFunctionFactory
this time.
public static class CoreGitHubRepositoriesHttpTrigger | |
{ | |
public static IFunctionFactory Factory = new CoreFunctionFactory(new CoreAppModule()); | |
[FunctionName("CoreGitHubRepositoriesHttpTrigger")] | |
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = "core/repositories")]HttpRequest req, ILogger log) | |
{ | |
var result = await Factory.Create<IGitHubRepositoriesFunction>(log) | |
.InvokeAsync<HttpRequest, object>(req) | |
.ConfigureAwait(false); | |
return new OkObjectResult(result); | |
} | |
} |
What the function trigger needs to do is to pass parameters and invoke the function that contains all the logics. It doesn't have to know what's going on inside the function. Testing the function trigger gets much easier.
[TestClass] | |
public class CoreGitHubRepositoriesHttpTriggerTests | |
{ | |
[TestMethod] | |
public async Task Given_TypeAndName_Run_Should_Return_Result() | |
{ | |
// Arrange | |
var result = new { Hello = "World" }; | |
var function = new Mock<IGitHubRepositoriesFunction>(); | |
function.Setup(p => p.InvokeAsync<HttpRequest, object>(It.IsAny<HttpRequest>(), It.IsAny<FunctionOptionsBase>())).ReturnsAsync(result); | |
var factory = new Mock<IFunctionFactory>(); | |
factory.Setup(p => p.Create<IGitHubRepositoriesFunction>(It.IsAny<ILogger>())).Returns(function.Object); | |
// Inject the mocked IFunctionFactory instance. | |
CoreGitHubRepositoriesHttpTrigger.Factory = factory.Object; | |
var query = new FakeQueryCollection(); | |
query["type"] = "lorem"; | |
query["name"] = "ipsum"; | |
var req = new Mock<HttpRequest>(); | |
req.SetupGet(p => p.Query).Returns(query); | |
var log = new Mock<ILogger>(); | |
// Action | |
// Inject the mocked parameters. | |
var response = await CoreGitHubRepositoriesHttpTrigger.Run(req.Object, log.Object).ConfigureAwait(false); | |
// Assert | |
response.Should().BeOfType<OkObjectResult>(); | |
(response as OkObjectResult).Value.Should().Be(result); | |
} | |
} |
Of course, all the logics also need to be tested, but it's much easier because they are NOT static
classes any longer. I'm not going to show how to test the rest here. Instead, I'll let you test them.
More Complex Dependency Injection Scenarios
Someone having hawk's eyes might have been wondering why I used IGitHubRepositoriesFunction
, instead of IFunction
. The dependency injection feature that ASP.NET Core provides is very simple. There is no control over multiple implementations with a same interface. For example, there might be multiple functions implementing the same IFunction
interface like:
services.AddTransient<IFunction, FunctionABC>(); | |
services.AddTransient<IFunction, FunctionPQR>(); | |
services.AddTransient<IFunction, FunctionXYZ>(); |
If I need to differentiate them from each other, the current workaround is to create another interfaces like IFunctionABC
, IFunctionPQR
and IFunctionXYZ
inheriting IFunction
and pass them, instead of directly using IFunction
. Alternatively, I can write a custom logic around them.
There is another scenario. Functions tend to live in a same assembly, ie. a same .dll
file. If I can scan a .dll file and automatically register all functions, it would be much easier. Unfortunately, this is not supported by ASP.NET Core either. If you really want to use those features, a 3rd-party library like Autofac needs to be considered. However, also unfortunately, it doesn't seem to get along with V2 yet.
Therefore, here's the suggestion. If you want to use the IoC container from the 3rd-party library, stay on V1. If you want to use the IoC container provided by ASP.NET Core, use V2.
So far, I've walked through how dependency injections are working on Azure Functions V2, with ASP.NET Core's DI feature. Obviously this is not an ideal solution yet and I know Azure Functions Team works really hard to enable this feature sooner rather than later. I hope this feature is released soon.
ACKNOWLEDGEMENT: This post has originally been posted at Mexia blog.