VContainer icon indicating copy to clipboard operation
VContainer copied to clipboard

Injection by ID

Open migus88 opened this issue 8 months ago • 9 comments

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

migus88 avatar Apr 11 '25 18:04 migus88

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

vercel[bot] avatar Apr 11 '25 18:04 vercel[bot]

@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 avatar Apr 29 '25 08:04 softshadedev

@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 avatar Apr 29 '25 08:04 migus88

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

migus88 avatar Apr 30 '25 08:04 migus88

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!

portlek avatar May 17 '25 11:05 portlek

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 avatar May 18 '25 07:05 migus88

@migus88 Thank you very much! I will test it in my project!

softshadedev avatar May 20 '25 10:05 softshadedev

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 avatar May 22 '25 08:05 migus88

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 avatar Jun 17 '25 08:06 migus88

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

hadashiA avatar Jun 30 '25 06:06 hadashiA

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

migus88 avatar Jun 30 '25 07:06 migus88

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.”
    • 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#, Key is generally considered the standard term I think.
  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!

hadashiA avatar Jul 04 '25 07:07 hadashiA

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 avatar Jul 04 '25 17:07 migus88

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

migus88 avatar Jul 05 '25 09:07 migus88

@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 avatar Jul 05 '25 10:07 hadashiA

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

hadashiA avatar Jul 06 '25 01:07 hadashiA

Awesome news!!! Thanks!

migus88 avatar Jul 06 '25 07:07 migus88