Using Autofac Registration middleware to resolve a version of the service

Viewed 32

I want to resolve the Named registration where the name is given at runtime.

Background story - I am exposing my endpoints with multiple versions, e.g.:

https://localhost/myservice/api/v1/allcities
https://localhost/myservice/api/v2/allcities
...

The controller needs to invoke a different version of the injected service based on the version of the invoked endpoint.

To illustrate, I would expect when I invoke https://localhost/myservice/api/v1/blahblah it executes using MyServiceV1 and when I invoke https://localhost/myservice/api/v2/blahblah it executes using MyServiceV2

I'm using .NET Core 3.1 API with Microsoft.AspNetCore.Mvc.Versioning, Autofac 6.

I can get what version was invoked via IHttpContextAccessor as contextAccessor.HttpContext.GetRequestedApiVersion(). In this question I want to focus on resolving a specific version of the service at runtime.

My idea was to register the service as Named (or Keyed, doesn't matter) and using the Registration middleware registration intercept the resolution process and inject the proper version of the Named service.

Code:

public interface IMyService 
{ 
   string GetImplementationVersion();
} 

[MyServiceVersioning("1.0")]
public class MyService1 : IMyService
{
    public string GetImplementationVersion() => "Example 1.0";
}

[MyServiceVersioning("2.0")]
public class MyService2 : IMyService
{
    public string GetImplementationVersion() => "Example 2.0";
}

public class MyMasterService
{
    private IMyService _myService;
    public MyMasterService(IMyService myService)
    {
       _myService = myService;
    }

    public string GetInjectedVersion() => _myService.GetImplementationVersion();
}

The registrations (Edited for completeness)

// ... 
builder.RegisterType<VersionService>().As<IVersionService>();
// next two lines registered using extension that's in the next code block example
builder.RegisterVersioned<MyServiceV1, IMyService>(); 
builder.RegisterVersioned<MyServiceV2, IMyService>();
builder.Register<MyMasterService>();

And finally the implementation of RegisterVersioned extension where the question lies:

public static class AutofacVersioningExtensions
{
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
            RegisterVersioned<T, TInterface>(this ContainerBuilder builder) where T: class
        {
            var versioningAttribute = typeof(T).GetCustomAttribute<MyServiceVersionAttribute>();
            if (versioningAttribute == null)
            {
                // no versioning exists, do it simply
                return builder.RegisterType<T>().As<TInterface>();
            }
            
            return builder.RegisterType<T>().As<TInterface>().Named<TInterface>(versioningAttribute.Version).ConfigurePipeline(p =>
            {
                p.Use(PipelinePhase.RegistrationPipelineStart, (context, next) =>
                {
                    var invokedVersion = context.Resolve<IVersionService>().CurrentVersion;
// HERE --> in the next line issue is that we have obvious circular resolution
// + it only resolves the last registration
// (I could do Resolve<IEnumerable<TInterface>> to get all registrations but that's an overhead that I'd like to avoid if possible).

                    var resolved = context.ResolveNamed<TInterface>(invokedVersion); 
                    if (resolved != null)
                    {
                        context.Instance = resolved;
                    }
                    else
                    {
                        next(context);
                    }
                });
            });
        }
}

Do you have any ideas? Is my approach even on the right path?

1 Answers

It seems like you could make this easier by using a lambda registration.

builder.RegisterType<MyServiceV1>().Keyed<IMyService>("1.0");
builder.RegisterType<MyServiceV2>().Keyed<IMyService>("2.0");
builder.Register<MyMasterService>();
builder.Register(ctx =>
{
  var version = ctx.Resolve<IHttpContextAccessor>().HttpContext.GetRequestedApiVersion();
  return ctx.ResolveKeyed<IMyService>(version);
}).As<IMyService>().InstancePerLifetimeScope();

This way, when you resolve an un-keyed IMyService, it'll go through the detection process and return the right keyed version. That keyed instance will be cached for the duration of the request.

Related