Skip to content

Injection by ID #764

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

Merged
merged 34 commits into from
Jul 6, 2025
Merged

Injection by ID #764

merged 34 commits into from
Jul 6, 2025

Conversation

migus88
Copy link
Contributor

@migus88 migus88 commented Apr 11, 2025

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).
Benchmark Comparison - GC
Benchmark Comparison - Millisecond_Median

Copy link

vercel bot commented Apr 11, 2025

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

@softshadedev
Copy link

@migus88 I encountered a problem, multiple registration for single interface does not work as expected. Inject IEnumerable will always contain zero elements if the method with ID was used.

@migus88
Copy link
Contributor Author

migus88 commented Apr 29, 2025

@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.

@migus88
Copy link
Contributor Author

migus88 commented May 22, 2025

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.

@migus88
Copy link
Contributor Author

migus88 commented Jun 17, 2025

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?

@hadashiA
Copy link
Owner

@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.
#218 (comment)

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!

@migus88
Copy link
Contributor Author

migus88 commented Jun 30, 2025

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. 🙂

@hadashiA
Copy link
Owner

hadashiA commented Jul 4, 2025

Apologize for the delayed response ><

I would like to request two corrections regarding the public API.

  1. This feature uses the term “Id,” but I would prefer the name “Key.”
  2. 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, the Inject attribute 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!

@migus88
Copy link
Contributor Author

migus88 commented Jul 4, 2025

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?

@migus88
Copy link
Contributor Author

migus88 commented Jul 5, 2025

@hadashiA anyway, I've pushed the change both with KeyAttribute and Keyed method name.
Also added the documentation to the website.

@hadashiA
Copy link
Owner

hadashiA commented Jul 5, 2025

@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.

@hadashiA hadashiA merged commit 63205c6 into hadashiA:master Jul 6, 2025
2 checks passed
@hadashiA
Copy link
Owner

hadashiA commented Jul 6, 2025

Merged. Thanks so much.
I might make some tweaks to the documentation later, just a heads up.

@migus88
Copy link
Contributor Author

migus88 commented Jul 6, 2025

Awesome news!!! Thanks!

@hadashiA hadashiA mentioned this pull request Jul 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants