Autofac(+MVC + EF + SignalR + Hangfire)生命周期范围

5

我有一个使用Entity Framework、SignalR和Hangfire任务的ASP.NET MVC项目。

我的主(根)容器定义如下:

builder.RegisterType<DbContext>().InstancePerLifetimeScope(); // EF Db Context
builder.RegisterType<ChatService>().As<IChatService>().SingleInstance(); // classic "service", has dependency on DbContext
builder.RegisterType<ChatHub>().ExternallyOwned(); // SignalR hub
builder.RegisterType<UpdateStatusesJob>().InstancePerDependency(); // Hangfire job
builder.RegisterType<HomeController>().InstancePerRequest(); // ASP.NET MVC controller
IContainer container = builder.Build();

我正在使用Autofac.MVC5 nuget软件包来实现MVC。依赖解析器:

DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

我正在使用Autofac.SignalR nuget包来处理SignalR。依赖解析器:

GlobalHost.DependencyResolver = new Autofac.Integration.SignalR.AutofacDependencyResolver(container);

我的 SignalR hub 是这样实例化的(http://autofac.readthedocs.org/en/latest/integration/signalr.html#managing-dependency-lifetimes):

private ILifetimeScope _hubScope;
protected IChatService ChatService;
public ChatHub(ILifetimeScope scope) {
  _hubScope = scope.BeginLifetimeScope(); // scope 
  ChatService = _hubScope.Resolve<IChatService>(); // this service is used in hub methods
}
protected override void Dispose(bool disposing)
{
  // Dipose the hub lifetime scope when the hub is disposed.
  if (disposing && _hubScope != null)
  {
    _hubScope.Dispose();
  }
  base.Dispose(disposing);
}

我正在使用Hangfire.Autofac包来使用Hangfire:

config.UseActivator(new AutofacJobActivator(container));

创建Job的方式如下:

private readonly ILifetimeScope _jobScope;
protected IChatService ChatService;
protected BaseJob(ILifetimeScope scope)
{
    _jobScope = scope.BeginLifetimeScope();
    ChatService = _jobScope.Resolve<IChatService>();
}
public void Dispose()
{
    _jobScope.Dispose();
}

问题/难题: 我总是在聊天室和工作中得到相同的DbContext实例。 我希望所有hub实例都能获取相同的ChatService,但是DbContext(它是ChatService的依赖项)将始终是新实例。 同样,Hangfire作业也应该表现出相同的特性。
这可以做到吗? 还是我有什么遗漏?
更新1:
经过思考(并且睡过觉),我认为我有两个选择。 我仍然希望保持“每个请求一个会话”(“每个hub一个会话”,“每个工作一个会话”)。
选项1:
更改所有服务将具有InstancePerLifetimeScope。 实例化服务并不昂贵。 对于维护某种状态的服务,我将创建另一个“存储”(类),它将是SingleInstance,并且不会依赖于会话(DbContext)。 我认为这也适用于聊天室和工作。
选项2:
创建一种工厂,如@Ric .Net建议的那样。 类似于下面这样:
public class DbFactory: IDbFactory
{
    public MyDbContext GetDb()
    {
        if (HttpContext.Current != null)
        {
            var db = HttpContext.Current.Items["db"] as MyDbContext;
            if (db == null)
            {
                db = new MyDbContext();
                HttpContext.Current.Items["db"] = db;
            }
            return db;
        }

        // What to do for jobs and hubs?
        return new MyDbContext();
    }
}

    protected void Application_EndRequest(object sender, EventArgs e)
    {
        var db = HttpContext.Current.Items["db"] as MyDbContext;
        if (db != null)
        {
            db.Dispose();
        }
    }

我认为这对于MVC来说是可行的,但我不知道如何使其适用于hubs(每个hub调用都是该hub的新实例)和jobs(每次运行作业都是该作业类的新实例)。
我倾向于选项1。你觉得呢?
非常感谢!

你不应该随意更改作用域。实际上,你根本不需要将其传递给构造函数。你的服务应该通过 DI 容器传递,并让 Autofac 处理你的作用域。 - ESG
你还应该仔细检查文档。 具有每个生命周期范围的组件将在嵌套生命周期范围中最多具有单个实例。 - ESG
@TheVedge:我同意你的观点。建议的样式(将容器传递给构造函数)来自文档:http://autofac.readthedocs.org/en/latest/integration/signalr.html#managing-dependency-lifetimes - rrejc
2个回答

4

我完全没有AutoFac的经验,但引起了我的注意的是:

我希望所有的hub实例都能得到相同的ChatService,但DbContext(ChatService的依赖项)将始终是一个新实例。

你基本上是在说:

“我的汽车由具有其车库依赖关系的同一汽车公司进行维护,但每次我带我的汽车时,我希望车库是一个新的。”

当你在某个其他组件中注入(完全构建的实例,包括依赖项)ChatService时,它所具有的其他依赖项当然也会被构建,无论它们是否具有其他类型的生命周期。当创建一个比它被注入的对象寿命更短的对象时,你已经创建了所谓的“captive dependency

获取ChatService中的新的DbContext的唯一方法不是注入DbContext本身,而是注入DbContextFactory,每当使用它时为你创建DbContext

一种实现方式可能如下:

public class DbContextFactory
{
    public DbContext Create()
    {
         return new DbContext();
    }
}

//usage:
public class ChatService
{
     private readonly DbContextFactory dbContextFactory;

     public ChatService(DbContextFactory dbContextFactory)
     {
         this.dbContextFactory = dbContextFactory;
     }

    public void SomeMethodInChatService()
    {
         using (var db = this.dbContextFactory.Create())
         {
             //do something with DbContext    
         }
     }
}
DbContextFactory 可以使用 Singleton Lifestyle 在 AutoFac 中注册。
然而,这可能不是您的目标。因为在这种情况下,每次使用 DbContext 都会得到一个新的 DbContext。另一方面,使用新的 DbContext 可能是最安全的方法,可以在这里阅读 here
这个伟大的答案值得阅读,有多个原因,因为它解释了如何使用 command / handler pattern,这对您的情况应该非常合适。
这将使您的 chatservice 完全不知道 DbContext,从而改善了应用程序的 'SOLID' 设计,并创建了测试 ChatService 的可能性,当直接注入 DbContextDbContextFactory 时,这是几乎不可行的。

@MattKocaj 我重新编辑了答案,因为在依赖注入中,生命周期管理时Lifestyle是正确的术语。 - Ric .Net
“Lifestyle” 不是正确的术语。只需在 Google 中搜索“Dependency injection”,您会发现使用的术语为[“lifetime”](https://en.wikipedia.org/wiki/Dependency_injection)和[“lifecycle”](https://dev59.com/Y3VC5IYBdhLWcg3w-WSs)。“Lifestyle” 应该是一个打字错误。 - Matt Kocaj
@MattKocaj 《依赖注入指南》一书提到了使用不同“生命周期”来管理对象的生存期。 - Ric .Net
虽然Seemann在他的模式集合中使用“*style”,但是这些模式(单例、瞬态等)在不同的容器中可能会有细微的实现差异。本问题涉及Autofac,因此针对该库的上下文,为了其他未读过Seemann书籍的读者着想,建议您使用最相关且有助于更广泛受众的术语,我认为是“生命周期”或“生存期”——这些术语也是Autofac文档中常用的术语。您在https://autofac.readthedocs.io中找不到“lifestyle”,我认为这是一个问题。 - Matt Kocaj
1
@MattKocaj:我不同意你的观点。Seemann的书已经成为依赖注入模式的事实标准参考。因此,我建议继续使用常用术语,就像我们会继续使用常用的模式名称,比如装饰器和代理。 - Steven

1
你需要解决一个工厂问题。 Autofac 内置支持 Func<T>,例如请参阅动态实例化
如果您的依赖项具有可释放的依赖项,则必须管理处理模式以避免内存泄漏。使用 Autofac 解决此问题的常见模式是使用 Func<Owned<T>>
public class ChatService
{
    public ChatService(Func<Owned<DbContext>> dbContextFactory)
    {
        this._dbContextFactory = dbContextFactory;
    }

    private readonly Func<Owned<DbContext>> _dbContextFactory;

    private void DoSomething()
    {
        using (Owned<DbContext> ownedDbContext = this._dbContextFactory())
        {
            DbContext context = ownedDbContext.Value;
        }
    }
}
Func<T>是一个工厂。每次调用该工厂时,autofac都会返回一个新的实例(取决于注册的生命周期如何配置)。 Owned<T>是一个轻量级的ILifetimescope,该类的主要目的是管理已解析组件的处理。 您可以在此处找到有关Func<Owned<T>>的更多信息:Owned<T>Func<T>结合使用

Owned<T> 接口的缺点是你的应用程序代码依赖于 DI 库,而这通常是我认为需要避免的。 - Steven
@Steven 我同意你的看法。虽然可以解决Func<DbContext>,但是不能保证在DbContext被释放时,它的直接或间接可释放依赖项也会被释放。据我所知,.Net框架中没有与Owned<T>等效的本地类。我修改了帖子以更好地解释Owned<T> - Cyril Durand
也许我对Autofac的工作方式有所误解,但是使用Owned<T>似乎是多余的。如果将DbContext注册为InstancePerLifetimeScope,那么Autofac应该在其周围的生命周期范围结束时自动处理掉该DbContext实例。换句话说,只需注入一个Func<DbContext>即可。然而,与Autofac的问题在于,如果没有活动的生命周期范围,DbContext将被解析为单例(在我看来是一种设计缺陷)。这使得它有点棘手。 - Steven
很抱歉,我的假设是这样工作的,因为 Func<DbContext> 是一个单例,就像 ChatService 一样。我假设 Func<DbContext> 将根据生命周期产生一个新实例,但经过一些尝试后,我不得不得出结论,当将 Func<T> 注入到单例中时,它的行为会有所不同。Autofac 中的 Func<T> 在注入单例时会产生一个单例,即使在生命周期范围内调用 DoSomething。在我看来,这真的很令人困惑。但这解释了为什么你必须依赖于 Autofac 中的 Owned<T> - Steven
在Autofac的上下文中,我无法回答关于“如何管理工厂创建的组件的处理”的问题。但是使用Simple Injector非常简单。您不需要做任何事情。Simple Injector将从活动范围中为您解析它(如果没有范围,则会引发异常),并且Simple Injector将在范围结束时处理您的实例。范围是环境的(就像TransactionScope一样),不必传递。这使生活变得更加轻松。 - Steven
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接