I'm trying to migrate to Autofac from NInject, and I'm running into a seemingly simple problem. It does not appear Autofac has a way of applying a factory method to an open generic binding.
NInject's solution seems about as concise as one could hope for:
Kernel.Bind(typeof(IOptions<>)).ToMethod(c => CreateOptions(c.Request.Service));
In other words, when a closed IOptions<> is required, call CreateOptions() with the requested type and let me create the implementation.
This solution is also nice because one can treat this binding just like any other and go on to fluently set interceptors, lifetime, etc.
The best I've come up with in Autofac is either:
IOptions<>, let Autofac construct it with RegisterGeneric(), then use ReplaceInstance() in the OnActivating event.IRegistrationSource that filters by type and then calls my factory method.Method (1) seems kludgey - surely I don't have to create an unused object just to get its type?
Method (2) is okay, but it's a mass of code (see below), and it gets even bigger if I want to set things like scope, etc.
In both cases the amount of code to simply delegate to a factory method seems wild, especially compared to Autofac's slick factory system for non-open generic types.
(For completeness, below is my IRegistrationSource. Note the two abstract members that must be overridden to get the NInject functionality.)
public abstract class DelegatingRegistrationSource : IRegistrationSource
{
protected abstract bool CanDelegate(Type limitType);
protected abstract DelegateActivator CreateDelegate(Type limitType);
public IEnumerable<IComponentRegistration> RegistrationsFor(Service service,
Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
{
if (!(service is IServiceWithType swt) || !CanDelegate(swt.ServiceType))
return Enumerable.Empty<IComponentRegistration>();
var registration = new ComponentRegistration(
Guid.NewGuid(),
CreateDelegate(swt.ServiceType),
new CurrentScopeLifetime(),
InstanceSharing.None,
InstanceOwnership.OwnedByLifetimeScope,
new[] { service },
new Dictionary<string, object>());
return new IComponentRegistration[] { registration };
}
public bool IsAdapterForIndividualComponents => false;
}
This request stems from my StackOverflow question and the request there from Alistair Evans.
Thanks for your time!
Interesting. It's like... A delegate binding to an open generic service rather than a reflection binding. Hmmm. I mean, it's not like five minutes' of work, but it sounds like a neat feature. I like it.
Agreed; I took a quick look at this last night when I got "nerd sniped" by the SO question.
The issue we have is largely around what that delegate returns; currently we pick up the limit type automatically from that at registration time, and we would have to not do that.
I'll have a think about what might be involved.
OK, so I've got something working locally; wanted to run the API signature/behaviour past people before I write more tests and raise a PR.
The simple description is that a delegate can now be registered which takes an array of types in addition to the normal parameters, which contains the generic type arguments for the requested service.
Registration Methods:
public static IRegistrationBuilder<object, OpenGenericDelegateActivatorData, DynamicRegistrationStyle>
RegisterGeneric(this ContainerBuilder builder, Func<IComponentContext, Type[], object> factory);
public static IRegistrationBuilder<object, OpenGenericDelegateActivatorData, DynamicRegistrationStyle>
RegisterGeneric(this ContainerBuilder builder, Func<IComponentContext, IEnumerable<Parameter>, Type[], object> factory);
You can use it like this:
[Fact]
public void CanResolveByGenericInterface()
{
var builder = new ContainerBuilder();
builder.RegisterGeneric((ctxt, types) => Activator.CreateInstance(typeof(ImplementationA<>).MakeGenericType(types)))
.As(typeof(IInterfaceA<>));
var container = builder.Build();
var instance = container.Resolve<IInterfaceA<string>>();
var implementedType = instance.GetType().GetGenericTypeDefinition();
Assert.Equal(typeof(ImplementationA<>), implementedType);
}
@ladenedge, that feels like it gives you a solution to your original scenario?
Yes, it's hard to see how it could be more perfect. It's even friendlier than the NInject version because of the instant access to the generic args. Easy to use, and in line with the non-generic factory registrations. Nice work!
Usage looks great.
On the second overload - context, parameters, types - swap so it's context, types, parameters to be consistent order with the first overload. Otherwise... Really nice job!