Search code examples
autofac

Autofac factories with named parameters


I'm using AutoFac as my DI container. As long as I keep everything in the same class, I'm set, but I keep reading guidance that I shouldn't just pass my IContainer into other classes, so I've looked to the delegate factories to handle this.

While trying to adapt to this approach, I'm running into how I'd go about using this with named property parameters. If I'm in the same class, I can do something like:

var builder = new ContainerBuilder();
builder.Register(c => new Service()).As<IService>();
builder.Register((c,n) => new Foo(n.Named<string>("name"))).As<IFoo>();
var container = builder.Build();

var foo = container.Resolve<IFoo>(new NamedPropertyParameter("name", "blah"));

But if I'm setting this up to use a factory per the guidance I'm seeing, how would I provide the named property? I understand I'd set this up like..

public class Foo : IFoo {
  public delegate Foo Factory(string something);

  IService _service {get;set;}
  string _something {get;set;}

  public Foo(string something, IService service) {
    _service = service;
    _something = something;
  }
}

This makes sense to me right up until the named property parameter. Where might I set that so it will resolve properly at runtime given that it contains a value that's not available in advance?

And of course, if I simply can't use a delegate factory here, that's fine, but is the only way forward then to pass IContainer into the class despite the best practices? Or is there some other avenue forward I'm not seeing?

Thank you!


Solution

  • TLDR: delegate factories don't do this.

    As you noted, the docs say:

    By default, Autofac matches the parameters of the delegate to the parameters of the constructor by name. If you use the generic Func types, Autofac will switch to matching parameters by type.

    What that means is the name of the parameters in the delegate signature need to match - name and type - with the parameters in the object constructor.

    Here's a passing unit test with some commentary to help clarify:

    using System;
    using Autofac;
    using Xunit;
    
    namespace AutofacRepro
    {
        public class UnitTest1
        {
            [Fact]
            public void Test1()
            {
                var builder = new ContainerBuilder();
                builder.RegisterType<Foo>();
    
                // Foo consumes a Bar in its constructor. If it's registered with
                // Autofac, it doesn't need to be in the delegate factory signature
                // - it'll come from the container.
                builder.RegisterType<Bar>();
                var container = builder.Build();
    
                // You may or may not be resolving from a lifetime scope, but if
                // other places in your app use IDisposable, you should use scopes
                // to avoid memory leaks.
                // https://autofaccn.readthedocs.io/en/latest/best-practices/index.html
                using var scope = container.BeginLifetimeScope();
    
                // The factory will take just the string parameter for the
                // constructor and the Bar parameter comes from the container.
                var fooFactory = scope.Resolve<Foo.Factory>();
                var foo = fooFactory("value");
    
                Assert.Equal("value", foo.Something);
                Assert.NotNull(foo.SomethingElse);
            }
    
            public class Foo
            {
                // The important thing here is that the NAME AND TYPE match. If you
                // switch the parameter NAME here to "Xsomething" then you can't
                // even resolve the factory because the name and type won't match
                // with the real constructor.
                public delegate Foo Factory(string something);
    
                public string Something { get; }
    
                public Bar SomethingElse { get; }
    
                // The string will come from the delegate factory, the Bar will come
                // from the container.
                public Foo(string something, Bar somethingElse)
                {
                    Something = something;
                    SomethingElse = somethingElse;
                }
            }
    
            public class Bar
            {
            }
        }
    }
    

    So, why is that name matching thing even interesting?

    Let's say you have an object with a constructor like this:

    public class Foo
    {
      public Foo(string first, string second, string third){}
    }
    

    If you tried using the standard Func<> relationship, it wouldn't work:

    var builder = new ContainerBuilder();
    builder.RegisterType<Foo>();
    var container = builder.Build();
    using var scope = container.BeginLifetimeScope();
    
    // The Foo object needs three strings, right?
    var factory = scope.Resolve<Func<string, string, string, Foo>>();
    
    // Every string parameter that goes to the Foo constructor
    // will be "a" even if you passed the right values otherwise
    // because the Func<T> relationship matches by TYPE.
    var foo = factory("a", "b", "c");
    

    If you need to be able to have more than one parameter of a given type, that's when delegate factories come in handy.

    Your question, though, is how to pass in a named property and delegate factories don't do that. Delegate factories are about constructor parameters, not properties. Autofac (and most IoC containers) really do focus on constructor injection while property and method injection are secondary. If it's actually a required thing, usually it should be in the constructor.

    If you really do need to resolve with a specific NamedPropertyParameter and you can't get around that design, you're likely going to have to use ILifetimeScope and resolve it yourself.

    Use ILifetimeScope and not IContainer. An ILifetimeScope constructor parameter will be injected with the same lifetime scope from which the object itself was resolved.

    public class Foo
    {
        public Foo(ILifetimeScope scope)
        {
            // Here's where you could resolve stuff using
            // named property parameters or whatever.
            Property = scope.Resolve<Bar>();
        }
    
        public Bar Property { get; }
    }
    

    Then it can work like this:

    var builder = new ContainerBuilder();
    builder.RegisterType<Foo>();
    var container = builder.Build();
    
    // Doesn't matter how many nested scopes you have...
    using var scope1 = container.BeginLifetimeScope();
    using var scope2 = container.BeginLifetimeScope();
    using var scope3 = container.BeginLifetimeScope();
    
    // ... the scope injected in Foo will be scope3 because
    // that's where Foo itself came from.
    var foo = scope3.Resolve<Foo>();
    

    That's not super pretty, but when it comes to having to resolve stuff specifically with named property parameters, there aren't a lot of options. You can open the door to decouple Autofac from your code if you look for designs that:

    • Use constructor parameters
    • Match on type rather than name