8

Sto usando Simple Injector per gestire la durata delle mie dipendenze iniettate (in questo caso UnitOfWork), e sono molto contento di avere un decoratore separato piuttosto che il mio il gestore di servizi o comandi che si occupa del salvataggio e dello smaltimento rende molto più semplice il codice durante la scrittura di livelli di logica aziendale (seguo l'architettura descritta in this blog post).Come configurare Simple Injector per eseguire thread in background in ASP.NET MVC

Quanto sopra funziona perfettamente (e molto facilmente) utilizzando il pacchetto Simple Injector MVC NuGet e il seguente codice durante la costruzione del contenitore radice della composizione, se nel grafico esiste più di una dipendenza, la stessa istanza viene iniettata attraverso tutto - perfetto per il contesto del modello Entity Framework.

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

ora ho bisogno di eseguire alcuni thread in background e capire da semplice iniettore documentation on threads che i comandi possono essere proxied come segue:

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); 
     }); 
    } 
} 

Tuttavia, da questa filettatura esempio di documentazione Posso vedere le fabbriche che sono usate, se introduco le fabbriche ai miei comandi e al livello di servizio le cose si confonderanno e saranno inconcludenti sistente come avrò diverse metodologie di risparmio per servizi diversi (un contenitore gestisce il risparmio, altre fabbriche istanziati all'interno maniglia servizi salva e lo smaltimento) - si può vedere come semplice e chiaro il codice di servizio scheletro è senza fabbriche:

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 quanto sopra il mio problema comincia a formare, come ho capito dal documentation on ASP .NET PerWebRequest lifetimes, il seguente codice viene utilizzato:

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; 
} 

è possibile che questo funziona bene per ogni richiesta HTTP ci sarà una valida HttpContext.Current, se mi spin una nuova discussione con lo ThreadedCommandHandlerProxy creato e un nuovo thread e lo HttpContext non esisterà più all'interno di quel thread.

Poiché il HttpContext sarebbe nullo in ogni chiamata successiva, tutte le istanze degli oggetti immessi nei costruttori di servizio sarebbero nuovi e unici, l'opposto del normale HTTP per richiesta Web in cui gli oggetti sono condivisi correttamente come la stessa istanza in tutti i servizi.

Quindi, per riassumere quanto sopra in domande:

Come potrei fare per ottenere gli oggetti costruiti e oggetti comuni iniettato indipendentemente dal fatto creata dalla richiesta HTTP o tramite un nuovo thread?

Esistono considerazioni speciali per avere un UnitOfWork gestito da un thread all'interno di un proxy gestore comandi? Come si può garantire che venga salvato e smaltito dopo l'esecuzione del gestore?

Se avessimo un problema all'interno del gestore di comandi/livello di servizio e non volessimo salvare lo UnitOfWork, dovremmo semplicemente lanciare un'eccezione? In tal caso, è possibile rilevarlo a livello globale o è necessario rilevare l'eccezione per richiesta all'interno di uno try - catch nel decoratore o nel proxy del gestore?

Grazie,

Chris

+1

@Steven grazie, sono passati attraverso questa domanda però io non sono sicuro al 100% su quello che devo fare con la RegisterPerWebRequest e RegisterLifetimeScope per il mio caso di IUnitOfWork e UnitOfWork, in il mio caso sarebbe DbContext => UnitOfWorkBase, MyDbContext => UnitOfWork e IObjectContextAdapter => IUnitOfWork? Se potessi chiarire, sarebbe apprezzato. Anche il suo citato ci sono un certo numero di modi per farlo, è un modo per fare un'estensione di durata esplicita per questo? Sarebbe bello se non dovesse derivare dalle classi base. – g18c

+0

Con i nuovi ActionResults 'async' per MVC5, questo problema sarà comune. – Mrchief

+1

Se l'unità di lavoro è un'attività di lunga durata e importante, considera una "coda di lavoro" del bus dei messaggi che puoi raccogliere quando l'app torna da un riciclo. A proposito, leggendo su 'QueueBackgroundWorkItem' ho visto che l'attività sarà terminata se ci vorrà più di * 90 secondi *, quindi rischierai se impieghi più di un minuto – KCD

risposta

7

Permettetemi di iniziare di avvertendo che se si desidera eseguire i comandi in modo asincrono in un'applicazione web, si potrebbe desiderare di fare un passo indietro e guardare a ciò che si sta cercando realizzare. Esiste sempre il rischio che l'applicazione Web venga riciclata solo dopo aver avviato il gestore su un thread in background. Quando un'app ASP.NET viene riciclata, tutti i thread in background verranno interrotti. Sarebbe forse meglio pubblicare comandi su una coda (transazionale) e lasciare che un servizio in background li prelevasse. Ciò garantisce che i comandi non possano "perdersi". E consente anche di rieseguire i comandi quando il gestore non riesce correttamente. Può anche salvarti dal dover eseguire alcune brutte registrazioni (che probabilmente non avrai a che fare con il framework DI scelto), ma probabilmente questo è solo un problema secondario. E se hai bisogno di eseguire gestori asincroni, prova almeno a minimizzare il numero di gestori che esegui async.

Con quello fuori mano, quello che ti serve è il seguente.

Come si è notato, poiché si eseguono (alcuni) gestori di comandi in modo asincrono, non è possibile utilizzare lo stile di vita per richiesta Web su di essi. Avrai bisogno di una soluzione ibrida, che si mescoli tra la richiesta web e "qualcos'altro". È probabile che qualcos'altro sarà uno per lifetime scope. Non ci sono estensioni incorporate per queste soluzioni ibride, a causa di un paio di motivi. Prima di tutto è una caratteristica piuttosto esotica che non molte persone hanno bisogno. In secondo luogo, puoi mescolare due o tre stili di vita insieme, in modo che sia una combinazione infinita di ibridi. E per ultimo, è (abbastanza) facile registrarlo da solo.

In Simple Injector 2 è stata aggiunta la classe Lifestyle e contiene un metodo CreateHybrid che consente di combinare due stili di vita per creare un nuovo stile di vita. Ecco un esempio:

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

È possibile utilizzare questo stile di vita ibrido per registrare l'unità di lavoro:

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

Dal momento che si sta registrando l'unità di lavoro come da Scope corso della vita, è necessario in modo esplicito creare e smaltire una portata a vita per un determinato thread. La cosa più semplice da fare è aggiungere questo al tuo ThreadedCommandHandlerProxy. Questo non è il modo più efficace di fare SOLID, ma è il modo più semplice per me di mostrarti come farlo.

Se abbiamo avuto un problema all'interno del comando-handler/service-layer e non ha voluto salvare l'UnitOfWork, dovremmo semplicemente un'eccezione ?

La tipica cosa da fare è lanciare un'eccezione. È infatti la regola generale delle eccezioni:

Se il tuo metodo non può fare come promette il nome, può lanciare. ->

Il gestore di comando dovrebbe essere ignoranti su come il contesto in cui viene eseguito, e l'ultima cosa che vuoi è quello di differenziare in se deve generare un'eccezione. Quindi il lancio è la tua migliore opzione. Tuttavia, quando si esegue un thread in background, è meglio rilevare tale eccezione, poiché .NET ucciderà l'intero AppDomain, se non lo si cattura.In un'applicazione web questo significa un riciclo AppDomain, il che significa che l'applicazione Web (o almeno quel server) sarà offline per un breve periodo di tempo.

D'altra parte, non si desidera perdere alcuna informazione di eccezione, quindi è necessario registrare tale eccezione e probabilmente si desidera registrare una rappresentazione serializzata di tale comando con tale eccezione, in modo da poter vedere quali dati sono stati . passò in volta aggiunto al metodo ThreadedCommandHandlerProxy.Handle, il che sarebbe simile a questa:

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); 
     } 
    }); 
} 

ho avvertito di come i gestori di esecuzione in modo asincrono potrebbe non essere la migliore idea. Tuttavia, poiché si applica questo modello di comando/gestore, sarà possibile passare all'utilizzo della coda in un secondo momento, senza dover modificare una singola riga di codice nell'applicazione. Si tratta solo di scrivere una sorta di QueueCommandHandlerDecorator<T> (che serializza il comando e lo manda in coda) e cambiare il modo in cui le cose sono cablate nella tua composizione root, e sei a posto (e ovviamente non dimenticarti di implementare il servizio che esegue i comandi dalla coda). In altre parole, l'aspetto positivo di questo design SOLID è che l'implementazione di queste funzionalità è costante per le dimensioni dell'applicazione.

+0

Grazie Steven, il tuo commento sull'avere un servizio in background fa girare tutto potrebbe essere esattamente quello che sto cercando senza alcun altro problema di preoccupazioni su thread o registrazione - da 'servizio' solo per chiarire potremmo avere il QueueCommandHandlerDecorator scrivere il lavoro nel database (o un servizio web WCF, o qualche altro processo metodo di comunicazione), e hanno un processo completamente separato (scrivere un secondo programma come servizio Windows o servizio WCF) in esecuzione che preleva queste richieste di lavoro e le elabora? Grazie per la grande conoscenza del design e per evitare le insidie. – g18c

+0

@ g18c: puoi usare qualsiasi coda che ti piace di più (database, MSMQ), e puoi scrivere nella tua coda come preferisci (direttamente da MVC, o lasciare che un servizio WCF scriva in coda), ma per prelevando i comandi dalla coda e eseguendoli, sarebbe meglio usare un servizio di Windows. O almeno, non utilizzare un servizio Web, dal momento che in questo caso si avranno gli stessi problemi con il riciclaggio del dominio app come si sta già avendo con la propria applicazione web. I servizi Web non sono intesi ad occuparsi dell'elaborazione in background. Sono per la gestione delle richieste. In bocca al lupo. – Steven

+0

Grandi cose, oltre alla mia app MVC esistente userò la mia libreria di classi di servizio/contratti comuni con un nuovo servizio Windows che registra i comandi richiesti in un contenitore Simple Injector (con per thread o perlifetime scope) - poiché l'accesso al database è transazionale e gestito dal contenitore, posso facilmente mettere in coda le attività in un lavoro per eseguire la tabella senza preoccuparsi delle condizioni di gara poiché il servizio può eseguire il polling di questa tabella per i nuovi record.Questa metodologia di progettazione è molto potente in quanto i servizi sono immediatamente utilizzabili indipendentemente dall'app MVC o dal servizio di Windows - molti ringraziamenti lo apprezzano molto. – g18c

5

Parte della problems of running background threads in ASP.NET can be overcome con un comodo gestore di comandi decoratore:

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) { } 
    } 
} 

Quando si inserisce questo decoratore tra un gestore di comandi e la ThreadedCommandHandlerProxy, il candidato dovrà garantire che (in condizioni normali) dominio di applicazione non viene scaricato mentre tale comando è in esecuzione.

Problemi correlati