【问题标题】:MVVM + Services + Entity Framework and Dependency Injection vs Service LocatorMVVM + 服务 + 实体框架和依赖注入 vs 服务定位器
【发布时间】:2014-10-30 08:24:45
【问题描述】:

我有许多将 WPF 与 MVVM 结合使用的系统。对于单元测试,我们将依赖项注入到视图模型中,但是我发现在构建时注入依赖类时,我们无法控制依赖对象的生命周期,例如实体框架 DbContext。

一个简单的场景如下:

public class FooVM
{
    private readonly IBarService _barService;

    // Set in the UI via Databinding
    public string Name { get; set; }
    public string OtherName { get; set; }

    public FooVM(IBarService barService)
    {
        _barService = barService;
    }

    public void SaveFoo()
    {
        _barService.SaveFoo(Name);
    }

    public void SaveBar()
    {
        _barService.SaveBar(OtherName);
    }
}

public class BarService : IBarService
{
    private readonly IEntityContext _entityContext;

    public BarService(IEntityContext entityContext)
    {
        _entityContext = entityContext;
    }

    public void SaveFoo(string name)
    {
        // some EF stuff here
        _entityContext.SaveChanges();
    }

    public void SaveBar(string otherName)
    {
        // some EF stuff here
        _entityContext.SaveChanges();
    }
}

VM 需要使用该服务,因此已将其注入,该服务需要 IEntityContext,因此已注入该服务。问题出现在我们调用SaveFooSaveBar 的VM 中,因为_entityContext 对象在一次调用后是脏的。理想情况下,我们希望在每次调用后处理 _entityContext 对象。

我找到的唯一方法是使用依赖注入来注入容器,然后调用代码如下:

public class FooVM
{
    private readonly IInjector _injector;

    // Set in the UI via Databinding
    public string Name { get; set; }
    public string OtherName { get; set; }

    public FooVM(IInjector injector)
    {
        _injector = injector;
    }

    public void SaveFoo()
    {
        var barService = _injector.GetUniqueInstance<IBarService>();
        barService.SaveFoo(Name);
    }

    public void SaveBar()
    {
        var barService = _injector.GetUniqueInstance<IBarService>();
        barService.SaveBar(OtherName);
    }
}

通过这种方式,容器 (IInjector) 就像一个服务定位器,效果很好,除了对于单元测试来说很笨重。有没有更好的方法来管理这个?我知道这样做几乎会使依赖注入的所有好处无效,但我想不出另一种方法。

编辑:进一步的例子

假设您有一个带有两个按钮的窗口。一项服务位于其后面,该服务已通过依赖注入注入。您单击按钮 A 并加载一个对象,对其进行修改并保存,但是这会失败(由于某种原因,假设 DbContext 中的某些验证失败),您会显示一条很好的消息。

现在您单击按钮 2。它加载了一个不同的对象并对其进行了修改并尝试保存,现在因为第一个按钮被按下,并且服务是相同的服务,具有相同的上下文,此操作将失败原因与单击按钮 A 时一样。

【问题讨论】:

标签: c# entity-framework mvvm dependency-injection service-locator


【解决方案1】:
  1. 我认为每次都创建和处理 DbContext 是一种不好的做法。这似乎非常耗费性能。
  2. 因此,您不想提取 SaveChanges 方法吗?它只会在 DbContext 上调用 SaveChanges。
  3. 如果你不能这样做,我认为创建一个 ContextFactory 是一个更好的方法,而不是 Service Locator。我知道例如 Windsor 可以为给定的接口 (http://docs.castleproject.org/Default.aspx?Page=Typed-Factory-Facility-interface-based-factories&NS=Windsor) 自动生成工厂实现。它在语义上和测试目的上更好。这里的重点是透明的工厂接口,该接口的实现基于 IoC 配置和生命周期策略。
  4. 最后,如果您对即时更改推送不感兴趣,您可以创建 IDisposable DbContext 包装器,它会在处理时保存更改。假设您正在使用一些请求/响应范例和每个请求的生命周期管理。

【讨论】:

  • 每次都创建和处理 DbContext 是不好的做法not。工厂与服务定位器有何不同?
  • 好吧,我记不太清了,但是“每次”是指每个请求多次创建上下文。工厂(具有自定义非通用接口,提供上下文创建方法)更适合 1)它具有强大的语义,2)类依赖关系似乎更清晰,3)有时它更便于测试。另外,Factory 被认为是依赖关系,所以这篇文章martinfowler.com/articles/injection.html 就是关于它的。 Windsor 自动生成工厂是将自定义界面与容器能力相结合的完美解决方案。
  • @RyanAmies - "How is a factory any different from a service locator?" 使用工厂时,您可以使用 Intellisense 查看对象构造函数,以准确了解您需要配置的类型。使用服务定位器时,您必须打开源代码并对其进行分析以找出需要哪些依赖项。考虑到您需要配置一个类(DI、单元测试、集成测试等)的次数乘以您在每个类中使用服务定位器并忘记其依赖关系的次数,这会导致大量浪费人力应用程序配置小时数。
【解决方案2】:

正如@ValentinP 也指出的那样,我也相信你走错了路,但出于不同的原因。

如果您不想使用在数据库查询期间已检索到的对象污染持久性方法中 DbContext 实例中的状态跟踪,那么您需要重新设计您的应用程序并将您的业务逻辑拆分为 2 个逻辑层.一层用于检索,一层用于持久化,每一层都将使用自己的DbContext 实例,这样您就不必担心意外检索和操作的对象会被另一个操作持久化(我假设这个这就是你问这个问题的原因)。

这是一种被广泛接受的模式,简称为命令查询职责分离或 CQRS。请参阅 Martin Fowler 的 CQRS article 模式或 this Microsoft article 以及代码示例。

使用此模式,您可以处置 DbContext 实例(直接或间接通过根拥有对象的处置)。

根据最新编辑进行编辑

这个场景解决了很多关于你想要完成什么的问题。

  1. 我支持实施 CQRS 的选项,因为我仍然相信它是适用的。
  2. 在应用程序中不使用长期存在的DbContext 实例是一种常见的方法。在需要时创建一个,然后在完成后将其丢弃。 DbContext 对象本身的创建/处理开销最小。然后,您应该将任何修改过的模型/集合重新附加到新的DbContext,您希望在其中保留更改,没有理由从基础存储中重新检索它们。如果发生故障,则该部分代码(在服务层或表示层中)的入口点应处理错误(显示消息,恢复更改等)。并发异常(使用 TimeStamp/Rowversion)也可以使用这种方法正确处理。此外,因为您使用了新的DbContext,您不必担心如果其他命令尝试执行独立的操作,也可能在同一视图上执行失败。

您应该能够指定要注入的每个对象的生命周期范围。对于您的IEntityContext,您可以specify Transient(这是默认设置)并将其注入到适当的服务层构造函数中。 IEntityContext 的每个实例都应该只有一个所有者/根。如果您使用 CQRS 模式,这将变得更容易管理。如果您使用的是 DDD 模式之类的东西,它会变得更加复杂,但仍然可行。或者,您也可以在线程级别指定生命周期范围,尽管我不推荐这样做,因为如果您忘记了这一点并尝试添加一些并行编程或使用异步/等待模式而不重新捕获原始模式,它可能会引入很多意想不到的副作用线程上下文。

【讨论】:

  • 你对我为什么问这个问题的假设是错误的。分离检索/持久性代码没有问题。问题是来自失败的持久性请求的污染。请参阅我编辑的问题以了解更多情况,但是您对 HttpRequests 的评论使我得出结论,您没有阅读该问题。
  • @RyanAmies - 我不知道它是如何滑入其中的(httprequest)我已经阅读了你的问题。感谢您澄清您要实现的目标。
【解决方案3】:

我的公司按照您的要求做同样的事情,我们通过使用 Repository 和 UnitOfWorkFactory 模式来解决它。

一个更简单的版本看起来像这样:

public class BarService : IBarService
{
    private readonly IEntityContextFactory _entityContextFactory;

    public BarService(IEntityContextFactory entityContextFactory)
    {
        _entityContextFactory = entityContextFactory;
    }

    public void SaveFoo(string name)
    {
        using (IEntityContext entityContext = _entityContextFactory.CreateEntityContext())
        {
            // some EF stuff here
            entityContext.SaveChanges();
        }
    }

    public void SaveBar(string otherName)
    {
        using (IEntityContext entityContext = _entityContextFactory.CreateEntityContext())
        {
            // some EF stuff here
            _entityContext.SaveChanges();
        }
    }
}

还有工厂:

public class EntityContextFactory : IEntityContextFactory
{
    private readonly Uri _someEndpoint = new Uri("http://somwhere.com");

    public IEntityContext CreateEntityContext()
    {
        // Code that creates the context.
        // If it's complex, pull it from your bootstrap or wherever else you've got it right now.
        return new EntityContext(_someEndpoint);
    }
}

您的 IEntityContext 需要实现 IDisposable 以使“using”关键字在此处起作用,但这应该是您需要的要点。

【讨论】:

  • 我实际上认为这可能是最好的方法。您仍然看不到依赖项,因为它没有注入,但至少它只是一个依赖项
  • 这里的想法是依赖关系被颠倒了。您仍然可以“注入”将用于访问上下文的对象,并且服务本身没有对具体实现的任何引用。当您想控制对象的生命周期,但不知道如何“创建”对象时,工厂确实是唯一干净的方法。有些人拉入他们的服务定位器服务并以这种方式创建对象,但我并不真正喜欢这种方式,因为它将您与特定的服务定位器耦合。
【解决方案4】:

我发自内心的建议,在 Autofac 等具有终生意识的 IoC 容器上利用您的设计。

看看这个了解如何使用 IoC 来控制生命周期:http://autofac.readthedocs.org/en/latest/lifetime/instance-scope.html

如果您需要有关如何实现此目的的更多详细信息,请在此处向我投诉。

【讨论】:

    【解决方案5】:

    您使用的是哪个 DI 框架?使用 Autofac 你有一个叫做 LifeTimeScope 的东西。可能其他框架也有类似的功能。

    http://docs.autofac.org/en/latest/lifetime/index.html

    基本上,您需要确定应用程序上的工作单元是什么(每个 ViewModel 实例?每个 ViewModel 操作?),并为每个 UoW 设置一个新的 LifeTimeScope,并使用生命周期范围解决您的依赖关系。根据您的实现,它最终可能看起来更像一个服务定位器,但它使管理依赖项的生命周期相对容易。 (如果您将 DBContext 注册为 PerLifeTimeScope,则可以确保在同一生命周期范围内解析的所有依赖项将共享相同的 dbcontext,并且不会与另一个生命周期范围解析的依赖项共享)。

    此外,由于生命周期范围实现了一个接口,因此可以轻松地对其进行模拟以解析模拟服务以进行单元测试。

    【讨论】:

    • Ninject,但问题不在于 DI 框架提供的依赖项的生命周期,它在什么时候需要这些依赖项
    • @RyanAmies 好吧,据我所知,WPF 没有提供任何机制来正确实现 IoC。如果您想解决每个操作的依赖关系,您别无选择,只能像第二个代码示例中那样手动执行。我仍然建议使用对象范围而不是根容器来解决依赖关系。这是 Ninject 等效的 github.com/ninject/ninject/wiki/Object-Scopes
    【解决方案6】:

    您应该每次都使用工厂创建数据库上下文。如果你想使用Autofac,它已经为此自动生成了工厂。每次都可以使用Dynamic Instantiation 创建dbcontext。您可以使用Controlled Lifetime 自己管理 dbcontext 的生命周期。如果将两者结合起来,您将每次都拥有 dbcontext,并且您将在方法中管理生命周期(自行处理)。

    在您进行测试时,您将只注册 IEntityContext 的模拟实例。

    public class BarService : IBarService
        {
            private readonly Func<Owned<IEntityContext>> _entityContext;
    
            public BarService(Func<Owned<IEntityContext>> entityContext)
            {
                _entityContext = entityContext;
            }
    
            public void SaveFoo(string name)
            {
                using (var context = _entityContext())
                {
                    context.SaveChanges();
                }
            }
    
            public void SaveBar(string otherName)
            {
                using (var context = _entityContext())
                {
                    context.SaveChanges();
                }
            }
        }
    

    如果您想管理您所有的 dbcontexts 生命周期,我们可以删除 Owned,我们可以注册您的上下文 ExternallyOwned。这意味着 autofac 不会处理这个对象的生命周期。

    builder.RegisterType<EntityContext>().As<IEntityContext>().ExternallyOwned();
    

    那么你的字段和构造函数应该是这样的:

    private readonly Func<IEntityContext> _entityContext;
    
                public BarService(Func<IEntityContext> entityContext)
                {
                    _entityContext = entityContext;
                }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-01-21
      • 1970-01-01
      • 1970-01-01
      • 2015-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多