Search code examples
c#autofac

The type XXX is not assignable to service YYY


I need help with my autofac generics configuration. I get the below error which I can't seem to get around. I have an interface using generics called IScript<TOptionType>.

Implementing that interface is an abstract class called Script<TOptionType> : IScript<TOptionType>. Derived from that abstract class are two concrete classes that set their preferred TOptionType.

I've uploaded a sample .net core application that exhibits the problem here: https://github.com/Strandedpirate/agr

To run go to agr\autofac-generic-registration and type in dotnet run.

C# seems to have no problems boxing the concrete types to either the interface or abstract base class. So why is autofac complaining here?

C:\Users\strandedpirate\source\repos\agr\autofac-generic-registration (master -> origin)
λ dotnet run

Unhandled Exception: System.ArgumentException: The type 'agr.TableScript' is not assignable to service 'agr.Script`1'.
   at Autofac.Builder.RegistrationBuilder.CreateRegistration(Guid id, RegistrationData data, IInstanceActivator activator, Service[] services, IComponentRegistration target) in C:\projects\autofac\src\Autofac\Builder\RegistrationBuilder.cs:line 192
   at Autofac.Builder.RegistrationBuilder.CreateRegistration[TLimit,TActivatorData,TSingleRegistrationStyle](IRegistrationBuilder`3 builder) in C:\projects\autofac\src\Autofac\Builder\RegistrationBuilder.cs:line 132
   at Autofac.Builder.RegistrationBuilder.RegisterSingleComponent[TLimit,TActivatorData,TSingleRegistrationStyle](IComponentRegistry cr, IRegistrationBuilder`3 builder) in C:\projects\autofac\src\Autofac\Builder\RegistrationBuilder.cs:line 249
   at Autofac.ContainerBuilder.Build(IComponentRegistry componentRegistry, Boolean excludeDefaultModules) in C:\projects\autofac\src\Autofac\ContainerBuilder.cs:line 240
   at Autofac.ContainerBuilder.Build(ContainerBuildOptions options) in C:\projects\autofac\src\Autofac\ContainerBuilder.cs:line 148
   at agr.Program.Main(String[] args) in C:\Users\strandedpirate\source\repos\agr\autofac-generic-registration\Program.cs:line 22

Program.cs

class Program
    {
        static void Main(string[] args)
        {
            var builder = new ContainerBuilder();

            // comment out the next registrations to see the program run.
            builder.RegisterType<TableScript>()
              .As<Script<TableScriptOptions>>()
              .InstancePerLifetimeScope();
            builder.RegisterType<TableScript>()
              .As(typeof(Script<>))
              .InstancePerLifetimeScope();
            builder.RegisterType<TableScript>()
              .As(typeof(IScript<>))
              .InstancePerLifetimeScope();

            // explodes here during autofac building.
            var container = builder.Build();

            // if you comment out the above autofac configuration this will succeed compile and run-time.
            // why does c# have no problems converting TableScript to IScript<T> and Script<T> but autofac is complaining?
            TestInterface(new TableScript());
            TestAbstractBase(new TableScript());

            using (var scope = container.BeginLifetimeScope())
            {
                Console.WriteLine("Resolving IScript instance...");
                var instance = scope.Resolve(typeof(IScript<>)) as IScript<object>;
                instance.Run();
            }
        }

        static void TestInterface<T>(IScript<T> a)
            where T : class, new()
        {
            Console.WriteLine($"{nameof(TestInterface)} called - {a.CanHandle("table")}");
        }

        static void TestAbstractBase<T>(Script<T> a)
            where T : class, new()
        {
            Console.WriteLine($"{nameof(TestAbstractBase)} called - {a.CanHandle("table")}");
        }
    }

IScript.cs

public interface IScript<TOptionType>
        where TOptionType : class, new()
    {
        bool CanHandle(string key);
        Task Run();
        bool Validate(TOptionType options);
    }

Script.cs

public abstract class Script<TOptionType> : IScript<TOptionType>
        where TOptionType : class, new()
    {
        public abstract bool CanHandle(string key);

        public abstract Task Run();

        public virtual bool Validate(TOptionType options)
        {
            return true;
        }
    }

TableScript.cs


    public class TableScript : Script<TableScriptOptions>
    {
        public override bool CanHandle(string key)
        {
            return key == "table";
        }

        public override Task Run()
        {
            Console.WriteLine($"{nameof(TableScript)} executed");
            return Task.CompletedTask;
        }
    }

FileScript.cs

    public class FileScript : Script<FileScriptOptions>
    {
        public override bool CanHandle(string key)
        {
            return key == "file";
        }

        public override Task Run()
        {
            Console.WriteLine($"{nameof(FileScript)} executed");
            return Task.CompletedTask;
        }
    }

Solution

  • There are a couple of issues.

    First, your TableScript registration:

    builder.RegisterType<TableScript>()
           .As(typeof(Script<>))
           .InstancePerLifetimeScope();
    builder.RegisterType<TableScript>()
           .As(typeof(IScript<>))
           .InstancePerLifetimeScope();
    

    You're trying to register a closed generic as an open generic. If you think about what that means, it'd be like saying "TableScript can allow any T for Script<T> and IScript<T>". For example, I see TableScript : Script<TableScriptOptions> - what the open generic registration is saying is it should somehow also work for Script<IntegerScriptOptions> or anything else that could possibly go inside those angle brackets.

    Instead, register it as the closed generic it is. And I would recommend doing it on the same registration or you could get two different instances of TableScript per lifetime scope depending on which service gets resolved.

    builder.RegisterType<TableScript>()
           .As<Script<TableScriptOptions>>()
           .As<IScript<TableScriptOptions>>()
           .InstancePerLifetimeScope();
    

    Next, the resolution of IScript<T>:

    scope.Resolve(typeof(IScript<>)) as IScript<object>;
    

    Think of Resolve as being a lot like new. If you can't use new or Activator.CreateInstance in its place (basically) then it won't work. For example, you can't do this:

    // This isn't a thing
    new Script<>();
    

    You also can't put an open generic into a constructor of an object quite like this:

    public class MyClass
    {
      // This also isn't a thing
      public MyClass(IScript<> script) { /* ... */ }
    }
    

    You can't new-up an open generic. The compiler needs to know what the T is in Script<T>. By that same token, you can't resolve an open generic. That doesn't make sense. You have to resolve a closed generic.

    scope.Resolve<IScript<TableScriptOptions>>();
    

    If you really want to use reflection, you still have to make it a closed generic.

    var script = typeof(IScript<>);
    var options = typeof(TableScriptOptions);
    var closed = script.MakeGenericType(new Type[] { options });
    scope.Resolve(closed);