Proper Dependency injection in Azure Functions on function level with scoped services!
In my last post you learned how to implement dependency injection in Azure Functions on function level. While this works for services registered as transient or singleton, it does not work with services registered as scoped. In this post you will learn how we can implement proper dependency injection with scopes!
Inject attribute
We again start with the Inject attribute. Since we are going to change how to resolve bindings, the attribute can now be empty.
[Binding]
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class InjectAttribute : Attribute
{
}
Binding provider
The binding provider is used to create bindings. The method TryCreateAsync
will be called for each instance of the InjectAttribute
.
There we get additional informations, e.g. the parameter on which the attribute is used. Our binding provider also receives the IServiceProvider
instance which will be passed to the binding.
public class InjectBindingProvider : IBindingProvider
{
private readonly IServiceProvider _serviceProvider;
public InjectBindingProvider(IServiceProvider serviceProvider) =>
_serviceProvider = serviceProvider;
public Task<IBinding> TryCreateAsync(BindingProviderContext context)
{
IBinding binding = new InjectBinding(_serviceProvider, context.Parameter.ParameterType);
return Task.FromResult(binding);
}
}
Inject binding
The binding class holds the information of the binding. In this case we pass the IServiceProvider
instance and Type
the attribute is bound to. The method BindAsync
will be called, when ever a binding is resolved which happens when a function will be executed. We resolve the service from the IServiceProvider
and pass it to a IValueProvider
.
The InjectValueProvider
holds the value (the instance of the requested service).
public class InjectBinding : IBinding
{
private readonly Type _type;
private readonly IServiceProvider _serviceProvider;
public InjectBinding(IServiceProvider serviceProvider, Type type)
{
_type = type;
_serviceProvider = serviceProvider;
}
public bool FromAttribute => true;
public Task<IValueProvider> BindAsync(object value, ValueBindingContext context) =>
Task.FromResult((IValueProvider)new InjectValueProvider(value));
public async Task<IValueProvider> BindAsync(BindingContext context)
{
await Task.Yield();
var value = _serviceProvider.GetRequiredService(_type);
return await BindAsync(value, context.ValueContext);
}
public ParameterDescriptor ToParameterDescriptor() => new ParameterDescriptor();
private class InjectValueProvider : IValueProvider
{
private readonly object _value;
public InjectValueProvider(object value) => _value = value;
public Type Type => _value.GetType();
public Task<object> GetValueAsync() => Task.FromResult(_value);
public string ToInvokeString() => _value.ToString();
}
}
Register attribute and the binding provider
Now we glue everything together and register the attribute and binding provider. We again use a IExtensionConfigProvider
implementation for this.
public class InjectConfiguration : IExtensionConfigProvider
{
public void Initialize(ExtensionConfigContext context)
{
var services = new ServiceCollection();
RegisterServices(services);
var serviceProvider = services.BuildServiceProvider(true);
context
.AddBindingRule<InjectAttribute>()
.Bind(new InjectBindingProvider(serviceProvider));
}
private void RegisterServices(IServiceCollection services)
{
services.AddSingleton<IGreeter, Greeter>();
}
}
We now have more a less the same state as in the last post except, that the attribute got simplified. But this implementation still works only with transient and singleton scoped services.
[FunctionName("Greeter")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")]HttpRequestMessage req,
[Inject]IGreeter greeter)
{
return req.CreateResponse(greeter.Greet());
}
Adding support for scoped services
In order to support scoped services we need to change some of the methods. First we need to create and store an IServiceScope
instance for each function invocation, so that we can share the same scope for the same function invocation. Luckily each function invocation gets a unique id, which will be passed our InjectBinding
. First we add a ConcurrentDictionary
to the InjectBindingProvider
to store the scopes.
public class InjectBindingProvider : IBindingProvider
{
public static readonly ConcurrentDictionary<Guid, IServiceScope> Scopes =
new ConcurrentDictionary<Guid, IServiceScope>();
...
}
Now we change the BindAsync
method in the InjectBinding
. The BindingContext
contains the FunctionInstanceId which is the unique id, created for each function invocation. So for each invocation we create a new scope and store the scope in the ConcurrentDictionary
. We then resolve the service from the scope.
public async Task<IValueProvider> BindAsync(BindingContext context)
{
await Task.Yield();
var scope = InjectBindingProvider.Scopes.GetOrAdd(context.FunctionInstanceId, (_) => _serviceProvider.CreateScope());
var value = scope.ServiceProvider.GetRequiredService(_type);
return await BindAsync(value, context.ValueContext);
}
At this point we are already able to resolve scoped services, BUT we do not clean up and destroy the scope, so all resolved service will life forever. In order to cleanup the scopes, we implement an IFunctionInvocationFilter
and IFunctionExceptionFilter
which are called whenever a function execution ended or when an exception within the function occurred.
public class ScopeCleanupFilter : IFunctionInvocationFilter, IFunctionExceptionFilter
{
public Task OnExceptionAsync(FunctionExceptionContext exceptionContext, CancellationToken cancellationToken)
{
RemoveScope(exceptionContext.FunctionInstanceId);
return Task.CompletedTask;
}
public Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancellationToken)
{
RemoveScope(executedContext.FunctionInstanceId);
return Task.CompletedTask;
}
public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken) =>
Task.CompletedTask;
private void RemoveScope(Guid id)
{
if (InjectBindingProvider.Scopes.TryRemove(id, out var scope))
{
scope.Dispose();
}
}
}
As the last step we need to register the filter. Therefor we change the Initialize
method of our InjectConfiguration
.
public void Initialize(ExtensionConfigContext context)
{
...
var registry = context.Config.GetService<IExtensionRegistry>();
var filter = new ScopeCleanupFilter();
registry.RegisterExtension(typeof(IFunctionInvocationFilter), filter);
registry.RegisterExtension(typeof(IFunctionExceptionFilter), filter);
}
Conclusion
Although Azure Functions does not come with dependency injection for you functions, it can be added quite easy. It is still a “hacky” solution, but it works :-) You can find the full example on GitHub.