Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can Autofac delegate factories pass parameters to nested objects?

Tags:

c#

autofac

I'd like to use an Autofac delegate factory to resolve an object graph where some of the nested objects need to be passed parameters. E.g. If the QuoteService in the Autofac delegate factory example required a url to retrieve data from, or the QuoteService itself had a dependency that required a parameter.

public class WebQuoteService : IQuoteService
{
  public WebQuoteService(Uri source)
  {
  }
}

public class Shareholding
{
  public Shareholding(string symbol, uint holding, IQuoteService quoteService)
  {
  }
}

I'd like to be able to declare and register a delegate like so:

public delegate Owned<Shareholding> ShareholdingFactory(string symbol, uint holding,
                                                        Uri source);
builder.RegisterGeneratedFactory<ShareholdingFactory>();

The problem I run into is that Autofac can't resolve the uri parameter of the WebQuoteService.

I've seen a few similar questions & solutions, but nothing particularly neat. Autofac-passing-parameter-to-nested-types suggests registering a lambda to explicitly implement the factory and resolve the nested dependency. I'm sure that would work, but it becomes very messy if the parameter is needed at a deeper level or when there are more dependencies.

The temporary solution I'm using is an improvement on that, resolving the IQuoteService in Shareholding OnPreparing, and forwarding the parameters created by the Autofac generated factory.

 builder.RegisterType<Shareholding>().OnPreparing(e =>
            {
                e.Parameters = e.Parameters.Union(new[]
                {
                    new TypedParameter(typeof (IQuoteService), e.Context.Resolve<IQuoteService>(e.Parameters))
                });
            });

That works ok and avoids manually resolving other parameters, but I actually need to do it twice to forward the parameters to a second nested level.

I've considered, but not tried to use BeginLifetimeScope(Action<ContainerBuilder>) as suggested by can-components-be-temporarily-registered-in-an-autofac-container. I think I'd have to implement the factory manually, but I could then register the uri so it would work at any nesting level.

What I'd actually like to be able to do is attach to WebQuoteService OnPreparing and access the delegate factory's parameters. Something like this can be made to work with reflection but that's obviously not ideal.

builder.RegisterType<WebQuoteService>().OnPreparing(e =>
{
    var parameters = e.Context._context._activationStack.Last().Parameters;
    e.Parameters = e.Parameters.Concat(parameters);
});

Can anyone suggest a cleaner alternative to pass parameters to objects nested two levels deep?

like image 890
Matt Sullivan Avatar asked Dec 16 '14 04:12

Matt Sullivan


2 Answers

Sorry to self-answer, but failing a better suggestion I thought I should document the best solution I have.

In OnPreparing, you can use reflection to access the Autofac activation stack and the parameters passed to the delegate factory. These can then be added to the parameters of the nested component being resolved. This works with any level of nesting (it only needs to be added to OnPreparing for the component that requires parameters.)

Register like so:

builder.RegisterType<WebQuoteService>()
       .OnPreparing(AutofacExtensions.ForwardFactoryParameters);

Using this helper class:

public static class AutofacExtensions
{
    private static readonly FieldInfo ContextFieldInfo;
    private static readonly FieldInfo ActivationStackFieldInfo;

    static AutofacExtensions()
    {
        var autofacAssembly = typeof(IInstanceLookup).Assembly;
        Type instanceLookupType = autofacAssembly.GetType("Autofac.Core.Resolving.InstanceLookup");
        ContextFieldInfo = instanceLookupType.GetField("_context", BindingFlags.Instance | BindingFlags.NonPublic);
        Type resolveOperationType = autofacAssembly.GetType("Autofac.Core.Resolving.ResolveOperation");
        ActivationStackFieldInfo = resolveOperationType.GetField("_activationStack", BindingFlags.Instance | BindingFlags.NonPublic);
    }

    public static IResolveOperation Context(this IInstanceLookup instanceLookup)
    {
        return (IResolveOperation)ContextFieldInfo.GetValue(instanceLookup);
    }

    public static IEnumerable<IInstanceLookup> ActivationStack(this IResolveOperation resolveOperation)
    {
        return (IEnumerable<IInstanceLookup>)ActivationStackFieldInfo.GetValue(resolveOperation);
    }

    /// <summary>
    /// Pass parameters from the top level resolve operation (typically a delegate factory call)
    /// to a nested component activation.
    /// </summary>
    public static void ForwardFactoryParameters(PreparingEventArgs e)
    {
        var delegateFactoryActivation = ((IInstanceLookup) e.Context).Context().ActivationStack().Last();
        e.Parameters = e.Parameters.Concat(delegateFactoryActivation.Parameters);
    }
}
like image 195
Matt Sullivan Avatar answered Sep 30 '22 01:09

Matt Sullivan


From version 6, Matt's answer no longer functions. Below is correct.

public static void ForwardFactoryParameters(PreparingEventArgs e)
{
    var ctx = e.Context;
    var oper = ctx.GetType().GetProperty("Operation").GetValue(ctx);
    var requestStack = oper.GetType().GetProperty("InProgressRequests").GetValue(oper) as SegmentedStack<ResolveRequestContext>;
    if (requestStack.Count == 1)
    {
        //Nothing to do; we are on the first level of the call stack.
        return;
    }
    var entryRequest = requestStack.Last();

    e.Parameters = entryRequest.Parameters;
}
like image 38
James Hutchison Avatar answered Sep 30 '22 02:09

James Hutchison