mercredi 11 décembre 2019

Write an abstract factory implementation by using the ASP.NET core DI container: how to manage object lifetime?

We are migrating a huge code base from .NET framework to .NET core. Unfortunately some of the code we are migrating suffers of design smells, but we can't freely break things during this phase and we need carefully plan changes over time.

One of the main problems is that our code relies on a great number of abstract factories which are known to be a code smell (at least when abused). One related issue is that the Castle Windsor container is widely used in the existing code and we would like to avoid using it inside ASP.NET core, where we prefer sticking to the default built in DI container.

I'm trying to understand whether it is possible to rewrite some of our abstract factories implementation, currently based on the castle windsor container, by using the ASP.NET core DI container. Based on my understanding of the dependency injection, writing these kind of classes in the application's composition root is not an issue; put another way having a class of the composition root which depends on the DI container is not a way to introduce the service locator code smell.

Currently our interface definition of our abstract factory is a leaky one, because it exposes a Release method which is there only to adhere to the Castle Windsor's register resolve release pattern. This is the current interface definition:

public interface ICommandHandlerFactory
{
  ICommandHandler CreateHandler(Type commandType);
  void Release(ICommandHandler handler);
}

The concrete implementation depends on the Castle Windsor container in order to resolve the command handlers and release them when the user is done:

public class WindsorCommandHandlerFactory
{
   private readonly IKernel _container;

   public WindsorCommandHandlerFactory(IKernel container)
   {
      _container = container;
   }

   public ICommandHandler CreateHandler(Type commandType)
   {
     // here we create the command handler type from the command type and then
     // we ask the container to resolve the command handler type
   }

   public void Release(ICommandHandler handler)
   {
     _container.ReleaseComponent(handler);
   }
}

My question is related with the object lifetime management. The point is that with the ASP.NET core DI container the lifetime management is not based on the Release method, instead it is based on the concept of scope.

The best practice is basically creating a scope from the application root container, resolve dependencies from the scope's container, use the dependencies and finally disposing of the scope. When the scope is disposed the scoped and transient services resolved inside the scope are decommissioned and memory leaks are avoided.

This is the corresponding code:

public class Worker 
{
  private readonly IServiceProvider container;

  public Worker(IServiceProvider container)
  {
    _container = container;
  }

  public void DoStuff()
  {
    using(var scope = container.CreateScope())
    {
      var service = scope.ServiceProvider.GetRequiredService<IService>();
      service.Work();
    }
  }
}

In order to adhere to this design I should clearly simplify the abstract factory definition, by removing the leaky (and now no more useful) Release method:

public interface ICommandHandlerFactory
{
  ICommandHandler CreateHandler(Type commandType);
}

Given this interface definition, how can I cope with the creation and disposal of the container scopes ?

I can't simply do the following thing, because when the scope is disposed the resolved dependencies are decommissioned and so the calling code can potentially have a reference to a disposed object:

// this WON'T work due to the scope disposal when the service is returned
public class CommandHandlerFactory 
    {
      private readonly IServiceProvider container;

      public CommandHandlerFactory(IServiceProvider container)
      {
        _container = container;
      }

      public ICommandHandler CreateHandler(Type commandType)
      {
         using(var scope = container.CreateScope())
        {
          Type commandHandlerType = ... // build the command handler type starting from the command type

          var service = scope.ServiceProvider.GetRequiredService(commandHandlerType);
          return service;
        }
      }
    }

Do you have any idea ?

Aucun commentaire:

Enregistrer un commentaire