繁体   English   中英

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

[英]MVVM + Services + Entity Framework and Dependency Injection vs Service Locator

我有很多系统使用WPF和MVVM。 对于单元测试,我们将依赖项注入View模型,但是我发现在构造时注入依赖类时,我们无法控制依赖对象的生命周期,例如Entity Framework 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.

我的公司做同样的事情,我们通过使用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需要为“using”关键字实现IDisposable才能在这里工作,但这应该是你需要的要点。

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

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

这是一种被广泛接受的模式,称为Command Query Responsibility Segregation或简称CQRS。 请参阅Martin Fowler关于模式的此CQRS文章或带有代码示例的Microsoft文章

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

根据最新编辑进行编辑

这种情况清除了很多关于你要完成什么的问题。

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

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

我心底的推荐,利用你的设计在一个终身感知的IoC容器,如Autofac。

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

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

你使用哪个DI框架? 使用Autofac,您可以使用LifeTimeScope。 可能其他框架具有类似的功能。

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

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

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

您应该使用factory来每次创建db上下文。 如果你想使用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;
            }
  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包装器,它将在处置时保存SaveChanges。 假设您正在使用某些请求/响应范例和每个请求的生命周期管理。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM