Search code examples
c#asp.net-mvcdependency-injectionautofac

Dependency injection issue when rendering an MVC view to a string in Parallel.ForEach


I have some code that renders a partial view to a string:

public static string RenderPartialViewToString(ControllerContext context, string viewPath)
{
    var viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath);
    var view = viewEngineResult.View;
    using (var sw = new StringWriter())
    {
        var ctx = new ViewContext(context, 
                                  view, 
                                  context.Controller.ViewData,
                                  context.Controller.TempData, 
                                  sw);
        view.Render(ctx, sw);
        return sw.ToString();
    }
}

For perfomance reasons this code is called multiple times inside a Parallel.ForEach loop. And it works until I try to introduce dependency injection for our controllers.

When I set the resolver to AutoFac's dependency resolver....

IContainer container = IoC.BuildContainer();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

...I get an exception...

The request lifetime scope cannot be created because the HttpContext is not available.

This exception doesn't occur for every view, only when there are two calls to get the same view. And the problem goes away if I change the Parallel.ForEach to a standard ForEach.

I have read that the controller context is not valid in a new thread but it didn't cause an issue until I introduced AutoFac.

Is there a solution to this that lets me keep the Parallel.ForEach? Ideally the solution will avoid making wholesale changes to the legacy rendering code - perhaps some AutoFac configuration?

Stack trace:

at Autofac.Integration.Mvc.RequestLifetimeScopeProvider.GetLifetimeScope(Action`1 configurationAction) at Autofac.Integration.Mvc.AutofacDependencyResolver.get_RequestLifetimeScope() at Autofac.Integration.Mvc.AutofacDependencyResolver.GetService(Type serviceType) at System.Web.Mvc.BuildManagerViewEngine.DefaultViewPageActivator.Create(ControllerContext controllerContext, Type type) at System.Web.Mvc.BuildManagerCompiledView.Render(ViewContext viewContext, TextWriter writer) at AgentDesktop.HelperClasses.ViewHelper.RenderPartialViewToString(ControllerContext context, String viewPath) in C:\Users\colinm\source\repos\Git-SyntelateXA\AgentDesktop\HelperClasses\ViewHelper.cs:line 24


Solution

  • Your issue is not directly related to Autofac but how HttpContext works with Parallel.ForEach.

    When you use InstancePerRequest Autofac will tie the dependency to the current HttpContext. If you can use InstancePerDependency or InstancePerLifetimeScope you should not have this issue anymore. By the way InstancePerRequest is deprecated on the latest version of Autofac : https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html#differences-from-asp-net-classic

    By using Parallel.ForEach you are creating many threads.HttpContext.Current is related to the current thread, the new thread doesn't have any httpcontext that's why autofac throws exception.

    HttpContext.Current is writable so you can do something like this

    // /!\ AVOID THIS CODE 
    var currentContext = HttpContext.Current;
    Parallel.ForEach(xxx, o =>
    {
        HttpContext.Current = currentContext;
        // do whatever you want
    });
    

    Settings HttpContext.current sound like a dirty hack to me and I would avoid doing this but it can help fix your issue without rewriting everything.

    Another solution would be setting the task scheduler used to create the parallel tasks. By using TaskScheduler.FromCurrentSynchronizationContext() .net will create a thread with a copy of the current synchronization context.

    Parallel.ForEach(xxx, new ParallelOptions() { 
        TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext() 
    },  o =>
    {
        // do whatever you want 
    });
    

    Again, this solution is not the best solution but could help you fix your issue without rewriting everything.