2015-04-08 3 views
5

Ho problemi a risolvere il problema che sto affrontando con async/attendi.Il metodo asincrono atteso non viene restituito prima che sia richiesto il risultato

In poche parole, ho un controller, decorato con un attributo. Questo attributo ottiene il contenuto specificato da un I/O intenso processo (file system/db/api ecc ...)

Si imposta quindi il contenuto restituito come Dictionary<string, string> sulla ViewBag

Poi, in una vista , posso fare qualcosa di simile, per recuperare il contenuto:

@(ViewBag.SystemContent["Common/Footer"]) 

il problema che sto avendo, è la prima volta che viene eseguito, il contenuto non ha restituito, e la chiamata per recuperare il valore da stringa indice fallisce, in quanto non esiste.
Premi F5 e va bene.

azione di controllo è piuttosto semplice:

[ProvideContent("Common/Footer")] 
public class ContactDetailsController : Controller 
{ 
    public async Task<ActionResult> Index() 
    { 
     //omitted for brevity - awaits some other async methods 
     return View(); 
    } 
} 

L'attributo

public override async void OnActionExecuting(ActionExecutingContext filterContext) 
{ 
    if (filterContext.Result is ViewResult) 
    { 
     var localPath = filterContext.RouteData.Values["controller"] + "/" + filterContext.RouteData.Values["action"]; 

     if (!_useControllerActionAsPath) 
      localPath = _path; 

     var viewResult = filterContext.Result as ViewResult; 

     //this seems to go off and come back AFTER the view requests it from the dictionary 
     var content = await _contentManager.GetContent(localPath); 

     if (viewResult.ViewBag.SystemContent == null) 
      viewResult.ViewBag.SystemContent = new Dictionary<string, MvcHtmlString>(); 

     viewResult.ViewBag.SystemContent[localPath] = new DisplayContentItem(content, localPath); 
    } 

EDIT

Cambiare la seguente riga nel mio attributo:

var content = await _contentManager.GetContent(localPath); 

To

var content = Task.Factory.StartNew(() => 
      manager.GetContent(localPath).Result, TaskCreationOptions.LongRunning).Result; 

risolve il problema, ma sento che va contro tutto quello che ho letto su Stephen Clearys blog ...

+0

Non sono un esperto asincrona, ma si' ri definire il contenuto e usarlo subito, non puoi semplicemente cambiarlo in una chiamata sincrona? – DLeh

+0

con contenuto, sto * aspettando * _contentmanager.GetContent - che è asincrono ..... anche se blocco con a. Risultato alla fine, è lo stesso risultato finale – Alex

+0

Ho il sospetto che il problema sia che si sta eseguendo l'override di un metodo sincrono con uno asincrono. Se è necessario eseguire il completamento prima dell'esecuzione dell'azione tes, perché l'hai reso asincrono? – Lee

risposta

8

Non ho dimestichezza con il 100% ASP.Net Stack MVC e come funziona tutto questo, ma ho intenzione di fare un tentativo.

La documentazione OnActionExecuting() dice:

Chiamato prima del metodo di azione viene richiamato.

Poiché si esegue l'override di un metodo sincrono in precedenza e lo si è reso asincrono, si aspetta che il codice sia completo e pronto per il prossimo passaggio di esecuzione.

teorica percorso di esecuzione:

public void ExecuteAction() 
{ 
OnActionExecuting(); 

OnActionExecution(); 

OnActionExecuted(); 
} 

Dal momento che escludeva il metodo OnActionExecuting(), lo stack di esecuzione è fondamentalmente sempre lo stesso, ma il codice successivo da eseguire (ExecuteAction() e OnActionExecuted() e qualunque chiamato ExecuteAction()) si aspettavano un chiamate sincrone da effettuare, quindi a loro conoscenza tutto è a posto e pronto per continuare a correre.

Fondamentalmente, questo si riduce a OnActionExecuting() non è un metodo asincrono e nulla si aspetta che sia. (Non è asincrono neanche in MVC 6.

Qualcosa, che viene chiamato in modo sincrono dopo OnActionExecuting() e le sue chiamate in sequenza, fa riferimento a viewResult.ViewBag.SystemContent e quindi non ottiene il valore desiderato. Come hai detto tu stesso nel titolo,

Il metodo asincrono atteso non restituisce prima che sia necessario il risultato.

Il 'catturare' con l'utilizzo di task è che non è possibile garantire quando il compito completerà, ma si sono garantiti che sarà completa.

Possibili soluzioni:

  • Spostare la chiamata GetContent() di quell'evento.
  • Memorizzare l'attività creata per , trovare il luogo successivo in cui si utilizza lo viewResult.ViewBag.SystemContent e controllare il completamento dell'attività o attendere il completamento.
  • Aggiungere un intervallo di timeout al metodo GetContent(). (Tonnellate di modi diversi di fare questo MSDN Docs for Task class Questo non risolverà il problema

Edit:.. Esempio di codice per la memorizzazione Task controller

[ProvideContent("Common/Footer")] 
public class ContactDetailsController : Controller 
{ 

/* 
* BEGINNING OF REQUIRED CODE BLOCK 
*/ 
    private Task<string> _getContentForLocalPathTask; 
    private string _localPath; 

/* 
* END OF REQUIRED CODE BLOCK 
*/ 
    public async Task<ActionResult> Index() 
    { 
     //omitted for brevity - awaits some other async methods 
/* 
* BEGINNING OF REQUIRED CODE BLOCK 
*/ 
     string content = await _getContentForLocalPath; 

     viewResult.ViewBag.SystemContent[_localPath] = new DisplayContentItem(content, _localPath);    
/* 
* END OF REQUIRED CODE BLOCK 
*/ 

     return View(); 
    } 

public override async void OnActionExecuting(ActionExecutingContext filterContext) 
{ 
    if (filterContext.Result is ViewResult) 
    { 
     var localPath = filterContext.RouteData.Values["controller"] + 
         "/" + filterContext.RouteData.Values["action"]; 

     if (!_useControllerActionAsPath) 
      localPath = _path; 

     var viewResult = filterContext.Result as ViewResult; 

/* 
* BEGINNING OF REQUIRED CODE BLOCK 
*/ 
     _localPath = localPath; 

     _getContentForLocalPathTask = _contentManager.GetContent(localPath); 
/* 
* END OF REQUIRED CODE BLOCK 
*/ 

     if (viewResult.ViewBag.SystemContent == null) 
      viewResult.ViewBag.SystemContent = new Dictionary<string, MvcHtmlString>(); 

    } 
} 
+3

+1. La prima regola di 'async' è di evitare' vuoto asincrono'. In questo caso, 'OnActionExecuting' ritorna ad ASP.NET quando raggiunge il suo' await', causando il proseguimento della pipeline della richiesta. Suggerirei di archiviare l'operazione asincrona in un 'Compito ' (cioè, nel tipo di controller) e 'attendere quell'attività all'interno dell'azione del controller. –

+0

@StephenCleary - non sono sicuro che seguo: memorizzare l'attività nell'azione del controller? Il mio attributo ProvideContent è attualmente sul Controller stesso, non su ActionMethod - Sono aperto a suggerimenti di binning di questa idea, ma sembra un approccio logico? – Alex

+1

@Alex Ho aggiornato la mia risposta con ciò a cui Stephen si riferiva. – Cameron