RepositoryUnit:有用的通用存储库
本文的目的是介绍一种基于 Entity Framework 的、有用的仓储模式的实现方法。
引言
本文的目的是介绍一种基于 Entity Framework 的、有用的仓储模式的实现方法。 如果您愿意,您可以轻松创建 RepositoryUnit
的 Linq2Sql 实现并编写单元测试。 也许,我将在后续文章中进行介绍。
背景
仓储模式提供对数据的统一访问,是数据源和数据使用者之间的中间层。 它允许从应用程序的业务逻辑中抽象数据源。 在实现这个概念时,我们可能会遇到一些问题
- 应用程序通常使用对象(类),对于这些对象,在抽象接口中定义了 CRUD(创建、读取、更新、删除)操作。 因此,即使类很少,仓储的接口也将包含每个类的相同 CRUD 函数集。 我们当然可以为每个类编写单独的接口,并将它们放在一个通用仓储中,但这样我们会有一堆相似的文件,这根本不好。 C# 的泛型类型可以帮助我们避免这个问题。
- 为了进行 CRUD 操作,我们应该能够识别对象。 通常,这个问题通过继承具有 ID 字段的类来解决,但这种情况下我们必须编辑 EF 生成的文件,这通常不会带来很多乐趣。
让我们结束这些困难,看看提议的解决方案的好处。
特点
- 强类型。
- 无需从基类继承的 CRUD 操作。 对象的 ID 通过反射获取。
- 安全性 - 您无法直接获取
IQueryable
。 仓储只返回列表及其派生。 如果您想返回Queryable
,您可以在继承类中执行此操作。 - Lambda 表达式排序,同时在仓储实例中保存排序。 您只能在创建仓储时定义一次排序,然后忘记它。 您还可以设置默认过滤,这将定义您的仓储将使用的数据(例如,忽略
IsDeleted=true
的对象)。 - CRUD 事件。 通常,使用事件比继承和重写方法更容易。
- 将类的属性和方法封装在通用仓储的一个属性中。
IRepositoryUnit
它很简单,没什么好写的。 该接口只是做了上面写的事情。
public interface IRepositoryUnit<T, TId>
{
T Create(T objectToCreate);
void Delete(TId id);
T Edit(T objectToUpdate);
T Get(TId id);
T Find(Expression<Func<T, bool>> predicate);
IEnumerable<T> List();
PaginatedList<T> List(int? page, int pageSize);
PaginatedList<T> FindList(int? page, int pageSize,
Expression<Func<T, bool>> predicate);
IEnumerable<T> FindList(Expression<Func<T, bool>> predicate);
void OrderBy<TKey>(Expression<Func<T, TKey>> key);
void ThenBy<TKey>(Expression<Func<T, TKey>> key);
void OrderByDesc<TKey>(Expression<Func<T, TKey>> key);
void ThenByDesc<TKey>(Expression<Func<T, TKey>> key);
Expression<Func<T, bool>> WherePredicate { get; set; }
event Action<T> Deleted;
event Func<T, bool> Creating;
event Func<T, T, bool> Editing;
}
EntityRepositoryUnit
由于 EF 的实现相当大,我们将只研究最有趣的时刻。
构造函数获取 ObjectContext
,可选地,还有 includePaths
。 第二个参数定义了应该包含在查询中的表。 必要的数据集的名称由方法 FetchEntitySetName
检测(感谢 Manuel Felicio),在对象创建例程中。
方法 Query
是最重要的。 每个请求处理(Get
除外)都以 Query
开头。 它准备查询,应用过滤和排序,并返回方便的 IQueryable
对象。
public class EntityRepositoryUnit<T, TId> : IRepositoryUnit<T, TId>
where T : EntityObject
{
protected ObjectContext ObjectContext { get; private set; }
protected string EntitySetName { get; private set; }
protected string[] IncludePaths { get; private set; }
private Type orderByType;
private Expression orderByExpression;
private bool orderByDesc;
private List<Type> thenByType;
private List<Expression> thenByExpression;
private List<bool> thenByDesc;
public Expression<Func<T, bool>> WherePredicate { get; set; }
...
public EntityRepositoryUnit(ObjectContext context, string[] includePaths)
{
this.ObjectContext = context;
this.IncludePaths = includePaths;
FetchEntitySetName();
}
//looks for an IQueryable<TEntity> property in the ObjectContext
//and gets its name to be used in other methods
private void FetchEntitySetName()
{
var entitySetProperty =
this.ObjectContext.GetType().GetProperties()
.Single(p => p.PropertyType.IsGenericType && typeof(IQueryable<>)
.MakeGenericType(typeof(T)).IsAssignableFrom(p.PropertyType));
this.EntitySetName = entitySetProperty.Name;
}
protected IQueryable<T> BuildQuery(ObjectQuery<T> q)
{
if (IncludePaths != null)
foreach (string path in IncludePaths)
q = q.Include(path);
if (WherePredicate != null)
return q.Where(WherePredicate);
else
return q;
}
protected IQueryable<T> ApplySorting(IQueryable<T> q)
{
if (orderByExpression != null)
{
var mc = Expression.Call(
typeof(Queryable),
orderByDesc ? "OrderByDescending" : "OrderBy",
new Type[] { q.ElementType, orderByType },
q.Expression,
orderByExpression);
for (int i = 0; i < thenByExpression.Count; i++)
{
mc = Expression.Call(
typeof(Queryable),
thenByDesc[i] ? "ThenByDescending" : "ThenBy",
new Type[] { q.ElementType, thenByType[i] },
mc,
thenByExpression[i]);
}
return q.Provider.CreateQuery(mc) as IQueryable<T>;
}
return q;
}
protected IQueryable<T> Query(Expression<Func<T, bool>> predicate)
{
var entitySet = String.Format("[{0}]", this.EntitySetName);
ObjectQuery<T> baseQuery = this.ObjectContext.CreateQuery<T>(entitySet);
var q = BuildQuery(baseQuery);
if(predicate!=null)
q = q.Where(predicate);
q = ApplySorting(q);
return q;
}
#region IRepositoryUnit<T,TId> Members
...
public T Get(TId id)
{
string queryString = string.Format(@"SELECT VALUE x FROM
[{0}] as x WHERE x.Id = @Id", EntitySetName);
var q = ObjectContext.CreateQuery<T>(queryString, new ObjectParameter("Id", id));
return BuildQuery(q).FirstOrDefault();
}
...
public PaginatedList<T> FindList
(int? page, int pageSize, Expression<Func<T, bool>> predicate)
{
return new PaginatedList<T>(Query(predicate), page, pageSize);
}
...
#endregion
}
PaginatedList
这是一个辅助类,它允许获取用于在指定页面上显示的数据。 您可以在下载的项目中查看其代码。
Using the Code
这非常容易。 您的项目通用仓储将包含属性 - 实现您需要的类型的接口 IRepositoryUnit
的对象。 只有在数据存储级别上运行复杂逻辑的非常复杂的方法才会被描述为您的通用仓储的方法。
例如
public interface IRepository
{
IRepositoryUnit<News, int> News { get; }
IRepositoryUnit<Message, Guid> Messages { get; }
IRepositoryUnit<Topic, Guid> Topics { get; }
IRepositoryUnit<PhotoType, int> PhotoTypes { get; }
IRepositoryUnit<Photo, Guid> Photos { get; }
IEnumerable<Message> VeryIntricateMethod1();
IEnumerable<Photo> VeryIntricateMethod1();
}
这样的仓储可以轻松地处理五个实体,并包含两个不宜在 RepositoryUnit
级别实现的复杂方法。
该接口的 EF 实现一定会让我们满意。 它简单而清晰
public class EntityRepository : IRepository
{
private YourEntities entities = new YourEntities();
public EntityRepository()
{
News = new EntityRepositoryUnit<News, int>(entities);
News.OrderByDesc(x => x.Date);
PhotoTypes = new EntityRepositoryUnit<PhotoType, int>(entities);
PhotoTypes.OrderBy(x => x.Name);
Messages = new EntityRepositoryUnit<Message, Guid>(entities);
Messages.WherePredicate = x => x.IsDeleted = false;
Messages.OrderBy(x => x.Date);
Topics = new EntityRepositoryUnit<Topic, Guid>
(entities, new string[] { "Message" });
Topics.WherePredicate = x => x.IsDeleted = false;
Topics.OrderBy(x => x.Date);
Photos = new EntityRepositoryUnit<Photo,
Guid>(entities, new string[] { "PhotoType" });
Photos.OrderBy(x => x.PhotoType.Id);
Photos.ThenBy(x => x.Date);
Photos.Deleted += new Action<Photo>(s => s.DeleteFiles());
}
#region IRepository Members
public IRepositoryUnit<News, int> News { get; private set; }
public IRepositoryUnit<Message, Guid> Messages { get; private set; }
public IRepositoryUnit<Topic, Guid> Topics { get; private set; }
public IRepositoryUnit<PhotoType, int> PhotoTypes { get; private set; }
public IRepositoryUnit<Photo, Guid> Photos { get; private set; }
public IEnumerable<Message> VeryIntricateMethod1()
{
//your intricate logic here
throw new NotImplementedException();
}
IEnumerable<Photo> IRepository.VeryIntricateMethod1()
{
//your intricate logic here
throw new NotImplementedException();
}
}
请注意,为每个仓储定义排序 - 否则您将在 PaginatedList
构造函数中收到错误。 您还应该记住,每个 Entity
对象都必须具有 ID
属性(参见方法 EntityRepositoryUnit.Get
)。 注意到照片文件在从仓储中删除 Photo
对象时是如何被漂亮地删除的。
我不会给出从业务逻辑中调用仓储函数的例子,因为它们已经足够明显了。 您将在下载的存档中找到一个简单的 MVC 示例。 如果您有任何问题,欢迎在下面的评论部分提问。
历史
- 2010 年 3 月 26 日:初始版本