Skip to content

Deduplication of components providing multiple services can break overrides of services depending on order of resolution. #1465

@PTwr

Description

@PTwr

Describe the Bug

Upon further debugging, the feature for deduplication of (open generic) components ignores overrides when assigning component to its other services, which breaks overrides of services resolved later on

When resolving for 'IAits overrideAis properly resolved and assigned, but then its earlier registration fromAB : IA, IBis assigned as implementation forIA(accidentally not causing an issue), andIBwhich still had no implementations resolved causing it to erroneously resolve toABinstead of its override ofB`.


Possibly related, or same, as #1464 - not related, my full usecase simply exposed both issues at the same time

Non-generic equivalent is working correctly, issue occurs only for open-generics.

For component exposing multiple services as open generic interfaces, not every override works.
There appears to be some caching going on under the hood, which goes haywire in entertaining use cases.
Issue is seemingly non-deterministic in real systems, as it depends on order of resolution.

From debugging linked issue in free time, and quick peek at same breakpoints when debugging test for this issue, it seems to boil down to Autofac guts mixing up open generic interfaces that look similar and thus succumbing to auto-illusion during further resolutions.

It was fun thing to debug and distill into simple tests :)

Steps to Reproduce

public class ReproTest
{
    interface IA { } interface IB { }
    class A : IA { } class B : IB { }
    class AB : IA, IB { }

    interface IA<T> { } interface IB<T> { }
    class A<T> : IA<T> { } class B<T> : IB<T> { }
    class AB<T> : IA<T>, IB<T> { }

    [Fact]
    public void Worky()
    {
        var builder = new ContainerBuilder();

        builder
            .RegisterType<AB>()
            .As<IA>()
            .As<IB>();
        builder
            .RegisterType<A>()
            .As<IA>();
        builder
            .RegisterType<B>()
            .As<IB>();

        var container = builder.Build();

        var a = container.Resolve<IA>();
        var b = container.Resolve<IB>();

        // works as expected
        Assert.True(a is A);
        Assert.True(b is B);
    }

    [Fact]
    public void NotWorky()
    {
        var builder = new ContainerBuilder();

        builder
            .RegisterGeneric(typeof(AB<>))
            .As(typeof(IA<>))
            .As(typeof(IB<>));
        builder
            .RegisterGeneric(typeof(A<>))
            .As(typeof(IA<>));
        builder
            .RegisterGeneric(typeof(B<>))
            .As(typeof(IB<>));

        var container = builder.Build();

        var aOfInt = container.Resolve<IA<int>>();
        var bOfInt = container.Resolve<IB<int>>();

        // resolve B first this time with another generic parameter
        var bOfStr = container.Resolve<IB<string>>();
        var aOfStr = container.Resolve<IA<string>>();

        // first resolution for given generic parameter is resolved correctly
        Assert.True(aOfInt is A<int>);
        Assert.True(bOfStr is B<string>);

        // services resolved later return AB<T> instead of overrides
        Assert.True(bOfInt is B<int>);
        Assert.True(aOfStr is A<string>);
    }
}

Expected Behavior

Open-Generics should behave the same as Non-Generic, and hallucinations should be reserved for AI not DI :)

Workaround

Registering AB<> separately for IA<> and IB<> solves the issue, but causes multiple instances of AB<> for each registration unless custom activator is provided (eg. AB<> registered as self, then resolved from ctx for its interface registration).

Dependency Versions

Autofac: 8.4.0 and 7.1.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions