MVVM + 服务 + 实体框架和依赖项注入与服务定位器

本文关键字:服务 注入 定位器 实体 框架 MVVM 依赖 | 更新日期: 2023-09-27 18:20:56

我有许多将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,因此注入了该服务。当我们在 VM 中调用 SaveFooSaveBar 时出现问题,因为_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 时相同。

MVVM + 服务 + 实体框架和依赖项注入与服务定位器

我的公司做与您要求相同的事情,我们使用存储库和 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"关键字在此处工作,但这应该是您需要的要点。

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

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

这是被广泛接受的模式,称为命令查询责任分离或简称 CQRS。请参阅 Martin Fowler 撰写的有关模式的 CQRS 文章或包含代码示例的这篇Microsoft文章。

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

根据最新编辑进行编辑

这种情况澄清了很多关于您要完成的任务的问题。

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

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

我发自内心的建议是,在像Autofac这样的终身感知IoC容器上利用您的设计。

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

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

你使用的是哪个 DI 框架?使用Autofac,您就有了称为LifeTimeScope的东西。可能其他框架具有类似的功能。

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

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

此外,由于 lifescopes 实现了一个接口,因此可以轻松地将其模拟为已解析的模拟服务以进行单元测试。

您应该每次都使用工厂创建数据库上下文。如果你想使用Autofac,它已经为此自动生成了工厂。每次都可以使用动态实例化来创建数据库上下文。您可以使用受控生存期为自己管理 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();
            }
        }
    }

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

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

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

private readonly Func<IEntityContext> _entityContext;
            public BarService(Func<IEntityContext> entityContext)
            {
                _entityContext = entityContext;
            }
  1. 我认为每次都创建和处置 DbContext 是一种不好的做法。这似乎非常注重性能。
  2. 因此,您不想提取保存更改方法吗?它只会在 DbContext 上调用 SaveChanges。
  3. 如果你不能做到这一点,我认为创建一个上下文工厂是一个更好的方法,而不是服务定位器。我知道例如温莎可以为给定接口自动生成工厂实现(http://docs.castleproject.org/Default.aspx?Page=Typed-Factory-Facility-interface-based-factories&NS=Windsor(。它在语义上和用于测试目的更好。这里的重点是透明的工厂接口,该接口的实现基于 IoC 配置和 lifitime 策略。
  4. 最后,如果您对立即推送更改不感兴趣,则可以创建 IDisposable DbContext 包装器,它将在释放时保存更改。假设您正在使用某种请求/响应范例和每个请求生存期管理。