DryIoc
DryIoc copied to clipboard
Delegate Factory Resolving (Incremental Improvement over Func<> Wrapper)
I am migrating from AutoFac to DryIoc, and there is one feature that I am missing that I wish would be in DryIoc. (With some help, I would be interested at some point to do the work to implement).
Consider the following:
class A { }
class B
{
public int Id { get; private set; }
public bool FeatureEnable { get; private set; }
public A A { get; private set; }
public delegate B Factory(int id, bool featureEnable);
public B(int id, bool featureEnable, A a)
{
this.Id = id;
this.FeatureEnable = featureEnable;
this.A = a;
}
}
var container = new Container();
container.Register<A>();
container.Register<B>();
var factory = container.Resolve<B.Factory>();
// equiv to
// var factory = container.Resolve<Func<int, bool, B>>().Invoke;
Using the Func<> wrapper is incredibly useful, but occasionally there are times where having the parameter names (especially for bool parameters) is more readable. However, I don't want to have to register the Factory delegate specifically as a service.
AutoFac identifies any delegate (Func<> or otherwise) and matches them against a constructor and provides services for the non-provided constructor parameters. Would it be possible to add a similar feature to DryIoc?
I am not sure how to approach this functionality...
we need to somehow convert the Delegate from the Resolve to the Func with corresponding parameters. Maybe you can use the Dynamic Registrations, here are the examples of those: https://github.com/dadhi/DryIoc/blob/be545dbd08bd9f750abac3f47225c91d08089bab/test/DryIoc.UnitTests/DynamicRegistrationsTests.cs
Oooh. That looks promising. I was looking for something like this earlier in the docs, but I couldn't find it.
For information's sake:
- Returning a
DynamicRegistrationmeans that it is registered only for the current service request, not permanently? Is there a way to choose whether it is per request or permanent (I'm preferring permanent here, since theFactorydelegate is already defined). - I believe I've figured out how to get the type parameters provided in the expected delegate
Type. I'm guessing the best way to provide a registration would be to use anExpressionFactoryon an expression that takes in the resolver context, resolves the matchingFunc<...>for the delegate, and converts theFunc<>to theFactorydelegate?
Returning a DynamicRegistration means that it is registered only for the current service request, not permanently
It is permanently, but on demand
I'm guessing the best way to provide a registration would be to use an ExpressionFactory...
Need to experiment myself. I would've reused Func<...> factory expressions from the WrappersSupport (converting its Invoke method to the desired delegate type), but afraid that some nstuff is internal there. At least you you can start from WrappersSupport implementation..
Thanks in advance for being patient with me. I hope this won't take too much longer. When I'm finished, I hope to have something that you could use in your documentation for others who might be migrating from AutoFac.
I've done this:
public static IEnumerable<DynamicRegistration> GetRegistrations(Type type, object serviceKey)
{
// only interested in delegates
if (!type.IsSubclassOf(typeof(Delegate)))
return null;
// don't care about Func<> wrappers
if (type.IsFunc())
return null;
var method = type.GetMethods()
.Single(m => m.Name == "Invoke");
var returnType = method.ReturnType;
var parameters = method.GetParameters();
var constructor = returnType.Constructors()
.SingleOrDefault(c => IsValidConstructor(c, parameters));
if (constructor == null)
return null;
var contextParameter = Expression.Parameter(typeof(IResolverContext), "context");
var resolveMethod = typeof(Resolver).GetRuntimeMethod(nameof(Resolver.Resolve), new[] { typeof(IResolver), typeof(IfUnresolved) });
var innerParameters = parameters.Select(lp => Expression.Parameter(lp.ParameterType, lp.Name)).ToArray();
var resolveParameters = constructor.GetParameters()
.Skip(innerParameters.Length)
.Select(cp => Expression.Call(
resolveMethod.MakeGenericMethod(cp.ParameterType),
contextParameter,
Expression.Constant(IfUnresolved.Throw)))
.ToArray();
var lambda =
Expression.Lambda(
Expression.Lambda(
type,
Expression.New(
constructor,
innerParameters.Cast<Expression>().Concat(resolveParameters).ToArray()),
innerParameters),
contextParameter);
return new[]
{
new DynamicRegistration(new DelegateFactory(lambda.CompileToFactoryDelegate(), knownImplementationType: type)),
};
}
I can confirm this code is being run, and does return the registration. However, I am still getting UnableToResolveUnknownService. I'm wondering now if I went down the wrong path, since the WrappersSupport looks like it uses the ExpressionFactory and responds to all of this stuff on the Request rather than up front.
Working version:
public static IEnumerable<DynamicRegistration> GetRegistrations(Type type, object serviceKey)
{
// only interested in delegates
if (!type.IsSubclassOf(typeof(Delegate)))
return null;
// don't care about Func<> wrappers
if (type.IsFunc())
return null;
return new[]
{
new DynamicRegistration(new ExpressionFactory(BuildRegistrationExpression))
};
}
private static Expression BuildRegistrationExpression(Request request)
{
var wrapperType = request.GetActualServiceType();
var method = wrapperType.GetMethods()
.Single(m => m.Name == "Invoke");
var serviceType = method.ReturnType;
var argTypes = method.GetParameters();
var argCount = argTypes.Length;
var argExprs = new ParameterExpression[argCount];
for (var i = 0; i < argCount; i++)
argExprs[i] = Expression.Parameter(argTypes[i].ParameterType, argTypes[i].Name);
request = request.WithInputArgs(argExprs);
var serviceRequest = request.Push(serviceType, flags: RequestFlags.IsWrappedInFunc | RequestFlags.IsDirectlyWrappedInFunc);
var container = request.Container;
var serviceExpr = container.ResolveFactory(serviceRequest)?.GetExpressionOrDefault(serviceRequest);
if (serviceExpr == null)
return null;
if (serviceExpr.Type != serviceType)
serviceExpr = Expression.Convert(serviceExpr, serviceType);
return Expression.Lambda(
wrapperType,
serviceExpr,
argExprs,
serviceType);
}
Question: Could we look at putting something like this into WrapperSupport?
That's working, good!
Probably if I expose the GetFuncOrActionExpression it should simplify things?
Regarding the actual wrapper for this it hard to do because there is no type to match the registration against - I mean statically. Maybe it would be enough to add this as a redefined DynamicRegistration.
I think this also should be included by default to DryIoc.Syntax.Autofac project / package.
- I don't think the existing
GetFuncOrActionExpressionOrDefault()would work, because there are enough differences. However, this could be a separate function that could be exposed. I'd want to touch it up a bit to a) handlevoidreturn (what's the purpose of handlingAction<>in your code? this would fall under that purpose, I guess), and b) handle the#ifdefscenarios that you are. - Ah, yes. I finally saw the static association w/ the
Func<>andAction<>types. Yes, this would have to be a dynamic registration instead. Which, of course, is why you've never done this before. - When you say add as a
DynamicRegistration, you mean internally via rules, or expose as a new type derived fromDynamicRegistration, or ...? - I agree, whatever support written for this should be enabled by default when using the DryIoc.Syntax.Autofac package
- I would be happy to help by submitting a PR at some point. Probably not this week, but let me know if you want some help!
When you say add as a DynamicRegistration, you mean internally via rules, or expose as a new type derived from DynamicRegistration, or ...?
The rule for this, there are already predefined rules, e.g. for concrete types.
PR is welcome! Thanks and cheers.
Was this ever finished? I have a simple use case for this, I have a specific (augmented with logger) DB connection class which I need to instanciate each time I am connecting to DB, currently I just register it like this:
const string mainConfigName = "foo";
container.RegisterInstance(mainConfigName, serviceKey: nameof(mainConfigName));
container.Register<MainDB>(Reuse.Transient,
Made.Of(() => new MainDB(
Arg.Of<string>(nameof(mainConfigName)),
Arg.Of<ILogger>())));
container.RegisterInstance<MainDB.Connector>(() => container.Resolve<MainDB>());
Where MainDB.Connector is just Func<MainDB>.
@Metadorius
Hi, you mean that MainDB.Connector is the custom delegate type?
If it is so and if it is the same as Func<MainDB> why don't use the Func<MainDB> directly as it is supported by the DryIoc.
And if you cannot, here is the better way of casting the Func to the custom delegate:
using System;
using DryIoc;
public class Program
{
public static void Main()
{
var container = new Container();
container.Register<ILogger, NullLogger>();
const string mainConfigName = "foo";
container.RegisterInstance(mainConfigName, serviceKey: nameof(mainConfigName));
container.Register<MainDB>(Reuse.Transient, Made.Of(() => new MainDB(Arg.Of<string>(nameof(mainConfigName)), Arg.Of<ILogger>())));
//container.RegisterInstance<MainDB.Connector>(() => container.Resolve<MainDB>());
// Here is just the proxy wich resolves the Func<MainDB> and casts its Invoke method to the result delegate type
container.RegisterDelegate<MainDB.Connector>(r => r.Resolve<Func<MainDB>>().Invoke);
var connector = container.Resolve<MainDB.Connector>();
Console.WriteLine($"Hello {connector()}");
}
public class MainDB
{
public delegate MainDB Connector();
public MainDB(string cfgName, ILogger logger)
{
}
}
public interface ILogger
{
}
public class NullLogger : ILogger
{
}
}
@dadhi hello, yeah I meant that. I just wanted to use the custom delegate for clarity in my code, so it's immediately evident what is this for (something that connects to MainDB is expressing what it's for, some function that returns MainDB is the underlying implementation which is not as important IMO).
Is there actually a difference between registering it like how you did and how I did?
Use mine, because it is uses the DryIoc own ability to resolve Func.. and you're just adding the missing cast to your delegate.
@Metadorius I forgot, there is even more ergonomic and preferred way to register the cast:
container.RegisterDelegate<Func<MainDB>, MainDB.Connector>(f => f.Invoke);