View.RenderAsync() causes Cannot access a closed Stream exception

400 Views Asked by At

I'm developing net6.0 MVC app. I use partial view rendering to the string format in my app. I make Ajax request to the app and expect it to form a Json containing rendered partial view as a prop. There is a method I use to render partial to a string below

public static string RenderPartialViewToString(Controller controller, string partialPath, object model)
        {
            if (string.IsNullOrEmpty(partialPath))
            {
                //Set Action name as partial name
                partialPath = controller.GetActionNameFromRouteData();
            }

            controller.ViewData.Model = model;

            using (var sw = new StringWriter())
            {
                // Find partial view file
                var viewEngine = DependencyResolverHelper.GetService<ICompositeViewEngine>();
                var viewResult = viewEngine.FindView(controller.ControllerContext, partialPath, false);
                
                var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw, new HtmlHelperOptions());
                
                viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult();

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

The problem is once I call viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult() it seems it captures and blocks Response.Body of current HttpContext so I can't form and return expected json anymore. There is custom json result class I use to form the response

enter image description here

The exception

System.ObjectDisposedException: Cannot access a closed Stream.
   at System.IO.MemoryStream.Write(Byte[] buffer, Int32 offset, Int32 count)
   at System.IO.MemoryStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)
--- End of stack trace from previous location ---
   at System.IO.Pipelines.StreamPipeWriter.FlushAsyncInternal(Boolean writeToStream, ReadOnlyMemory`1 data, CancellationToken cancellationToken)
   at Web.Controllers.BaseCustomController`1.JsonNetResult.ExecuteResultAsync(ActionContext context) 

Could anyone assist?

1

There are 1 best solutions below

0
snipervld On

This problem can be solved by creating fake http context:

public static string RenderPartialViewToString(Controller controller, string partialPath, object model)
{
    if (string.IsNullOrEmpty(partialPath))
    {
        //Set Action name as partial name
        partialPath = controller.GetActionNameFromRouteData();
    }

    var httpContextFactory = DependencyResolverHelper.GetService<IHttpContextFactory>();
    var newHttpContext = httpContextFactory.Create(new FeatureCollection(controller.HttpContext.Features));

    using var newHttpContextBody = new MemoryStream();
    newHttpContext.Response.Body = newHttpContextBody;
    var actionContext = new ActionContext(newHttpContext, controller.ControllerContext.RouteData, new ActionDescriptor());

    using (var sw = new StringWriter())
    {
        // Find partial view file
        var viewEngine = DependencyResolverHelper.GetService<ICompositeViewEngine>();
        var viewResult = viewEngine.FindView(actionContext, partialPath, false);
        var viewData = new ViewDataDictionary(controller.MetadataProvider, controller.ModelState) { Model = model };

        var viewContext = new ViewContext(actionContext, viewResult.View, viewData, controller.TempData, sw, new HtmlHelperOptions());
        viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult();

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

new FeatureCollection(...) is important. If you look at source of DefaultHttpResponse, when changing response's body, IHttpResponseBodyFeature feature is replaced in http context. That's why new FeatureCollection(...) is used here, because it creates a snapshot of original http context's features, so original IHttpResponseBodyFeature feature won't be replaced, e.g. original response body won't be changed.