Search code examples
c#.netdesign-patternsinversion-of-controlcastle-windsor

Castle Windsor explicitly sharing dependencies


How does one share temporary objects across component graphs executed at different times?

I have a state engine from some old legacy code. Each state is represented by an IState and is responsible for creating the next state in a process.

public interface IState
{
    Guid Session { get; }
    IState GetNextState();
}

The starting state is initialized by a model:

public class Model
{
    private readonly IStateFactory _stateFactory;

    public Model(IStateFactory stateFactory)
    {
        _stateFactory = stateFactory;
    }

    public IState GetFirstState()
    {
        return _stateFactory.GetStateA();
    }
}

Each state contains a session context (simplified to only contain a GUID here).

public class Context : IDisposable
{
    public static int CreatedCount = 0;
    public static int DisposedCount = 0;
    //Has other DI injected dependencies.

    public Context()
    {
        CreatedCount++;
    }

    public Guid SessionGuid { get; } = Guid.NewGuid();

    public void Dispose()
    {
        DisposedCount++;
    }
}

The "CreatedCount" and "DisposedCount" have been added to assist in demonstrating the problem. Note that they are static ints.

An implementation of a State might be as such:

public class MyState : IState
{
    private readonly Context _context;
    private readonly IStateFactory _stateFactory;

    public MyState(IStateFactory stateFactory, Context context)
    {
        _context = context;
        _stateFactory = stateFactory;
    }

    public Guid Session => _context.SessionGuid;

    public IState GetNextState()
    {
        var nextState = _stateFactory.GetStateB(_context);
        _stateFactory.DestroyState(this);
        return nextState;
    }
}

The state factory is a simple Castle Windsor implemented TypedFactory interface.

public interface IStateFactory
{
    IState GetFirstState(); 
    IState GetStateB(Context context);

    void DestroyState(IState state);
}

The idea is that each "state" can initiate the next state based on some action, and that the current states "context" should be used in the next state.

The container is built in the expected way:

var container = new WindsorContainer();
container.AddFacility<TypedFactoryFacility>();

container.Register(
    Component.For<Context>().LifestyleTransient(),
    Component.For<IState>().ImplementedBy<MyState>().Named("stateA").LifestyleTransient(),
    Component.For<IState>().ImplementedBy<MyState>().Named("stateB").LifestyleTransient(),
    Component.For<IStateFactory>().AsFactory()
);

Essentially, I want "stateB" to take ownership of the Context. But when I release "stateA" (through a call to MyState.GetNextState), the Context is released and disposed! How do I tell Castle.Windsor to transfer ownership to the next state?

var model = container.Resolve<Model>();
var initialState = model.GetFirstState();
var nextState = initialState.GetNextState(); //releases the initial State.

Assert.That(initialState.Session, Is.EqualTo(nextState.Session)); //The context was 'shared' by stateA by passing it into the factory method for stateB.

Assert.That(Context.CreatedCount, Is.EqualTo(1));
Assert.That(Context.DisposedCount, Is.EqualTo(0)); //FAIL! Castle Windsor should see that the shared "Context" was passed into the constructor of modelB, and added a reference to it.

container.Release(model);
container.Release(nextState); //usually done by the model.
Assert.That(Context.CreatedCount, Is.EqualTo(1));
Assert.That(Context.DisposedCount, Is.EqualTo(1)); 

It should be noted that state transition can be initiated from another thread, but invoked on the creating thread. This messes up the CallContext used by the default Castle Windsor scoped lifestyle. This is for a desktop application so the default WCF and web-request scope lifestyles do not apply.


Solution

  • Use LifestyleScoped:

    Component.For<Context>().LifestyleScoped()
    
    using (container.BeginScope())
    {
        var model = container.Resolve<Model>();
        var initialState = model.GetFirstState();
        var nextState = initialState.GetNextState();
        Assert.That(Context.CreatedCount, Is.EqualTo(0));
    }
    
    Assert.That(Context.CreatedCount, Is.EqualTo(1));