Home > Back-end >  Why can Autofac not create this class that takes a non-generic implementation of a generic interface
Why can Autofac not create this class that takes a non-generic implementation of a generic interface

Time:01-28

Using Autofac for injection.

Given

interface IStateMachine<TState, TTrigger> { } 
class ConcreteStateMachine : IStateMachine<MachineState, Trigger> { }
builder.RegisterType<ConcreteStateMachine>().As<IStateMachine<MachineState, Trigger>>();
class Consumer { Consumer(IStateMachine<MachineState, Trigger> machine) { } }

Why does container.Resolve<Consumer>(); fail with this exception:

Unhandled exception. 
Autofac.Core.DependencyResolutionException: An exception was thrown while activating Consumer.
 ---> Autofac.Core.DependencyResolutionException: An exception was thrown while invoking the 
constructor 'Void .ctor(IStateMachine`2[MachineState,Trigger])' on type 'Consumer'.
 ---> System.NullReferenceException: Object reference not set to an instance of an object.

Full code contrived example: Requires nuget for Autofac and Stateless

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Autofac;
using Stateless;
                    
public class Program
{
    public static void Main()
    {
        var builder = new ContainerBuilder();
        
        builder.RegisterType<Consumer>()
               .InstancePerLifetimeScope();
        
        builder.RegisterType<ConcreteStateMachine>()
               .As<IStateMachine<MachineState, Trigger>>()
               .InstancePerLifetimeScope();

        
        var container = builder.Build();
        
        using (var scope = container.BeginLifetimeScope())
        {
            var consumer = scope.Resolve<Consumer>();
            
            consumer.TurnOn();
            
            consumer.GetData();
            
            consumer.TurnOff();
        }
            
    }
}

public class Consumer
{
    protected IStateMachine<MachineState, Trigger> Machine;
    
    public Consumer(IStateMachine<MachineState, Trigger> machine)
    {
        Machine = machine;
        
        Machine.Register(MachineState.Off, () => Debug.WriteLine("State Off"));
        Machine.Register(MachineState.On, () => Debug.WriteLine("State On"));
    }
    
    public List<string> GetData()
    {
        if (!Machine.IsInState(MachineState.On)) throw new InvalidOperationException("Can't GetData when machine is off!");
        
        Debug.WriteLine("Getting Data");
        
        return new List<String> {"Data", "Data", "Data"};
    }
    
    public void TurnOn()
    {
        Machine.Fire(Trigger.TurnOn);
    }

    public void TurnOff()
    {
        Machine.Fire(Trigger.TurnOff);
    }
}

public class ConcreteStateMachine : IStateMachine<MachineState, Trigger>
{
    protected StateMachine<MachineState, Trigger> Machine;

    public MachineState CurrentState { get; set; }

    public void Fire(Trigger trigger)
    {
        Machine.Fire(trigger);
    }

    public async Task FireAsync(Trigger trigger)
    {
        await Machine.FireAsync(trigger);
    }

    public bool IsInState(MachineState state)
    {
        return Machine.IsInState(state);
    }

    public void Register(MachineState state, Action callback)
    {
        Machine.Configure(state)
               .OnEntry(() => callback.Invoke());
    }

    public void RegisterAsync(MachineState state, Func<Task> callback)
    {
        Machine.Configure(state)
               .OnEntryAsync(async () => await callback.Invoke());
    }

    public void Start()
    {
        ConfigureMachine();
        Machine.Activate();
    }
    
    protected void ConfigureMachine()
    {
        Machine = new StateMachine<MachineState, Trigger>(MachineState.Off);
    
        Machine.Configure(MachineState.Off)
               .Permit(Trigger.TurnOn, MachineState.On);
               
        Machine.Configure(MachineState.On)
               .Permit(Trigger.TurnOff, MachineState.Off);
    }
}

public interface IStateMachine<TState, TTrigger>
{
    TState CurrentState { get; set; }
    bool IsInState(TState state);
    void Fire(TTrigger trigger);
    Task FireAsync(TTrigger trigger);
    void Start();
    void Register(TState state, Action callback);
    void RegisterAsync(TState state, Func<Task> callback);
}

public enum MachineState
{
    Off,
    On
}

public enum Trigger
{
    TurnOff,
    TurnOn
}

I've tried to find an example of this type of injection using Autofac and I can't find anyone else who's had a similar problem.

The container will resolve an instance of ConcreteStateMachine directly.

As in container.Resolve<IStateMachine<MachineState, Trigger>>() correctly provides a ConcreteStateMachine, but Autofac doesn't seem to know how to send an instance to Consumer.

CodePudding user response:

Machine field of ConcreteStateMachine is not initialized and is null. So it fails with NullReferenceException in:

public void Register(MachineState state, Action callback)
{
    Machine.Configure(state)
        .OnEntry(() => callback.Invoke());
}

CodePudding user response:

Just a case of introducing a bug while creating the contrived example...

I never called Machine.Start() in the constructor of Consumer, which instantiates the internal StateMachine, prior to calling Register, which invokes a method on the underlying StateMachine. This caused an exception to be thrown while creating Consumer and was not in fact an Autofac configuration issue.

Sorry!

  • Related