在WPF MVVM应用中管理DbContext

10

我已经烦恼了几天,仍然无法决定哪种方法是正确的。
这个问题特别针对 WPF ,因为与 Web 应用程序不同,许多在线文章和帖子推荐采用每个 view-model 对应一个 context 的方法,而不是每个 request 对应一个 context
我有一个使用 Entity-Framework DB first 模型的 WPF MVVM 应用程序。
下面是我应用程序中使用的两个模型的示例(由 EF 设计器创建):

public partial class User
{
    public User()
    {
        this.Role = new HashSet<Role>();
    }

    public string ID { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Role> Role { get; set; }
}

public class Role
{
    public Role()
    {
        this.User = new HashSet<User>();
    }

    public int ID { get; set; }
    public string Name { get; set; }

    public virtual ICollection<User> User { get; set; }
}
我已经将如何处理这个问题的选择缩小到以下几点:

1) 创建一个 DataAccess 类,在每次方法调用时创建和释放 DbContext

public class Dal
{
    public User GetUserById(object userId)
    {
        using (var db = new DbEntities())
        {
            return db.User.Find(userId);
            db.SaveChanges();
        }
    }

    public void RemoveUser(User userToRemove)
    {
        using (var db = new DbEntities())
        {
            db.User.Remove(userToRemove);
            db.SaveChanges();
        }
    }
}

我可以在我的ViewModel中使用它,如下所示:

public class UserManagerViewModel : ObservableObject
{
    private readonly Dal dal = new Dal();

    // models...
    //commands...
}

2) 类似于方法1,但是不使用 Using 语句:

public class Dal : IDisposable
{
    private readonly DbEntities db = new DbEntities();
    public User GetUserById(object userId)
    {
        return db.User.Find(userId);
        db.SaveChanges();

    }

    public void RemoveUser(User userToRemove)
    {
        db.User.Remove(userToRemove);
        db.SaveChanges();
    }

    public void Dispose()
    {
        db.SaveChanges();
    }
}

ViewModel 内部使用方法与上述相同。

3) 为每个 entity 创建一个 repository。看起来与上面的选项相同(也存在 using 的困扰),然而每个 repository 只包含与其 entity 相关的方法。据我所知,在我的 ViewModel 中使用方式与以上相同。

4) 创建一个 Unit-Of-Work 类,将根据需要传递适当的 Repository

public class UnitOfWork : IDisposable
{
    private DbEntities db = new DbEntities();

    private IUserRepository userRepository;
    public IUserRepository UserRepository
    {
        get
        {
            return userRepository ?? new UsersRepository(db);
        }
    }

    public void Save()
    {
        db.SaveChanges();
    }

    public void Dispose()
    {
        db.Dispose();
    }
}

然后在我的ViewModel中使用它,如下所示:

public class UserManagerViewModel : ObservableObject
{
    private readonly UnitOfWork unit = new UnitOfWork();

    // models...
    //commands...
}

在数据并发、更好的抽象和分层以及总体性能方面,上述方法中哪种(如果有)是首选?
编辑 -这篇文章中发现了以下段落:

在使用 Windows Presentation Foundation (WPF) 或 Windows Forms 时,每个表单都应该使用一个上下文实例。这样可以使用上下文提供的更改跟踪功能。

然而,这引出了一个问题,我应该在我的视图模型中创建一个 DbContext 对象,还是最好有一个类似于我的 DAL 类的实用程序类并引用它。


3
EF拥有出色的UoW(上下文)和仓库(DbSet)。为什么要自己创建呢?这些额外的层很少有帮助,它们往往会导致数据中心的应用程序,将业务逻辑拉到客户端,而不是任务中心的应用程序,在接近EF模型的服务中封装用例。这是抽象数据与抽象任务的区别(抽象意味着隐藏持久性实现)。 - Gert Arnold
1
请查看此链接:http://mehdi.me/ambient-dbcontext-in-ef6/ - ErikEJ
2
我们尝试了这两种方法,它们都有优点和缺点。没有一种通用的解决方案,你只需要找到最适合自己的方法。 话虽如此,我们决定采用第二种方法,并尽可能缩短上下文的生命周期,因为这样可以避免在同时打开多个视图(比如选项卡或窗口)和多个ViewModel以及多个上下文时出现问题。 - Daniel Sklenitzka
@DanielSklenitzka 感谢您的回复。您能否解释一下您是如何进行实际的数据库访问的?是通过在命令内直接查询,还是有某种存储所有查询的仓库? - Yoav
1
通常情况下,会有一种类似于 Dal 的业务逻辑层位于其间,由 ViewModels 调用。每个业务逻辑操作都会创建一个新的上下文,并使用该上下文完成所有必要的操作。 - Daniel Sklenitzka
显示剩余2条评论
2个回答

2
这就是依赖注入框架的设计初衷。是的,这又是一个需要添加到您的项目中的技术,但一旦您开始使用 DI,您就会爱不释手。
真正的问题在于,您正在尝试在视图模型中做出这个决定,而您真正应该采用控制反转并在更高层面上做出决策。WPF/MVVM 应用程序将希望每个表单拥有一个上下文,以便仅在用户完成编辑后提交更改,并且为用户提供取消更改的机会。我知道您没有在 Web 应用程序中使用它,但是一个良好设计的架构意味着您应该能够在其中使用,此时您将需要每个请求一个上下文。您可能想编写一个控制台应用程序实用程序,用于填充静态数据的数据库,在这种情况下,您可能需要全局/单例上下文以获得性能和易用性。最后,您的单元测试也需要模拟上下文,通常是每个测试一个。这四种情况都应该在您的注入框架中设置,并且您的视图模型既不需要知道也不关心任何一个。
以下是一个示例。我个人使用 Ninject,这是专门为 .NET 设计的。我还喜欢 NHibernate,尽管 ORM 的选择在这里无关紧要。我有具有不同作用域要求的会话对象,并且这些会话对象在初始化我的 ORM 类时在 Ninject 模块中设置:
var sessionBinding = Bind<ISession>().ToMethod(ctx =>
{
    var session = ctx.Kernel.Get<INHibernateSessionFactoryBuilder>()
        .GetSessionFactory()
        .OpenSession();
    return session;
});

if (this.SingleSession)
    sessionBinding.InSingletonScope();
else if (this.WebSession)
    sessionBinding.InRequestScope();
else
    sessionBinding.InScope(ScreenScope);

这将为ISession设置作用域,它是NHibernate中类似于您上下文类的对象。我的存储库类管理内存中的数据库对象,并包含与其关联的会话的引用:

public class RepositoryManager : IRepositoryManager
{
    [Inject]
    public ISession Session { get; set; }

    ... etc...
{

[Inject] 属性告诉 Ninject 使用我设置的作用域规则来自动填充这个字段。目前,这一切都发生在我的领域类中,但是它也扩展到了我的视图模型层。在我的作用域规则中,我传入了一个名为 "ScreenScope" 的对象,虽然我不会在这里进行详细解释,但基本上意味着每当我在 ScreenViewModel 中请求一个会话对象时,或者在其成员(包括它们自己的子级)中请求时,相同的 ISession 对象会自动创建并传递给所有这些对象。通过使用 DI 作用域,我甚至不必考虑它,只需声明带有 [Inject] 属性的成员即可。
public class ScreenViewModel
{
    [Inject] public CustomerService CustomerService { get; set; }
    [Inject] public SalesService SalesService { get; set; }
    [Inject] public BillService BillService { get; set; }
    ...etc...
}

这些服务类都包含一个已注入的RepositoryManager,由于它们都在ScreenViewModel中,因此ISession对象将是相同的,至少在我的WPF版本中是如此。如果我切换到我的MVC版本,则对于给定请求创建的所有视图模型都是相同的,如果我切换到控制台版本,则整个程序中使用相同的ISession。

简而言之:使用依赖注入并将上下文范围限制为每个表单一次。


我正在研究UI拓扑中的DI,并且想要为每个事件/命令(在WPF的情况下)创建上下文。不幸的是,我不知道如何实现这一点:(唯一我能想到的解决方案是将注入作为方法参数传递,但那样做相当糟糕。 - Bogdan Mart

0
在我之前使用MVVM和WPF的时候,我是每个VM都使用一个开放的上下文,但是一旦应用程序发展到更好地利用异步操作,我很快就遇到了DBContext线程安全性的问题。
虽然开发工作量更大,但现在我使用依赖注入来提供一个DBContextFactory(而不是DBContext本身)。我在VM中使用using语句创建一个上下文,通过EF的plinq调用填充observableCollections。这种方法的另一个性能优势是使用AsNoTracking()运行查询。让人烦恼的部分是管理新对象或修改后的对象与短暂上下文的重新附加:
shortDBContext.Attach(myEntity).State = EntityState.Added; // or modified
await shortDBContext.SaveChangesAsync();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接