Skip to content

Discussion: ContainerBuilder.Update Marked Obsolete #811

Description

@tillig

In 8a89e94 the ContainerBuilder.Update methods have been marked obsolete.

The plan is to leave them obsolete for a year or more, weaning developers off the use of Update, and in a future major release remove the methods altogether.

This issue is to learn from developers why they believe they need Update and see if there are better ways to handle the situations. If it turns out there is a totally unavoidable situation where Update is literally the only way to handle it, we need to determine how best to fix Update.

If You Want to Keep ContainerBuilder.Update...

If you believe you need ContainerBuilder.Update please provide the following information with your comment - "me too" or "I use it" isn't enough.

  • Which overload do you use? Update(IContainer), Update(IContainer, ContainerBuildOptions), or Update(IComponentRegistry)?
  • What is the scenario (use case) in which you are unable to pass the ContainerBuilder prior to container building?
  • When you use Update, have you already resolved something from the container before calling Update?

We actually need real information to understand the use cases and see if there are different ways your code could be doing things to work around needing Update. If it turns out we're missing a feature, hopefully we can figure out what that is and get you what you need while at the same time removing the need to update the container post-facto.

Why ContainerBuilder.Update Was Marked Obsolete

Here are some of the reasons why the use of ContainerBuilder.Update is generally bad practice and why we're looking at this.

Container Contents Become Inconsistent

Once you resolve something out of a built container, a lot of things get put in motion.

  • If it's a singleton, its dependency chain gets resolved and the singleton gets cached.
  • Anything marked AutoActivate or IStartable gets resolved.
  • Disposable items may be cached and/or tracked.

If you change the contents of the container, there's a chance that the change will actually affect these things, rendering cached or tracked items as inconsistent with the current contents of the container.

In unit test form (with a little pseudocode)...

[Fact]
public void InconsistentContainer()
{
  var builder = new ContainerBuilder();

  // In this example, the "HandlerManager" takes a list of message handlers
  // and does some work with them.
  builder.RegisterType<HandlerManager>()
         .As<IHandlerManager>()
         .SingleInstance();

  // Register a couple of handlers that the manager will use.
  builder.RegisterType<FirstHandler>().As<IHandler>();
  builder.RegisterType<SecondHandler>().As<IHandler>();

  using (var container = builder.Build())
  {
    // The manager is resolved, which resolves all of the currently registered
    // handlers, and is cached. This manager instance will have two handlers
    // in it.
    var manager = container.Resolve<IHandlerManager>();

    // Update the container with a new handler.
    var updater = new ContainerBuilder();
    updater.RegisterType<ThirdHandler>().As<IHandler>();
    updater.Update(container);

    // The manager still only has two handlers... which is inconsistent with
    // the set of handlers actually registered in the container.
    manager = container.Resolve<IHandlerManager>();
  }
}

Update Isn't Consistent With Build

When you Build a container, a couple of things happen:

  • A set of default registration sources is added to the container. This is how things like IEnumerable<T> and Func<T> are resolved. These should only be added to the container one time.
  • Startable components (AutoActivate and IStartable) are automatically resolved.

When you Update a container, these things don't happen. We intentionally don't want the base registration sources duplicated; and unless you manually specify it we don't run startable components that may have been added during an update. Even if you specify it, your existing startable components aren't re-run; only the newly added ones would be run.

Why not fix it? There's not a clear "right way" to do that due to the container contents being inconsistent (see above). Most startable components are singletons where the point of starting them is to initialize a cache or execute some other startup logic proactively instead of lazily. If we don't re-run startables, maybe they don't pick up the things that they need. If we do re-run startables, maybe that invalidates even more things... or maybe it doesn't have any effect (in the case of singletons).

Child lifetime scopes spawned from the container get a sort of "copy" of the set of registrations in the base container. Updating the container after a child lifetime scope is spawned doesn't automatically propagate the new registrations into the child scope (Issue #608).

Basically, Update really isn't the same as Build and using it may not be doing 100% of the things you think it's doing.

Diagnostics and Optimizations Difficult to Implement

We see a lot of StackOverflow questions, issues, tweets, etc. about some of the challenges folks have around diagnosing missing dependencies. We'd love to be able to provide some better diagnostics, but one of the challenges in that is with Update: If folks assume they can change the container contents later, we conversely can't assume we can do any sort of proactive analysis or diagnostics when a container is built.

Further, we could potentially implement optimizations that, for example, proactively cache component activation data based on the registered set of components... but doing that and making sure it's all flushed/regenerated on each update doesn't always turn out to be the easiest thing to do.

Why Not Just "Fix" Update?

The question that logically follows is... why not just "fix Update so it behaves correctly?"

Easier Said Than Done

If you think about what would actually have to happen to make Update work "correctly" it includes...

  • Flush all caches of singletons, disposing of tracked disposables, so singletons can be regenerated with the updated container contents.
  • Invalidate all child lifetime scopes currently spawned because the root container has changed. (Note that we currently don't track child lifetime scopes, so that would also be something we'd need to change.)
  • Re-run all startable components so they are created with the new container contents.

...and so on. Basically, rebuild the whole container. This can really mess with your app if have something that's holding onto a resolved item that gets disposed or becomes invalid.

Locking a Container Isn't Unprecedented

Looking at other containers out there, locking a container after it's built and ready to resolve isn't uncommon. Simple Injector, LightInject, and the Microsoft.Extensions.DependencyInjection containers all disallow updating the container post-facto.

For those that do - StructureMap, Ninject, and Windsor to name a few - it appears they actually do all the work mentioned in "easier said than done" - flushing caches, rebuilding the whole container. And, as mentioned, this can cause inconsistent behavior in the application if it isn't managed very, very carefully.

Possible Workarounds for Update

Instead of using ContainerBuilder.Update, you may be able to...

Pass Around ContainerBuilder Instead of IContainer

Instead of passing the container around and conditionally updating it, change your logic to pass around the ContainerBuilder and conditionally register things correctly the first time. With the new ContainerBuilder.Properties dictionary available, you can add some context and perform business logic during the initial building of the container if you need to rather than wait until afterwards.

Add Registrations to Child Scopes

Occasionally what you need is something available during a child lifetime scope for a specific task, unit of work, or request. You can add registrations to just that child scope using a lambda:

using (var scope = container.BeginLifetimeScope(b =>
  {
    b.RegisterType<NewRegistration>();
  })
{
  // The new registration is available in this scope.
}

You may even want to cache that child lifetime scope and reuse it - like a smaller sub-container with a special purpose. That's how the multitenant integration works - cached lifetime scopes per tenant.

Use Lambdas

If you're trying to change something that's registered based on an environment parameter or some other runtime value, register using a lambda rather than reflection:

builder.Register(ctx =>
{
  if (Environment.GetEnvironmentVariable("env") == "dev")
  {
    return new DevelopmentService();
  }
  else
  {
    return new ProductionService();
  }
}).As<IMyService>();

Use Configuration

Just like with web.config transforms, you may choose to switch deployed Autofac configuration files based on an environment. For example, you may have a development configuration and a production configuration.

Use Modules

If you have a lot of registrations that need to change based on runtime, you can encapsulate that in a module.

public class MyModule : Module
{
  private readonly bool _isProduction;

  public MyModule(bool isProduction)
  {
    this._isProduction = isProduction;
  }

  protected override void Load(ContainerBuilder builder)
  {
    if (this._isProduction)
    {
      builder.RegisterType<FirstProductionService>().As<IMyService>();
      builder.RegisterType<SecondProductionService>().As<IOtherService>();
    }
    else
    {
      builder.RegisterType<FirstDevelopmentService>().As<IMyService>();
      builder.RegisterType<SecondDevelopmentService>().As<IOtherService>();
    }
  }
}

Use Conditional Registrations

Autofac 4.4.0 introduced OnlyIf() and IfNotRegistered extensions. These allow you to execute a specific registration only if some other condition is true. Here's the documentation. Quick example:

// Only ServiceA will be registered.
// Note the IfNotRegistered takes the SERVICE TYPE to
// check for (the As<T>), NOT the COMPONENT TYPE
// (the RegisterType<T>).
builder.RegisterType<ServiceA>()
       .As<IService>();
builder.RegisterType<ServiceB>()
       .As<IService>()
       .IfNotRegistered(typeof(IService));

Handling Application Startup / Bootstrap Items

A common scenario for wanting to update the container is when an app tries to use a container to register plugins or perform app startup actions that generate additional registrations. Ideas for handling that include:

Consider Two Containers

If you are using DI during app startup and then also using it during the execution of the app, it may be that you need two containers: one for each stage in the app lifecycle.

The first is a container that has services used to index plugins (the "assembly scanning" mechanism, logging, that sort of thing); the second is a container into which runtime requirements are registered like the set of plugin assemblies, required common dependencies, and so on.

Don't Over-DI Bootstrap Items

It's good to use DI, but you can easily get into a chicken/egg situation where you try to resolve bootstrap items (like your application configuration system, logging that will run during application startup, and so on) out of the container... that you're trying to set up during app startup.

Don't do that.

If you look at many of the newer ASP.NET Core examples, you'll see a good pattern where app configuration, base logging, and other "bootstrap" elements are actually just directly instantiated or built. Those instances/factories can then later be registered with Autofac for use during runtime, but the initial construction proper isn't done out of Autofac.

Share Instances Across Containers

If you go with the two-container startup, you can always register the same instance of a thing (e.g., application configuration, logging factory, etc.) into two different containers. At that point it's effectively a singleton.

(You can also use Autofac modules to share registrations if you have bunches of them that need to be duplicated, though you'll get different instances of things so be aware.)

Lambdas, Lambdas, Lambdas

Many, many container updates could be worked around using a lambda registration. "I need to register XYZ based on the result of resolving ABC!" - do that with a lambda registration.

Nancy Framework Users

If you use Nancy, it internally uses Update(). There is already an issue filed for Nancy to be updated - you can follow that issue or chime in over there if you're interested in how that is progressing.

Prism (WPF) Framework Users

Prism only supports modules in mutable containers. This is an architectural choice of the Prism project owners. An issue was filed here to alert Prism of the changes around Update() and as part of a major IoC integration refactor the decision was made to only support modules for mutable containers. While it may be possible to enable modules for Autofac via one of the above strategies or something like registration sources, Prism is expecting the community to submit and support that code. Head over there if you'd like to follow up with them; there is no current plan to create an Autofac-project-supported Prism integration library.


Short Term Fix

If your code uses Update and you want to keep using it for the time being, you can disable the warning just around that call.

#pragma warning disable 612, 618
builder.Update(container);
#pragma warning restore 612, 618

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions