Using external values when resolving
See original GitHub issueHi
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
Issue Analytics
- State:
- Created a year ago
- Comments:23 (16 by maintainers)
Top GitHub Comments
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.
Please try 1.1.54, the sample is here.