8

Tengo varios servicios, cada uno de los cuales tiene un UnitOfWork inyectado en el constructor utilizando el contenedor Simple Injector IoC.Inyector simple: Inyectar la misma instancia de UnitOfWork en los servicios del mismo gráfico

Actualmente puedo ver que cada instancia de UnitOfWork es un objeto separado, esto es malo ya que estoy usando Entity Framework y requiero la misma referencia de contexto para todas las unidades de trabajo.

¿Cómo puedo garantizar que la misma instancia de UnitOfWork se inyecte en todos los servicios por cada solicitud de resolución? Mi UnitOfWor será guardado por un decorador de controlador de comando externo después de que el comando finalice.

Tenga en cuenta que esta es una biblioteca común y se utilizará tanto para MVC como para Windows Forms, sería bueno tener una solución genérica para ambas plataformas, si es posible.

Código es el siguiente:

// snippet of code that registers types 
void RegisterTypes() 
{ 
    // register general unit of work class for use by majority of service layers 
    container.Register<IUnitOfWork, UnitOfWork>(); 

    // provide a factory for singleton classes to create their own units of work 
    // at will 
    container.RegisterSingle<IUnitOfWorkFactory, UnitOfWorkFactory>(); 

    // register logger 
    container.RegisterSingle<ILogger, NLogForUnitOfWork>(); 

    // register all generic command handlers 
    container.RegisterManyForOpenGeneric(typeof(ICommandHandler<>), 
     AppDomain.CurrentDomain.GetAssemblies()); 

    container.RegisterDecorator(typeof(ICommandHandler<>), 
     typeof(TransactionCommandHandlerDecorator<>)); 

    // register services that will be used by command handlers 
    container.Register<ISynchronisationService, SynchronisationService>(); 
    container.Register<IPluginManagerService, PluginManagerService>(); 
} 

El resultado deseado de la línea de abajo es crear un objeto que tiene una instancia UnitOfWork compartida por todo el gráfico de objetos construidos:

var handler = Resolve<ICommandHandler<SyncExternalDataCommand>>(); 

Aquí están mis servicios :

public class PluginManagerService : IPluginSettingsService 
{ 
    public PluginManagerService(IUnitOfWork unitOfWork) 
    { 
     this.unitOfWork = unitOfWork; 
    } 

    private readonly unitOfWork; 

    void IPluginSettingsService.RegisterPlugins() 
    { 
     // manipulate the unit of work 
    } 
} 

public class SynchronisationService : ISynchronisationService 
{ 
    public PluginManagerService(IUnitOfWork unitOfWork) 
    { 
     this.unitOfWork = unitOfWork; 
    } 

    private readonly unitOfWork; 

    void ISynchronisationService.SyncData() 
    { 
     // manipulate the unit of work 
    } 
} 

public class SyncExternalDataCommandHandler 
    : ICommandHandler<SyncExternalDataCommand> 
{ 
    ILogger logger; 
    ISynchronisationService synchronisationService; 
    IPluginManagerService pluginManagerService; 

    public SyncExternalDataCommandHandler(
     ISynchronisationService synchronisationService, 
     IPluginManagerService pluginManagerService, 
     ILogger logger) 
    { 
     this.synchronisationService = synchronisationService; 
     this.pluginManagerService = pluginManagerService; 
     this.logger = logger; 
    } 

    public void Handle(SyncExternalDataCommand command) 
    { 
     // here i will call both services functions, however as of now each 
     // has a different UnitOfWork reference internally, we need them to 
     // be common. 
     this.synchronisationService.SyncData(); 
     this.pluginManagerService.RegisterPlugins(); 
    } 
} 

Respuesta

20

Qué registro necesita depende del tipo de aplicación ion. Ya que está hablando de dos marcos diferentes (MVC y WinForms), ambos tendrán un registro diferente. Para aplicaciones MVC (o aplicaciones web en general), lo más común es registrar la unidad de trabajo en un per web request basis. Por ejemplo, el siguiente registro se almacena en caché la unidad de trabajo durante una única solicitud Web:

container.Register<IUnitOfWork>(() => 
{ 
    var items = HttpContext.Current.Items; 

    var uow = (IUnitOfWork)items["UnitOfWork"]; 

    if (uow == null) 
    { 
     items["UnitOfWork"] = uow = container.GetInstance<UnitOfWork>(); 
    } 

    return uow; 
}); 

La desventaja de este registro es que la unidad de trabajo no está dispuesto (si es necesario). Hay an extension package para el inyector simple que agrega RegisterPerWebRequest métodos de extensión al contenedor, lo que asegurará automáticamente que la instancia se elimine al final de la solicitud web. El uso de este paquete, usted será capaz de hacer lo siguiente inscripción:

container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork>(); 

¿Qué es un acceso directo a:

container.Register<IUnitOfWork, UnitOfWork>(new WebRequestLifestyle()); 

una aplicación Windows Forms, por otra parte, suele ser de un solo subproceso (una sola el usuario usará esa aplicación). Creo que no es inusual tener una sola unidad de trabajo por formulario, que se elimina cuando el formulario se cierra, pero con el uso del patrón comando/controlador, creo que es mejor adoptar un enfoque más orientado al servicio. Lo que quiero decir con esto es que sería bueno diseñarlo de tal manera que pueda mover la capa empresarial a un servicio WCF, sin la necesidad de hacer cambios en la capa de presentación. Puede lograr esto dejando que sus comandos solo contengan primitivas y (otro) DTO s. Por lo tanto, no almacene las entidades de Entity Framework en sus comandos, ya que esto hará que la serialización del comando sea mucho más difícil y dará lugar a sorpresas más adelante.

Al hacer esto, sería conveniente crear una nueva unidad de trabajo antes de que el controlador comience a ejecutar, reutilizar esa misma unidad de trabajo durante la ejecución de ese controlador, y confirmarla cuando el manejador se haya completado correctamente (y siempre deséchelo). Este es un escenario típico para el Per Lifetime Scope lifestyle. Hay an extension package que agrega RegisterLifetimeScope métodos de extensión al contenedor. El uso de este paquete, usted será capaz de hacer lo siguiente inscripción:

container.RegisterLifetimeScope<IUnitOfWork, UnitOfWork>(); 

¿Qué es un acceso directo a:

container.Register<IUnitOfWork, UnitOfWork>(new LifetimeScopeLifestyle()); 

Sin embargo, el registro, es sólo la mitad de la historia. La segunda parte es decidir cuándo guardar los cambios de la unidad de trabajo, y en el caso del uso del estilo de vida de Lifetime Scope, dónde comenzar y finalizar dicho alcance. Dado que debe iniciar explícitamente un alcance de por vida antes de que se ejecute el comando, y finalizarlo cuando el comando termine de ejecutarse, la mejor manera de hacerlo es mediante el uso de un decodificador de controlador de comando, que puede ajustar sus controladores de comando. Por lo tanto, para la aplicación de formularios, normalmente registraría un decorador de controlador de comando adicional que administra el ámbito de duración. Este enfoque no funciona en este caso. Echar un vistazo a la siguiente decorador, pero tenga en cuenta que no es correcto :

private class LifetimeScopeCommandHandlerDecorator<T> 
    : ICommandHandler<T> 
{ 
    private readonly Container container; 
    private readonly ICommandHandler<T> decoratedHandler; 

    public LifetimeScopeCommandHandlerDecorator(...) { ... } 

    public void Handle(T command) 
    { 
     using (this.container.BeginLifetimeScope()) 
     { 
      // WRONG!!! 
      this.decoratedHandler.Handle(command); 
     } 
    } 
} 

Este enfoque no funciona , porque se crea el controlador de comandos decorada antes se inicia el alcance toda la vida.

Podríamos estar tentados a tratar de resolver este problema de la siguiente manera, pero eso no es correcto, ya sea:

using (this.container.BeginLifetimeScope()) 
{ 
    // EVEN MORE WRONG!!! 
    var handler = this.container.GetInstance<ICommandHandler<T>>(); 

    handler.Handle(command); 
} 

Aunque solicitando una ICommandHandler<T> dentro del contexto de un ámbito de la vida, en efecto inyectar un IUnitOfWork de ese alcance, el contenedor devolverá un controlador que está (nuevamente) decorado con un LifetimeScopeCommandHandlerDecorator<T>. Llamar al handler.Handle(command) dará como resultado una llamada recursiva y terminaremos con una excepción de desbordamiento de pila.

El problema es que el gráfico de dependencia ya está creado antes de que podamos iniciar el ámbito de duración. Por lo tanto, tenemos que romper el gráfico de dependencia difiriendo la creación del resto del gráfico. La mejor manera de hacer esto que le permite mantener limpio el diseño de su aplicación] es cambiando el decorador a un proxy e inyectando una fábrica que creará el tipo que se suponía que debía envolver. Tal LifetimeScopeCommandHandlerProxy<T> se verá así:

// This class will be part of the Composition Root of 
// the Windows Forms application 
private class LifetimeScopeCommandHandlerProxy<T> : ICommandHandler<T> 
{ 
    // Since this type is part of the composition root, 
    // we are allowed to inject the container into it. 
    private Container container; 
    private Func<ICommandHandler<T>> factory; 

    public LifetimeScopeCommandHandlerProxy(Container container, 
     Func<ICommandHandler<T>> factory) 
    { 
     this.factory = factory; 
     this.container = container; 
    } 

    public void Handle(T command) 
    { 
     using (this.container.BeginLifetimeScope()) 
     { 
      var handler = this.factory(); 

      handler.Handle(command);   
     } 
    } 
} 

Mediante la inyección de un delegado, podemos retrasar la vez que se crea la instancia y al hacer esto nos retrasar la construcción de (el resto de) la gráfica de la dependencia. El truco ahora es registrar esta clase de proxy de forma que inyecte las instancias empaquetadas, en lugar de (por supuesto) inyectarse de nuevo. Simple Injector admite la inyección de fábricas Func<T> en decoradores, por lo que puede usar simplemente el RegisterDecorator y en este caso incluso el método de extensión RegisterSingleDecorator.

Tenga en cuenta que el orden en el que los decoradores (y este proxy) están registrados (obviamente) es importante. Como este proxy inicia un nuevo alcance de por vida, debe envolver al decorador que compromete la unidad de trabajo.En otras palabras, un registro más completo sería el siguiente:

container.RegisterLifetimeScope<IUnitOfWork, UnitOfWork>(); 

container.RegisterManyForOpenGeneric(
    typeof(ICommandHandler<>), 
    AppDomain.CurrentDomain.GetAssemblies()); 

// Register a decorator that handles saving the unit of 
// work after a handler has executed successfully. 
// This decorator will wrap all command handlers. 
container.RegisterDecorator(
    typeof(ICommandHandler<>), 
    typeof(TransactionCommandHandlerDecorator<>)); 

// Register the proxy that starts a lifetime scope. 
// This proxy will wrap the transaction decorators. 
container.RegisterSingleDecorator(
    typeof(ICommandHandler<>), 
    typeof(LifetimeScopeCommandHandlerProxy<>)); 

Registro del proxy y decorador al revés que significaría que el TransactionCommandHandlerDecorator<T> dependería de una diferente IUnitOfWork que el resto de la gráfica de dependencia hace, lo cual significaría que no se comprometerán todos los cambios realizados en la unidad de trabajo en ese gráfico. En otras palabras, su aplicación dejará de funcionar. Por lo tanto, siempre revise este registro cuidadosamente.

Buena suerte.

Cuestiones relacionadas