前一阵我总结过一篇关于IRepository的文章:我所理解的IRepository,当时只是纯属从单一接口上来做分享,但毕竟接口并不是单独存在的,它需要很好的融入到项目中去,这就是一个员工需要融入到团队中一样。而要想让IRepositoyr在项目中发挥得当,最好根据自己项目的需求来做相应调整,毕竟适应自己公司大环境的解决方案才是最有可能成功的应用,比如我们是EntityFrame+MVC。
第一:在原来接口的基础上做些补充。
1:增加了一个支持分页的方法。这个其实是非常见的方法,只需要在原来查询接口中做些封装即可。接口定义如下:
客户可以这样调用:
注意:这个分页方法的参数,最好传表达式树,让其动态构建SQL,如果传Func这种泛型委托的话,一定要在调用Context时将其转换成表达式树,否则就无法在服务器端完成分页了。
2:为Update方法增加了一个重载方法,定义如下:
其实T Update(T entity)这个接口也能完成更新操作,但需要传入的实体参数自身包含主键相关查询字段信息,否则无法找到需要更新的记录是哪条。之所以增加一个重载方法,我个人认为新增加的方法更加符合平时的操作习惯,跟SQL的更新语法是比较符合的,第一步是确认需要更新的字段,第二步是选择更新的记录是哪条。调用示例如下:
第二:对IRepository的应用注入一些自定义的事件。
在应用EntityFramword对其表进行CRUD时,在某些环境下,表中的一部分字段属于系统字段(比如记录的创建日期CreatedOn,修改日期ModifiedOn),为了简化操作,我们可以在操作数据时将这些系统字段由人工处理变成自动处理,即客户端调用IRepository时不传递这些系统字段的信息,只传和业务逻辑紧密相关的字段(比如学生表中的姓名,学号,地址等),然后IRepository会自动的将这些系统字段的值填充上。这样从某种程度上可以使我们的领域模型变得更加简单,也可以减轻程序员对领域模型的维护成本。
以前听说过一句话:代码的行数和出错误的概率是成正比的,这里同样的道理,领域模型的字段数量与出错误的概率是成正比的。
解决方案:在Context上订阅自定义事件,这样在正式提交数据库之前会先执行我们定义的自定义事件之后再提交数据库,所以我们可以利用EntityObject提供的MarkCreated方法来自动的将我们定义的一些系统字段给加上去。
注意:这里之所以可以这样做,也需要取决于我们的数据库设计,数据表都遵守一定的约束,比如所有表都会具备相同的系统字段。只有这样我们才能放开手脚使用下面的解决方案。
public void ContextPersistent(object sender, EventArgs e)
{
ObjectContext oc = sender as ObjectContext;
ObjectStateManager osm = oc.ObjectStateManager;
IEnumerable<ObjectStateEntry> newEntities = osm.GetObjectStateEntries(EntityState.Added);
foreach (ObjectStateEntry ose in newEntities)
{
EntityObject newAdded = (EntityObject)context.GetObjectByKey(ose.EntityKey);
newAdded.MarkCreated(CurrentUser);
}
}
public static void MarkCreated(this EntityObject eo, Guid user)
{
Type type = eo.GetType();
PropertyInfo pInfo = type.GetProperty("CreatedOn");
if (pInfo != null)
pInfo.SetValue(eo, DateTime.UtcNow, null);
PropertyInfo mInfo = type.GetProperty("ModifiedOn");
if (mInfo != null)
mInfo.SetValue(eo, DateTime.UtcNow, null);
}
第三:如何解决IRepository,数据库模型和ViewModel之间的关系。
我们的项目是asp.net mvc项目,所以我们有ViewModel的概念,而我们采用了数据库优先的方式调用EntityFramwork,在添加完edmx后,我们也就有了数据库对象,这些数据库对象都是系统自动生成的,当然和数据库中的字段是一对一的关系,但ViewModel并不一定和数据表字段是一对一的关系。
问题:既然我们定义了IRepository接口,那么意味着MVC在调用数据时也会遵守此接口的契约,问题在于,IRepository在处理数据时,参数是数据库对象,而在MVC调用时我们是不太可能直接传递数据库对象的,(数据库对象属于DataAccess层,MVC属于UI层,this.entitySet.FirstOrDefault(filter);这其中的filter表达式中的实体需要是数据库对象。)它只有ViewModel的概念,所以我们需要将Query(Expression<Func<ViewModel, bool>> filter);转换成Query(Expression<Func<DataBaseModel, bool>> filter);
解决方案:利用自定义表达式来完成转达式树之间的转换,原理就是将ViewModel佣有的属性在DataBaseModel中遍历,如果有相同属性,就赋值。
比如:ViewModel为StudentModel,它有一个Name属性,DataBaseModel为Student,它也有一个Name属性,目标就是将p=>p.Name=="张三"(p的类型是StudentModel)转换成p=>p.Name=="张三"(p的类型是Student).
{
// Methods
public static Expression<Func<TToB, TR>> Convert<TFrom, TR>(Expression<Func<TFrom, TR>> e)
{
ParameterExpression oldParameter = e.Parameters[0];
ParameterExpression newParameter = Expression.Parameter(typeof(TToB), oldParameter.Name);
ConversionVisitor<TToB> visitor = new ConversionVisitor<TToB>(newParameter, oldParameter);
return Expression.Lambda<Func<TToB, TR>>(visitor.Visit(e.Body), new ParameterExpression[] { newParameter });
}
// Nested Types
private class ConversionVisitor : ExpressionVisitor
{
// Fields
private readonly ParameterExpression newParameter;
private readonly ParameterExpression oldParameter;
// Methods
public ConversionVisitor(ParameterExpression newParameter, ParameterExpression oldParameter)
{
this.newParameter = newParameter;
this.oldParameter = oldParameter;
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression != this.oldParameter)
{
return base.VisitMember(node);
}
Expression expression = this.Visit(node.Expression);
MemberInfo member = this.newParameter.Type.GetMember(node.Member.Name).First<MemberInfo>();
return Expression.MakeMemberAccess(expression, member);
}
protected override Expression VisitParameter(ParameterExpression node)
{
return this.newParameter;
}
}
}
感叹表达树是多么的强大,几十行代码就能完成一些意想不到的效果,总之以上是我从自己项目出发,对于IRepository应用上的一些理解。