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;
}
}
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);