2012-10-31 16 views
6

Poiché MVC 4 non supporta azioni secondarie asincrone (tramite Html.Action) Sto cercando un modo per forzare le azioni secondarie di esecuzione sincrona. Una semplice soluzione a questo limite è quello di fornire versioni sincrone di tutte le mie azioni controller:Forza esecuzione sincrona dell'azione asincrona in ASP.NET MVC 4

public class FooAsyncController : Controller { 
    public async Task<ActionResult> IndexAsync() { 
     var model = await service.GetFoo().ConfigureAwait(false); 
     return View(model); 
    } 
} 

public class FooSyncController : FooAsyncController { 
    public ActionResult Index() { 
     return IndexAsync().Result; // blocking call 
    } 
} 

Tuttavia, poiché permettiamo richieste di azione bambino su tutte le nostre azioni di controllo, fare questo per ogni controller è una vera e propria valle di lacrime.

Esistono punti di estensibilità nel framework in cui è possibile esaminare il valore restituito di un'azione e, se restituisce un Task<T> e stiamo gestendo un'azione figlio, forzare una chiamata sincrona?

+1

La prima cosa che viene in mente è quello di fare un nuovo metodo di estensione 'Html.ActionAsync' che semplicemente invocare il gasdotto come si vuole, ma aggiungere il' .Result' chiamare alla fine del metodo di supporto corpo, trasformando così le chiamate asincrone in chiamate di sincronizzazione. Problema interessante, di sicuro. – Tejs

+1

Se questo fosse, vorrei creare un nuovo controller che sovrascriva CreateActionInvoker o HandleUnknownAction e con lo sguardo riflesso su un controller di destinazione per eseguire un'invocazione sincrona. Dovresti solo implementare il controller una volta e riutilizzarlo per qualsiasi azione asincrona. –

+0

@Tejs sì questa sarebbe la soluzione idea anche se questa area del framework non è molto async friendly. Il problema sembra essere da qualche parte nel profondo 'HttpServerUtilityBase.Execute'. @ThikingSites - perché non pubblicare una risposta con maggiori dettagli? questo suona come una buona soluzione. –

risposta

3

Dopo aver trascinato per ore il codice sorgente ASP.NET MVC per la soluzione migliore che sono riuscito a ottenere (oltre a creare versioni sincrone di ogni azione del controller) è necessario richiamare manualmente il descrittore di azione per l'azione asincrona metodi entro Controller.HandleUnknownAction.

Non sono particolarmente soddisfatto di questo codice e spero che possa essere migliorato, ma è lavoro.

L'idea è di richiedere espressamente un'azione non valida (preceduta da "_") che invocherà il metodo HandleUnknownAction sul controller. Qui cerchiamo un'azione di sincronizzazione asincrona (rimuovendo prima il carattere di sottolineatura da actionName) e invochiamo il metodo AsyncActionDescriptor.BeginExecute. Chiamando immediatamente il metodo EndExecute stiamo effettivamente eseguendo il descrittore di azione in modo sincrono.

public ActionResult Index() 
{ 
    return View(); 
} 


public async Task<ActionResult> Widget(int page = 10) 
{ 
    var content = await new HttpClient().GetStringAsync("http://www.foo.com") 
     .ConfigureAwait(false); 
    ViewBag.Page = page; 
    return View(model: content); 
} 

protected override void HandleUnknownAction(string actionName) 
{ 
    if (actionName.StartsWith("_")) 
    { 
     var asyncActionName = actionName.Substring(1, actionName.Length - 1); 
     RouteData.Values["action"] = asyncActionName; 

     var controllerDescriptor = new ReflectedAsyncControllerDescriptor(this.GetType()); 
     var actionDescriptor = controllerDescriptor.FindAction(ControllerContext, asyncActionName) 
      as AsyncActionDescriptor; 

     if (actionDescriptor != null) 
     { 
      AsyncCallback endDelegate = delegate(IAsyncResult asyncResult) 
      { 

      }; 

      IAsyncResult ar = actionDescriptor.BeginExecute(ControllerContext, RouteData.Values, endDelegate, null); 
      var actionResult = actionDescriptor.EndExecute(ar) as ActionResult; 

      if (actionResult != null) 
      { 
       actionResult.ExecuteResult(ControllerContext); 
      } 
     } 
    } 
    else 
    { 
     base.HandleUnknownAction(actionName); 
    } 
} 

La vista

<h2>Index</h2> 

@Html.Action("_widget", new { page = 5 }) <!-- note the underscore prefix --> 

Sono quasi certo c'è un modo migliore sovrascrivendo Controller.BeginExecute. Di seguito l'implementazione predefinita. L'idea sarebbe quella di eseguire immediatamente Controller.EndExecuteCore anche se finora non ho avuto alcun successo.

protected virtual IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state) 
{ 
    if (DisableAsyncSupport) 
    { 
     // For backwards compat, we can disallow async support and just chain to the sync Execute() function. 
     Action action =() => 
     { 
      Execute(requestContext); 
     }; 

     return AsyncResultWrapper.BeginSynchronous(callback, state, action, _executeTag); 
    } 
    else 
    { 
     if (requestContext == null) 
     { 
      throw new ArgumentNullException("requestContext"); 
     } 

     // Support Asynchronous behavior. 
     // Execute/ExecuteCore are no longer called. 

     VerifyExecuteCalledOnce(); 
     Initialize(requestContext); 
     return AsyncResultWrapper.Begin(callback, state, BeginExecuteCore, EndExecuteCore, _executeTag); 
    } 
}