DevTeam / Pure.DI

Pure DI for .NET without frameworks!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Make it possible to resolve multiple types without introducing a composition root

NikolayPianikov opened this issue · comments

Hey

Sorry about the delay.

What I actually meant when talking about resolving multiple types, was the ability to resolve a generic anonymous composition root, like:

record Service1() { }
record Service2() { }

public record ExplicitDependencyScope<T1, T2>(T1 D1, T2 D2);

internal static partial class Composer4 {
    static Composer4() =>
        DI.Setup()
        .Bind<Service1>().To<Service1>()
        .Bind<Service2>().To<Service2>()
        .Root<(TT1, TT2)>()
        // Or if tuples are problematic then
        .Root<ExplicitDependencyScope<TT1, TT2>>()
        ;
}

// To be used like
var s0 = Composer4.Resolve<(Service1, Service2)>();
var s1 = Composer4.Resolve<ExplicitDependencyScope<Service1, Service2>>();

Both of the above attempts fail runtime with:
'Cannot resolve an instance System.ValueTuple`2[Pure_DI_TTest_MultiResolve.Service1,Pure_DI_TTest_MultiResolve.Service2], consider adding it to the DI setup.'

Before Root was introduced I also attempted this with Bind/To in various configurations.

The main reason for needing to resolve multiple is so that they'll share PerResolve lifetimed dependencies.

All of this was needed to support some very old code retrofitted with DI, specifically for running DI backed async tasks. It is not a good or clean design, but a total rewrite was out of scope. With our current DI framework we solve it simply by resolving each of the dependencies in the same lifetime scope.
I could have gone through all the code and added each combination of dependencies explicitly, but that would be a more fragile solution than a generic solution would be.

As previously mentioned, time constraints forced me to abandon Pure.DI (for now), so I don't need this and on new projects I could at least work around this need.

When I think about this in a more functional setting it does have some merits, but it's just a gut-feeling.

Unfortunately this is not possible. TT is a universal marker. When you define Root, you add a hint to build the object graph starting from the specified type. This happens at compile time. But TT can potentially be replaced by any .NET type. Therefore, we cannot generate code for all .NET types. Now it only works like:

using Pure.DI;

var s = Composer4.Resolve<(Service1, Service2)>();

record Service1() { }
record Service2() { }

internal static partial class Composer4 {
    static Composer4() =>
        DI.Setup()
            .Bind<Service1>().To<Service1>()
            .Bind<Service2>().To<Service2>()
            .Root<(Service1, Service2)>()
    ;
}

All types must be known at compile time in order to generate code for creating object graphs that contains types that can be created by the new operator (not interfaces, not abstract classes, and not marker types like TT).

This code is fine too there:

using Pure.DI;

var s = Composer4.Resolve<(Service1, Service2)>();

record Service1() { }
record Service2() { }

internal static partial class Composer4 {
    static Composer4() =>
        DI.Setup()
            .Root<(Service1, Service2)>()
            ;    
}

I can hack it quite simply so it works as intended, but it requires copying the generated code each time the Setup changes.

I hope this explains it better:

interface IShared { void DoSomething(); }
class Shared: IShared {
    public void DoSomething() {}
}

record Service1(IShared Shared);
record Service2(IShared Shared);
record Service3(IShared Shared);
record Service4(IShared Shared);

internal static partial class Composer4 {
    static Composer4() =>
        DI.Setup()
        .Bind<Service1>().To<Service1>()
        .Bind<Service2>().To<Service2>()
        .Bind<Service3>().To<Service3>()
        .Bind<Service4>().To<Service4>()
        .Bind<IShared>().As(Lifetime.PerResolve).To<Shared>()
        // I'd rather not map all possible permutations needed in the application
        //.Root<(Service1, Service2)>()
        //.Root<(Service1, Service2, Service3)>()
        //.Root<(Service1, Service3)>()
        //.Root<(Service1, Service4)>()
        //.Root<(Service2, Service3)>()
        //.Root<(Service2, Service4)>()
        //.Root<(Service3, Service4)>()
        // ... All the other permutations with dependencies not ordered
        ;

    // A hack. I'd need to update this when adding PerResolve objects to the graph
    public static (T1, T2) Resolve<T1, T2>() {
        System.Threading.Interlocked.Increment(ref _deepness);
        try {
            return (
                (T1)Resolver<T1>.Resolve(),
                (T2)Resolver<T2>.Resolve()
            );
        } finally {
            if (System.Threading.Interlocked.Decrement(ref _deepness)==0) {
                _perResolveAppStartPure_DI_TTest_MultiResolveShared=default(AppStart.Pure_DI_TTest_MultiResolve.Shared?);
            }
        }
    }

    public static (T1, T2, T3) Resolve<T1, T2, T3>() {
        // same but with 3 Resolves
        return default;
    }
}

Used like:

    var (s1,s2) = Composer4.Resolve<Service1, Service2>();
    Assert(ReferenceEquals(s1.Shared, s2.Shared));
    var shared12 = s1.Shared;

    var (s3, s4) = Composer4.Resolve<Service3, Service4>();
    Assert(!ReferenceEquals(shared12, s3.Shared));

It's possible that adding something like:

    static void DecreaseDeepness(ref int _deepness) {
        if (System.Threading.Interlocked.Decrement(ref _deepness)==0) {
            _perResolveAppStartPure_DI_TTest_MultiResolveShared=default(AppStart.Pure_DI_TTest_MultiResolve.Shared?);
        }
    }

To the generated code would be enough, because then then hack would be:

    public static (T1, T2) Resolve<T1, T2>() {
        System.Threading.Interlocked.Increment(ref _deepness);
        try {
            return (
                (T1)Resolver<T1>.Resolve(),
                (T2)Resolver<T2>.Resolve()
            );
        } finally {
            DecreaseDeepness(ref _deepness);
        }
    }

And not really a hack anymore, but just a poor mans scoping.

Thanks for the clarification, I understand better now this your case!

@johnjuuljensen could you please review this ticket?