S. LaBani
S. LaBani

Reputation: 1

Circular dependency was detected when I tried to implement state pattern

I am having this issue:

System.InvalidOperationException: 'A circular dependency was detected for the service of type 'LibraryExamples.Water.States.IGasState'. LibraryExamples.Water.IWater(LibraryExamples.Water.WaterMe) -> LibraryExamples.Water.IWaterStateFactory(LibraryExamples.Water.WaterStateFactory) -> LibraryExamples.Water.States.IGasState(LibraryExamples.Water.States.GasState) -> LibraryExamples.Water.States.ILiquidState(LibraryExamples.Water.States.LiquidState) -> LibraryExamples.Water.States.IGasState'

namespace LibraryExamples.Water.States
{
    public class GasState : IGasState
    {
        private readonly ILiquidState LiquidState;

        public GasState(ILiquidState nextState)
        {
            this.LiquidState = nextState;
        }

        public IWaterState Heat()
        {
            Console.WriteLine("Already in a gaseous state. Heating has no effect.");

            return this;
        }

        public IWaterState Cool()
        {
            Console.WriteLine("Cooling gas (steam) turns it into liquid water.");

            return this.ToLiquidState();
        }

        public void Describe()
        {
            Console.WriteLine("Current state: Gas");
        }

        public ILiquidState ToLiquidState()
        {
            return this.LiquidState;
        }
    }
}
namespace LibraryExamples.Water.States
{
    public interface IGasState : IWaterState
    {

        ILiquidState ToLiquidState();
    }
}
namespace LibraryExamples.Water.States
{
    public interface ILiquidState : IWaterState
    {
        IGasState ToGasState();
    }
}
namespace LibraryExamples.Water.States
{
    public interface ISolideState : IWaterState
    {
        ILiquidState ToLiquidState();
    }
}
namespace LibraryExamples.Water.States
{
    public class LiquidState : ILiquidState
    {
        private readonly IGasState GasState;

        private readonly ISolideState solideState;

        public LiquidState(IGasState GastState, ISolideState solideState)
        {
            this.GasState = GastState;
            this.solideState = solideState;
        }

        public IWaterState Cool()
        {
            Console.WriteLine("Cooling liquid water turns it into solid water.");

            return this.ToSolidState();
        }

        public IWaterState Heat()
        {
            Console.WriteLine("Heating liquid water turns into gas water");

            return this.ToGasState();
        }

        public void Describe()
        {
            Console.WriteLine("Current state: Solid");
        }

        public IGasState ToGasState()
        {
            return this.GasState;
        }

        public ISolideState ToSolidState()
        {
            return this.solideState;
        }
    }
}
namespace LibraryExamples.Water.States
{
    public class SolidState : ISolideState
    {
        private readonly ILiquidState LiquidState;

        public SolidState(ILiquidState liquidState)
        {
            this.LiquidState = liquidState;
        }

        public IWaterState Heat()
        {
            Console.WriteLine("Heating solid water turns it into liquid water.");

            return this.ToLiquidState();
        }

        public IWaterState Cool()
        {
            Console.WriteLine("Already solid. Cooling has no effect.");

            return this;
        }

        public void Describe()
        {
            Console.WriteLine("Current state: Solid");
        }

        public ILiquidState ToLiquidState()
        {
            return this.LiquidState;
        }
    }
}
namespace LibraryExamples.Water
{
    public interface IWater
    {
        void Cool();
        void DescribeState();
        void Heat();
    }
}
namespace LibraryExamples.Water
{
    public interface IWaterState
    {
        IWaterState Heat();
        IWaterState Cool();
        void Describe();
    }
}
namespace LibraryExamples.Water
{
    public class Water : IWater
    {
        private IWaterState State;

        public Water(IWaterState initialState)
        {
            this.State = initialState;
        }

        public void Heat()
        {
            this.State.Heat();
        }

        public void Cool()
        {
            this.State.Cool();
        }

        public void DescribeState()
        {
            this.State.Describe();
        }
    }
}

I am having this issue:

System.InvalidOperationException: 'A circular dependency was detected for the service of type 'LibraryExamples.Water.States.IGasState'.

Upvotes: -1

Views: 212

Answers (2)

Olivier Jacot-Descombes
Olivier Jacot-Descombes

Reputation: 112342

I have already added an answer using keyed services and was wondering if a solution with specialized interfaces would work instead. But I ran into the problem that the class Lazy<T> is not covariant. Finally I could solve it by implementing a covariant lazy class. Only interfaces and delegates can be variant. Therefore, I declare:

public interface ILazy<out T> // the 'out' keyword makes it covariant.
{
    T Value { get; }
}

public class CovariantLazy<T>(Func<T> valueFactory)
    : Lazy<T>(valueFactory), ILazy<T>
{
}

Note that I did implement only the members and constructors required for this solution. It is interesting to note that in C# a derived class can implement an interface solely by inheriting the required members from a base class.


We declare the aggregate state interfaces like this:

public interface IAggregateState
{
    IAggregateState? CoolerState { get; }
    IAggregateState? HotterState { get; }

    string Description { get; }
}
public interface ISolid : IAggregateState { }
public interface ILiquid : IAggregateState { }
public interface IGaseous : IAggregateState { }

The implementation now works against the ILazy<T> interface:

public abstract class AggregateState(
    ILazy<IAggregateState>? cooler, ILazy<IAggregateState>? hotter)
    : IAggregateState
{
    public abstract string Description { get; }

    public IAggregateState? CoolerState => cooler?.Value;

    public IAggregateState? HotterState => hotter?.Value;
}

public class Solid(ILazy<ILiquid> hotter)
    : AggregateState(null, hotter), ISolid
{
    public override string Description => "Aggregate state: Solid";
}

public class Liquid(ILazy<ISolid> cooler, ILazy<IGaseous> hotter)
    : AggregateState(cooler, hotter), ILiquid
{
    public override string Description => "Aggregate state: Liquid";
}

public class Gaseous(ILazy<ILiquid> cooler)
    : AggregateState(cooler, null), IGaseous
{
    public override string Description => "Aggregate state: Gaseous";
}

Similarly I declare and implement matter and water:

public interface IMatter
{
    IAggregateState State { get; }
    void Heat();
    void Cool();
    string Name { get; }
    string Description { get; }
}
public interface IWater : IMatter { }

public abstract class Matter(IAggregateState initialState) : IMatter
{
    public abstract string Name { get; }
    public string Description => $"{Name}: {State.Description}";

    public IAggregateState State { get; private set; } = initialState;

    public void Cool()
    {
        State = State.CoolerState ?? State;
    }

    public void Heat()
    {
        State = State.HotterState ?? State;
    }
}

public class Water(ILiquid initialState) : Matter(initialState), IWater
{
    public override string Name => "Water";
}

We can now register the services like this:

IHostBuilder builder = Host.CreateDefaultBuilder()
    .ConfigureServices(services => {
        services.AddTransient<ISolid, Solid>();
        services.AddTransient<ILiquid, Liquid>();
        services.AddTransient<IGaseous, Gaseous>();
        services.AddSingleton<ILazy<ISolid>>(
            sp => new CovariantLazy<ISolid>(() => sp.GetService<ISolid>()!));
        services.AddSingleton<ILazy<ILiquid>>(
            sp => new CovariantLazy<ILiquid>(() => sp.GetService<ILiquid>()!));
        services.AddSingleton<ILazy<IGaseous>>(
            sp => new CovariantLazy<IGaseous>(() => sp.GetService<IGaseous>()!));
        services.AddTransient<IWater, Water>();
    });
IHost host = builder.Build();

I get the water for the console test like this:

IMatter matter = host.Services.GetService<IWater>()!;

The test itself remains the same as in the other answer.

Upvotes: 0

Olivier Jacot-Descombes
Olivier Jacot-Descombes

Reputation: 112342

To be able to work with DI, we must use a lazy creation of the aggregate states to avoid circular references. I will be using a Lazy<IAggregateState> for this. Note that we cannot completely avoid factories, as Lazy<T> requires a factory delegate.

We declare only one interface for the different states to be able to access and work with them in the same way and will be using keyed services introduced in .NET 8.0 to differentiate the different states.

public interface IAggregateState
{
    IAggregateState? CoolerState { get; }
    IAggregateState? HotterState { get; }

    string Description { get; }
}

The aggregate states have references to the cooler and hotter states, so that we can link them together in a logical way. Note that the states are nullable, as the coolest state has no predecessor and the hottest state has no successor.

Except for the description, all the states have the same implementation. Therefore, we implement a base state as an abstract class.

public abstract class AggregateState(Lazy<IAggregateState>? cooler,
    Lazy<IAggregateState>? hotter) : IAggregateState
{
    public abstract string Description { get; }

    public IAggregateState? CoolerState => cooler?.Value;

    public IAggregateState? HotterState => hotter?.Value;
}

We can no derive the different states. Note that we specify a service key for the aggregate states through the FromKeyedServicesAttribute:

public class Solid([FromKeyedServices("liquid")]Lazy<IAggregateState> hotter) : 
    AggregateState(null, hotter)
{
    public override string Description => "Aggregate state: Solid";
}

public class Liquid(
    [FromKeyedServices("solid")]Lazy<IAggregateState> cooler,
    [FromKeyedServices("gaseous")]Lazy<IAggregateState> hotter) :
    AggregateState(cooler, hotter)
{
    public override string Description => "Aggregate state: Liquid";
}

public class Gaseous([FromKeyedServices("liquid")]Lazy<IAggregateState> cooler) :
    AggregateState(cooler, null)
{
    public override string Description => "Aggregate state: Gaseous";
}

Not only water has aggregate states. Therefore, instead of having an interface IWater, I declare a more general interface:

public interface IMatter
{
    IAggregateState State { get; }
    void Heat();
    void Cool();
    string Name {  get; }
    string Description { get; }
}

Here, only the Name will be different for different kinds of matter and the description is supposed to be constructed from the matter name and the current aggregate state. Therefore, we implement matter as an abstract class again:

public abstract class Matter(IAggregateState initialState) : IMatter
{
    public abstract string Name { get; }
    public string Description => $"{Name}: {State.Description}";

    public IAggregateState State { get; private set; } = initialState;

    public void Cool()
    {
        State = State.CoolerState ?? State;
    }

    public void Heat()
    {
        State = State.HotterState ?? State;
    }
}

If the cooler or hotter sates are null, we just keep the same state by using the null-coalesce operator.

public class Water([FromKeyedServices("liquid")] IAggregateState initialState) : Matter(initialState)
{
    public override string Name => "Water";
}

Now, we can set up DI. I am adding the states as transients and the Lazy<T> as singletons. Lazy<T> inherently returns a singleton.

IHostBuilder builder = Host.CreateDefaultBuilder()
    .ConfigureServices(services => {
        services.AddKeyedTransient<IAggregateState, Solid>("solid");
        services.AddKeyedTransient<IAggregateState, Liquid>("liquid");
        services.AddKeyedTransient<IAggregateState, Gaseous>("gaseous");
        services.AddKeyedSingleton<Lazy<IAggregateState>>("solid",
            (sp, key) => new Lazy<IAggregateState>(() => sp.GetRequiredKeyedService<IAggregateState>(key)));
        services.AddKeyedSingleton<Lazy<IAggregateState>>("liquid",
            (sp, key) => new Lazy<IAggregateState>(() => sp.GetRequiredKeyedService<IAggregateState>(key)));
        services.AddKeyedSingleton<Lazy<IAggregateState>>("gaseous",
            (sp, key) => new Lazy<IAggregateState>(() => sp.GetRequiredKeyedService<IAggregateState>(key)));
        services.AddKeyedTransient<IMatter, Water>("water");
    });
IHost host = builder.Build();
IMatter matter = host.Services.GetKeyedService<IMatter>("water")!;

Console.WriteLine(matter.Description);
for (int i = 0; i < 12; i++) {
    if (Random.Shared.NextDouble() < 0.5) {
        Console.WriteLine("> cooling...");
        matter.Cool();
    } else {
        Console.WriteLine("> heating...");
        matter.Heat();
    }
    Console.WriteLine(matter.Description);
}

It will print something like this:

Water: Aggregate state: Liquid
> heating...
Water: Aggregate state: Gaseous
> cooling...
Water: Aggregate state: Liquid
> cooling...
Water: Aggregate state: Solid
> cooling...
Water: Aggregate state: Solid
> heating...
Water: Aggregate state: Liquid
> heating...
Water: Aggregate state: Gaseous
> heating...
Water: Aggregate state: Gaseous
> cooling...
Water: Aggregate state: Liquid
> cooling...
Water: Aggregate state: Solid
> cooling...
Water: Aggregate state: Solid
> heating...
Water: Aggregate state: Liquid
> heating...
Water: Aggregate state: Gaseous

Upvotes: 0

Related Questions