Autofac:在一个基于.NET 4.8、使用OWIN的 C# ASP.NET 5 MVC应用中,如何为Hangfire正确创建一个生命周期作用域?
我们有一个遗留的C#应用,基于.NET 4.8框架,使用ASP.NET MVC 5和 ASP.NET Web API 2,托管在OWIN(在IIS上)中的环境。由于Ninject的异步问题,我们正在从Ninject迁移到Autofac。
内部,它使用Hangfire进行作业管理,服务器和仪表板托管在同一个OWIN网站中。
我们在 StartUp.cs 中按照OWIN的要求创建了 HttpConfigration - 见问题底部。
对于主处理流水线,我们创建一个新的Autofac容器,并在使用的服务中注册 InstancePerLifetimeScope。
对于Hangfire,我们创建一个独立的Autofac容器,在使用的服务中注册 InstancePerBackgroundJob(由 Hangfire.Autofac 提供——它是一个带标签的作用域,名称为 "BackgroundJobScope")
几乎所有服务(在MVC/Web API请求管线和后台处理,即Hangfire处理中)都像以前在Ninject的 IoC设置中那样工作——然而现在我们在一个后台服务中遇到了一个错误,该后台服务使用Razor从 .cshtml 视图生成字符串以供邮件发送。
这是有问题的方法——异常发生在对 result.View.Render(viewContext, output); 的调用处
public static string GenerateHtmlOfView(string viewToRender, object model, Controller controller = null)
{
ControllerContext controllerContext = controller?.ControllerContext;
if (controllerContext == null)
{
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "FakeController");
ControllerContext fakeControllerContext = new ControllerContext(new HttpContextWrapper(new HttpContext(new HttpRequest(null, "https://stackoverflow.com", null), new HttpResponse(null))), routeData, new FakeController());
controllerContext = fakeControllerContext;
}
var result = ViewEngines.Engines.FindPartialView(controllerContext, viewToRender);
if (result.View == null)
{
throw new InvalidOperationException($"Cannot find view {viewToRender}");
}
using (StringWriter output = new StringWriter())
{
var viewData = controllerContext.Controller.ViewData;
viewData.Model = model;
var viewContext = new ViewContext(controllerContext, result.View, viewData, controllerContext.Controller.TempData, output);
result.View.Render(viewContext, output); // Exception occurs here
result.ViewEngine.ReleaseView(controllerContext, result.View);
return output.ToString();
}
}
抛出的异常为:
System.InvalidOperationException: The request lifetime scope cannot be created because the HttpContext is not available.
at Autofac.Integration.Mvc.RequestLifetimeScopeProvider.GetLifetimeScope(Action
1 configurationAction) at Autofac.Integration.Mvc.AutofacDependencyResolver.GetServices(Type serviceType) at System.Web.Mvc.DependencyResolverExtensions.GetServices[TService](IDependencyResolver resolver) at System.Web.Mvc.MultiServiceResolver.GetCombined[TService](IList1 items, IDependencyResolver resolver)
at System.Web.Mvc.ViewEngineCollection.get_CombinedItems()
at System.Web.Mvc.ViewEngineCollection.Find(Func`2 lookup, Boolean trackSearchedPaths)
at System.Web.Mvc.ViewEngineCollection.FindPartialView(ControllerContext controllerContext, String partialViewName)
at XXXX.Services.Helpers.RazorRenderHelpers.GenerateHtmlOfView(String viewToRender, Object model, Controller controller)
at XXXX.Services.UserNotificationService.SendPasswordExpiryEmails()
该方法在Ninject中一直工作正常,这意味着要么后台/Hangfire请求时HttpContext仍然可用(呃——这就不好了……),要么错误来自我们在Autofac/Hangfire中创建作用域的方式?
为什么在后台作业中会引用 Autofac.Mvc 集成 / AutofacDependencyResolver?为什么Hangfire不使用我们创建的独立容器?顺序是否有误?
这是启动配置:
public void Configuration(IAppBuilder app)
{
var builder = new ContainerBuilder();
var config = new HttpConfiguration();
builder.RegisterDatabases(); // InstancePerLifetimeScope
#region Identity Services
builder.RegisterIdentityForOWIN(app, Constants.ApplicationName); // InstancePerLifetimeScope
#endregion
#region Other Services
builder.RegisterServicesForWeb(); // InstancePerLifetimeScope
#endregion
#region MVC
var mvcAssembly = typeof(MvcApplication).Assembly;
// Register your MVC controllers.
builder.RegisterControllers(mvcAssembly)
.PropertiesAutowired();
// OPTIONAL: Register model binders that require DI.
builder.RegisterModelBinders(mvcAssembly);
builder.RegisterModelBinderProvider();
// OPTIONAL: Register web abstractions like HttpContextBase.
builder.RegisterModule<AutofacWebTypesModule>();
// OPTIONAL: Enable property injection in view pages.
builder.RegisterSource(new ViewRegistrationSource());
// Enables property injection on filters
builder.RegisterFilterProvider();
#endregion MVC
#region WebAPI
var currentAssembly = Assembly.GetExecutingAssembly();
// Register your API controllers.
builder.RegisterApiControllers(currentAssembly)
.PropertiesAutowired();
builder.RegisterWebApiFilterProvider(config);
builder.RegisterWebApiModelBinderProvider();
builder.RegisterType<System.Web.Http.AuthorizeAttribute>()
.AsSelf()
.InstancePerLifetimeScope()
.PropertiesAutowired();
#endregion MVC
config.Services.Add(typeof(IExceptionLogger), new Shared.Exceptions.FullExceptionLogger());
// Run other optional steps, like registering model binders,
// web abstractions, etc., then set the dependency resolver
// to be Autofac.
var container = builder.Build();
// For MVC
DependencyResolver.SetResolver(new Autofac.Integration.Mvc.AutofacDependencyResolver(container));
// For Web API
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
// Register the Autofac middleware FIRST, then the Autofac MVC middleware.
app.UseAutofacMiddleware(container);
app.Use(async (IOwinContext context, Func<System.Threading.Tasks.Task> next) =>
{
var scope = context.GetAutofacLifetimeScope(); // Scope from OWIN
var service = scope.Resolve<ICorrelationIdContext>();
context.Set("CorrelationId", service.CorrelationId);
await next();
});
app.UseAutofacMvc();
app.UseAutofacWebApi(config);
ConfigureAuth(app);
var hangfireContainer = AutofacHangfireContainer.CreateContainer(); // Create a completely different container with different service registration - InstancePerBackgroundJob()
var storageOptions = HangfireSetup.CreateHangfireStorageOptions();
Hangfire.GlobalConfiguration.Configuration
.UseAutofacActivator(hangfireContainer, true) // What should be passed in here to be a seperate container / scope from the MVC / HTTP context request?
.UseSqlServerStorage("HangfireDB", storageOptions);
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
WebApiConfig.Register(config);
app.UseWebApi(config); // Make this last after auth and routes etc.
}
以下是正在使用的版本:
- Hangfire 1.8.23
- Hangfire.Autofac 2.7.0
- Autofac 6.5.0
- Autofac.Mvc5 6.1.0
- Autofac.Mvc5.Owin 6.1.0
- Autofac.Owin 7.1.0
- Autofac.WebApi2 6.1.1
- Autofac.WebApi2.Owin 6.2.1
这个 问题 与此类似,但也没有给出答案……
编辑 - 这是方法的调用方式:
var emailViewPathname = "~/Views/Account/PasswordExpiryEmail.cshtml";
string body = RazorRenderHelpers.GenerateHtmlOfView(emailViewPathname, new PasswordExpiryEmailNotificationModel()
{
Username = user.UserName,
NumberOfDaysUntilExpiry = numberOfDaysUntilExpiry.GetValueOrDefault(0),
PasswordHasExpired = passwordHasExpired,
Title = passwordHasExpired ? "Your Password has expired" : "Your Password is due to expire",
ForgotPasswordLink = $"{pmsBaseUrl}/Account/ForgotPassword"
});
解决方案
好了,我已经找到了问题所在——不是 Autofac 或 Hangfire,而是我们如何使用 RazorViewEngine 渲染HTML。Autofac 只是暴露了这个问题,而 Ninject 以不同的方式处理了它。
解决办法是使用一个独立的 ViewEngineCollection,并配合一个自定义的 IViewPageActivator。Hangfire论坛上的这篇 帖子 帮助解决了这个问题。
为了完整性(并帮助下一个遇到同样问题的人),完整代码如下:
在一个新服务中更新的 GenerateHtmlOfView 方法(不再像以前那样是一个静态函数)
public string GenerateHtmlOfView(string viewToRender, object model, Controller controller = null)
{
ControllerContext controllerContext = controller?.ControllerContext;
if (controllerContext == null)
{
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "FakeController");
ControllerContext fakeControllerContext = new ControllerContext(
new HttpContextWrapper(new HttpContext(
new HttpRequest(null, "https://stackoverflow.com", null),
new HttpResponse(null))), routeData, new FakeController());
controllerContext = fakeControllerContext;
}
/*
This is change - _isolatedDependencyResolver is of type System.Web.Mvc.IDependencyResolver
and can be null if you wish
*/
var engines = new IsolatedViewEngineCollection(_isolatedDependencyResolver);
var result = engines.FindPartialView(controllerContext, viewToRender);
if (result.View == null)
{
throw new InvalidOperationException($"Cannot find view {viewToRender}");
}
using (StringWriter output = new StringWriter())
{
var viewData = controllerContext.Controller.ViewData;
viewData.Model = model;
var viewContext = new ViewContext(controllerContext,
result.View, viewData, controllerContext.Controller.TempData, output);
result.View.Render(viewContext, output);
result.ViewEngine.ReleaseView(controllerContext, result.View);
return output.ToString();
}
}
新的View引擎集合
public class IsolatedViewEngineCollection : ViewEngineCollection
{
public IsolatedViewEngineCollection(IDependencyResolver dependencyResolver)
: base(new IViewEngine[] {
new RazorViewEngine(
new IsolatedViewPageActivator(dependencyResolver))
})
{
// Set private _dependencyResolver to new value via reflection to prevent using DependencyResolver.Current
var field = typeof(ViewEngineCollection).GetField("_dependencyResolver",
BindingFlags.NonPublic | BindingFlags.Instance);
field?.SetValue(this, dependencyResolver);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
// Bypass any DependencyResolver
foreach (var engine in this)
{
var result = engine.FindView(controllerContext, viewName, masterName, true);
if (result.View != null)
{
return result;
}
}
return null;
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName)
{
foreach (var engine in this)
{
var result = engine.FindPartialView(controllerContext, partialViewName, false);
if (result.View != null)
{
return result;
}
}
return null;
}
}
新的IsolatedViewPageActivator —— 你甚至不需要 IDependencyResolver,你可以直接使用 Activator.CreateInstance(type)
public class IsolatedViewPageActivator : IViewPageActivator
{
private IDependencyResolver _dependencyResolver;
public IsolatedViewPageActivator(IDependencyResolver dependencyResolver)
{
_dependencyResolver = dependencyResolver;
}
public object Create(ControllerContext controllerContext, Type type)
{
object obj = null;
if (_dependencyResolver != null)
{
obj = _dependencyResolver.GetService(type);
}
if (obj == null)
{
obj = Activator.CreateInstance(type);
}
return obj;
}
}