2012-06-14 21 views
8

Estoy usando Inyector simple para administrar la vida útil de mis dependencias inyectadas (en este caso UnitOfWork), y estoy muy contento de tener un decorador por separado en lugar de mi El servicio o controlador de comando que se ocupa de guardar y eliminar hace que el código sea mucho más fácil cuando se escriben capas de lógica de negocios (sigo la arquitectura que se describe en this blog post).Cómo configurar Inyector simple para ejecutar subprocesos de fondo en ASP.NET MVC

Lo anterior funciona perfectamente (y muy fácilmente) utilizando el paquete Simple Injector MVC NuGet y el siguiente código durante la construcción del contenedor raíz de composición, si existe más de una dependencia en el gráfico, la misma instancia se inyecta en todo perfecto para el contexto del modelo de Entity Framework.

private static void InitializeContainer(Container container) 
{ 
    container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork>(); 
    // register all other interfaces with: 
    // container.Register<Interface, Implementation>(); 
} 

ahora tengo que hacer unos hilos de fondo y entender de inyector simple documentation on threads que los comandos se pueden proxy de la siguiente manera:

public sealed class TransactionCommandHandlerDecorator<TCommand> 
    : ICommandHandler<TCommand> 
{ 
    private readonly ICommandHandler<TCommand> handlerToCall; 
    private readonly IUnitOfWork unitOfWork; 

    public TransactionCommandHandlerDecorator(
     IUnitOfWork unitOfWork, 
     ICommandHandler<TCommand> decorated) 
    { 
     this.handlerToCall = decorated; 
     this.unitOfWork = unitOfWork; 
    } 

    public void Handle(TCommand command) 
    { 
     this.handlerToCall.Handle(command); 
     unitOfWork.Save(); 
    } 
} 

ThreadedCommandHandlerProxy:

public class ThreadedCommandHandlerProxy<TCommand> 
    : ICommandHandler<TCommand> 
{ 
    Func<ICommandHandler<TCommand>> instanceCreator; 

    public ThreadedCommandHandlerProxy(
     Func<ICommandHandler<TCommand>> creator) 
    { 
     this.instanceCreator = creator; 
    } 

    public void Handle(TCommand command) 
    { 
     Task.Factory.StartNew(() => 
     { 
      var handler = this.instanceCreator(); 
      handler.Handle(command); 
     }); 
    } 
} 

Sin embargo, desde este enhebrado la documentación de la muestra que puedo ver fábricas se utilizan, si introduzco fábricas a mis comandos y la capa de servicio las cosas se confunden y no sistente como voy a tener diferentes métodos de ahorro para los distintos servicios (un contenedor se encarga de ahorro, otras fábricas instanciados dentro del mango servicios de guarda y desechar) - se puede ver cómo clara y sencilla el esqueleto código de servicio es sin ningún tipo de fábricas:

public class BusinessUnitCommandHandlers : 
    ICommandHandler<AddBusinessUnitCommand>, 
    ICommandHandler<DeleteBusinessUnitCommand> 
{ 
    private IBusinessUnitService businessUnitService; 
    private IInvoiceService invoiceService; 

    public BusinessUnitCommandHandlers(
     IBusinessUnitService businessUnitService, 
     IInvoiceService invoiceService) 
    { 
     this.businessUnitService = businessUnitService; 
     this.invoiceService = invoiceService; 
    } 

    public void Handle(AddBusinessUnitCommand command) 
    { 
     businessUnitService.AddCompany(command.name); 
    } 

    public void Handle(DeleteBusinessUnitCommand command) 
    { 
     invoiceService.DeleteAllInvoicesForCompany(command.ID); 
     businessUnitService.DeleteCompany(command.ID); 
    } 
} 

public class BusinessUnitService : IBusinessUnitService 
{ 
    private readonly IUnitOfWork unitOfWork; 
    private readonly ILogger logger; 

    public BusinessUnitService(IUnitOfWork unitOfWork, 
     ILogger logger) 
    { 
     this.unitOfWork = unitOfWork; 
     this.logger = logger; 
    } 

    void IBusinessUnitService.AddCompany(string name) 
    { 
     // snip... let container call IUnitOfWork.Save() 
    } 

    void IBusinessUnitService.DeleteCompany(int ID) 
    { 
     // snip... let container call IUnitOfWork.Save() 
    } 
} 

public class InvoiceService : IInvoiceService 
{ 
    private readonly IUnitOfWork unitOfWork; 
    private readonly ILogger logger; 

    public BusinessUnitService(IUnitOfWork unitOfWork, 
     ILogger logger) 
    { 
     this.unitOfWork = unitOfWork; 
     this.logger = logger; 
    } 

    void IInvoiceService.DeleteAllInvoicesForCompany(int ID) 
    { 
     // snip... let container call IUnitOfWork.Save() 
    } 
} 

con lo anterior mi problema comienza a formar, como yo lo entiendo desde el documentation on ASP .NET PerWebRequest lifetimes, se utiliza el siguiente código:

public T GetInstance() 
{ 
    var context = HttpContext.Current; 

    if (context == null) 
    { 
     // No HttpContext: Let's create a transient object. 
     return this.instanceCreator(); 
    } 

    object key = this.GetType(); 
    T instance = (T)context.Items[key]; 

    if (instance == null) 
    { 
     context.Items[key] = instance = this.instanceCreator(); 
    } 
    return instance; 
} 

lo anterior funciona bien para cada solicitud HTTP habrá una válida HttpContext.Current, sin embargo si spin un nuevo hilo con el ThreadedCommandHandlerProxy creará e un nuevo hilo y el HttpContext ya no existirá dentro de ese hilo. Dado que el HttpContext sería nulo en cada llamada posterior, todas las instancias de objetos inyectadas en los constructores de servicio serían nuevas y únicas, lo contrario al HTTP normal por solicitud web donde los objetos se comparten correctamente como la misma instancia en todos los servicios.

Para resumir lo anterior en preguntas:

¿Cómo hago para conseguir los objetos construidos y elementos comunes inyectados con independencia de que creado a partir de la solicitud HTTP o por medio de un nuevo hilo?

¿Hay alguna consideración especial para tener un UnitOfWork administrado por un subproceso dentro de un proxy de controlador de comando? ¿Cómo se puede garantizar que se guarde y elimine después de que se haya ejecutado el controlador?

Si tuviéramos un problema con el controlador de comandos/service-layer y no quisiéramos guardar el UnitOfWork, ¿simplemente lanzaríamos una excepción? En caso afirmativo, ¿es posible detectar esto a nivel global o necesitamos detectar la excepción por solicitud desde un try - catch en el decorador o proxy del gestor?

Gracias,

Chris

+1

@Steven gracias, he pasado por esa pregunta, pero no estoy 100% seguro de lo que debería hacer con RegisterPerWebRequest y RegisterLifetimeScope para mi caso de IUnitOfWork y UnitOfWork, en mi caso sería DbContext => UnitOfWorkBase, MyDbContext => UnitOfWork y IObjectContextAdapter => IUnitOfWork? Si pudiera aclarar sería apreciado. También se menciona que hay una serie de formas de hacerlo, ¿es una forma de hacer una extensión explícita de por vida para esto? Sería bueno si no tuviera que derivar de las clases base. – g18c

+0

Con el nuevo 'async' ActionResults para MVC5, este problema va a ser uno común. – Mrchief

+1

Si la unidad de trabajo es una tarea larga e importante, considere una "cola de trabajos" del bus de mensajes que puede retomar cuando su aplicación regrese de un reciclaje. FYI al leer sobre 'QueueBackgroundWorkItem' Vi que la tarea se matará si tarda más de * 90 segundos * por lo que estás en riesgo si tardas más de un minuto – KCD

Respuesta

7

Permítanme comenzar de al advertir que si se desea ejecutar comandos de forma asincrónica en una aplicación web, es posible que desee dar un paso atrás y mirar lo que está tratando conseguir. Siempre existe el riesgo de que la aplicación web se recicle justo después de que haya iniciado su controlador en una cadena de fondo. Cuando una aplicación ASP.NET se recicla, todos los hilos de fondo serán abortados. Quizás sería mejor publicar comandos en una cola (transaccional) y dejar que un servicio en segundo plano los recoja. Esto asegura que los comandos no puedan "perderse". Y también le permite volver a ejecutar comandos cuando el controlador no tiene éxito. También puede ahorrarte tener que hacer algunos registros desagradables (que probablemente no importa qué marco DI elijas), pero esto probablemente solo sea un problema secundario. Y si necesita ejecutar controladores como sincronización, al menos intente minimizar el número de controladores que ejecuta de forma sincronizada.

Con eso fuera del camino, lo que necesita es lo siguiente.

Como ha señalado, ya que está ejecutando (algunos) controladores de comandos de forma asincrónica, no puede utilizar el estilo de vida de solicitud por web en ellos. Necesitará una solución híbrida, que mezcle entre solicitud por web y 'algo más'. Ese algo más probablemente sea un per lifetime scope. No existen extensiones integradas para estas soluciones híbridas, debido a un par de razones. En primer lugar, es una característica bastante exótica que no muchas personas necesitan. En segundo lugar, puede mezclar dos o tres estilos de vida juntos, por lo que sería casi una combinación interminable de híbridos. Y por último, es (bastante) fácil registrar esto usted mismo.

En Simple Injector 2, se ha agregado la clase Lifestyle y contiene un método CreateHybrid que permite combinar dos estilos de vida para crear un nuevo estilo de vida. He aquí un ejemplo:

var hybridLifestyle = Lifestyle.CreateHybrid(
    () => HttpContext.Current != null, 
    new WebRequestLifestyle(), 
    new LifetimeScopeLifestyle()); 

Se puede usar esta forma de vida híbrida para registrar la unidad de trabajo:

container.Register<IUnitOfWork, DiUnitOfWork>(hybridLifestyle); 

Desde que está registrando la unidad de trabajo como de por vida Alcance, debe crear de forma explícita y disponer un alcance de por vida para un determinado hilo. Lo más simple es agregar esto a su ThreadedCommandHandlerProxy. Esta no es la forma más fácil de hacer las cosas, pero es la manera más fácil para mí de mostrarte cómo hacerlo.

Si hemos tenido un problema en el controlador de comandos-/ de capa de servicios y no desee guardar el UnitOfWork, sería simplemente una excepción ?

Lo típico que se debe hacer es lanzar una excepción. De hecho, es la regla general de excepciones:

Si su método no puede hacer lo que su nombre promete, puede tirar. ->

El controlador de comandos debe ser ignorante acerca de cómo el contexto en el que se ejecuta, y la última cosa que quiere es diferenciar en si debe lanzar una excepción. Entonces tirar es tu mejor opción. Sin embargo, cuando se ejecuta en un hilo de fondo, es mejor que capte esa excepción, ya que .NET matará todo el dominio de la aplicación, si no lo capta.En una aplicación web, esto significa un reciclaje de AppDomain, lo que significa que su aplicación web (o al menos ese servidor) estará fuera de línea durante un corto período de tiempo.

Por otro lado, tampoco desea perder ninguna información de excepción, por lo que debe registrar esa excepción, y probablemente desee registrar una representación serializada de ese comando con esa excepción, para que pueda ver qué datos eran . aprobada en Cuando se añade al método ThreadedCommandHandlerProxy.Handle, la que se vería así:

public void Handle(TCommand command) 
{ 
    string xml = this.commandSerializer.ToXml(command);  

    Task.Factory.StartNew(() => 
    { 
     var logger = 
      this.container.GetInstance<ILogger>(); 

     try 
     { 
      using (container.BeginTransactionScope()) 
      { 
       // must be created INSIDE the scope. 
       var handler = this.instanceCreator(); 
       handler.Handle(command); 
      } 
     } 
     catch (Exception ex) 
     { 
      // Don't let the exception bubble up, 
      // because we run in a background thread. 
      this.logger.Log(ex, xml); 
     } 
    }); 
} 

me advirtió acerca de cómo los controladores ejecutan de forma asíncrona podría no ser la mejor idea. Sin embargo, dado que está aplicando este patrón de comando/controlador, podrá pasar a utilizar la cola más adelante, sin tener que modificar una sola línea de código en su aplicación. Solo se trata de escribir algo así como QueueCommandHandlerDecorator<T> (que serializa el comando y lo envía a la cola) y cambia la forma en que están las cosas conectadas en la raíz de tu composición, y ya estás listo (y por supuesto no te olvides de implementar el servicio que ejecuta comandos desde la cola). En otras palabras, lo bueno de este diseño SOLIDO es que la implementación de estas características es constante al tamaño de la aplicación.

+0

Gracias Steven, tu comentario sobre tener un servicio en segundo plano ejecuta todo puede ser exactamente lo que estoy buscando sin ningún otro problema de inquietudes sobre hilos o registro - por 'servicio' solo para aclarar si pudiéramos hacer que QueueCommandHandlerDecorator escriba el trabajo en la base de datos (o un servicio web de WCF, o algún otro proceso interproceso) método de comunicación), y tener un proceso completamente separado (escriba un segundo programa como un servicio de Windows o servicio WCF) ejecutándose que selecciona estas solicitudes de trabajo y las procesa? Gracias por la gran visión del diseño y evitar riesgos. – g18c

+0

@ g18c: puede usar la cola que más le guste (base de datos, MSMQ), y puede escribir en su cola de la forma que desee (directamente desde MVC, o deje que un servicio WCF escriba en la cola), pero para seleccionando comandos de la cola y ejecutándolos, sería mejor usar un servicio de Windows. O al menos, no use un servicio web, ya que en ese caso enfrentará los mismos problemas con el reciclaje del dominio de la aplicación que ya tiene con su aplicación web. Los servicios web no están destinados a tratar el procesamiento en segundo plano. Son para manejar solicitudes. Buena suerte. – Steven

+0

Excelentes cosas, además de mi aplicación MVC existente, usaré mi biblioteca de clase de servicios/contratos comunes con un nuevo servicio de Windows que registra los comandos requeridos en un contenedor Simple Injector (con subproceso o alcance perlifetime), ya que el acceso a la base de datos es transaccional y manejado por el contenedor, puedo fácilmente poner en cola tareas en una tabla de trabajos para ejecutar sin preocuparme por las condiciones de carrera, ya que el servicio puede sondear esta tabla para nuevos registros.Esta metodología de diseño es muy poderosa ya que los servicios se pueden usar de forma instantánea independientemente de la aplicación MVC o del Servicio de Windows; muchas gracias lo apreciamos mucho. – g18c

5

Parte de la problems of running background threads in ASP.NET can be overcome con un cómodo decorador de controlador de comandos:

public class AspNetSafeBackgroundCommandHandlerDecorator<T> 
    : ICommandHandler<T>, IRegisteredObject 
{ 
    private readonly ICommandHandler<T> decorated; 

    private readonly object locker = new object(); 

    public AspNetSafeBackgroundCommandHandlerDecorator(
     ICommandHandler<T> decorated) 
    { 
     this.decorated = decorated; 
    } 

    public void Handle(T command) 
    { 
     HostingEnvironment.RegisterObject(this); 

     try 
     { 
      lock (this.locker) 
      { 
       this.decorated.Handle(command); 
      } 
     } 
     finally 
     { 
      HostingEnvironment.UnregisterObject(this); 
     }    
    } 

    void IRegisteredObject.Stop(bool immediate) 
    { 
     // Ensure waiting till Handler finished. 
     lock (this.locker) { } 
    } 
} 

Cuando se pone este decorador entre un controlador de comandos y la ThreadedCommandHandlerProxy, se asegurará de que (en condiciones normales) dominio de aplicación no se descarga mientras dicho comando se está ejecutando.

Cuestiones relacionadas