-
-
Notifications
You must be signed in to change notification settings - Fork 837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature Proposal - Generic Overloads for Register to make delegate registration more concise #1320
Comments
Off the top of my head, would this have worked? builder
.RegisterType<MyComponent>()
.WithParameter(TypedParameter.From(myOpts)); On the resolve side, you can also use the delegate factories thing: var factory = container.Resolve<Func<MyComponent, ConfigOptions>>();
var thing = factory(opts); Or is there perhaps some use case I'm missing here? It seems like the proposed solution, while potentially interesting, may be overly complicated compared to adding a parameter and I'm not sure if it's just because the use case in the proposal was possibly oversimplified. |
The a) The component in question is activated very frequently in a perf hot-path; using delegate activation let's me side-step the bunch of extra code that I happen to know runs in the reflection activator. I will say though, ignoring what already works, that just from the perspective of someone relatively new to Autofac, which one requires you to know more about how to use Autofac to get going? It wasn't immediately obvious to me from the intellisense how best to register the parameter (the var config = "something";
// Feels fairly intuitive, and not super Autofac-y.
builder.Register((IMyDep1 dep) => new MyComponent(dep, config));
// Need to go and consult the WithParameter docs...
builder.RegisterType<MyComponent>()
.WithParameter(TypedParameter.From(config)); The resolve-side factory thing doesn't really help me unless I want to inject that func into something else that consumes I do get that there are existing ways to achieve this, but in some situations like having some external value, it does feel more intuitive to capture into the closure than using |
Totally fair to suggest some new syntax for something like this; I just didn't see it in the proposal and the "alternatives" section didn't acknowledge parameters so I figured I'd ask. I also kinda figured that the resolve side of things wouldn't help, but omitting it would have been somewhat incomplete from an answer perspective. I don't think it's unfair to ask people to be familiar with Autofac syntax. Again, not against making things easier, but there are a lot of things (from implementing modules to middleware to even conditionally registering decorators) that aren't entirely fall-down-easy without taking some time to read the docs. It's a new domain. You don't get IntelliSense to tell you what order to set up the middleware in your ASP.NET app or how to properly set up your AutoMapper mappings, either. And having More Than One Way To Do It means multiple paths to test and maintain. The delegate mechanism coupled with the perf aspect does seem to lend some credence to this being more than just syntax, though, and that's cool. It would be good to see if you get to keep the good perf if you have three or four parameters that end up still having to be resolved. Like, is this still cheaper? var config = "config";
builder
.Register(
(IDep1 dep1, IDep2 dep2, IDep3 dep3, IDep4 dep4) =>
new MyComponent(dep1, dep2, dep3, dep4, config)); Than this? // UsingConstructor to remove the constructor location overhead
builder
.RegisterType<MyComponent>()
.UsingConstructor(typeof(IDep1), typeof(IDep2), typeof(IDep3), typeof(IDep4), typeof(string))
.WithParameter(TypedParameter.From("config")); Absolutely the first one is prettier, but, again, just trying to make sure the various options are considered. One solution already exists and, yeah, it's not pretty, but needing to do this sort of thing feels pretty edge-case-y so it'd just be interesting to know. Admittedly, when I have config like this, I usually just register the instance and call it good. builder.RegisterInstance(configObject);
builder.RegisterType<MyComponent>(); Definitely use cases that aren't covered there - different config options by context, trying to avoid keyed instances, wanting the perf of the lambda without the redundant Let me set up a couple of benchmarks to see what I get. The syntax proposed does look kinda nice, and if it is faster, hey, faster + prettier is a big win. |
All points understood, see what you get with those benchmarks; not sure the I've done that Also...
Maybe it should (he says bitterly). |
OK, legit science, looks like lambda is still faster. Here's my benchmark so you can tell me how I messed it up because I such writing benchmarks. I have two different setups - one that builds the container with the reflection container, one that builds it with the lambda. The benchmark itself is basically the same - just resolve the thing. public class ParameterBenchmark
{
private IContainer _container;
[GlobalSetup(Target = nameof(ReflectionRegistration))]
public void SetupReflectionContainer()
{
var config = new Configuration { Value = "Value" };
var builder = new ContainerBuilder();
builder.RegisterType<Dependency1>().As<IDependency1>();
builder.RegisterType<Dependency2>().As<IDependency2>();
builder.RegisterType<Dependency3>().As<IDependency3>();
builder.RegisterType<Dependency4>().As<IDependency4>();
builder
.RegisterType<Component>()
.UsingConstructor(typeof(IDependency1), typeof(IDependency2), typeof(IDependency3), typeof(IDependency4), typeof(Configuration))
.WithParameter(TypedParameter.From(config));
this._container = builder.Build();
}
[Benchmark]
public void ReflectionRegistration()
{
var instance = this._container.Resolve<Component>();
GC.KeepAlive(instance);
}
[GlobalSetup(Target = nameof(LambdaRegistration))]
public void SetupLambdaContainer()
{
var config = new Configuration { Value = "Value" };
var builder = new ContainerBuilder();
builder.RegisterType<Dependency1>().As<IDependency1>();
builder.RegisterType<Dependency2>().As<IDependency2>();
builder.RegisterType<Dependency3>().As<IDependency3>();
builder.RegisterType<Dependency4>().As<IDependency4>();
builder
.Register(ctx =>
{
return new Component(
ctx.Resolve<IDependency1>(),
ctx.Resolve<IDependency2>(),
ctx.Resolve<IDependency3>(),
ctx.Resolve<IDependency4>(),
config
);
});
this._container = builder.Build();
}
[Benchmark]
public void LambdaRegistration()
{
var instance = this._container.Resolve<Component>();
GC.KeepAlive(instance);
}
private interface IDependency1 { }
private class Dependency1 : IDependency1 { }
private interface IDependency2 { }
private class Dependency2 : IDependency2 { }
private interface IDependency3 { }
private class Dependency3 : IDependency3 { }
private interface IDependency4 { }
private class Dependency4 : IDependency4 { }
private class Configuration
{
public string Value { get; set; }
}
private class Component
{
private readonly IDependency1 _dep1;
private readonly IDependency2 _dep2;
private readonly IDependency3 _dep3;
private readonly IDependency4 _dep4;
private readonly string configValue;
public Component(
IDependency1 dep1,
IDependency2 dep2,
IDependency3 dep3,
IDependency4 dep4,
Configuration config)
{
this._dep1 = dep1;
this._dep2 = dep2;
this._dep3 = dep3;
this._dep4 = dep4;
this.configValue = config.Value;
}
public void DoWork()
{
// So the compiler doesn't try optimizing anything out.
Console.WriteLine($"{this._dep1.GetType()}{this._dep2.GetType()}{this._dep3.GetType()}{this._dep4.GetType()}{this.configValue.GetType()}");
}
}
} Results:
So, not only is it faster, it also appears to allocate less. Color me legitimately shocked. I guess let's go for it. Better syntax plus potentially keeping the perf seems like a win. |
Side note, it makes me wonder if there's a way to further optimize the situation where there's a |
Nice; as you say, science! Benchmarks look good. Extra memory allocations probably come from the bound constructor objects allocated inside the ReflectionActivator (at a guess). I think the optimisation of |
Just curious, what's the limit of type parameters I can pass to Edit: just seen the PR, it's 10 as off now :) |
Yeah, I decided to just pick a number. If you want to do more dependencies than that, I think it's OK to make people use the old way of doing things. |
I've always wondered if there is a recommendation or best-practice. I've seen for instance Cool idea anyway, saves on writing loads of boilerplate code :) |
Not sure what the best practice is, if there is one. The System.HashCode.Combine method in the runtime goes up to 8, but I'm not sure if there's a reason they chose that number. Can't see any obvious reasoning for it in the API proposal. |
I now realise I should have made it 11, so we can say we turned the method all the way up to 11... |
A follow-up to adding all the generic overloads of 1 to 10 arguments; while writing some docs, I realised we don't have a version of builder.Register(() => new Component()); // Not allowed
builder.Register(c => new Component()); // allowed (IComponentContext). I was using the term "a variable number of arguments", and realised that maybe that should include none? I'm not sure if there's a reason we never had such an overload? Easy implementation: public static IRegistrationBuilder<T, SimpleActivatorData, SingleRegistrationStyle>
Register<T>(
this ContainerBuilder builder,
Func<T> @delegate)
where T : notnull
{
if (@delegate == null)
{
throw new ArgumentNullException(nameof(@delegate));
}
return builder.Register((c, p) => @delegate());
} |
There's probably not a great reason it's not there, other than that folks could just ignore the context parameter and move on with life. It also might be confusing as to why it exists for new folks - "I thought you'd just Though it does make me think about optimizations we could make. For example, we could detect things like:
Basically when there's only one constructor to use in the reflection case, somehow automatically switching to a delegate behind the scenes could be a free-to-the-consumer perf boost. |
Looking at those potential optimisations, possibly intriguing. In the reflection activator we throw a The new delegate activator methods largely just "have a go" at resolving what ever arguments are there, so throw a The choices here I think are:
|
Actually, thinking on it a bit more, there's something else to consider here. Consider a component: public class Component
{
public Component(A dep1, B dep2)
{
}
} If I resolve If I resolve Would that be a breaking change if the reflection activator started switching to the former pattern for single-constructor types? This is sort of a non-issue for single-constructor-with-no-parameter components, in which case we can optimise quite easily. |
Eeeesh I didn't think about that activation scenario of one working but one not. Of course, that's also a gotcha with the new lambda syntax - it's going to resolve all the dependencies until it fails. Which is what a hand-written lambda would do, it just may not be obvious. Possibly something to document. You know, registration sources are a nightmare. Without them, we could query the container for the set of concrete, registered services and always get a reliable answer back. I think registration sources are the biggest hurdle to container validation, too. Hmmm. I've never considered exception message text as part of the contract. The exception type maybe, but the text? No. If that breaks the way you're handling exceptions, you need to figure out how to handle exceptions better. If folks want more specific details on an exception type to differentiate between two different conditions, that's two different exception types. (No, I'm not proposing we create a new exception type.) But I like the catch-and-rethrow idea. Outer exception matches the original "we can't bind" message, inner exception explains more detail about what went wrong. Seems like the best of both worlds. Whether resolving, say, five-out-of-six of the arguments first and then failing on the sixth is breaking... it seems like it could have, at the very least, a memory impact that folks aren't expecting. Admittedly failure to resolve a dependency like that should be sort of a catastrophic/fatal issue that needs to be fixed, so ideally it's really rare, but who knows? I've seen weirder things than a try/catch to ignore something relatively serious like that. Right up there with my favorite unit test assertion ever, My gut says in the 90% case it's probably OK to do the resolutions and fail on one of them. If we assume that's a rare occurrence that folks will troubleshoot and fix, optimizing for the failure condition is less interesting than optimizing for overall performance. However, it may be interesting to consider an opt-out option. Like, the ability to say, "I want to resolve this through reflection, I accept responsibility for the lower perf, keep doing it the old way, thanks." Sort of like turning off compiler optimizations. Perhaps we don't provide or expose that immediately out of the gate and wait for folks to ask for it, but whatever solution comes out, it might be good to have the possibility of an escape hatch. I also could be overthinking it. |
Closing this issue now #1325 has been merged; I've been considering the "zero arguments" delegate registration method now we have improved the performance of a reflection no-args-constructor resolve to almost the same as a delegate resolve, and don't think we need to add the |
Hi. I have just upgraded from 6.2.0 to 6.4.0 and noticed an unexpected behavior while resolving new instances. |
@SzybkiDanny If the component only has one constructor and that constructor has no parameters, what's the use case for having a constructor selector? I don't have any problem updating the docs, but I need to be able to explain the scenario. |
We have a custom implementation of |
I think a note in the docs to mention that
...but maybe that's not clear enough. I can update the release notes to be more explicit as to what that means. |
To be honest, I consider Autofac's docs to be as good as it gets for OS projects so I have never had a need to look into release notes and that's why I suggested putting the info in the docs. |
Added this issue for the docs. |
Problem Statement
Today I was registering some Autofac components, and realised I wanted to capture a configuration object, and pass that into my component constructor, along with it's dependencies.
So, something like this:
All good, works as expected, but constructing that instance of
MyComponent
feels reallly verbose, since I need to callctxt.Resolve
each time. It also smacks of service location in a way, because we're accessing an autofac service locator directly:Desired Solution
What I'd like to be able to do is this:
Personally I feel like this brings things closer to feeling like a normal factory method, plus it has the advantage that I can treat any method that accepts dependencies as a factory method, without leaking Autofac types into it.
I also have to know less Autofac 'stuff', like a component context, to get this working.
But, I'd like to retain the option to access the full component context if I need to do something a bit more complex:
Implementation
The implementation is actually pretty simple; we pick a maximum number of generic parameters we want to support; let's say 10 to start, we then create a set of methods for each number of parameters we want to support. Here are the new extension methods for my example with 2 parameters:
Granted, it would be a fair number of extra overloads, but it doesn't seem too onerous, since the methods themselves are lightweight. There are prior examples of this in the runtime (e.g. HashCode.Combine), and I've done it in a couple of private projects.
I tested the edges of this solution a little to see if there were obvious pitfalls. One of the nice things (of sorts) is that the compiler won't just 'infer' delegate argument types automatically, you need to specify them. On top of that, if you specify one, you specify all. So, this largely prevents clashes with our existing overloads:
Alternatives You've (not) Considered
It's entirely possible that this has been suggested before by someone else, and dismissed because of some glaring thing I've missed.
The text was updated successfully, but these errors were encountered: