RenderPartialViewToString and async methods in ASP.NET MVC 5

Methods like RenderPartialViewToString can be extremely useful when trying to render HTML partial views in JSON results. However, they don't play well with async methods.

My reasoning is it has something to do with the thread-marshaling-black-magic ASP.NET seems to be doing with it comes to anything async in ASP.NET.

Anyhow, here's what a typical RenderPartialViewToString method looks like, taken from one of our project:

public string RenderPartialViewToString(string viewName, object model)
{
    if (string.IsNullOrEmpty(viewName))
        viewName = ControllerContext.RouteData.GetRequiredString("action");

    ViewData.Model = model;

    using (var sw = new StringWriter())
    {
        var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
        var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
        viewResult.View.Render(viewContext, sw);

        return sw.GetStringBuilder().ToString();
    }
}

No problem here per say, but when it's used to feed a JSON result inside an async method, everything is done from within the controller action. That means, the view is actually rendering from within the action too, but on what thread? What worker? What context? Only ASP.NET's guts seem to hold the answer to that.

public async Task<ActionResult> MyAction()
{
    await Something();
    return Json(new { Html = RenderPartialViewToString("MyView") });
}

One way around the problem is simply to defer the execution of the RenderPartialViewToString in an ActionResult:

public class DeferredResult : ActionResult
{
    private readonly Func<ActionResult> Func;

    public DeferredResult(Func<ActionResult> func)
    {
        Func = func;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        Func().ExecuteResult(context);
    }
}

We can then simply use it like so:

public async Task<ActionResult> MyAction()
{
    await Something();
    return new DeferredResult(() => Json(new { Html = RenderPartialViewToString("MyView") }));
}

We can make it even slicker if you have access to a base controller class:

public class MyBaseController : Controller
{
    protected ActionResult Deferred(Func<ActionResult> func)
    {
        return new DeferredResult(func);
    }
}

The final result:

public async Task<ActionResult> MyAction()
{
    await Something();
    return Deferred(() => Json(new { Html = RenderPartialViewToString("MyView") }));
}
Posted by: Bryan Menard
Last revised: 22 Jun, 2016 04:35 PM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.