Introduction
Welcome back to the mini series on using Autofac with Isolated Azure Functions in .NET 6. So far we have looked at how to plug in Autofac into the host builder, use keyed dependencies indirectly in a function and use keyed dependencies directly in a function. In this post I will show you have you can drive these keyed dependencies at run time. This technique is not limited to Azure Functions and I have done a very similar setup with ASP.NET Core using the IHttpContextAccessor
to achieve the same goal. This technique also requires the ServiceBasedFunctionActivator
processing from the previous post.
Show me the code
The example code for this can be found in the http-request-driven branch.
HttpRequestAccessor
With ASP.NET Core there was a concept introduced call the IHttpContextAccessor
which allowed access to the current HttpContext
throughout the current request. It did not require the developer to pass around the HttpContext
to other services which required it anymore as the service which required it could take a dependency on IHttpContextAccessor
at any point. This process is based on a similar technique however as Azure Functions does not do this out of the box yet we need to jump through a few hoops to get there.
First off we need to define the accessor interface and implementation to work with. This is heavily influenced by the HttpContextAccessor
implementation.
First up the interface.
public interface IHttpRequestAccessor
{
HttpRequestData? HttpRequest { get; set; }
}
Secondly the implementation.
public class HttpRequestAccessor : IHttpRequestAccessor
{
private readonly AsyncLocal<ContextHolder> _context = new();
public HttpRequestData? HttpRequest
{
get => _context.Value?.Context;
set
{
var holder = _context.Value;
if (holder is not null)
{
holder.Context = null;
}
if (value is not null)
{
_context.Value = new ContextHolder { Context = value };
}
}
}
private class ContextHolder
{
public HttpRequestData? Context;
}
}
This is now registered in the HostBuilder
setup in the configure services section.
services.AddSingleton<IHttpRequestAccessor, HttpRequestAccessor>();
Now we have the construct to hold the HttpRequestData
for the current request we now need a construct to read it from the request and add it to the registration.
If you’re curious about the registration being done as a Singleton then check out my post “Are you registering IHttpContextAccessor correctly?”.
HttpRequestMiddleware
The HttpRequestMiddleware
is where the magic happens but due to current limitations there is some manual processing required. I am hopefull this will become a little more “out of the box” in the future. If you have read my previous post this technique will look familiar.
public class HttpRequestMiddleware : IFunctionsWorkerMiddleware
{
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
// determine the type, the default is Microsoft.Azure.Functions.Worker.Context.Features.GrpcFunctionBindingsFeature
(Type featureType, object featureInstance) = context.Features.SingleOrDefault(x => x.Key.Name == "IFunctionBindingsFeature");
// find the input binding of the function which has been invoked and then find the associated parameter of the function for the data we want
var inputData = featureType.GetProperties().SingleOrDefault(p => p.Name == "InputData")?.GetValue(featureInstance) as IReadOnlyDictionary<string, object>;
var requestData = inputData?.Values.SingleOrDefault(obj => obj is HttpRequestData) as HttpRequestData;
if (requestData is not null)
{
// set the request data on the accessor from DI
var accessor = context.InstanceServices.GetRequiredService<IHttpRequestAccessor>();
accessor.HttpRequest = requestData;
}
await next(context);
}
}
In the middleware we are accessing the functions binding feature and then interrogating the provided InputData
for the http request based construct. As we could have any number of different Azure Functions types we need to check to see if they are http based. If the request data is located then the IHttpRequestAccessor
instance is requested from the service provider and the value of the HttpRequestData
is set. More details about the process can be found here.
New Attribute
Now we’ve got the data we require and it’s stashed somewhere we can get access to we now need to use the values to determine how to get the required dependency.
First off we need to start by defining our custom attribute which will decorate the dependency. This will be used in a similar fashion to how we currently are using the KeyFilter
attribute. To do this we start off by defining our own attribute which derives from ParameterFilterAttribute
and requires the following methods to be implemented.
public override object? ResolveParameter(ParameterInfo parameter, IComponentContext context)
{
}
public override bool CanResolveParameter(ParameterInfo parameter, IComponentContext context)
{
}
CanResolveParameter
is called early in the dependency construction lifecycle to determine whether the dependency which has been decorated with this attribute can be resolved. If this returns true
then a bit later in the resolving lifecycle the ResolveParameter
method is called which is responsible for using the IComponentContext
instance to resolve the dependency.
We now need to find the key value, provided by the Http header value, and using the IComponentContext
provided by the method overrides we can now resolve the IHttpRequestAccessor
which we registered and setup earlier. We now have access to the HttpRequest and the header values provided.
if (context.TryResolve<IHttpRequestAccessor>(out var httpRequestAccessor)
&& httpRequestAccessor.HttpRequest is not null)
{
if (httpRequestAccessor.HttpRequest.Headers.TryGetValues(HeaderName, out var values)
&& values.Any())
{
key = values.First();
}
}
Now we have the header key value we can either see of the keyed service is registered in the CanResolveParameter
method.
return context.IsRegisteredWithKey(key!, parameter.ParameterType);
And if it is successful we can then try and resolve the instance of the required service in the ResolveParameter
method.
if (context.TryResolveNamed(key!, parameter.ParameterType, out var instance))
{
return instance;
}
The full implementation is below.
public class HttpRequestDrivenAttribute : ParameterFilterAttribute
{
private const string HeaderName = "X-Greeting";
public override object? ResolveParameter(ParameterInfo parameter, IComponentContext context)
{
if (TryResolveServiceKey(context, out var key))
{
if (context.TryResolveNamed(key!, parameter.ParameterType, out var instance))
{
return instance;
}
}
return null;
}
public override bool CanResolveParameter(ParameterInfo parameter, IComponentContext context)
{
if (TryResolveServiceKey(context, out var key))
{
return context.IsRegisteredWithKey(key!, parameter.ParameterType);
}
return false;
}
private bool TryResolveServiceKey(IComponentContext context, out string? key)
{
key = null;
if (context.TryResolve<IHttpRequestAccessor>(out var httpRequestAccessor)
&& httpRequestAccessor.HttpRequest is not null)
{
if (httpRequestAccessor.HttpRequest.Headers.TryGetValues(HeaderName, out var values)
&& values.Any())
{
key = values.First();
}
}
return !string.IsNullOrWhiteSpace(key);
}
}
And that is it. But how do you call this?
Update the Function
We’re almost done. The final piece of the puzzle is to update the Azure Function constructor to tell Autofac where the dependency should come from. This is done by using the attribute we have defined above to decorate the dependency.
public GetWelcome(
[HttpRequestDriven] IGreeting greeting,
ILoggerFactory loggerFactory)
{
_greeting = greeting;
_logger = loggerFactory.CreateLogger<GetWelcome>();
}
Calling the Function
To get this to work we now need to provide a Http Header in the request to the Azure Function. This can be done through code or while testing through Postman by adding the header into the request. The header key in this scenario is X-Greeting
and the related values which are acceptable are the keyed values we registered the services with originally; “hello” and “goodbye”. In a production environment I would likely add in some sort of mapping layer in between the header key value and the internal value. This will allow for checking the value and being defensive about it but also you should never expose an internal implementation detail to the caller. This will allow the internals to change and evolve without the caller needing to change the value they send.
Conclusion
In this post we have looked at how to drive a runtime dependency resolution through a value provided from the http request. This allows for what I call “hot swapping” dependencies at runtime and not have to decide at compile time. This allows for a lot of more complex scenarios. The keyed value also doesn’t have to come from a provided value it can also come from other means such as environment variables in a similar fashion.
How would you use this functionality? Do you think it’s useful? Let me know on Twitter @WestDiscGolf as I’d be interested in hearing your thoughts.