【问题标题】:Entity base class design in C# for repository model用于存储库模型的 C# 中的实体基类设计
【发布时间】:2009-04-09 11:21:21
【问题描述】:

我正在努力通过更多地了解依赖注入/IoC 和其他最佳实践方法来成为一名优秀的编程公民。为此,我有一个项目,我试图做出正确的选择并以“正确”的方式设计一切,无论这意味着什么。 Ninject、Moq 和 ASP.NET MVC 有助于提高可测试性并将应用程序“推出”。

但是,我有一个关于如何为我的应用程序所包含的对象设计实体基类的问题。我有一个简单的类库,网络应用程序建立在它之上。这个库公开了一个 IRepository 接口,默认实现(应用程序使用的那个)在幕后使用 Linq-to-SQL(DataContext 等不暴露给 Web 应用程序)并且只包含获取这些实体的方法。存储库基本上看起来像这样(简化):

public interface IRepository
{
    IEnumerable<T> FindAll<T>() where T : Entity
    T Get<T>(int id) where T : Entity
}

默认实现使用 DataContext 上的 GetTable() 方法来提供正确的数据。

但是,这要求基类“实体”具有一些特性。通过创建与 Linq-to-SQL 给我的映射对象同名的部分类,很容易让我的对象从它继承,但是“正确”的方法是什么?

例如,上面的接口有一个通过它的 id 获取实体的功能 - 所有从实体派生的不同类型的类确实都有一个 int 类型的“id”字段(从它们各自的主键映射表),但我怎样才能以一种让我像这样实现 IRepository 的方式指定它呢?

public class ConcreteRepository : IRepository
{
    private SomeDataContext db = new SomeDataContext();

    public IEnumerable<T> FindAll<T>() where T : Entity
    {
        return db.GetTable<T>().ToList();
    }

    public T Get(int id) where T : Entity
    {
        return db.GetTable<T>().Where(ent => ent.id == id).FirstOrDefault();
    }
}

我是在无编译器的 PC 上从内存中执行此操作的,因此请原谅任何错误,希望您能理解我的意图。

这里的诀窍当然是要编译它,必须确定 Entity 承诺从它派生的每个人都有一个 id 字段。

我可以创建一个抽象字段,或者一个普通字段,它被 Linq-to-SQL 粘贴在生成的类中的 id 字段“隐藏”。

但这一切都感觉有点像作弊,甚至会给出编译器警告。

“Entity”是否真的应该是“IEntity”(一个接口),并且我应该尝试以 Linq-to-SQL 将实现的方式定义 id 字段?这也可以很容易地指定实体实现者需要实现的其他接口。

或者“实体”应该是一个带有抽象id字段的抽象基类,它是否也应该以抽象的方式实现所需的接口以供其他人覆盖?

我对 C# 的了解还不够,无法看到一个优雅的解决方案,所以我很想听听更有经验的系统设计师与一些基类经验权衡。

谢谢!

4 月 10 日编辑:

我知道我在这里遗漏了一些重要的东西。我的 IRepository 有两个具体的实现 - 一个是使用 LINQ to SQL 的“真实”实现,另一个是 InMemoryRepository,现在只使用 List,用于单元测试等。

添加的两种解决方案都适用于其中一种情况,而不适用于另一种情况。因此,如果可能的话,我需要一种方法来定义从 Entity 继承的所有内容都将具有“id”属性,并且这将与 LINQ to SQL DataContext 一起工作而不会“作弊”。

【问题讨论】:

    标签: c# linq-to-sql oop


    【解决方案1】:

    如果您在所有表中使用“Id”作为主键,那么所有生成的类都将具有如下公共属性:

    public int Id {}
    

    然后创建一个接口

    public interface IIdentifiable {
        Id { get; }
    }
    

    然后是最繁琐的部分:(,为所有实体创建一个部分类并使其实现IIdentifiable。

    存储库类可能如下所示:

    public class Repository<T> : IRepository where T : IIdentifiable {
    }
    

    以下代码将起作用:

    db.GetTable<T>().SingleOrDefault(ent => ent.Id.Equals(id));
    

    如果您不使用生成的类并自己制作,从这个角度来看会更简单。

    编辑:
    而不是 ent => ent.Id == id 使用 ent => ent.Id.Equals(id)。刚刚测试,下面是一个完整的工作示例:

    public interface IIdentifiable {
        int Id { get; }
    }
    
    public class Repository<T> where T : class, IIdentifiable {
        TestDataContext dataContext = new TestDataContext();
    
        public T GetById(int id) {
            T t = dataContext.GetTable<T>().SingleOrDefault(elem => elem.Id.Equals(id));
            return t;
        }
    }
    
    public partial class Item : IIdentifiable {
    }
    
    class Program {
        static void Main(string[] args) {
            Repository<Item> itemRepository = new Repository<Item>();
    
            Item item = itemRepository.GetById(1);
    
            Console.WriteLine(item.Text);
        }
    }
    

    【讨论】:

    • 这与我所做的很接近,不幸的是有一个我似乎无法解决的问题; LINQ to SQL 不知道“额外”Id 属性以及如何在 SQL 查询中使用它。所以我需要一些方法来挂钩 LINQ to SQL 生成的实际 id 字段。
    • 这不起作用。我昨天试过了:stackoverflow.com/questions/735140/…
    • @Dreas - 我已经纠正了:)。我更新了代码以使用 Equals 而不是 == ()。经过测试,它非常适合这种情况。
    【解决方案2】:

    “FindAll”是个坏主意,会强制它获取所有数据。返回IQueryable&lt;T&gt; 会更好,但仍不理想,IMO。

    通过 ID 重新查找 - 请参阅 here for a LINQ-to-SQL answer;这使用 LINQ-to-SQL 的元模型来定位主键,因此您不必这样做。它也适用于属性模型和基于资源的模型。

    我不会强制使用基类。这在 Entity Framework 中不是很流行,而且不太可能变得更流行......

    【讨论】:

    • 谢谢你的信息,马克。我喜欢这样做的方式,但由于它并没有真正提供“id”字段,我发现很难实现以便真正的 DBRepository 和 InMemoryRepository 以相同的方式工作..(参见上面的编辑)。
    【解决方案3】:

    我有一个类似的项目,我主要基于 Rob Conery's Storefront 的想法,就像上面的建议一样,我使用界面来指示 int ID 字段:

    public interface IModelObject
    {
        int ID { get; set; }
    }
    

    但我没有强制我的存储库实现IRepository,而是选择了IQueryable&lt;T&gt; 类型的扩展方法。所以我使用过滤器:

    public static class ModelObjectFilters
    {
        /// <summary>
        /// Filters the query by ID
        /// </summary>
        /// <typeparam name="T">The IModelObject being queried</typeparam>
        /// <param name="qry">The IQueryable being extended</param>
        /// <param name="ID">The ID to filter by</param>
        /// <returns></returns>
        public static IQueryable<T> WithID<T>(this IQueryable<T> qry,
            int ID) where T : IModelObject
        {
            return qry.Where(result => result.ID == ID);
        }
    }
    

    .. 这样我就可以从我的仓库中返回一个 IQueryable&lt;T&gt; 类型,然后简单地使用 .WithID(x) 来拉取单个对象。

    我也将此代码与 LinqToSql 对象一起使用到堆栈中,因此相等比较器确实转换为 SQL。

    【讨论】:

      【解决方案4】:

      看看Denis Troller作为我question的答案发布的以下方法:

      public virtual T GetById(short id)
                  {
                      var itemParameter = Expression.Parameter(typeof(T), "item");
                      var whereExpression = Expression.Lambda<Func<T, bool>>
                          (
                          Expression.Equal(
                              Expression.Property(
                                  itemParameter,
                                  GetPrimaryKeyName<T>()
                                  ),
                              Expression.Constant(id)
                              ),
                          new[] { itemParameter }
                          );
                      var table = DB.GetTable<T>();
                      return table.Where(whereExpression).Single();
                  }
      
      
      public string GetPrimaryKeyName<T>()
                  {
                      var type = Mapping.GetMetaType(typeof(T));
      
                      var PK = (from m in type.DataMembers
                                where m.IsPrimaryKey
                                select m).Single();
                      return PK.Name;
                  }
      

      【讨论】:

        【解决方案5】:

        你做的和我们一样。我们有一个通用的 Persist 接口和两个实现:一个使用 LinqToSql,另一个用于模拟。

        我们在 GetById 上遇到了同样的问题。我们的解决方案是让所有实体都继承自一个基类。该基类有一个虚方法

        protected object GetId()
        

        为了简化复合主键情况,我们更改了数据库架构,以便每个表都有一个主键列。旧的复合主键成为唯一约束。

        我们还发现有一个基类很方便,因为后来我们意识到我们需要一些其他通用特性来跨越所有实体。

        【讨论】:

          【解决方案6】:

          Id 字段实际上有 2 个选项:

          1. 使用对象来表示实体中的 Id。 (就主键而言,这为您提供了更多的灵活性)

          2. 只需使用一个 int Id。 (这使您的实体非常易于使用,并成为您的实体/存储库的约定。

          现在关于如何实现您的实体。您的实体类可能是抽象的。 看看 Sharp-Architechture 或 CommonLibrary.NET 或 SubSonic 中的 Entity 基类实现

          通常您有审计字段,也许还有验证方法。我正在使用 CommonLibrary.NET

          实体:IEntity

          • 身份证
          • 创建日期
          • 更新日期
          • 创建用户
          • 更新用户
          • 验证(...)
          • 有效
          • 错误

          IEntity 接口是审计字段(CreateDate、UpdateUser 等)和实体验证方法的组合。

          【讨论】:

            猜你喜欢
            • 2012-12-29
            • 1970-01-01
            • 2017-08-26
            • 2013-11-06
            • 2010-09-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-11-19
            相关资源
            最近更新 更多