Search code examples
autofac

Singleton per string ctor parameter value, for both class A and dependent class B instances


I need to create a single instance, not based on the type or a lifetime scope decl, but on the value of a string parameter to the ctor. I also need this same effect for a dependent instance of another class, using the same string in its own unrelated ctor.

class A {
  public A(string appId, B b) {}
}
class B {
  public B(string appId) {}
}

In the above example, I need to create an A singleton and a B singleton, for unique values of appId.

I can resolve A and B with a TypedParameter, but the singleton-per-appId-value part I can't figure out. I tried just A alone to simplify (without dependent B involved). I looked at Keyed, Indexed, etc in the docs but none seemed to fit singleton-per-some-user-defined-key-value, unless I write my own Register lambda that uses my own memory cache of unique appId keys.

Does Autofac have a built-in terse way to enforce this?


Solution

  • There is not going to be a good solution to this because dependency injection is generally based on types, not parameter values. It's not the same as, say, setting up caching for a web app based on parameter values. More specifically, Autofac does not have anything "baked in" that will help you. (Nor, to my knowledge, does any other DI framework.)

    Likely what you're going to need to do is create a tiny factory that does the caching for you and use that.

    Here's an example that I'm not compiling and not testing but is coming off the top of my head to get you unblocked.

    First, you'd need to create a little factory for your B class.

    public class BFactory
    {
      private ConcurrentDictionary<string, B> _cache = new ConcurrentDictionary<string, B>();
    
      public B GetB(string appId)
      {
        return this._cache.GetOrAdd(
          appId,
          a => new B(a));
      }
    }
    

    Now you'll register BFactory as singleton, which will get you the one-instance-per-app-ID behavior you want. Register B as a lambda and it can use parameters.

    builder.RegisterType<BFactory>().SingleInstance();
    builder.Register((c, p) =>
      {
        var appId = p.Named<string>("appId");
        var factory = c.Resolve<BFactory>();
        return factory.GetB(appId);
      });
    

    Now you can resolve a B as long as there's a parameter passed to Resolve, like

    using var scope = container.BeginLifetimeScope();
    var b = scope.Resolve<B>(new NamedParameter("appId", "my-app-id"));
    

    You can build something similar for A, but the AFactory can take a BFactory as a parameter so it can get the right instance of A.

    public class AFactory
    {
      private ConcurrentDictionary<string, B> _cache = new ConcurrentDictionary<string, B>();
    
      private readonly BFactory _factory;
      
      public AFactory(BFactory factory)
      {
        this._factory = factory;
      }
    
      public A GetA(string appId)
      {
        return this._cache.GetOrAdd(
          appId,
          a => new A(a, this._factory.GetB(a)));
      }
    }
    

    Same thing here, register the factory as a singleton and get A from the factory.

    builder.RegisterType<AFactory>().SingleInstance();
    builder.Register((c, p) =>
      {
        var appId = p.Named<string>("appId");
        var factory = c.Resolve<AFactory>();
        return factory.GetA(appId);
      });
    

    You can get fancy with it, too, like using the Func relationships if there are some things that need to come from the container. For example, let's say your B class really looks like this:

    public class B
    {
      public B(string appId, IComponent fromContainer) { /* ... */ }
    }
    

    In there, maybe IComponent needs to come from the container but the appId comes from a parameter. You could make the BFactory be like this:

    public class BFactory
    {
      private ConcurrentDictionary<string, B> _cache = new ConcurrentDictionary<string, B>();
    
      private ILifetimeScope _scope;
    
      public BFactory(ILifetimeScope scope)
      {
        // This will be the CONTAINER / root scope if BFactory
        // is registered as a singleton!
        this._scope = scope;
      }
    
      public B GetB(string appId)
      {
        return this._cache.GetOrAdd(
          appId,
          a => {
            // Auto-generated factory! It will get IComponent from
            // the container but let you put the string in as a parameter.
            var func = this._scope.Resolve<Func<string, B>>();
            return func(a);
          });
      }
    }
    

    Be aware if you use that auto-generated factory thing (the Func<string, B> thing) that you will need to register B by itself, like:

    // You can't register the factory as the provider for B
    // because it's cyclical - the BFactory will want to resolve
    // a Func<string, B> which, in turn, will want to execute the
    // BFactory.
    builder.RegisterType<B>();
    builder.RegisterType<BFactory>().SingleInstance();
    

    That means you'd have to switch your code around to take a BFactory instead of a B. You can probably monkey with it to make it work, but you get the idea - you're going to have to make the caching mechanism yourself and that's what you'll hook into Autofac. Hopefully the above snippets can give you some ideas you can expand on and get you unblocked.