简体   繁体   English

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

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

I have many systems that use WPF with MVVM. 我有很多系统使用WPF和MVVM。 For unit testing we inject dependencies into the View Models, however I have found that when injecting the dependent class at construction time we cannot control the lifetime of the dependent object such as an Entity Framework DbContext. 对于单元测试,我们将依赖项注入View模型,但是我发现在构造时注入依赖类时,我们无法控制依赖对象的生命周期,例如Entity Framework DbContext。

A simple scenario is a follows: 一个简单的场景如下:

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();
    }
}

The VM needs to use the service so has it injected, the service needs an IEntityContext and thus has that injected. VM需要使用该服务,因此注入后,该服务需要一个IEntityContext ,因此注入了该服务。 The problem comes when in the VM we call SaveFoo and SaveBar , as the _entityContext object is dirty after a single call. 问题出现在VM中我们调用SaveFooSaveBar ,因为_entityContext对象在单次调用后是脏的。 Ideally we want to dispose of the _entityContext object after each call. 理想情况下,我们希望在每次调用后处理_entityContext对象。

The only way I've found round this is to use Dependency Injection to inject the container which then calls the code as follows: 我发现这一点的唯一方法是使用依赖注入注入容器,然后调用代码,如下所示:

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);
    }
}

In this way the container ( IInjector ) is acting like a service locator which works great, except is clunky for unit testing. 通过这种方式,容器( IInjector )就像一个服务定位器,它工作得很好,除了单元测试的笨重之外。 Is there a better way to manage this? 有没有更好的方法来管理这个? I understand that doing this pretty much voids all the benefits of Dependency Injection, but I can't figure another way. 我知道这样做几乎会使依赖注入的所有好处都无效,但我无法想到另一种方式。

EDIT: Further Example 编辑:进一步的例子

Say you have a window with two buttons. 假设你有一个带两个按钮的窗口。 One service sits behind it which has been injected via dependency injection. 一个服务位于它后面,通过依赖注入注入。 You click button A and it loads an object, modifies it, and saves, however this fails (for some reason, lets say some validation fails in the DbContext), you show a nice message. 您单击按钮A并加载一个对象,修改它并保存,但是这会失败(出于某种原因,假设某些验证在DbContext中失败),您会显示一条好消息。

Now you click button 2. It loads a different object and modifies it and tries to save, now because the first button was pressed, and the service is the same service, with the same context, this operation will fail for the same reason as when clicking button A. 现在你点击按钮2.它加载一个不同的对象并修改它并尝试保存,现在因为按下第一个按钮,并且服务是相同的服务,具有相同的上下文,此操作将失败的原因与点击按钮A.

My company does the same thing as you are asking, and we solve it by using the Repository and UnitOfWorkFactory patterns. 我的公司做同样的事情,我们通过使用Repository和UnitOfWorkFactory模式来解决它。

A simpler version of this would look something like so: 一个更简单的版本看起来像这样:

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();
        }
    }
}

And the factory: 而工厂:

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);
    }
}

Your IEntityContext needs to implement IDisposable for the "using" keyword to work here, but this should be the gist of what you need. 你的IEntityContext需要为“using”关键字实现IDisposable才能在这里工作,但这应该是你需要的要点。

As also pointed out by @ValentinP, I also believe you are going down the wrong path but for a different reason. 正如@ValentinP所指出的那样,我也相信你会走错路,但出于不同的原因。

If you do not want to pollute the state tracking in the DbContext instance for your persistence methods with the objects that have already been retrieved during database queries then you need a redesign your application and split your business logic into 2 logical tiers. 如果您不希望在DbContext实例中使用在数据库查询期间已检索到的对象的持久性方法污染状态跟踪,则需要重新设计应用程序并将业务逻辑拆分为2个逻辑层。 One tier for retrieval and one for persistence, each tier would use its own instance of a DbContext , this way you never have to worry about objects that were retrieved and manipulated accidentally being persisted back with another operation ( I assume this is why you asked the question ). 一层用于检索,一层用于持久性,每层都使用自己的DbContext实例,这样你就不必担心被意外检索和操作的对象被另一个操作持久化( 我假设这就是你问的原因)问题 )。

This is widely accepted pattern called Command Query Responsibility Segregation or CQRS for short. 这是一种被广泛接受的模式,称为Command Query Responsibility Segregation或简称CQRS。 See this CQRS article by Martin Fowler on the pattern or this Microsoft article with code samples. 请参阅Martin Fowler关于模式的此CQRS文章或带有代码示例的Microsoft文章

Using this pattern you can dispose of the DbContext instances (directly or indirectly via the Dispose of the root owning object). 使用此模式,您可以处置DbContext实例(直接或间接通过根拥有对象的Dispose)。

Edit based on latest edit 根据最新编辑进行编辑

That scenario clears up a lot of questions on what you are trying to accomplish. 这种情况清除了很多关于你要完成什么的问题。

  1. I stand by the option of implementing CQRS as I still believe it is applicable. 我坚持实施CQRS的选择,因为我仍然相信它是适用的。
  2. It is a common approach to not use long lived DbContext instances in applications. 在应用程序中不使用长寿DbContext实例是一种常见方法。 Create one when you need it and then dispose of it when you are done with it. 在需要时创建一个,然后在完成后将其丢弃。 There is minimal overhead in the creation/disposal of a DbContext object itself. 创建/处理DbContext对象本身的开销很小。 You should then re-attach any modified models / collections to the new DbContext where you are looking to persist the changes, there is no reason to re-retrieve them from the underlying store. 然后,您应该将任何已修改的模型/集合重新附加到您希望保留更改的新DbContext ,没有理由从底层存储中重新检索它们。 If a failure then occurs the entry point to that part of the code (either in the service layer or the presentation layer) should handle the error (display message, revert changes, etc). 如果发生故障,则代码的该部分的入口点(在服务层或表示层中)应该处理错误(显示消息,恢复更改等)。 Concurrency exceptions (using TimeStamp/Rowversion) are also handled correctly using this approach. 使用此方法也可以正确处理并发异常(使用TimeStamp / Rowversion)。 Also because you used a new DbContext you do not have to worry about other commands that could also be carried out on the same view failing if they try to execute something independent. 另外,因为您使用了新的DbContext ,所以如果他们尝试执行独立的操作,您也不必担心也可能在同一视图上执行的其他命令失败。

You should be able to specify the life time scope of each object you are injecting. 您应该能够指定要注入的每个对象的生命周期范围。 For your IEntityContext you could specify Transient (which is the default) and inject it into the appropriate service layer constructor. 对于您的IEntityContext您可以指定Transient (这是默认值)并将其注入适当的服务层构造函数。 Each instance of IEntityContext should have exactly one owner / root. IEntityContext每个实例应该只有一个所有者/ root。 If you use a CQRS pattern this becomes a little easier to manage. 如果您使用CQRS模式,这将变得更容易管理。 If you are using something like a DDD pattern it becomes a little more convoluted but still doable. 如果你使用类似DDD模式的东西,它会变得有点复杂,但仍然可行。 Alternatively you could also specify life time scope at Thread level although I would not recommend this as it could introduce a lot of unexpected side effects if you ever forget this and try to add some parallel programming or using the async/await pattern without recapturing the original thread context. 或者你也可以在线程级别指定生命时间范围,虽然我不建议这样做,因为如果你忘记了这一点并尝试添加一些并行编程或使用async / await模式而不重新获取原始版本它会引入许多意想不到的副作用线程上下文。

My recomendation from the bottom of my heart, leverage your design on a lifetime aware IoC container like Autofac. 我心底的推荐,利用你的设计在一个终身感知的IoC容器,如Autofac。

Take a look of this to know how to control the lifetime even with IoC: http://autofac.readthedocs.org/en/latest/lifetime/instance-scope.html 看一下即使使用IoC也知道如何控制生命周期: http//autofac.readthedocs.org/en/latest/lifetime/instance-scope.html

If you need more details about how to implement this for your purpose, complain with me here. 如果您需要有关如何实现此目的的更多详细信息,请在此处与我联系。

Which DI framework are you using? 你使用哪个DI框架? With Autofac you have something called LifeTimeScope. 使用Autofac,您可以使用LifeTimeScope。 Probably other frameworks have similar functionality. 可能其他框架具有类似的功能。

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

Basically, you'd need to identify what's the Unit of Work on your application (Each ViewModel instance? Each ViewModel action? ), and have a new LifeTimeScope for each UoW, and resolve your dependencies with the lifetime scope. 基本上,您需要确定应用程序上的工作单元(每个ViewModel实例?每个ViewModel操作?),并为每个UoW创建一个新的LifeTimeScope,并使用生命周期范围解决依赖关系。 Depending on your implementation it might end up looking more like a service locator, but it makes managing the lifetime of the dependencies relatively easy. 根据您的实现,它可能最终看起来更像服务定位器,但它使得管理依赖项的生命周期相对容易。 (If you register the DBContext as PerLifeTimeScope, you can be sure that all the dependencies resolved in the same lifetime scope will share the same dbcontext,and that it will not be shared for dependencies resolved with another lifetimescope). (如果将DBContext注册为PerLifeTimeScope,则可以确保在同一生命周期范围内解析的所有依赖项将共享相同的dbcontext,并且不会为使用其他lifetimescope解析的依赖项共享它。

Plus, since the lifetimescopes implements an interface, it can be easily mocked to resolved mock services for unit test purposes. 此外,由于lifetimescopes实现了一个接口,因此可以轻松地模拟解析模拟服务以进行单元测试。

You should use factory to create db context for every time. 您应该使用factory来每次创建db上下文。 If you want to use Autofac , It has already auto generated factory for this. 如果你想使用Autofac ,它已经为此自动生成了工厂。 You can use Dynamic Instantiation to create dbcontext every time. 您可以使用Dynamic Instantiation每次创建dbcontext。 You can use Controlled Lifetime to manage lifetime of dbcontext for yourself. 您可以使用Controlled Lifetime自行管理dbcontext的生命周期。 If you combine both, you will have dbcontext everytime and you will manage life time in method (Dispose it yourself). 如果你将两者结合起来,你每次都会有dbcontext,你将在方法中管理生命时间(自己配置)。

While you are testing you will just registered mocked instance of IEntityContext . 在测试时,您将只注册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();
            }
        }
    }

If you want to manage all your dbcontexts life time, we can remove Owned and we can register your context ExternallyOwned . 如果您想管理所有dbcontexts的生命周期,我们可以删除Owned ,我们可以注册您的上下文ExternallyOwned That means autofac will not handle lifetime of this object. 这意味着autofac不会处理此对象的生命周期。

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

Then your field and constructor should be like this: 那么你的字段和构造函数应该是这样的:

private readonly Func<IEntityContext> _entityContext;

            public BarService(Func<IEntityContext> entityContext)
            {
                _entityContext = entityContext;
            }
  1. I think it's a bad practice to create and dispose DbContext every time. 我认为每次创建和处理DbContext都是不好的做法。 It seems to be very performance-costly. 它似乎非常昂贵。
  2. Thus, don't you want to extract SaveChanges method? 那么,你不想提取SaveChanges方法吗? It will just call SaveChanges on DbContext. 它只会在DbContext上调用SaveChanges。
  3. If you can not do that, I consider to create a ContextFactory is a better way instead of Service Locator. 如果你不能这样做,我认为创建一个ContextFactory是一种更好的方式而不是Service Locator。 I know that for example Windsor can auto-generate factory implementations for given interface ( http://docs.castleproject.org/Default.aspx?Page=Typed-Factory-Facility-interface-based-factories&NS=Windsor ). 我知道例如Windsor可以为给定的接口自动生成工厂实现( http://docs.castleproject.org/Default.aspx?Page=Typed-Factory-Facility-interface-based-factories&NS=Windsor )。 It's better semantically and for testing purposes. 它在语义上更好,并且用于测试目的。 The focus here is in transparent factory interface, which implementation is based on IoC configuration and lifitime policies. 这里的重点是透明的工厂界面,该实现基于IoC配置和生命周期策略。
  4. Finally, if you are not interested in immediate changes pushing, you may create IDisposable DbContext wrapper, which will SaveChanges on disposing. 最后,如果您对即时更改推送不感兴趣,可以创建IDisposable DbContext包装器,它将在处置时保存SaveChanges。 Assuming you are using some request/response paradigm and per request lifetime management. 假设您正在使用某些请求/响应范例和每个请求的生命周期管理。

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

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