Injection by ID
Description
As a long time fan of this DI framework, I use it constantly, but I was always missing one feature that I got really used to in Zenject - injection by ID.
I know we can use Factories and that's what I did up until recently, but since I had a little spare time, I wanted to implement the feature in VContainer as well.
My aim was to not introduce any regression and to do it as performant as possible.
Usage:
// Registration:
builder.Register<IWeapon, Sword>(Lifetime.Singleton).WithId(SomeEnum.Primary);
builder.Register<IWeapon, Bow>(Lifetime.Singleton).WithId("secondary");
builder.Register<IWeapon, Knife>(Lifetime.Singleton).WithId(3);
// Container Resolution:
var primaryWeapon = container.Resolve<IWeapon>(SomeEnum.Primary);
var secondaryWeapon = container.Resolve<IWeapon>("secondary");
// Constructor Injection:
public SomeClass(
[Inject(SomeEnum.Primary)] IWeapon primaryWeapon,
[Inject("secondary")] IWeapon secondaryWeapon)
{
this.primaryWeapon = primaryWeapon;
this.secondaryWeapon = secondaryWeapon;
}
// Method Injection:
[Inject]
public void Construct(
[Inject(SomeEnum.Primary)] IWeapon primaryWeapon,
[Inject("secondary")] IWeapon secondaryWeapon)
{
PrimaryWeapon = primaryWeapon;
SecondaryWeapon = secondaryWeapon;
}
// Property and Field Injection
[Inject(SomeEnum.Primary)] public IWeapon PrimaryWeapon;
[Inject("secondary")] public IWeapon SecondaryWeapon { get; set; }
Benchmarks
Implementation Change
In order to support resolution by ID, I had to turn the FixedTypeKeyHashTable class into FixedTypeObjectKeyHashtable.
I've created a quick benchmark (using BenchmarkDotNet) for the class (see the benchmark here) and here are the results for it:
| Method | DictionarySize | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|---|
| 'Dictionary<Type, object>' | 100 | 77.33 ns | 1.567 ns | 3.057 ns | - |
| 'Dictionary<(Type, object), object>' | 100 | 114.69 ns | 0.902 ns | 0.843 ns | - |
| FixedTypeKeyHashtable | 100 | 22.63 ns | 0.251 ns | 0.223 ns | - |
| FixedTypeObjectKeyHashtable | 100 | 27.26 ns | 0.543 ns | 0.557 ns | - |
| 'Dictionary<Type, object>' | 1000 | 76.30 ns | 0.187 ns | 0.156 ns | - |
| 'Dictionary<(Type, object), object>' | 1000 | 137.71 ns | 1.099 ns | 0.918 ns | - |
| FixedTypeKeyHashtable | 1000 | 22.76 ns | 0.250 ns | 0.195 ns | - |
| FixedTypeObjectKeyHashtable | 1000 | 26.14 ns | 0.075 ns | 0.062 ns | - |
| 'Dictionary<Type, object>' | 10000 | 76.80 ns | 0.510 ns | 0.477 ns | - |
| 'Dictionary<(Type, object), object>' | 10000 | 104.48 ns | 1.152 ns | 1.021 ns | - |
| FixedTypeKeyHashtable | 10000 | 22.94 ns | 0.476 ns | 0.445 ns | - |
| FixedTypeObjectKeyHashtable | 10000 | 26.46 ns | 0.162 ns | 0.135 ns | - |
We can see that the performance of FixedTypeObjectKeyHashtable is pretty comperable to FixedTypeKeyHashtable. Slight slowdown is expected due to hash combination.
Built in benchmarks
I've fixed and ran the built in benchmarks both on master and on my branch. There is a slight performance degradation during container building due to a bit more complex registration. Resolving is unchanged (the benchmarks show that in my branch the resolving is a bit faster, but I think it's inside a margin of error).
The latest updates on your projects. Learn more about Vercel for Git ↗︎
| Name | Status | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| vcontainer | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Jul 5, 2025 9:37am |
@migus88 I encountered a problem, multiple registration for single interface does not work as expected. Inject IEnumerable<SomeInterface> will always contain zero elements if the method with ID was used.
@softshadedev thanks, I will take a look at it! Probably this scenario isn't covered by unit tests - will add them as well.
I also want to see how I can move the registration with ID to use the FixedTypeKeyHashtable instead of dictionary and to merge the functionalities. This should also solve the issue you've mentioned.
@softshadedev I've merged the handling of non-id registrations with id registrations and added a unit test to make sure that collections returned as expected no matter how the dependency is registered. Give it a try.
Thanks for the effort! I really needed this(#768). I will use this in my personal projects to see if anything wrong with it or not.
PS: Works perfectly, thank you!
I'm currently in the process of trying it out in a big scope, mobile project. Once it's live, it is expected to be tested on hundreds of thousands of users. If everything goes fine, I'll open this PR for a review.
@migus88 Thank you very much! I will test it in my project!
Well, on my side - it passed all checks I wanted it to pass and the PR is now ready. @hadashiA, I would appreciate a review.
I understand that this is a huge PR and it takes time.
Maybe we can start by running the rest of the tests and in case something's wrong I'll fix it?

@migus88 Thank you very much for your hard work. There are benchmarks, and the quality seems to be fine.
I am positive about merging it.
There are two reasons for the delayed response.
First, I have less time to devote to this project. I apologize for keeping you waiting.
Second, as I've mentioned in previous issues, I've been somewhat hesitant about this feature. https://github.com/hadashiA/VContainer/issues/218#issuecomment-841645605
I have been considering this PR and the feedback. Now that the user base has grown and Microsoft.Extensions.DI has added similar functionality, the situation has changed. Additionally, it seems that most people other than myself are in favor of this feature.
Therefore, I plan to merge it in principle. This is a major feature, so I'd like to think about whether this API is appropriate. Thank you very much!
Hey @hadashiA, first of all - huge thanks for the fantastic work you’re doing. I completely understand your reasons for taking the time on this PR. Please, don't hesitate to comment here, or even in private - I’d be happy to collaborate on adapting this feature so it aligns well with the project’s needs.
On a personal note: a few years back, I was in a similar position. Initially, I was skeptical too - but working in an environment where this approach was heavily used helped me grow to really like it. At the end of the day, almost any pattern can be abused, but that alone shouldn’t disqualify its potential when applied thoughtfully. 🙂
Apologize for the delayed response ><
I would like to request two corrections regarding the public API.
- This feature uses the term “Id,” but I would prefer the name “Key.”
- Microsoft.Extensions is currently the standard library in C#, and it uses the name “KeyedService.”
- https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-9.0#keyed-services
- VContainer initially referenced AutoFac, which also uses the name KeyedService.
- https://autofac.readthedocs.io/en/latest/advanced/keyed-services.html#keyed-services
- So, in C#,
Keyis generally considered the standard term I think.
- Microsoft.Extensions is currently the standard library in C#, and it uses the name “KeyedService.”
- I want to use a different attribute than
[Inject]to specify the key.[Inejct(Id = ...)]is easy to understand as DI, but I think it becomes a little difficult to use when there are multiple uses. Also, theInjectattribute currently means “method/ctor where Inject is automatically executed.” I think it would be clearer not to give it another meaning.
Based on the above, my proposal is as follows.
// Registration
builder.RegisterType<Weapon>().Keyed(SomeEnum.Primary);
// Constructor Injection
public SomeClass(
[Key(SomeEntry.Primary)] Weapon primaryWeapon
)
{
// ...
}
I welcome any other opinions or counterarguments. I would appreciate your confirmation. Thanks!
I’m not sure about the name Keyed — it doesn’t sound quite right to me. Maybe something like WithKey would feel more intuitive?
That said, it’s definitely not a deal breaker, especially if Keyed is becoming a sort of convention.
Regarding KeyAttribute — I’m generally fine with it, but Key feels a bit too generic. I can imagine it clashing with existing code in some projects.
Maybe something like InjectKey or a similarly specific name would be safer?
@hadashiA anyway, I've pushed the change both with KeyAttribute and Keyed method name.
Also added the documentation to the website.
@migus88 Thanks for the quick response on this! Sorry for making you revise it multiple times!
I'm not sure about the name Keyed — it doesn't sound quite right to me. Maybe something like WithKey would feel more intuitive?
You're probably right that WithKey is more straightforward and easier to understand.
I'm actually a bit torn on this point myself.
But as you said, the term Keyed is used fairly often, so either one seems fine to me. Actually, Keyed() is borrowed from Autofac.
Since you've already made the changes with Keyed, I'd like to go with this!
I'm generally fine with it, but Key feels a bit too generic.
Fair point here too, but I personally prefer the conciseness of [Key].
You're right that InjectKey is clearer. But for attributes that get specified multiple times at the field or parameter level, I think it's worth prioritizing visual clarity and conciseness in the declarations.
Actually, System.ComponentModel.DataAnnotations and MessagePack-CSharp both use [Key] as their attribute name.
There's always the risk of naming conflicts, but we have namespaces for that.
Anyway, thanks again for the quick fixes! I'm happy with the API as it is now. I'll do a quick check to make sure there aren't any major functional issues and then merge it.
Merged. Thanks so much. I might make some tweaks to the documentation later, just a heads up.
Awesome news!!! Thanks!