2014-12-03 14 views
6

Nella nostra applicazione abbiamo CQRS: abbiamo IAsyncCommand con IAsyncCommandHandler<IAsyncCommand>.MVC5 Async ActionResult. È possibile?

Di solito il comando è trasformati tramite il mediatore in questo modo:

var mediator = //get mediator injected into MVC controller via constructor 
var asyncCommand = // construct AsyncCommand 
// mediator runs ICommandValidator and that returns a list of errors if any 
var errors = await mediator.ProcessCommand(asyncCommand); 

che funziona bene. Ora ho notato che faccio un sacco di codice ripetitivo in azioni di controllo:

public async virtual Task<ActionResult> DoStuff(DoStuffAsyncCommand command) 
{ 
    if (!ModelState.IsValid) 
    { 
     return View(command); 
    } 

    var result = await mediator.ProcessCommandAsync(command); 

    if (!result.IsSuccess()) 
    { 
     AddErrorsToModelState(result); 
     return View(command); 
    } 
    return RedirectToAction(MVC.HomePage.Index()); 
} 

E questo modelli ripete più e più in molti-molti regolatori. semplificazione Così per i comandi single-threaded che ho fatto:

public class ProcessCommandResult<T> : ActionResult where T : ICommand 
{ 
    private readonly T command; 
    private readonly ActionResult failure; 
    private readonly ActionResult success; 
    private readonly IMediator mediator; 


    public ProcessCommandResult(T command, ActionResult failure, ActionResult success) 
    { 
     this.command = command; 
     this.success = success; 
     this.failure = failure; 

     mediator = DependencyResolver.Current.GetService<IMediator>(); 
    } 

    public override void ExecuteResult(ControllerContext context) 
    { 
     if (!context.Controller.ViewData.ModelState.IsValid) 
     { 
      failure.ExecuteResult(context); 
      return; 
     } 

     var handlingResult = mediator.ProcessCommand(command); 

     if (handlingResult.ConainsErrors()) 
     { 
      AddErrorsToModelState(handlingResult); 
      failure.ExecuteResult(context); 
     } 

     success.ExecuteResult(context); 
    } 
    // plumbing code 
} 

E dopo qualche impianto idraulico fatto, la mia azione di controllo è simile al seguente:

public virtual ActionResult Create(DoStuffCommand command) 
{ 
    return ProcessCommand(command, View(command), RedirectToAction(MVC.HomePage.Index())); 
} 

Questo funziona bene per sync-comandi dove ho don' t Devo fare i modelli async-await. Appena provo a eseguire le operazioni async, questo non viene compilato, in quanto non esiste alcun AsyncActionResult in MVC (o non esiste e non riesco a trovarlo) e non riesco a rendere il framework MVC utilizza le operazioni asincrone su void ExecuteResult(ControllerContext context).

Quindi, qualche idea su come posso realizzare un'implementazione generica dell'azione del controller che ho citato in cima alla domanda?

+5

Non vedo dove viene chiamato qualcosa "AsyncActionResult". È sufficiente restituire un'attività o Task se si implementa un metodo generico. Le azioni asincrone restituiscono sempre le attività. 'async void' è una sintassi molto specifica usata solo per gestori di eventi asincroni (o metodi simili ai gestori) e * da nessuna parte * altro. L'equivalente asincrono di un metodo 'void' è una funzione che restituisce' Task'. L'equivalente di una funzione è una funzione che restituisce un 'Task ' –

+0

@PanagiotisKanavos sì, sono abbastanza consapevole che non dovrei usare 'async void'. E non posso semplicemente restituire Task perché devo controllare se 'ModelState' è in uno stato valido prima di eseguire il mediatore, e ciò implica passare attraverso la pipeline MVC e tirare fuori' ModelState' dal framework in qualche modo. – trailmax

+0

Sembra che tu stia mescolando preoccupazioni diverse, come la richiesta in arrivo con l'azione stessa * e * il risultato previsto. A questo punto la tua classe ProcessCommandResult sembra un controller. Se si desidera sovrascrivere la convalida, l'associazione ecc., Esistono altri meccanismi in MVC. In effetti, ciò che hai qui viola CQRS - stai usando la risposta (ActionResult) come se fosse il comando stesso implementando ICommand. –

risposta

0

Sembra che un'azione sia ancora il posto migliore per gestire la logica invece di utilizzare ActionResult.

Se il codice è duplicato, perché non utilizzare una classe base con un metodo di supporto protetto ...?

public class BaseCommandController : Controller 
{ 
    protected IMediator Mediator { get { return DependencyResolver.Current.GetService(typeof (IMediator)) as IMediator; } } 

    public async virtual Task<ActionResult> BaseDoStuff<TCommand>(TCommand command, Func<ActionResult> success, Func<ActionResult> failure) 
    { 
     if (!ModelState.IsValid) 
     { 
      return failure(); 
     } 

     var result = await Mediator.ProcessCommand(command); 

     if (!result.IsSuccess()) 
     { 
      AddErrorsToModelState(result); 
      return failure(); 
     } 

     return success(); 
    } 

    private void AddErrorsToModelState(IResponse result) 
    { 
    } 

} 

azioni del controller vengono poi resi come ...

public class DefaultController : BaseCommandController 
{ 
    protected async virtual Task<ActionResult> DoStuff(DoStuffAsyncCommand command) 
    { 
     return await BaseDoStuff(command,() => RedirectToAction("Index"),() => View(command)); 
    } 
} 
+0

Abbiamo avuto esattamente questo e non funziona per noi per vari motivi. – trailmax

1

La soluzione sembra troppo complesso, altamente puzzolente (contiene sia la posizione del servizio, e altri odori) e sembra perdere il punto di ciò che ActionResults sono (comandi oggetti stessi, davvero).

In realtà, questo è un buon esempio di The XY Problem. Piuttosto che chiedere del tuo problema reale, che è il refactoring del codice comune nei tuoi metodi di azione in modo asincrono, hai invece trovato una soluzione eccessivamente complessa che pensi risolvi il tuo problema. Sfortunatamente, non puoi capire come farlo funzionare, quindi chiedi di QUEL problema invece del tuo vero problema.

È possibile ottenere ciò che si desidera con una semplice funzione di supporto. Qualcosa di simile a questo:

public async virtual Task<ActionResult> DoStuff(DoStuffAsyncCommand command) 
{ 
    return await ControllerHelper.Helper(command, ModelState, _mediator, 
     RedirectToAction(MVC.HomePage.Index()), View(command), View(command)); 
} 

public static class ControllerHelper 
{ 
    // You may need to constrain this to where T : class, didn't test it 
    public static async Task<ActionResult> Helper<T>(T command, 
     ModelStateDictionary ModelState, IMediator mediator, ActionResult returnAction, 
     ActionResult successAction, ActionResult failureAction) 
    { 
     if (!ModelState.IsValid) 
     { 
      return failureResult; 
     } 

     var result = await mediator.ProcessCommandAsync(command); 

     if (!result.IsSuccess()) 
     { 
      ModelState.AddErrorsToModelState(result); 
      return successResult; 
     } 

     return returnAction; 
    } 

    public static void AddErrorsToModelState(this ModelStateDictionary ModelState, ...) 
    { 
     // add your errors to the ModelState 
    } 
} 

In alternativa, si potrebbe rendere un oggetto stateful e iniettare il mediatore attraverso le dipendenze in cascata tramite iniezione costruttore. Sfortunatamente, non è facile iniettare ModelState, quindi è ancora necessario passare come parametro al metodo.

Si potrebbe anche solo passare la stringa per ActionResults, ma poiché non c'è alcun oggetto RedirectToActionResult su new up, si dovrebbe fare un pasticcio con l'inizializzazione di un oggetto RedirectToRoute ed è solo più semplice passare ActionResult. È anche molto più semplice usare la funzione Controller View() piuttosto che costruire un nuovo ViewResult da solo.

È anche possibile utilizzare l'approccio Func<ActionResult> utilizzato da Sambo, che lo rende pigro, quindi chiama solo il metodo RedirectToAction quando necessario. Non penso che RedirectToAction abbia abbastanza overhead per farne la pena.

+0

Questo è quasi identico a quello che ho finito con me stesso dopo abbastanza iterazioni di refactoring. – trailmax

+1

@trailmax - sì, non ho notato che la domanda era di pochi mesi .. è stata urtata da Sambo. Sono contento che tu sia arrivato alla stessa conclusione ... Non mi piacciono i controller di base. –

Problemi correlati