DevTeam / Pure.DI

Pure DI for .NET without frameworks!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Using external values when resolving

johnjuuljensen opened this issue · comments

Hi

While trying this out I ran into an issue that I've been unable to solve.

Most of our services needs to know the identity of the current user.
In our current DI setup we get this from ambient data when resolving (HttpContext.Current to be specific). I've never liked that much, but I'm not sure there's any way around that in old Asp.Net.

I've been trying to solve that issue with Pure.DI and have come up with 4(5) solutions, but none are to my satisfaction and one doesn't compile. So I'm writing now in the hope of some guidance or ideally pointers to some obvious solution that I've missed.

I'll detail the five solutions here:

Two of the solutions are partially implemented in my fork of this project, but neither have been finished. There's quite a lot of work to do yet and since I'm not really interested in maintaining my own fork, I'm not going to continue the work unless there is some chance of getting the changes merged.

The first api change introduces IConfiguration BindAtResolve<T>(); to IConfiguration. The idea is that T will be added as an argument to the Resolve methods generated and those arguments being used to resolve types of T. I've yet to figure out if adding a new property for these in ResolverMetadata or adding them to ResolverMetadata.Bindings and adding a new binder or something else is the right way to do this.
This change is backwards-compatible and should work well with DependsOn.

The other api change adds generic argument TContextArgs to Setup and adds TContextArgs Args {get;} to IContext. This change requires IConfiguration, IBinding etc. to become generic on TContextArgs. An non-generic overload Setup() => Setup<Unit>() is provided to avoid breaking changes.
The generated Resolve methods will take TContextArgs as first argument unless it is Unit and that argument will then be available in the ctx argument in To<> methods.
This change will not play nice with DependsOn unless TContextArg is the same for all dependencies.
When I started work on this I hadn't understood that ctx mostly (only) acts as a placeholder and isn't actually available at runtime, so it's possible that it's never going to work.

Three of the solutions are using the project as-is and are reproduced in the following:

using System;
using Pure.DI;

namespace AppStart.Pure_DI;


public struct Identity {
    public int UserId;
    // More fields
}

public interface IIdentityService {
    Identity Identity { get; }
}

public class IdentityService: IIdentityService {
    public IdentityService(Identity identity) {
        Identity=identity;
    }

    public Identity Identity { get; }
}


internal class SomeService {
    public SomeService(IIdentityService identityService) {
        IdentityService=identityService;
    }

    public IIdentityService IdentityService { get; }
}

public static class Test {
    public static void Run() {
        Run(new Identity { UserId = 42 });
    }

    public static void Run(Identity ident) {
        // This fails at compile-time. See details in bindings
        SomeService s1 = ComposerX.Resolve<Func<Identity, WithIdentityResolver>>()(ident).Resolve<SomeService>();


        // This fails at run-time. See details in bindings
        SomeService s2 = ComposerX.Resolve<Func<Identity, GenericCompositionRootWithIdentity<SomeService>>>()(ident).Value;

        SomeService s3 = ComposerX.Resolve<Func<Identity, SpecializedCompositionRootWithIdentity>>()(ident).SomeService;
    }
}


class WithIdentityResolver {
    public WithIdentityResolver(IContext resolverContext) {
        ResolverContext=resolverContext;
    }

    public IContext ResolverContext { get; }

    public T Resolve<T>() {
        return ResolverContext.Resolve<T>();
    }
}


internal class GenericCompositionRootWithIdentity<T> {
    public GenericCompositionRootWithIdentity(T value) {
        Value=value;
    }

    public T Value { get; }
}


internal class SpecializedCompositionRootWithIdentity {
    public SpecializedCompositionRootWithIdentity(SomeService someService) {
        SomeService=someService;
    }

    public SomeService SomeService { get; }
}


static partial class ComposerX {

    class CachedService<TService> {
        public TService Service { get; set; }
    }

    static ComposerX() =>
        // out=C:\tmp
        // verbosity=Diagnostic
        DI.Setup()
            .Default(Lifetime.PerResolve)
            .Bind<CachedService<TT>>().To<CachedService<TT>>()
            .Bind<Func<Identity, IdentityService>>().To(ctx => new Func<Identity, IdentityService>(ident =>
                ctx.Resolve<CachedService<IdentityService>>().Service = new(ident)
            ))
            .Bind<IIdentityService>().To(ctx => ctx.Resolve<CachedService<IdentityService>>().Service)
            .Bind<SomeService>().To<SomeService>()

            // Doesn't work because Funcs with generic return values can't be found
            // Cannot resolve an instance System.Func`2[AppStart.Pure_DI.Identity,AppStart.Pure_DI.GenericCompositionRootWithIdentity`1[AppStart.Pure_DI.SomeService]], consider adding it to the DI setup.
            .Bind<Func<Identity, GenericCompositionRootWithIdentity<TT>>>().To(ctx =>
                new Func<Identity, GenericCompositionRootWithIdentity<TT>>(ident => {
                    // This Func resolve ensures that IdentityService is cached for the current resolve
                    ctx.Resolve<Func<Identity, IdentityService>>()(ident);
                    return new(ctx.Resolve<TT>());
                }))

            // Doesn't work because resolves on ctx are rebound and ctx itself is not available in the rewritten Func
            //  error CS0103: The name 'ctx' does not exist in the current context
            //[System.Runtime.CompilerServices.MethodImplAttribute((System.Runtime.CompilerServices.MethodImplOptions)768)]private static System.Func<AppStart.Pure_DI.Identity, AppStart.Pure_DI.WithIdentityResolver> GetPerResolveSystemFuncAppStartPure_DIIdentityAppStartPure_DIWithIdentityResolver(){if( _perResolveSystemFuncAppStartPure_DIIdentityAppStartPure_DIWithIdentityResolver==default(System.Func<AppStart.Pure_DI.Identity, AppStart.Pure_DI.WithIdentityResolver>)){ _perResolveSystemFuncAppStartPure_DIIdentityAppStartPure_DIWithIdentityResolver=                new Func<Identity,WithIdentityResolver>(ident => {
            //( GetPerResolveSystemFuncAppStartPure_DIIdentityAppStartPure_DIIdentityService())(ident);
            //                    return new(ctx);
            //            });
            .Bind<Func<Identity, WithIdentityResolver>>().To(ctx =>
                new Func<Identity, WithIdentityResolver>(ident => {
                    // This Func resolve ensures that IdentityService is cached for the current resolve
                    ctx.Resolve<Func<Identity, IdentityService>>()(ident);
                    return new WithIdentityResolver(ctx);
                }))

            // Does work but will cause binding bloat
            .Bind<Func<Identity, SpecializedCompositionRootWithIdentity>>().To(ctx =>
                new Func<Identity, SpecializedCompositionRootWithIdentity>(ident => {
                    // This Func resolve ensures that IdentityService is cached for the current resolve
                    ctx.Resolve<Func<Identity, IdentityService>>()(ident);
                    return new(ctx.Resolve<SomeService>());
                }))


            ;
}

I'm not really sure where to go from here, so any helpful would be appriciated.

Cheers,
John

Hi

Why you would not just separate logic to setup an identity from some data and to get this identity. Please see my example:

using System;
using Pure.DI;

namespace AppStart.Pure_DI;

public struct Identity {
    public int UserId;
    // More fields
}

public interface IIdentityService {
    Identity Identity { get; }
}

public interface IIdentityConfiguration {
    Identity Identity { set; }
}

// Uses interface segregation to separate the "IIdentityService" from the ability to configure it from some data "IIdentityConfiguration".
public class IdentityService: IIdentityService, IIdentityConfiguration {
    public IdentityService(Identity identity) {
        Identity=identity;
    }

    public Identity Identity { get; set; }
}


internal class SomeService {
    public SomeService(IIdentityService identityService) {
        IdentityService=identityService;
    }

    public IIdentityService IdentityService { get; }
}

public class Program {
    public static void Main() {
        ComposerX.Resolve<Program>().Run(new Identity { UserId = 42 });
    }
    
    private readonly IIdentityConfiguration _identityConfiguration;
    private readonly SomeService _someService;

    internal Program(IIdentityConfiguration identityConfiguration, SomeService someService)
    {
        _identityConfiguration = identityConfiguration;
        _someService = someService;
    }

    private void Run(Identity ident) {
        _identityConfiguration.Identity = ident;
        // Your logic
    }
}


static partial class ComposerX {
    static ComposerX() =>
        // out=C:\tmp
        // verbosity=Diagnostic
        DI.Setup()
            .Default(Lifetime.PerResolve)
            .Bind<IIdentityService>().Bind<IIdentityConfiguration>().To<IdentityService>()
            .Bind<SomeService>().To<SomeService>()
            .Bind<Program>().To<Program>();
}

Regarding your example, just imagine that all your configuration is provided by some "configuration services". And this service provides this data to other services.

Also please see this sample or this. It is not a problem to configure Pure DI to work with ASP.NET services.

Thanks for the response.

I should have included some motivation and justification, sorry about that.

What you show is what we do today. For asp controllers we do it during injection, using ambient data to get current user, and for background tasks we do it exactly as you describe.

The main problem with this approach is that it's not safe to do any work in constructors, for instance initialization of legacy components, because identity is not yet valid.
For this reason I'm trying to make it so that any service is initialized/usable when it is first seen in the constructor.
Ideally I wouldn't even need IIdentityService and IdentityService, but simply have Identity as a resolvable entity.

I hope that makes it clearer.
I understand that this is not conventional, it's just something I think would make things simpler and more robust.

Yes, thanks for details. May be this approach will be suitable:

using System;
using Pure.DI;

namespace AppStart.Pure_DI;

public struct Identity {
    public int UserId;
    // More fields
}

public interface IIdentityService {
    Identity Identity { get; }
}

// Uses interface segregation to separate the "IIdentityService" from the ability to configure it from some data "IIdentityConfiguration".
public class IdentityService: IIdentityService {
    public IdentityService(Identity identity) {
        Identity=identity;
    }

    public Identity Identity { get; set; }
}


internal class SomeService {
    public SomeService(IIdentityService identityService) {
        IdentityService=identityService;
    }

    public IIdentityService IdentityService { get; }
}

public class Program {
    public static void Main() {
        ComposerX.Resolve<Program>(new Identity { UserId = 42 }).Run();
    }
    
    private readonly SomeService _someService;

    internal Program(SomeService someService)
    {
        _someService = someService;
    }

    private void Run() {
        // Your logic
    }
}


static partial class ComposerX
{
    private static readonly ThreadLocal<Identity> Identity = new();

    static ComposerX() =>
        // out=C:\tmp
        // verbosity=Diagnostic
        DI.Setup()
            .Default(Lifetime.PerResolve)
            .Bind<IIdentityService>().To<IdentityService>()
            .Bind<SomeService>().To<SomeService>()
            .Bind<Program>().To<Program>()
            .Bind<Identity>().To(ctx => Identity.Value);

    public static T Resolve<T>(Identity identity)
    {
        Identity.Value = identity;
        return Resolve<T>();
    }
}

I did consider ThreadLocal, but dropped the idea again since it felt too much like what I was trying to avoid. In hindsight that was silly.
I ended up fool-proofing it a bit:

[Obsolete("Don't use InnerComposer directly, use Compose instead")]
static partial class InnerComposer {

    private static readonly ThreadLocal<Identity?> Identity = new();

    static InnerComposer() =>
        // out=C:\tmp
        // verbosity=Diagnostic
        DI.Setup()
            .Default(Lifetime.PerResolve)
            .Bind<Identity>().To(ctx => Identity.Value.Value)
            .Bind<IIdentityService>().To(ctx => new IdentityService(ctx.Resolve<Identity>()))
            .Bind<SomeService>().To<SomeService>()
            ;

    internal static T ResolveWithIdentity<T>(Identity identity) {
        Identity.Value = identity;
        T resolved = Resolve<T>();
        Identity.Value = default;
        return resolved;
    }
}

internal static class Composer {
#pragma warning disable CS0618 // Type or member is obsolete
    internal static T Resolve<T>(Identity ident) => InnerComposer.ResolveWithIdentity<T>(ident);
#pragma warning restore CS0618 // Type or member is obsolete
}

Clean and neat.

Thank you very much for help and guidance, it's much appriciated.

In closing I'll note that it's been illimunating to set up our dependency hierarchy in dependency order. This new DI setup much better represents the strata of the application, I'm quite pleased with it.

Looks good in my opinion. But one small thing:
.Bind<IIdentityService>().To(ctx => new IdentityService(ctx.Resolve<Identity>()))
May be it could be replaced something like:
.Bind<IIdentityService>().To<IdentityService>()

and

internal static T ResolveWithIdentity<T>(Identity identity) {
        Identity.Value = identity;
        T resolved = Resolve<T>();
        Identity.Value = default;
        return resolved;
    }

could be replaced:

internal static T ResolveWithIdentity<T>(Identity identity) {
        Identity.Value = identity;
        try {
            return Resolve<T>();
        }
        finally {
            Identity.Value = default;
        }
    }

But still, if you would suggest what an API might look like, which, in your opinion, solves problems, you will share, and I (or we together) will try to add it?
I have another library that solved similar issues like this.

A solution with a "dynamic state" which can be used later when injecting dependencies, works well only if we know the exact initial composition, then only for it can such a method be generated.

But in theory, one might think the user configures Composer as
static partial class InnerComposer<TState> { }

Then all the Resolve methods have one more parameter of type TState (or several), this does not look too clear in the implementation yet. Then we can use this state when resolving the object graph. What do you think?

I've spent some more time on this and I do have some proposals.

The first is regarding the issue discussed in this thread.
Even before adding the binding of Identity to ThreadLocal, I had started splitting things up and combining them with DependsOn.
It turns out that embedding the ambient data in the composer is not compatible with DependsOn as illustrated by the following:

using System.Threading;
using Pure.DI;

namespace AppStart.Pure_DI_Test_DependsOn_1;

struct Identity { }

record CommonService(Identity Identity) {}

record RootService(Identity Identity, CommonService CommonService) {}

internal static partial class CommonComposer1 {
    internal static readonly ThreadLocal<Identity?> AmbientIdentity = new();

    static CommonComposer1() =>
        DI.Setup()
        .Bind<Identity>().To(ctx => AmbientIdentity.Value!.Value)
        .Bind<CommonService>().To<CommonService>()
        ;

    // This works, but is a bit useless. We want to resolve from RootComposer
    internal static T ResolveWithIdentity<T>(Identity identity) {
        AmbientIdentity.Value = identity;
        return Resolve<T>();
    }
}


internal static partial class RootComposer1 {
    static RootComposer1() =>
        DI.Setup()
        .DependsOn(nameof(CommonComposer1))
        .Bind<RootService>().To<RootService>()
        ;

    // This doesn't work because CommonComposer isn't actually used.
    // Generated code for RootComposer below
    internal static T ResolveWithIdentity<T>(Identity identity) {
        CommonComposer1.AmbientIdentity.Value = identity;
        return Resolve<T>();
    }
}


internal static partial class RootComposer1 {
    // ---- 8< ------
    [System.Runtime.CompilerServices.MethodImplAttribute((System.Runtime.CompilerServices.MethodImplOptions)768)]
    // This doesn't compile. AmbientIdentity doesn't exist in RootCompoase
    internal static AppStart.Pure_DI_Test_DependsOn_1.Identity ResolveIdentity() { return AmbientIdentity.Value!.Value; }
    // ---- 8< ------
}

The fix is simple enough, and arguably cleaner. The ambient data is moved to a seperate class:

using Pure.DI;

namespace AppStart.Pure_DI_Test_DependsOn_2;

struct Identity { public int UserId; }

record CommonService(Identity Identity) { }

record RootService(Identity Identity, CommonService CommonService) { }

internal static partial class CommonAmbientData {
    internal static readonly ThreadLocal<Identity?> Identity = new();
}

internal static partial class CommonComposer2 {

    static CommonComposer2() =>
        DI.Setup()
        .Bind<Identity>().To(ctx => CommonAmbientData.Identity.Value!.Value)
        .Bind<CommonService>().To<CommonService>()
        ;
}

internal static partial class RootComposer2 {
    static RootComposer2() =>
        DI.Setup()
        .DependsOn(nameof(CommonComposer2))
        .Bind<RootService>().To<RootService>()
        ;

    internal static T ResolveWithIdentity<T>(Identity identity) {
        CommonAmbientData.Identity.Value = identity;
        return Resolve<T>();
    }
}

// Generated code
internal static partial class RootComposer2 {
    // ---- 8< ------
    [System.Runtime.CompilerServices.MethodImplAttribute((System.Runtime.CompilerServices.MethodImplOptions)768)]
    // It now correctly refers to CommonAmbientData.Identity
    internal static AppStart.Pure_DI_Test_DependsOn_2.Identity ResolveIdentity() { return CommonAmbientData.Identity.Value!.Value; }
    // ---- 8< ------
}

As I mentioned in the first message, I've already considered how to extend the api to support this scenario:

using System.Threading;
using Pure.DI;

namespace AppStart.Pure_DI_Test_DependsOn_3;

struct Identity { }
record CommonService(Identity Identity) { }
struct OtherData { }
record RootService(OtherData Data, CommonService CommonService) { }


internal static partial class CommonComposer3 {

    static CommonComposer3() =>
        DI.Setup()
        .ResolveToExternal<Identity>()
        .Bind<CommonService>().To<CommonService>()
        ;

    // The generated Resolve method would have the following signature:
    T Resolve<T>(Identity ext1);
}

internal static partial class RootComposer3 {
    static RootComposer3() =>
        DI.Setup()
        .ResolveToExternal<OtherData>()
        .DependsOn(nameof(CommonComposer3))
        .Bind<RootService>().To<RootService>()
        ;

    // The generated Resolve method would have the following signature:
    T Resolve<T>(Identity ext1, OtherData ext2);
}

I'm not sure making the composer class generic would suffice, because we want those explicit classes to be injected at resolve time.

I went with ResolveToExternal (the name could be anything) over BindToExternal or Bind<T>().ToExternal() to set it apart from the Bind/To combo.

Besides simpler code this api change also eliminates a foot-gun that I tried to get around with the InnerComposer/Composer thing from my last message, where I marked InnerComposer Obsolete to warn about calling Resolve without first setting the required ambient data.

That in turn led to two other nice-to-haves:
For composers whose only purpose is to be depended on, it should be possible to turn off code generation. When I started splitting up my dependencies into seperate composers I ended up with a chain of 5-6 composers where only the lowest would ever be used for resolving anything. One or two of the others could theoretically be used stand-alone, but for most of them it wouldn't make any sense.

The next thing is a different take on the same thing. If it was possible to make the generated resolve methods private, then I could add my own public Resolve methods and have more control.

I actually have one more wish, but it's a completely seperate thing from the current discussion:
Make it possible to resolve multiple types without introducing a composition root

I thought I'd made this work but it's tricky/impossible.

Maybe I should start a seperate question about that.

Thanks for the really great research. As for DependsOn - in fact, this method just copies the set of bindings from one composer to another as is. It doesn't take into account how factories are defined and compilation may not work.

As for ResolveToExternal - good idea and I was thinking something similar :) My method looked like Arg(string name = ""). name is required, for example, when there are multiple arguments of the same type. According to a set of Arg, a ResolveContext private class will be generated automatically with a list of all arguments (like for lambda call with context), and dependency resolution will occur taking into account all fields from the ResolveContext class and taking into account the name of the argument - it can be defined as a tag in order to determine which particular parameter will be used (with the same type for instance):

internal static partial class Composer
{
    private static void Setup() => DI.Setup()
        .Arg<int>("timeout")
        .Arg<int>("delay")
        .Arg<string>()
        .Bind<Program>().As(Singleton).To<Program>()
        .Bind<IInput>().Bind<IOutput>().As(Singleton).To<ConsoleAdapter>();
}
class Program
{
    Program(IInput input, IOutput output, [Tag("delay")] int delay, string stringSetting)
    {   
    }
}

class ConsoleAdapter: IInput, IOutput
{
    ConsoleAdapter([Tag("timeout")] int timeout)
    {
    }
}

Composer.Resolve<Program>(10, 20, "Abc")

In this case all resolve methods will have this set of arguments

I'm not sure I understand the idea:

The next thing is a different take on the same thing. If it was possible to make the generated resolve methods private, then I could add my own public Resolve methods and have more control.

Could you rephrase or add some details please?

Regarding to Make it possible to resolve multiple types without introducing a composition root yes I think it is good idea to create a new ticket and we will continue discussion there

The Arg<>(name) solution looks really nice, for all the reasons you mention.

Could you rephrase or add some details please?

Sure.

When I was messing around with the ambient stuff, I wanted to make sure that you couldn't call Resolve without first setting the ambient data, but the solution Shown here wasn't very nice.

If the generated Resolve method could be made private in the static class, then there'd be no need for all the Obsolete stuff:

// Generated by Pure.DI
internal static partial class Composer{
----- 8< ------

// I'd like to be able to make this private
[System.Runtime.CompilerServices.MethodImplAttribute((System.Runtime.CompilerServices.MethodImplOptions)768)]public static T Resolve<T>(){return (T)Resolver<T>.Resolve();}

----- 8< ------
}
// My implementation
internal static partial class Composer{
    internal static T ResolveWithIdentity<T>(Identity identity) {
        Identity.Value = identity;
        T resolved = Resolve<T>();
        Identity.Value = default;
        return resolved;
    }
}

With Resolve<T> being private, the only way to resolve anything is through ResolveWithIdentity, thus eliminating a potential source of confusion.

As for the "Make it possible to resolve multiple types without introducing a composition root" thing. I could create a new ticket and I wouldn't mind describing my use-case, but by now it'd just be for fun. The real world caught up with me and I had to finish the project I was working on using MeDI. So if you feel there is value in it, I'll go ahead with that.

@johnjuuljensen If you were to do this with MeDI, how would you go about doing it?

@MisinformedDNA Do what? Inject identity? If so: That's partially handled by asp.net core in a blackboxy way. I dump some claims into the ClaimsIdentity in my auth handler, and take a dependency on IHttpContextAccessor to read those claims when I resolve my IdentityService. It's honestly weird and unpleasant and I really hope that there's a better way I never found. I ended up doing it that way because the DI scope of any AuthenticationHandler<> is seperate from the DI scope of the Controller/handler method and the ClaimsIdentity seems to be the only thing that survives between the two scopes.

@johnjuuljensen What I was hinting at is that you might be asking for features that are beyond the scope of a DI container. So if you can demonstrate an existing container that can do what you want, then let's see that and talk about it can transfer over here. But if not, then I'm not sure the project should add features only one person is asking for.

@MisinformedDNA I don't think it's beyond the scope of a DI container, quite the opposite. I've presented the use-case AND how I do it today. The specifics of how I do it doesn't transfer to Pure.DI, since it relies on being able to resolve multiple times within a scope, but it suffers from the same deficiencies.

Furthermore, I'm not asking for features, I was asking about the usage of Pure.DI for my scenario AND if the project would be interested in accepting PRs with the extensions I've proposed.

I wanted to emphasize that Pure.DI is not a DI container, it is a code generator that creates code as if we were writing it without containers, but followed the pure DI approach - when we write by hand all the code for creating an object graph. In this case, we were able to pass in some parameters of the constructor the values we need not from the dependency graph. Could pass values from, for example, from some variables. Frankly, I don't often have such needs, but there are no restrictions on this in the pure DI approach I think.

@johnjuuljensen I've create the issue #14 about access modifiers

#15 is about multiple types

Please try 1.1.54, the sample is here.

This is great.
Arg is a much better name ofcourse and obvious in hindsight :)
I also tested it with DependsOn and the args propagate nicely to the dependant Resolve methods. Very cool.