autofac / Autofac

An addictive .NET IoC container
https://autofac.org
MIT License
4.44k stars 836 forks source link

Delegate factory vs func factory - different resolution #1391

Closed ABDB619 closed 11 months ago

ABDB619 commented 11 months ago

Describe the Bug

When using delegate factory, it looks like that if the argument passed to the factory can be resolved from the container, it is resolved and the value passed to the delegate factory is ignored (maybe this only manifests when the passed value doesn't match the type of the factory exactly, but is a subinterface instead). This behavior is different if one uses Func, where it works as expected.

Steps to Reproduce

I think the code explains it the best:


public class ReproTests
{

    [Fact]
    public void Repro()
    {
        var built = GetTestingCaseContainer();

        var resolvedWithDelegate = built.Resolve<ResolvedWithDelegate>();

        Assert.IsType<TestSomeMore>(resolvedWithDelegate.InvokeFactory().Test);
    }

    [Fact]
    public void WorksWithFunc()
    {
        var built = GetTestingCaseContainer();

        var resolvedWithFunc = built.Resolve<ResolvedWithFunc>();

        Assert.IsType<TestSomeMore>(resolvedWithFunc.InvokeFactory().Test);
    }

    private static IContainer GetTestingCaseContainer()
    {
        var cb = new ContainerBuilder();
        cb.RegisterType<Test>().As<ITest>();
        cb.RegisterType<TestSomeMore>().As<ITestSomeMore>();
        cb.RegisterType<ToFactory>().AsSelf();
        cb.RegisterType<ResolvedWithDelegate>().AsSelf();
        cb.RegisterType<ResolvedWithFunc>().AsSelf();

        var built = cb.Build();
        return built;
    }

    interface ITest
    {

    }

    interface ITestSomeMore : ITest
    {

    }

    class TestSomeMore : Test, ITestSomeMore
    {

    }

    class Test : ITest
    {

    }

    class ResolvedWithDelegate
    {
        private readonly Factory _factory;
        private readonly ITestSomeMore _someMore;

        public ToFactory InvokeFactory() => _factory(_someMore); 

        public override string ToString() => InvokeFactory().ToString();

        public ResolvedWithDelegate(Factory factory, ITestSomeMore someMore)
        {
            _factory = factory;
            _someMore = someMore;
        }

    }

    class ResolvedWithFunc
    {
        private readonly Func<ITest, ToFactory> _factory;
        private readonly ITestSomeMore _someMore;

        public override string ToString() => InvokeFactory().ToString();

        public ToFactory InvokeFactory() => _factory(_someMore); 

        public ResolvedWithFunc(Func<ITest, ToFactory> factory, ITestSomeMore someMore)
        {
            _factory = factory;
            _someMore = someMore;
        }

    }

    class ToFactory
    {
        public ITest Test { get; }
        public override string ToString() => $"I use: {Test.GetType()}.";

        public ToFactory(ITest test)
        {
            Test = test;
        }

    }

    delegate ToFactory Factory(ITest value);

}

Expected Behavior

I would expect the delegate factory to behave exactly the same as the Func as we often refactor usage of Func into factory when there are more arguments.

Dependency Versions

Autofac: 7.0.1

tillig commented 11 months ago

I will need to take some time to peel this apart, but it'd be good to remember that Func<T> and delegate factories are the effective equivalent of doing a container.Resolve<T>() call outside of the standard resolve operation. The code in the example is showing:

This may just be an artifact of the testing code - that it's maybe just confusingly-written test code. Again, I haven't pasted this into an editor to compile it. Just saying, there's some weird circular stuff here, at least from a reading perspective, and without peeling that apart I can't rule out that it, at the very least, reads oddly.

tillig commented 11 months ago

Something I did notice, and which is documented, is that the name of the parameter in the delegate is value but the name of the parameter in the ToFactory constructor is test. From the docs:

By default, Autofac matches the parameters of the delegate to the parameters of the constructor by name. If you use the generic Func relationships, Autofac will switch to matching parameters by type. The name matching is important here - it allows you to provide multiple parameters of the same type if you want, which isn’t something the Func implicit relationships can support. However, it also means that if you change the names of parameters in the constructor, you also have to change those names in the delegate.

tillig commented 11 months ago

Here's a repro that shows Func<X, Y>, delegate factories, and parameters are all working and that it honors the parameter you passed over the container:


public class UnitTest1
{
    private readonly ITestOutputHelper _output;
    public UnitTest1(ITestOutputHelper output)
    {
        this._output = output;
    }

    [Fact]
    public void ResolveOnlyFromContainer()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<Component>();
        builder.RegisterType<Dependency>();
        var container = builder.Build();
        var resolved = container.Resolve<Component>();
        Assert.NotNull(resolved.Dependency);
    }

    [Fact]
    public void ResolveUsingDelegateFactory()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<Component>();
        builder.RegisterType<Dependency>();
        var container = builder.Build();
        var parameter = container.Resolve<Dependency>();
        var factory = container.Resolve<FactoryDelegate>();
        var actual = factory(parameter);

        // If it's using the parameter, the IDs will be the same; if it's not
        // using the parameter, they'll be different.
        Assert.Equal(parameter.Id, actual.Dependency.Id);

    }

    [Fact]
    public void ResolveUsingFunc()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<Component>();
        builder.RegisterType<Dependency>();
        var container = builder.Build();
        var parameter = container.Resolve<Dependency>();
        var factory = container.Resolve<Func<Dependency, Component>>();
        var actual = factory(parameter);

        // If it's using the parameter, the IDs will be the same; if it's not
        // using the parameter, they'll be different.
        Assert.Equal(parameter.Id, actual.Dependency.Id);
    }

    [Fact]
    public void ResolveUsingParameters()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<Component>();
        builder.RegisterType<Dependency>();
        var container = builder.Build();
        var parameter = container.Resolve<Dependency>();
        var actual = container.Resolve<Component>(TypedParameter.From(parameter));

        // If it's using the parameter, the IDs will be the same; if it's not
        // using the parameter, they'll be different.
        Assert.Equal(parameter.Id, actual.Dependency.Id);
    }
}

// The parameter name here MUST MATCH the parameter name in the Component constructor!
delegate Component FactoryDelegate(Dependency dependency);

public class Dependency
{
    public string Id { get; } = Guid.NewGuid().ToString();
}

public class Component
{
    public Component(Dependency dependency)
    {
        Dependency = dependency;
    }

    public Dependency Dependency { get; }
}

And I did verify that, in your repro, if you change your delegate parameter name to match the constructor parameter name...

delegate ToFactory Factory(ITest test);

...then the tests in the repro pass, too.

Closing this issue - functioning as designed.