Autofac:在一个基于.NET 4.8、使用OWIN的 C# ASP.NET 5 MVC应用中,如何为Hangfire正确创建一个生命周期作用域?

后端开发 2026-07-09

我们有一个遗留的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(Action1 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"
});

解决方案

好了,我已经找到了问题所在——不是 AutofacHangfire,而是我们如何使用 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;
    }
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章