Autofac
Autofac copied to clipboard
Hook/Extension point modifying IRegistrationBuilder
(I'm not 100% sure that this isn't possible today but afaik it isn't...)
Problem Statement
We use our own little aop fw for our services. To use this in combination with autofac we use the same technique as eg autofac.extras.dynamicproxy does, having an extension method that replaces the implemented type of the service. It works fine but as we want this for all our services (replace registered service type with our generated one), it would be nice if there was any hook/extension point or similar in autofac that would be called for every registered component so we could skip this ext method call for each and every registration (many, many, many registrations in our system).
Desired Solution
Don't know the inner working of autofac good enough to have a good suggestion. AttachToRegistrationSource and Registered event seem to happen "too late".
Would it be possible to inject something to ContainerBuilder that will be called for each registration maybe?
Alternatives You've Considered
We can create a composition for ContainerBuilder and extend its methods behavior there. Or we could create our own extension methods directly on ContainerBuilder. Still, would be great if a "registration hook" would be available in autofac itself, especially for us with a big legacy system with many different processes and containerbuilder instances.
If you could, would you be able to expand a bit on how you envision this working so we can get a better feel for the concept? This will also help us think through some of the more detailed use cases to determine if this is reasonable:
"If there was any hook/extension point or similar in Autofac that would be called for every registered component..."
- Can you show some example code to illustrate what you're looking for? It doesn't have to work, just help visualize what you're thinking.
- How does this relate to the existing set of lifetime events -
OnPreparing,OnActivating,OnActivated,OnRelease? Is this new or would these events work? - Can you opt out? Like, "I have all but one component where this needs to happen." Is that a thing?
AttachToRegistrationSourceandRegisteredevent seem to happen "too late".
- When exactly do you think that should be called?
- When does it get called on nested lifetime scopes? (You can register components dynamically in child scopes.)
- Does it get called for components registered using a
RegistrationSourcelikeIEnumerable<T>or just components explicitly registered?
"Global" sorts of things get really tricky based on when things are registered/resolved, etc. Getting some clarification on this will help us better understand what the issue here is and how we might be able to address it. Thanks!
Our code today looks like this....
builder.RegisterType<Svc1>().ApplyAspect().....;
builder.RegisterInstance(svc2).ApplyAspect()....;
builder.Register(x => ....).ApplyAspect()....;
...
...where our extension method(s) ApplyAspect checks if the component should be aspected, and if it should, replace its type by setting registration.ActivatorData.ImplementationType.
What would be nice is to not having to call our ext method ApplyAspect on each of our components but instead have some way injecting our ApplyAspect code so it ran for all our registrations.
Another way trying to describe what we are after... In Autofac.Extras.DynamicProxy, EnableClassInterceptor (and EnableInterfaceInterceptor) does similar things as ours ApplyAspect. The thing we are looking for, is same as making Autofac.Extras.DynamicProxy work without these extension methods and instead allowing its Enable... code to be injected "somewhere" and to be called for each registration.
That helps a bit, but if you could also answer the other relevant items I mentioned when you get a chance, that'd help.
(I noticed that Autofac.Extras.DynamicProxy's Enable... ext methods does a lot more than we need, so maybe I shouldn't mentioned that library at all. What we need is basically do some ifs and set registration.ActivatorData.ImplementationType)
How does this relate to the existing set of lifetime events - OnPreparing, OnActivating, OnActivated, OnRelease? Is this new or would these events work?
I don't know but I don't think it's possible to change type here?
Can you opt out? Like, "I have all but one component where this needs to happen." Is that a thing?
That check we do in our ext method today.
When exactly do you think that should be called?
Dont know but if I get the code right, IRegistrationBuilder is only available at the time the component is registered. Present hooks seems to fire later.
When does it get called on nested lifetime scopes? (You can register components dynamically in child scopes.)
Dont know how "register components dynamically in child scopes" works so cannot answer this/have no opinion about this.
Does it get called for components registered using a RegistrationSource like IEnumerable<T> or just components explicitly registered?
Not sure, dont know enough about RegistrationSource.
"Global" sorts of things get really tricky based on when things are registered/resolved, etc.
Totally understand! Thanks for your replies.
Can you opt out? Like, "I have all but one component where this needs to happen." Is that a thing?
That check we do in our ext method today.
Think more about what this looks like if it's a global method. Answer the questions in the context of the proposed feature rather than what you to today.
For example, maybe it's
containerBuilder.AddSomeGlobalAwesomeThing(registration => registration.DoSomething());
You set that up globally. Is it the responsibility of the extension method to do the filtering? Or is there some way to say
containerBuilder.Register<T>().WithoutTheGlobalCoolThing();
?
Help us think through what this looks like if we implement it and what that means - including all the crazy edge cases. Unfortunately, you're catching the project at a time when we don't have a consistent set of folks working on it and, post-COVID, there's some admittedly pretty harsh maintainer burnout. The more you help, the faster we'll be able to determine if this is even possible or how it'd work.
It might mean spelunking into the tests to understand how more stuff works. For example, RegistrationSource is how we get IEnumerable<T> support - it's a dynamic source that provides registrations at runtime. (We don't actually execute builder.Register<IEnumerable<T>>() for every possible type in the system, right? 😉 ) Do the global methods affect those? Can the registration sources opt out?
And, of course, whether you do those deep dives and start fleshing out the design of the potential feature or not is all up to you. If you don't have the time or don't feel like you are able, don't worry. We'll get to it when we get to it, though it'll likely be pretty far down the priority list since stuff like this is a lot of work.
If today's ext methods in RegistrationExtensions that actually creates new registrations (RegisterInstance(...), RegisterType(...) and Register(...) were virtual instance methods on ContainerBuilder, we could control the registration the way we want. On the other hand, having registration methods as ext methods seems to be the way to go in all autofac eco system so wouldn't help in all registrations anyway (mvc and others).
More suitable would probably be to change the RegistrationBuilder to not be static but a injectable component. Don't know enough details there to know exactly how/what though.
I also fully understand that there will be "many crazy edge cases". That in combination that this isn't by any mean necessary for us (anyone?) => probably not worth the effort to fix it. But please keep it in the back of your head if you rewrite logic around the registration part in the future ;).
thanks.
Just taking a look over this issue, if we assume that we have registered some sort of 'global' thing that runs inline with the normal registration extension methods, I can imagine it gets complicated when you have multiple methods called when registering a single component. For example:
builder.RegisterType<SomeComponent>().As<IService>().SingleInstance();
In that registration, if you've got some GlobalCallbackThatRunsOnEveryRegistration, when does it run? After RegisterType? After As? After SingleInstance?
One of the reasons that the actual creation of registrations happens in a delayed fashion is precisely to make sure we have collected all of the potential modifications to an IRegistrationBuilder before we build the container.
If we go back to the original requirement stated here, it seems like what @RogerKratz is after is to be able to modify the IInstanceActivator for a component inside the Registered event, to change it from the "default" to some custom AOP form.
To do that, we could add an IComponentRegistration.ReplaceActivator(IInstanceActivator activator) method that throws an InvalidOperationException if you try to call it after BuildResolvePipeline has been called (which is the point at which the registration actually becomes readonly).
We would also need to re-apply the limit type checks in that method currently applied in RegistrationBuilder to verify the services can all be satisfied by the new IInstanceActivator.LimitType.
Personally it feels like that solution leans on the existing event handlers we already have, integrates nicely with dynamically registered components (because Registered gets called for registrations from registration sources as well), and prevents ambiguity of use.
Is that something that could be globally applied with a module, maybe, in AttachToComponentRegistration? That would solve the need to apply the extension to every registration.
Yes, AttachToComponentRegistration is just attached directly to the Registered event, and has access to IComponentRegistration before the pipeline build happens.
OK, so after much going round the houses on a ReplaceActivator method, we realised we didn't need to do any of it, because this scenario already "just works" using resolve pipeline middleware.
If the desire from @RogerKratz is to replace the activator for all/some components, you can add middleware to do it. In the below example I create middleware that ignores the existing registered activator at resolve time, and uses its own. The middleware is added in the Registered method via the PipelineBuilding event.
public class ActivatorReplacementWithMiddleware
{
[Fact]
public void CanReplaceActivatorWithMiddleware()
{
var builder = new ContainerBuilder();
builder.ComponentRegistryBuilder.Registered += (sender, args) =>
{
if (args.ComponentRegistration.Activator.LimitType == typeof(ServiceA))
{
args.ComponentRegistration.PipelineBuilding += (sender, pipeline) =>
{
pipeline.Use(new ActivatorReplacementMiddleware(() => new ServiceA(createdCustom: true)));
};
}
};
builder.RegisterType<ServiceA>();
var container = builder.Build();
var instance = container.Resolve<ServiceA>();
Assert.True(instance.CreatedCustom);
}
}
class ActivatorReplacementMiddleware : IResolveMiddleware
{
private readonly Func<object> _getInstance;
public PipelinePhase Phase => PipelinePhase.Activation;
public ActivatorReplacementMiddleware(Func<object> getInstance)
{
this._getInstance = getInstance;
}
public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
{
// If you want, you can call the existing activator by invoking next,
// but you don't need to.
// next(context);
context.Instance = _getInstance();
}
}
@alistairjevans Thanks for providing this solution. However, I think the suggested approach cannot acheive exactly what we want. If you look at the snippet from EnableClassInterceptors [line 96], the snippet looks like following
public static IRegistrationBuilder<TLimit, TConcreteReflectionActivatorData, TRegistrationStyle> EnableClassInterceptors<TLimit, TConcreteReflectionActivatorData, TRegistrationStyle>(
this IRegistrationBuilder<TLimit, TConcreteReflectionActivatorData, TRegistrationStyle> registration,
ProxyGenerationOptions options,
params Type[] additionalInterfaces)
where TConcreteReflectionActivatorData : ConcreteReflectionActivatorData
{
if (registration == null)
{
throw new ArgumentNullException(nameof(registration));
}
registration.ActivatorData.ImplementationType =
ProxyGenerator.ProxyBuilder.CreateClassProxyType(
registration.ActivatorData.ImplementationType,
additionalInterfaces ?? Type.EmptyTypes,
options);
var interceptorServices = GetInterceptorServicesFromAttributes(registration.ActivatorData.ImplementationType);
AddInterceptorServicesToMetadata(registration, interceptorServices, AttributeInterceptorsPropertyName);
registration.OnPreparing(e =>
{
var proxyParameters = new List<Parameter>();
int index = 0;
if (options.HasMixins)
{
foreach (var mixin in options.MixinData.Mixins)
{
proxyParameters.Add(new PositionalParameter(index++, mixin));
}
}
proxyParameters.Add(new PositionalParameter(index++, GetInterceptorServices(e.Component, registration.ActivatorData.ImplementationType)
.Select(s => e.Context.ResolveService(s))
.Cast<IInterceptor>()
.ToArray()));
if (options.Selector != null)
{
proxyParameters.Add(new PositionalParameter(index, options.Selector));
}
e.Parameters = proxyParameters.Concat(e.Parameters).ToArray();
});
return registration;
}
When I try to achieve what it is doing in the pipeline I encountered at least 3 issues.
- As you can see, the above codes run only once, when registering with
ContainerBuilderwithEnableClassInterceptors(). The approach suggested by you may force the logic be executed everytime when the request resolve pipeline being triggered - Since
registration.ActivatorData.ImplementationTypeno longer exist while the request resolve pipeline being triggered, at the same time, theregistration.ActivatorData.ImplementationTypewas hided as a private field insideIInstanceActivatoras_impletementationType, so I cannot access and update it as what the above snippets done - There are some logics hooked to
IRegistrationBuilder.OnPreparingwhich I don't know where should I put into the request resolve pipeline
I think a global AoP approach is quite useful for modern large scale application, e.g. logging. I hope I could find a way to make it works with Autofac as what OP desired to.