1

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

Colin
  • 22,328
  • 17
  • 103
  • 197
  • Duplicate? https://stackoverflow.com/questions/13982600/using-dependencies-on-multiple-threads-with-parallel-foreach – Steven Sep 17 '21 at 19:01
  • @Steven that is a really useful link. It explains the root cause and a potential solution. "It is therefore safest to let each newly started threads build a new object graph by asking the container for it." But how do I do that with Controllers? – Colin Sep 17 '21 at 20:40
  • You can't do that for controllers, because a controller runs on the request thread. You might want to redesign your solution such that you paralize things without the controller. – Steven Sep 17 '21 at 21:12
  • @Steven yeah. Looks like it's wholesale changes to the legacy rendering code versus dependency injection :-( – Colin Sep 17 '21 at 22:11
  • What about using `TaskScheduler.FromCurrentSynchronizationContext()` ? it should be an option in `Parallel.ForEach` – Cyril Durand Sep 20 '21 at 13:22
  • @CyrilDurand that stops the exception occurring. It looks like it might be a bit slower though - I will need to test it a bit more to be sure. Would you expect it to be slower? Anyways, if you can flesh your comment into an answer I think you've got the bounty :-) – Colin Sep 21 '21 at 15:29
  • context switching is always a complexe subject and you should not rely on this, it kind of hack and help resolve some complex situation without rewriting everything. It is expected to be slower than normal `Parallel.For` . I will make a more complete answer later – Cyril Durand Sep 22 '21 at 07:14

1 Answers1

3

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.

Cyril Durand
  • 15,834
  • 5
  • 54
  • 62
  • I already changed all the InstancePerRequest scopes but that didn't solve the problem. It stopped the exception occurring on every call to Render but it still occurs if you have two calls to render the same view – Colin Sep 23 '21 at 16:06
  • Could you update the question with the stack trace of the exception ? – Cyril Durand Sep 23 '21 at 17:06
  • I added the stack trace. It looks like AutoFac tries to create RequestLifetimeScope even if that scope isn't specified when registering components – Colin Sep 28 '21 at 09:14
  • @Colin I just read the code of [`AutofacDependencyResolver`](https://github.com/autofac/Autofac.Mvc/blob/develop/src/Autofac.Integration.Mvc/AutofacDependencyResolver.cs) and you can implement your own `ILifetimeScopeProvider` but there is no easy way to find the current `ILifetimeScope` except by storing it in some ambiant context. That's why `HttpContext` is used here. It's not the case anymore with ASP.net core – Cyril Durand Sep 28 '21 at 12:49