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

时间:2014-09-05 11:30:08

标签: c# entity-framework mvvm dependency-injection 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时的原因。

6 个答案:

答案 0 :(得分:5)

我的公司做同样的事情,我们通过使用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”关键字,但这应该是您需要的要点。

答案 1 :(得分:1)

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

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

这是一种被广泛接受的模式,称为Command Query Responsibility Segregation或简称CQRS。请参阅Martin Fowler对模式的CQRS articlethis Microsoft article的代码示例。

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

根据最新修改进行修改

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

  1. 我坚持选择实施CQRS,因为我仍然相信它是适用的。
  2. 在应用程序中不使用长期DbContext实例是一种常见方法。在需要时创建一个,然后在完成后将其丢弃。创建/处理DbContext对象本身的开销很小。然后,您应该将任何已修改的模型/集合重新附加到您希望保留更改的新DbContext,没有理由从底层存储中重新检索它们。如果发生故障,则代码的该部分的入口点(在服务层或表示层中)应该处理错误(显示消息,恢复更改等)。使用此方法也可以正确处理并发异常(使用TimeStamp / Rowversion)。另外,因为您使用了新的DbContext,所以如果他们尝试执行独立的操作,您不必担心也可能在同一视图上执行的其他命令失败。
  3. 您应该能够指定要注入的每个对象的生命期范围。对于您的IEntityContext,您可以specify Transient(这是默认设置)并将其注入相应的服务层构造函数。 IEntityContext的每个实例应该只有一个所有者/ root。如果您使用CQRS模式,这将变得更容易管理。如果你使用类似DDD模式的东西,它会变得有点复杂,但仍然可行。或者你也可以在线程级别指定生命时间范围,虽然我不建议这样做,因为如果你忘记了这一点并尝试添加一些并行编程或使用async / await模式而不重新获取原始版本它会引入许多意想不到的副作用线程上下文。

答案 2 :(得分:1)

我心底的建议,将您的设计用于像Autofac这样的终身感知IoC容器。

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

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

答案 3 :(得分:1)

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

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

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

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

答案 4 :(得分:1)

您应该每次都使用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;
            }

答案 5 :(得分:0)

  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。假设您正在使用某些请求/响应范例和每个请求的生命周期管理。