jeudi 18 juin 2015

Autofac Singleton OnActivating Resolve

The crux of my misunderstanding is that I'm looking to directly Resolve() a type within a nested method called as a result of an OnActivating event, for the same singleton type, and autofac is attempting to create a second instance of that singleton.

The much, much longer version:

First a complete example, and then I'll summarize:

public static class AutofacTest
{
    public static void Test()
    {
        var builder = new ContainerBuilder();

        // Register the environment as a singleton, and call Initialize when created
        builder.RegisterType<Environment>().AsSelf().As<IEnvironment>().SingleInstance().OnActivating(e => e.Instance.Initialize());

        // Register the simulator, also a singleton and dependent on 
        builder.RegisterType<Simulator>().AsSelf().As<ISimulator>().SingleInstance();

        // Register a simple class, that needs an initialized environment
        builder.RegisterType<IndependentClass>();

        // Build/scope
        var context = builder.Build();
        var scope = context.BeginLifetimeScope();

        // Register the service locator
        ServiceLocator.GlobalScope = scope;

        //var childScope = scope.BeginLifetimeScope(cb =>
        //{
        //    cb.RegisterType<IndependentClass>();
        //});

        // Now resolve the independent class, which will trigger the environment/simulator instantiation
        var inst = scope.Resolve<IndependentClass>();
    }
}

public static class ServiceLocator
{
    public static ILifetimeScope GlobalScope { get; set; }
}

public interface IEnvironment 
{
    bool IsInitialized { get; }
}

public class Environment : IEnvironment
{
    private static Environment Instance;

    private SampleComponent _component;
    private bool _isInitialized;

    public bool IsInitialized
    {
        get { return _isInitialized; }
    }

    public void Initialize()
    {
        if (Instance != null) throw new InvalidOperationException();
        Instance = this;

        // Canonical complex code which forces me into what I think is a tricky situation...

        _component = new SampleComponent(SampleServiceType.SimulatedThing);

        _component.Initialize();

        _isInitialized = true;
    }
}

public interface ISimulator { }

public class Simulator : ISimulator
{
    private static Simulator Instance;

    private readonly IEnvironment _environment;

    public Simulator(IEnvironment environment)
    {
        if (Instance != null) throw new InvalidOperationException();
        Instance = this;

        _environment = environment;
    }
}

public enum SampleServiceType
{
    None = 0,
    RealThing,
    SimulatedThing,
}

public class SampleComponent
{
    private readonly SampleServiceType _serviceType;

    public SampleComponent(SampleServiceType serviceType)
    {
        _serviceType = serviceType;
    }

    public void Initialize()
    {
        // Sample component that has different types of repositories
        switch (_serviceType)
        {
            case SampleServiceType.SimulatedThing:
                var sim = ServiceLocator.GlobalScope.Resolve<ISimulator>();
                // Create a repositiry object or something requriing the simulator
                break;
        }
    }
}

public class IndependentClass
{
    public IndependentClass(IEnvironment env)
    {
        if (!env.IsInitialized) throw new InvalidOperationException();
    }
}

So the key points:

  • An Environment is the top level container, a simulator depends on an environment, and a component of the environment (SampleComponent) depends on both the environment and the simulator.

  • Critically, the component doesn't always use the simulator (not unusual), so there's a place for a factory style pattern here. I will typically use a global service locator in case like this (and I believe I understand why that can be evil, and very well might be biting me here) - but the main reason being that precisely for something like a simulator (or often for UI purposes), I don't want to take the dependency on the simulator in the constructor since it's only used in some scenarios. (More below.)

  • The environment should be initialized after creation. Hence the use of OnActivating here, which works well except for one caveat...

  • The IndependentClass takes an IEnvironment, and I want a fully initialized IEnvironment at this point. In this case though, the Resolve of the IndependentClass is what triggers the resolve of IEnvironment. So if I use OnActivated, then we don't have the resolve problem, but the environment isn't Initialized until after the constructor is called.

The actual problem (finally!):

As written, what currently happens:

  • Resolve<IndependentClass> triggers..
  • Resolve<IEnvironment>
  • OnActivating<Environment> triggers Environment.Initialize...
  • Which then calls SampleComponent.Initialize...
  • Which calls the global scope Resolve<IEnvironment>..
  • Which then resolves/instantiates a second Environment

So even though I have registered Environment as a singleton, two instances are created.

It's not a bug, it appears to be the expected behavior (since the Initialize call occurs in OnActivating and the instance hasn't yet be registered), but what should I do to resolve this?

I'd like to require:

  • That the Environment Resolve occurs on a deferred basis when SampleComponent is Resolve'd. (Since an environment isn't always needed.)

  • That the Environment.Initialize call is made before the instance is passed to the SampleComponent ctor.

  • That SampleComponent not have to take an ISimulator ctor argument since it's not required usually. (But I wouldn't be opposed to restructuring the factory pattern into something more Autofac friendly, as long as I don't have to require my (non-top level) components to be Autofac aware.)

Basically, I just want to require that the Initialization call is made prior to the IEnvironment instance being used, and since

Things I've tried:

  • Explicitly resolving the trade environment first: As noted, this works, but I find the requirement a little too constraining. Mainly because I have some optional configuration that I like to allow (via a UI or whatever) after the container is built (but before the environment is resolved), and since an environment (or simulator) isn't always required, I don't want to instantiate it until it's needed. (Same holds true with IStartable or AutoActivate, unless there's an alternate way to use them I'm not seeing.)

  • Abandoning the service locator pattern. In that case though, I'd need to express that SampleComponent needs to resolve an ISimulator only for certain values of serviceType, and otherwise pass null to the constructor (or Property/etc). Is there a clean way to express that?

  • Finally, creating my own instance registration, and storing the Environment instance as a static singleton. Something like:

    builder.Register(c => CreateInstance()).AsSelf().As().SingleInstance().OnActivating(e => e.Instance.Initialize());

    Where:

        private static Environment _globalInstance;
    private static Environment CreateInstance()
    {
        if (_globalInstance == null)
        {
            _globalInstance = new Environment();
        }
        return _globalInstance;
    }
    
    

    This works, though: 1. OnActivating is still called for every "new" instance. 2. Just feels way too hacky - ultimately I'm now managing the instance and construction, which is what the container is for. (It's also a little more annoying when you actually want to used the container to resolve parameters, but again can be worked around easily enough.)

So ALL that said (and I do greatly appreciate you making it this far), it seems like I have a fundamental misunderstanding here. (I'm guessing it relates to the service locator pattern and/or the haphazard factory in the SampleComponent, but I'll stop speculating.)

I guess the real question is: What am I missing?

Aucun commentaire:

Enregistrer un commentaire