将 MVC 视图呈现为 Parallel.ForEach 中的字符串时的依赖注入问题

Posted

技术标签:

【中文标题】将 MVC 视图呈现为 Parallel.ForEach 中的字符串时的依赖注入问题【英文标题】:Dependency injection issue when rendering an MVC view to a string in Parallel.ForEach 【发布时间】:2021-09-15 09:38:34 【问题描述】:

我有一些代码可以将部分视图呈现为字符串:

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();
    

出于性能原因,此代码在Parallel.ForEach 循环内被多次调用。在我尝试为我们的控制器引入依赖注入之前,它一直有效。

当我将解析器设置为 AutoFac 的依赖解析器时......

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

...我遇到了一个异常...

无法创建请求生命周期范围,因为 HttpContext 不可用。

这个异常不会发生在每个视图上,只有当有两次调用获取同一个视图时。如果我将Parallel.ForEach 更改为标准ForEach,问题就会消失。

我已经阅读了 controller context is not valid in a new thread,但直到我引入 AutoFac 后它才引起问题。

有没有解决方案让我保留Parallel.ForEach?理想情况下,该解决方案将避免对遗留渲染代码进行大规模更改——也许是一些 AutoFac 配置?

堆栈跟踪:

在 Autofac.Integration.Mvc.RequestLifetimeScopeProvider.GetLifetimeScope(Action`1 配置动作)在 Autofac.Integration.Mvc.AutofacDependencyResolver.get_RequestLifetimeScope() 在 Autofac.Integration.Mvc.AutofacDependencyResolver.GetService(类型 服务类型)在 System.Web.Mvc.BuildManagerViewEngine.DefaultViewPageActivator.Create(ControllerContext 控制器上下文,类型类型)在 System.Web.Mvc.BuildManagerCompiledView.Render(ViewContext viewContext,TextWriter 作家)在 AgentDesktop.HelperClasses.ViewHelper.RenderPartialViewToString(ControllerContext 上下文,字符串视图路径)在 C:\Users\colinm\source\repos\Git-SyntelateXA\AgentDesktop\HelperClasses\ViewHelper.cs:line 24

【问题讨论】:

重复? ***.com/questions/13982600/… @Steven 这是一个非常有用的链接。它解释了根本原因和潜在的解决方案。 “因此,让每个新启动的线程通过向容器请求它来构建一个新的对象图是最安全的。”但是如何使用控制器来做到这一点? 您不能对控制器执行此操作,因为控制器在请求线程上运行。您可能想要重新设计您的解决方案,以便在没有控制器的情况下使事情瘫痪。 @Steven 是的。看起来这是对遗留渲染代码与依赖注入的大规模更改:-( 使用TaskScheduler.FromCurrentSynchronizationContext() 怎么样?它应该是Parallel.ForEach中的一个选项 【参考方案1】:

您的问题与 Autofac 没有直接关系,而是与 HttpContext 如何与 Parallel.ForEach 一起使用。

当你使用InstancePerRequest时,Autofac 会将依赖关系绑定到当前的HttpContext。如果您可以使用InstancePerDependencyInstancePerLifetimeScope,您应该不会再遇到这个问题了。顺便说一句,InstancePerRequest 在最新版本的 Autofac 上已弃用:https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html#differences-from-asp-net-classic

通过使用Parallel.ForEach,您正在创建许多线程。HttpContext.Current 与当前线程相关,新线程没有任何 httpcontext,这就是 autofac 抛出异常的原因。

HttpContext.Current 是可写的,所以你可以做这样的事情

// /!\ AVOID THIS CODE 
var currentContext = HttpContext.Current;
Parallel.ForEach(xxx, o =>

    HttpContext.Current = currentContext;
    // do whatever you want
);

设置HttpContext.current 对我来说听起来像是一个肮脏的黑客,我会避免这样做,但它可以帮助解决您的问题而无需重写所有内容。

另一种解决方案是设置用于创建并行任务的任务调度程序。通过使用TaskScheduler.FromCurrentSynchronizationContext() .net 将创建一个带有当前同步上下文副本的线程。

Parallel.ForEach(xxx, new ParallelOptions()  
    TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext() 
,  o =>

    // do whatever you want 
);

同样,此解决方案不是最佳解决方案,但可以帮助您解决问题,而无需重写所有内容。

【讨论】:

我已经更改了所有 InstancePerRequest 范围,但这并没有解决问题。它停止了每次调用 Render 时发生的异常,但如果您有两个调用来呈现相同的视图,它仍然会发生 你能用异常的堆栈跟踪更新问题吗? 我添加了堆栈跟踪。看起来 AutoFac 会尝试创建 RequestLifetimeScope,即使在注册组件时未指定该范围 @Colin 我刚刚阅读了AutofacDependencyResolver 的代码,您可以实现自己的ILifetimeScopeProvider,但没有简单的方法可以找到当前的ILifetimeScope,除非将其存储在一些环境上下文中。这就是这里使用HttpContext 的原因。 ASP.net 核心不再是这种情况

以上是关于将 MVC 视图呈现为 Parallel.ForEach 中的字符串时的依赖注入问题的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET MVC 将部分视图呈现为字符串以返回 JSON [重复]

csharp 如果AcceptTypes包含“application / pdf”或querystring包含asPDF = 1,则过滤MVC4应用程序以将所有视图呈现为PDF。使用w

如何在 iPhone 中呈现 Mvc 移动视图?

没有呈现视图的 Spring MVC 请求

如何以 PDF 格式呈现 ASP.NET MVC 视图

在 ASP.NET MVC 中单击按钮时呈现部分视图