8

Ho diversi servizi, ognuno dei quali ha un UnitOfWork iniettato nel costruttore utilizzando il contenitore IoC Simple Injector.Iniettore semplice: inserisce la stessa istanza UnitOfWork tra i servizi dello stesso grafico

Attualmente sono in grado di vedere ogni istanza di UnitOfWork è un oggetto separato, questo è negativo poiché sto utilizzando Entity Framework e richiedono lo stesso riferimento di contesto su tutte le unità di lavoro.

Come posso garantire che la stessa istanza UnitOfWork sia iniettata in tutti i servizi per ogni richiesta di risoluzione? Il mio UnitOfWor verrà salvato da un decoratore del gestore di comandi esterno al termine del comando.

Si noti che questa è una libreria comune e verrà utilizzata sia per MVC sia per Windows Form, sarebbe bello avere una soluzione generica per entrambe le piattaforme, se possibile.

codice è qui sotto:

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

Il risultato desiderato della riga sottostante è quello di creare un oggetto che ha un esempio UnitOfWork condiviso in tutto il grafico oggetto costruito:

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

Qui ci sono i miei servizi :

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

risposta

20

Quale registrazione è necessaria dipende dal tipo di applicazione ionico. Dato che stai parlando di due diversi framework (MVC e WinForms), entrambi avranno una registrazione diversa.

Per un'applicazione MVC (o applicazioni Web in generale), la cosa più comune da fare è registrare l'unità di lavoro su un per web request basis. Ad esempio, la seguente registrazione cache l'unità di lavoro durante una singola richiesta 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; 
}); 

Lo svantaggio di questa registrazione è che l'unità di lavoro non è disposto (se necessario). C'è il an extension package per il semplice iniettore che aggiunge i metodi di estensione RegisterPerWebRequest al contenitore, che assicurerà automaticamente che l'istanza sia eliminata alla fine della richiesta web. Utilizzando questo pacchetto, sarà in grado di effettuare le seguenti operazioni di registrazione:

container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork>(); 

Che è una scorciatoia per:

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

un'applicazione Windows Form d'altra parte, è in genere a thread singolo (una sola l'utente utilizzerà quell'applicazione). Credo che non sia raro avere una singola unità di lavoro per modulo, che viene smaltita, il modulo si chiude, ma con l'uso del modello di comando/gestore, penso che sia meglio adottare un approccio più orientato al servizio. Ciò che intendo è che sarebbe opportuno progettarlo in modo tale da poter spostare il livello aziendale in un servizio WCF, senza la necessità di apportare modifiche al livello di presentazione. Puoi ottenere questo risultato lasciando che i tuoi comandi contengano solo primitive e (altro) DTO s. Quindi non immagazzinare entità Entity Framework nei tuoi comandi, perché questo renderà la serializzazione del comando molto più difficile, e porterà a sorprese in seguito.

Quando si esegue questa operazione, sarebbe opportuno creare una nuova unità di lavoro prima che il gestore comandi inizi l'esecuzione, riutilizzare la stessa unità di lavoro durante l'esecuzione di quel gestore e impegnarlo quando il gestore è stato completato correttamente (e smaltirlo sempre). Questo è uno scenario tipico per lo Per Lifetime Scope lifestyle. C'è an extension package che aggiunge i metodi di estensione RegisterLifetimeScope al contenitore. Utilizzando questo pacchetto, sarà in grado di effettuare le seguenti operazioni di registrazione:

container.RegisterLifetimeScope<IUnitOfWork, UnitOfWork>(); 

Che è una scorciatoia per:

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

La registrazione però, è solo metà della storia. La seconda parte è decidere quando salvare le modifiche dell'unità di lavoro e, nel caso dell'uso dello stile di vita di Lifetime Scope, dove iniziare e terminare tale ambito. Poiché è necessario avviare esplicitamente un ambito di vita prima dell'esecuzione del comando e terminarlo quando il comando è terminato, il modo migliore per farlo è utilizzare un decoratore del gestore comandi, che può avvolgere i gestori di comandi. Pertanto, per l'applicazione Forms, si registra in genere un decoratore di gestore di comandi aggiuntivo che gestisce l'ambito di validità. Questo approccio non funziona in questo caso. Date un'occhiata al seguente decoratore, ma tieni presente che non è corretto :

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

Questo approccio non funziona, perché il gestore di comandi decorato è creato prima viene avviato l'ambito di vita.

Potremmo essere tentati per cercare di risolvere questo problema come segue, ma che non è corretto o:

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

    handler.Handle(command); 
} 

Anche se richiede un ICommandHandler<T> all'interno del contesto di un ambito di vita, non davvero iniettare un IUnitOfWork per tale ambito, il contenitore restituirà un gestore che è (di nuovo) decorato con un LifetimeScopeCommandHandlerDecorator<T>. Chiamando il numero handler.Handle(command) si otterrà quindi una chiamata ricorsiva e finiremo con un'eccezione di overflow dello stack.

Il problema è che il grafico delle dipendenze è già stato creato prima di poter avviare l'ambito della durata. Pertanto, dobbiamo interrompere il grafico delle dipendenze rinviando la costruzione del resto del grafico. Il modo migliore per fare ciò che ti permette di mantenere pulito il tuo design dell'applicazione è cambiare il decoratore in un proxy e iniettare una fabbrica in esso che creerà il tipo che avrebbe dovuto avvolgere. Tale LifetimeScopeCommandHandlerProxy<T> sarà simile a questa:

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

Iniettando un delegato, siamo in grado di ritardare il tempo viene creata l'istanza e facendo questo abbiamo ritardare la costruzione (il resto), il grafico delle dipendenze. Il trucco ora è di registrare questa classe proxy in modo tale da iniettare le istanze avvolte, anziché (ovviamente) iniettarsi di nuovo. Iniettore semplice supporta l'iniezione di fabbriche Func<T> in decoratori, quindi è possibile utilizzare semplicemente il RegisterDecorator e in questo caso anche il metodo di estensione RegisterSingleDecorator.

Si noti che l'ordine in cui decoratori (e questo proxy) sono registrati (ovviamente) è importante. Poiché questo proxy avvia un nuovo ambito di vita, dovrebbe avvolgere il decoratore che impegna l'unità di lavoro.In altre parole, una registrazione più completa sarebbe simile a questa:

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

Registrazione del proxy e decoratore il contrario significherebbe che il TransactionCommandHandlerDecorator<T> sarebbe dipendere da una diversa IUnitOfWork rispetto al resto del grafo delle dipendenze fa, che significherebbe che tutte le modifiche apportate all'unità di lavoro in quel grafico non verranno commesse. In altre parole, la tua applicazione smetterà di funzionare. Quindi rivedi sempre questa registrazione attentamente.

Buona fortuna.

Problemi correlati