简体   繁体   中英

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 the Environment / SampleComponent / Simulator object graph is entirely separate, that seems like something that should be able to wired up/expressed.

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?

Trying to run your exact code from the example, I am not able to resolve IndependentClass because I (correctly) get an exception. The exception stack looks like a circular dependency where it nests and nests and nests the same exception, like a stack overflow:

Autofac.Core.DependencyResolutionException was unhandled
  _HResult=-2146233088
  _message=An exception was thrown while executing a resolve operation. See the InnerException for details.
  HResult=-2146233088
  IsTransient=false
  Message=An exception was thrown while executing a resolve operation. See the InnerException for details. ---> Operation is not valid due to the current state of the object. (See inner exception for details.)
  Source=Autofac
  StackTrace:
       at Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.Core.Lifetime.LifetimeScope.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.TryResolveService(IComponentContext context, Service service, IEnumerable`1 parameters, Object& instance)
       at Autofac.ResolutionExtensions.ResolveService(IComponentContext context, Service service, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.Resolve(IComponentContext context, Type serviceType, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.Resolve[TService](IComponentContext context, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.Resolve[TService](IComponentContext context)
       at SingletonRepro.SampleComponent.Initialize() in e:\dev\spike\SingletonRepro\SingletonRepro\Program.cs:line 120
       at SingletonRepro.Environment.Initialize() in e:\dev\spike\SingletonRepro\SingletonRepro\Program.cs:line 75
       at SingletonRepro.Program.<Main>b__0(IActivatingEventArgs`1 e) in e:\dev\spike\SingletonRepro\SingletonRepro\Program.cs:line 17
       at Autofac.Builder.RegistrationBuilder`3.<>c__DisplayClass6.<OnActivating>b__5(Object s, ActivatingEventArgs`1 e)
       at Autofac.Core.Registration.ComponentRegistration.RaiseActivating(IComponentContext context, IEnumerable`1 parameters, Object& instance)
       at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters)
       at Autofac.Core.Resolving.InstanceLookup.<Execute>b__0()
       at Autofac.Core.Lifetime.LifetimeScope.GetOrCreateAndShare(Guid id, Func`1 creator)
       at Autofac.Core.Resolving.InstanceLookup.Execute()
       at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.Core.Resolving.InstanceLookup.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.Core.Activators.Reflection.AutowiringParameter.<>c__DisplayClass2.<CanSupplyValue>b__0()
       at Autofac.Core.Activators.Reflection.ConstructorParameterBinding.Instantiate()
       at Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters)
       at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters)
       at Autofac.Core.Resolving.InstanceLookup.Execute()
       at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.Core.Resolving.ResolveOperation.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.Core.Lifetime.LifetimeScope.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.TryResolveService(IComponentContext context, Service service, IEnumerable`1 parameters, Object& instance)
       at Autofac.ResolutionExtensions.ResolveService(IComponentContext context, Service service, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.Resolve(IComponentContext context, Type serviceType, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.Resolve[TService](IComponentContext context, IEnumerable`1 parameters)
       at Autofac.ResolutionExtensions.Resolve[TService](IComponentContext context)
       at SingletonRepro.Program.Main(String[] args) in e:\dev\spike\SingletonRepro\SingletonRepro\Program.cs:line 38
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: System.InvalidOperationException
       _HResult=-2146233079
       _message=Operation is not valid due to the current state of the object.
       HResult=-2146233079
       IsTransient=false
       Message=Operation is not valid due to the current state of the object.
       Source=SingletonRepro
       StackTrace:
            at SingletonRepro.Environment.Initialize() in e:\dev\spike\SingletonRepro\SingletonRepro\Program.cs:line 68
            at SingletonRepro.Program.<Main>b__0(IActivatingEventArgs`1 e) in e:\dev\spike\SingletonRepro\SingletonRepro\Program.cs:line 17
            at Autofac.Builder.RegistrationBuilder`3.<>c__DisplayClass6.<OnActivating>b__5(Object s, ActivatingEventArgs`1 e)
            at Autofac.Core.Registration.ComponentRegistration.RaiseActivating(IComponentContext context, IEnumerable`1 parameters, Object& instance)
            at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters)
            at Autofac.Core.Resolving.InstanceLookup.<Execute>b__0()
            at Autofac.Core.Lifetime.LifetimeScope.GetOrCreateAndShare(Guid id, Func`1 creator)
            at Autofac.Core.Resolving.InstanceLookup.Execute()
            at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters)
            at Autofac.Core.Resolving.InstanceLookup.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
            at Autofac.Core.Activators.Reflection.AutowiringParameter.<>c__DisplayClass2.<CanSupplyValue>b__0()
            at Autofac.Core.Activators.Reflection.ConstructorParameterBinding.Instantiate()
            at Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters)
            at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters)
            at Autofac.Core.Resolving.InstanceLookup.<Execute>b__0()
            at Autofac.Core.Lifetime.LifetimeScope.GetOrCreateAndShare(Guid id, Func`1 creator)
            at Autofac.Core.Resolving.InstanceLookup.Execute()
            at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters)
            at Autofac.Core.Resolving.ResolveOperation.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
            at Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration registration, IEnumerable`1 parameters)
       InnerException: ...

In a comment on the question you correctly noted that Autofac does support circular dependencies . That's true, but it's true in context of a single resolution cycle . The problem here is that the single resolution chain has been broken up by adding service location in the middle, specifically in the SampleComponent.Initialize method.

No matter how you stack it - whether the problem is that you're somehow getting two singletons or you're getting this exception - it comes down to needing to break that circular dependency.

If you absolutely must use service location, one way to break the dependency is to use the Lazy<T> relationship . What this does is provide you with a delayed resolution for a component. In your SampleComponent.Initialize method, change the service location method to look like this:

var sim = ServiceLocator.GlobalScope.Resolve<Lazy<ISimulator>>();

If you create a repository that needs the ISimulator try changing the constructor of that repository to take a Lazy<ISimulator> and only call Lazy<ISimulator>.Value at the very last possible moment. That will delay the resolution operation of the Environment long enough to let the whole chain complete correctly the first time and get you out of that circular resolution problem.

A better option would be to refactor to use DI all the way down. Right now you're sort of mixing dependency injection, service location, and manual instance construction through the code. Environment manually creates a SampleComponent ; SampleComponent uses service location to get an ISimulator ; ISimulator uses DI to get an IEnvironment . Mixing and matching like this is going to lead you into all sorts of trouble like you're seeing now.

In fact, using DI all the way down means you don't actually need to implement the singleton pattern anywhere - instead just use the constructor and register things SingleInstance as needed.

Here is an updated version of your code (in console app form) that shows some ideas of what might be done. Obviously your real code is probably more complex so I can't literally show you every possible solution to every edge case, but this is one way to break the chain . You can leverage ideas from here and from the other available implicit relationship types to figure out ways around the challenge.

using System;
using Autofac;
using Autofac.Features.Indexed;

namespace SingletonRepro
{
  class Program
  {
    static void Main()
    {
      var builder = new ContainerBuilder();

      // You can still keep the Initialize call if you want.
      builder.RegisterType<Environment>().As<IEnvironment>().SingleInstance().OnActivated(args => args.Instance.Initialize());

      // Everything's in DI now, not just some things.
      builder.RegisterType<Simulator>().As<ISimulator>().SingleInstance();
      builder.RegisterType<IndependentClass>();

      // Using keyed services to choose the repository rather than newing things up.
      builder.RegisterType<RealRepository>().Keyed<IRepository>(SampleServiceType.RealThing);
      builder.RegisterType<SimulatedRepository>().Keyed<IRepository>(SampleServiceType.SimulatedThing);
      builder.RegisterType<SampleComponent>().WithParameter("serviceType", SampleServiceType.SimulatedThing);

      var context = builder.Build();
      using (var scope = context.BeginLifetimeScope())
      {
        // Using Lazy<T> in the IndependentClass to defer the need for
        // IEnvironment right away - breaks the dependency circle.
        var inst = scope.Resolve<IndependentClass>();
        inst.DoWork();
        Console.WriteLine("Instance: {0}", inst);
      }
    }
  }

  public interface IEnvironment
  {
    bool IsInitialized { get; }
  }

  public class Environment : IEnvironment
  {
    public SampleComponent _component;

    public Environment(SampleComponent component)
    {
      this._component = component;
    }

    public void Initialize()
    {
      this._component.DoSomethingWithRepo();
      this.IsInitialized = true;
    }

    public bool IsInitialized { get; private set; }
  }

  public interface ISimulator
  {
  }

  public class Simulator : ISimulator
  {
    public Simulator(IEnvironment environment)
    {
      this.Environment = environment;
    }
    public IEnvironment Environment { get; private set; }
  }

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

  public class SampleComponent
  {
    private IIndex<SampleServiceType, IRepository> _repositories;

    private SampleServiceType _serviceType;

    // Use indexed/keyed services to pick the right one from a dictionary
    // rather than newing up the repository (or whatever) manually.
    public SampleComponent(IIndex<SampleServiceType, IRepository> repositories, SampleServiceType serviceType)
    {
      this._repositories = repositories;
      this._serviceType = serviceType;
    }

    public void DoSomethingWithRepo()
    {
      // You could always take the service type parameter in this function
      // rather than as a constructor param.
      var repo = this._repositories[this._serviceType];
      repo.DoWork();
    }
  }

  public interface IRepository
  {
    void DoWork();
  }

  public class SimulatedRepository : IRepository
  {
    private ISimulator _simulator;

    public SimulatedRepository(ISimulator simulator)
    {
      this._simulator = simulator;
    }

    public void DoWork()
    {
    }
  }

  public class RealRepository : IRepository
  {
    public void DoWork()
    {
    }
  }

  public class IndependentClass
  {
    private Lazy<IEnvironment> _env;

    // Delaying the need for the IEnvironment in the constructor
    // can help break the circular dependency chain, as well as not
    // immediately checking that it's initialized. (Can you just
    // TRUST that it's initialized and call it good?)
    public IndependentClass(Lazy<IEnvironment> env)
    {
      this._env = env;
    }

    public void DoWork()
    {
      if (!this._env.Value.IsInitialized)
        throw new InvalidOperationException();
    }
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM