Adam
Adam

Reputation: 2570

Why does Autofac's implicit relationship type for dynamic instantiation (Func<B>) respect lifetime scopes?

Why does Autofac's implicit relationship type for dynamic instantiation respect lifetime scopes?


The docs on the subject states:

Lifetime scopes are respected using this relationship type. If you register an object as InstancePerDependency() and call the Func<B> multiple times, you’ll get a new instance each time. However, if you register an object as SingleInstance() and call the Func<B> to resolve the object more than once, you will get the same object instance every time.

The docs also claim the purpose of this implicit relationship type is for these two purposes:

  1. resolving instances at runtime without taking a dependency on Autofac itself
  2. resolving multiple instances of a given service

Delayed instantiation (Lazy<B>) accomplishes purpose #1 in a manner which I understand and I agree with its design to respect lifetime scopes. I don't understand or agree with Autofac's design for Func<B> as there is no way to accomplish purpose #2 with the dynamic instantiation implicit relationship type.

To work around this and actually accomplish #2, we manually register Func<B> using the same factory method as we use to register B and a closure:

void RegisterDependencies(ContainerBuilder builder)
{
    builder.Register<B>(bFactory).InstancePerLifetimeScope();
    builder.Register<Func<B>>(context => () => bFactory(context)).SingleInstance();

    B bFactory(IComponentContext context) => new B(context.Resolve<A>(), ...);
}

This works to accomplish both purposes that dynamic instantiation claims to accomplish but requires us to be very verbose with many of our registrations.


Can someone shed some light on to why Autofac designed its implicit relationship type for dynamic instantiation to respect lifetime scopes?

Thank you in advance :)

Upvotes: 3

Views: 156

Answers (1)

Travis Illig
Travis Illig

Reputation: 23924

Knowing that Func<B> is basically a "shortcut" for myCurrentLifetimeScope.Resolve<B> wrapped in a non-Autofac-specific object, it might help to turn this question around and ask why Func<B> wouldn't respect lifetime scopes, and what consequences it would have if it didn't.

The vast majority of applications that make use of lifetime scopes do so to isolate units of work. For example, web applications that have per-request lifetime scopes. There are lots of great reasons why this is an interesting pattern, like being able to only create resources that are required for a given request and then clean them up with the request is finished - use only the memory you need.

Given that, let's say that, in general, lifetime scopes are interesting to have, if for anything but a nice way to track and clean up allocated resources.

Now let's say you have a connection to, like, a web service or something. Maybe it's a WCF service. There are some things you might remember about WCF service clients:

  • Once the channel is faulted, the client is worthless. You can't just create one client because if the service ever faults the channel on you, you have to throw that client instance away and create a new one.
  • You need to dispose of the clients when you're done. They can keep channels open and eat up resources if you don't do that.

Given that, Func<IServiceClient> sorts of relationships become very interesting.

Let's say you have an MVC controller. This is kinda pseudocode, I'm not going to run it through a compiler.

public class MyController : Controller
{
  private readonly Func<IServiceClient> clientFactory;
  public MyController(Func<IServiceClient> clientFactory)
  {
    this._clientFactory = clientFactory;
  }

  public string GetSomethingReallyCool()
  {
    var retryCount = 0;
    while(retryCount < 5)
    {
      var client = this._clientFactory();
      try
      {
        return client.GetSomethingCool();
      }
      catch
      {
        retryCount++;
      }
    }

    return "We failed to get the cool data.";
  }
}

We have sort of a poor-man's retry built in here because the service is flaky. I'm not recommending this exact code, it's just quick and easy to demonstrate the concept.

The stage is set! Now, for the sake of discussion, let's pretend that the relationships didn't respect lifetime scopes, just for a moment.

What happens during a web request?

  • Your MVC controller gets resolved from the per-request lifetime scope.
  • The controller takes a Func<IServiceClient> function to create - dynamically - a WCF service client for when it's needed.
  • The controller action creates a service client and opens a channel. This service client lives in the root container which means it'll never be disposed until your whole app shuts down.
  • The service faults. The channel's faulted, not closed. It'll just kinda hang out... but you can't use the same service client instance. That one's toast.
  • The controller action creates another service client and opens a channel. This service client also lives in the root container and will be allocated forever.
  • The service call succeeds and the value is returned. But while the clients are going out of scope, they're disposable - the container is handling disposal, right? So the locals are out of scope but they're still allocated.

This is a pretty horrible memory leak.

Turn the normal behavior back on! We're done pretending!

If it obeys lifetime scopes, it works like this:

  • Your MVC controller gets resolved from the per-request lifetime scope.
  • The controller takes a Func<IServiceClient> function to create - dynamically - a WCF service client for when it's needed.
  • The controller action creates a service client and opens a channel. This service client lives in the request lifetime scope which means it'll be disposed when the request is over.
  • The service faults. The channel's faulted, not closed. It'll just kinda hang out... but you can't use the same service client instance. That one's toast.
  • The controller action creates another service client and opens a channel. This service client also lives in the request scope and will be disposed at the end of the request.
  • The service call succeeds and the value is returned.
  • The request lifetime scope ends and disposes the clients - closing the channels, deallocating resources.

No memory leak, nice cleanup.

Another scenario - Let's say you register things when lifetime scopes are created.

var builder = new ContainerBuilder();
builder.RegisterType<Alpha>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope(b => b.RegisterType<Beta>())
{
  var f = scope.Resolve<Func<Beta>>();

  // f is a Func<Beta> - call it, and you should get a B.
  // If Func<Beta> doesn't respect lifetime scopes, what
  // does this yield?
  var beta = f();
}

This is actually a common pattern for things like WebAPI integration - a request message comes in, and when the request lifetime scope is created the request message is dynamically registered into the request scope. It doesn't exist at the global level.

You can probably see from some of this that the only logical way for Func<B> to behave is to obey lifetime scopes. It just wouldn't work if it didn't.

Upvotes: 4

Related Questions