VContainer icon indicating copy to clipboard operation
VContainer copied to clipboard

Resolution of multiple interface registration

Open longbombus opened this issue 3 years ago • 2 comments

I tried such approach: I has classes DataA, DataB, DataC, ... Container registers in transient-way: ControllerA, ControllerB, ControllerC, ... (each controller implements IController and constructor requires only one exact Data* instance, ex: ControllerB(DataB data))

Then I creating sub-scope with only one registered Data* instance; then I instantiate View, which represents any controller, so it injects controller with [Inject] private readonly IController controller;. I assumed that IController will be resolved as any resolvable type, but it fails for each controller except first registered.

Is this a bug or should I search for another approach?

longbombus avatar Mar 21 '22 08:03 longbombus

I'm not 100% sure what you mean, but if you're registering multiple concrete classes to a single interface, they will be implicitly registered as a collection:

https://vcontainer.hadashikick.jp/registering/register-collection

So in your views construction method you would need something like:

[Inject]
private void Construct(IReadOnlyList<IController> registeredControllers)
{
    foreach(var controller in registeredControllers)
    {
        if( controllerB is ControllerB)
            _thisController = controllerB;
    }
}

I think if you just have IController it will always return the first registered.

SimonNordon4 avatar Apr 19 '23 01:04 SimonNordon4

I have registered ControllerA : IController, ControllerB : IController, ... and only one Data in same time, then I try container.CreateScope(b => b.RegisterInstance(concreteData)).Resolve<IController>(), so I want Resolve to find any resolvable IController, but it tries to create only ControllerA.

From VContainer I want to automate this:

IController controller = concreteData switch
{
  DataA dataA => new ControllerA(dataA),
  DataB dataB => new ControllerB(dataB),
  _ => throw new NotImplementedException()
}

It have a lot of applications:

  • Game Rewards described in ScriptableObject that obviously should be separated with Controller logic and in runtime I do not need exact types of reward data, I just do container.CreateScope(b => b.RegisterInstance(rewardData)).Resolve<IReward>().Give()
  • Game Quests works in same way container.CreateScope(b => b.RegisterInstance(nextQuestData)).Resolve<IQuestFlow>().Start()

I have already solved it, but it would be great if VContainer can act this way out of box. Workaround (it works only if resolvable type requires single type, but in general I want resolve even ControllerAX(DataA, DataX)):

public static class CoupleFactory<TAbstractSource, TAbstractDestination>
{
  private static readonly Dictionary<Type, Type> couples = new();

  public static IReadOnlyCollection<Type> Destinations => couples.Values;

  public static void Register<TConcreteSource, TConcreteDestination>()
    where TConcreteSource : TAbstractSource
    where TConcreteDestination : TAbstractDestination
    => couples[typeof(TConcreteSource)] = typeof(TConcreteDestination);

  public static TAbstractDestination GetResolver(IObjectResolver container)
  {
    try
    {
      var concreteSource = container.Resolve(typeof(TAbstractSource));
      var concreteDestination = couples[concreteSource.GetType()];
      return (TAbstractDestination)container.Resolve(concreteDestination);
    }
    catch (Exception e)
    {
      Debug.LogException(e);
    }
    return default;
  }
}

public static class CoupleFactory
{
  public static void RegisterCoupleFactory<TAbstractSource, TAbstractDestination>(this IContainerBuilder builder)
    where TAbstractDestination : class
  {
    foreach (var destination in CoupleFactory<TAbstractSource, TAbstractDestination>.Destinations)
      builder.Register(destination, Lifetime.Transient);
			
    builder.Register(
      CoupleFactory<TAbstractSource, TAbstractDestination>.GetResolver, Lifetime.Transient);
  }
}

and each couple IData and IController should be registered with:

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void Register() => CoupleFactory<IData, IController>.Register<ConcreteData, ConcreteController>();

also container building should contain this:

builder.RegisterCoupleFactory<IData, IController>()

longbombus avatar Apr 19 '23 08:04 longbombus