用自定义的DbSet/IDbSet包装DbSet<TEntity>?

18
首先,我认为这样做有点荒谬,但我的团队其他成员坚持要这么做,而我无法提出好的反对理由,除了“我觉得这很蠢”...
我们试图做的是创建一个完全抽象的数据层,然后有不同的数据层实现。这很简单,对吧? 进入Entity Framework 4.1...
我们的最终目标是程序员(我尽力只留在数据层)从未想要暴露给具体类。他们只想在代码中使用接口,除了明显需要实例化工厂之外。
我想要实现以下内容:
首先,我们有所有接口的“Common”库,我们将其称为“Common.Data”:
public interface IEntity
{
    int ID { get; set; }
}

public interface IUser : IEntity
{
    int AccountID { get; set; }
    string Username { get; set; }
    string EmailAddress { get; set; }
    IAccount Account { get; set; }
}

public interface IAccount : IEntity
{
    string FirstName { get; set; }
    string LastName { get; set; }
    DbSet<IUser> Users { get; set; } // OR IDbSet<IUser> OR [IDbSet implementation]?
}

public interface IEntityFactory
{
    DbSet<IUser> Users { get; }
    DbSet<IAccount> Accounts { get; }
}

从那里,我们就有了一个实现库,我们称其为 "Something.Data.Imp":

internal class User : IUser
{
    public int ID { get; set; }
    public string Username { get; set; }
    public string EmailAddress { get; set; }
    public IAccount Account { get; set; }

    public class Configuration : EntityTypeConfiguration<User>
    {
        public Configuration() : base()
        {
             ...
        }
    }
}

internal class Account : IAccount
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DbSet<IUser> Users { get; set; } // OR IDbSet<IUser> OR [IDbSet implementation]?

    public class Configuration : EntityTypeConfiguration<Account>
    {
        public Configuration() : base()
        {
             ...
        }
    }
}

工厂:

public class ImplEntityFactory : IEntityFactory
{
    private ImplEntityFactory(string connectionString) 
    {
        this.dataContext = new MyEfDbContext(connectionString);
    }
    private MyEfDbContext dataContext;

    public static ImplEntityFactory Instance(string connectionString)
    {
        if(ImplEntityFactory._instance == null)
            ImplEntityFactory._instance = new ImplEntityFactory(connectionString);

        return ImplEntityFactory._instance;
    }
    private static ImplEntityFactory _instance;

    public DbSet<IUser> Users // OR IDbSet<IUser> OR [IDbSet implementation]?
    { 
        get { return dataContext.Users; }
    }

    public DbSet<IAccount> Accounts // OR IDbSet<IUser> OR [IDbSet implementation]?
    {
        get { return dataContext.Accounts; }
    }
}

背景:

public class MyEfDataContext : DbContext
{
    public MyEfDataContext(string connectionString)
        : base(connectionString)
    {
        Database.SetInitializer<MyEfDataContext>(null);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new User.Configuration());
        modelBuilder.Configurations.Add(new Account.Configuration());
        base.OnModelCreating(modelBuilder);
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Account> Accounts { get; set; }
}

然后前端程序员将会这样使用它:

public class UsingIt
{
    public static void Main(string[] args)
    {
        IEntityFactory factory = new ImplEntityFactory("SQLConnectionString");
        IUser user = factory.Users.Find(5);
        IAccount usersAccount = user.Account;

        IAccount account = factory.Accounts.Find(3);
        Console.Write(account.Users.Count());
    }
}

所以这就是全部内容了...我希望在这里有人可以指点我方向,或者帮我找到一个好的论据来反驳开发团队。我看了这个网站上关于EF无法与接口一起使用的一些文章和一个回答,说你不能实现IDbSet(我觉得很奇怪,如果你不能实现它,为什么会提供它?),但到目前为止都没有结果。

预先感谢您的任何帮助! J


顺便提一下,IDbSet 可以在单元测试中进行模拟,但是模拟 EF 是另一个困难的任务。参考链接:https://dev59.com/IWw15IYBdhLWcg3wMY-L。 - Ladislav Mrnka
也许可以使用 https://github.com/a-h/FakeDbSet - hB0
1个回答

47
第一个论点是EF不支持接口,必须使用实体类来定义DbSet
第二个论点是你的实体类不应该包含DbSet,因为这是与上下文相关的类,除非你打算实现Active Record模式。即使在这种情况下,你也绝对不能访问另一个实体中的DbSet。即使你包装了集合,你仍然离EF太近,实体永远没有属性可以访问其他实体类型的所有实体(而不仅仅是与当前实例相关的实体)。
只是为了明确DbSet在EF中有着非常特殊的意义——它不是一个集合。它是访问数据库的入口点(例如每个LINQ查询都会访问DbSet),在正常情况下,它不会在实体上公开。
第三个论点是你正在使用单个上下文实例。除非你正在执行一些单次批处理应用程序否则这是绝对错误的
最后一个论点很简单。你被付费是为了交付功能,而不是浪费时间在没有业务价值的抽象上。这不是证明为什么你不应该创建这个抽象层。而是要证明为什么你应该这样做。使用它会给你带来什么价值?如果你的同事不能提出具有业务价值的论点,你可以直接去找你的产品经理让他行使权力——他掌握着预算。
通常,抽象是良好设计的面向对象应用程序的一部分——这是正确的。但是:
  • 每个抽象都会使您的应用程序在某种程度上更加复杂,这将增加开发成本和时间
  • 并非每个抽象都会使您的应用程序变得更好或更易维护-过多的抽象会产生相反的效果
  • 抽象EF很难。说你将以一种可以替换为另一个实现的方式抽象数据访问是数据访问专家的任务。首先,您必须具有许多数据访问技术的非常好的经验,才能定义这样的抽象,该抽象将与所有这些技术一起使用(最终,您只能告诉自己的抽象与您设计时考虑的技术一起工作)。您的抽象仅适用于EF DbContext API,而不适用于其他任何内容,因为它不是抽象。如果要构建通用抽象,则应开始研究存储库模式、工作单元模式和规范模式-但是要使它们并实现它们是一项艰巨的工作。所需的第一步是将与数据访问相关的所有内容隐藏在该抽象后面,包括LINQ!
  • 仅当您现在需要支持多个API时,抽象数据访问才有意义。如果您只认为它在未来可能有用,则在面向业务的项目中,这是错误的决定,并且提出该想法的开发人员不适合做出业务定位决策。

什么情况下会有必要进行“大量”抽象?

  • 如果您现在有这样的要求——将这样的决定责任转移给负责预算/项目范围/需求等方面的人。
  • 现在需要抽象来简化设计或解决某些问题。
  • 您正在进行开源或业余项目,您的驱动力不是商业需求,而是项目的纯洁性和质量。
  • 您正在开发平台(长期使用的零售产品),或公共框架——这通常会回到第一点,因为这种类型的产品通常具有这种抽象作为要求。

如果您只针对应用程序工作(大多数按需或外包解决方案的单一目的应用程序),则仅在必要时使用抽象。这些应用程序受成本驱动——目标是以最小成本和最短时间提供可行的解决方案。即使结果应用程序在内部不是很好,也必须实现此目标——唯一重要的是应用程序是否符合要求。基于“如果……发生”或“也许我们会需要……”的任何抽象都会通过虚拟(不存在的)要求增加成本,在大多数情况下,与客户的最初合同没有考虑这些额外的成本。

顺便说一句,这种应用程序是微软API和设计师策略的目标——微软将创建许多设计师和代码生成器,这些生成器将创建非最优但便宜且快速的解决方案,可以由技能较小且非常便宜的人创建。最后一个例子是LightSwitch。


首先,我完全同意Ladislav的观点..尤其是“这是为什么您应该这样做的证明”..赞同!追求模式是EF4.1的新黑客和头痛的配方。接口到调用存储库的服务,并利用您的类是POCO的优势。 - Gats
非常感谢你们的明智建议!我也怀疑了很多相同的事情...由于我的DBA背景,我似乎是这里唯一一个认为在数据层中过多的抽象不合适的工程师。 - Jason
1
抽象本身并不是问题,如果需要它并且它能够增加价值,但它也有其成本和复杂性。 - Ladislav Mrnka
你的回答第一行正是我所需要的!谢谢。 - Stephanvs
截至今日,EF已经支持IDbSet接口了!抱歉之前误导了。 - JuninZe

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