在过去的两年中,很对开发者都抱怨EF中预先加载的执行方式。
下面是你经常会使用的预先加载的方式:
var results = from b in ctx.Blogs.Include(“°Posts”±) where b.Owner == “°Alex”± select b;
上面代码的意思很明确,要求EF预先加载所有符合要求的Blog的相关的Posts,代码会很好的执行。
但是问题是这里使用了"Posts”这样的字符串的方式,这对于已经习惯于在常规的Linq语句或者Linq to SQL语句中使用强类型的我们来说,用string的这样类型不安全的方式显然不是我们所希望的,我们希望的形式或许是这样的:
var results = from b in ctx.Blogs.Include(b => b.Posts) where b.Owner == “°Alex”± select b; 上面这样的方式是类型安全的,很多人也尝试着这样做。
下面的方式或许更好:
var strategy = new IncludeStrategy<Blog>(); strategy.Include(b => b.Owner); var results = from b in strategy.ApplyTo(ctx.Blogs) where b.Owner == “°Alex”± select b;
上面这样的方式可以让你在不同的数据检索之间重用strategies。
根据上面的想法,我打算扩展我的思路来支持这里的strategies。
下面是我想做的针对Blog的Stretegy方式的类的实力:
var strategy = Strategy.NewStrategy<Blog>(); strategy.Include(b => b.Owner) .Include(p => p.Comments); //sub includes strategy.Include(b => b.Posts); //multiple includes 上面的类将支持子类继承: public class BlogFetchStrategy: IncludeStrategy<Blog> { public BlogFetchStrategy() { this.Include(b => b.Owner); this.Include(b => b.Posts); } }
有了上面的类,你可以像下面这样来使用:
var results = from b in new BlogFetchStrategy().ApplyTo(ctx.Blogs) where b.Owner == “°Alex”± select b;
下面我们来实现上面的这个stretegy类:
1) 创建 IncludeStrategy<T> 类:
public class IncludeStrategy<TEntity> where TEntity : class, IEntityWithRelationships { private List<string> _includes = new List<string>(); public SubInclude<TNavProp> Include<TNavProp>( Expression<Func<TEntity, TNavProp>> expr ) where TNavProp : class, IEntityWithRelationships { return new SubInclude<TNavProp>( _includes.Add, new IncludeExpressionVisitor(expr).NavigationProperty ); } public SubInclude<TNavProp> Include<TNavProp>( Expression<Func<TEntity, EntityCollection<TNavProp>>> expr ) where TNavProp : class, IEntityWithRelationships { return new SubInclude<TNavProp>( _includes.Add, new IncludeExpressionVisitor(expr).NavigationProperty ); } public ObjectQuery<TEntity> ApplyTo(ObjectQuery<TEntity> query) { var localQuery = query; foreach (var include in _includes) { localQuery = localQuery.Include(include); } return localQuery; } }
从上面的代码中我们注意到,代码使用了一个string数组来存储我们要添加的Includes,同时还有ApplyTo(…)方法允许调用者
可以将所有符合T类型的ObjectQuery<T>, 绑定到Includes中来。
淡然代码主要的功能实在两个Include(..) 方法中,之所以要有两个重载方法,其中第一个是为了加载References引用,另一个是为了加载数组。这样的实现是针对.NET 3.5 SP1的,所以我可以依赖于那些实现了IEntityWithRelationships接口,并具有relationships的类(可以对包含Include的类进行检测)。有趣的是真毒载入数组的Include方法,每一个Expression语句都被设置成isExpression<Func<TEntity, EntityCollection<TNavProp>>> ,而在创建Sub-includes的时候返回的类型却是TNavProp,这样的做法让我们避免在使用的时候使用如下的代码:
Include(b => b.Posts.SelectMany(p => p.Author));
同时也可以避免下面的写法:
Include(b => b.Posts.And().Author);
取而代之的是下面的更简洁和合理的写法:
Include(b => b.Posts).Include(p => p.Author);
上面的实现方式更简单,这也是整个设计中的核心部分。
2) 类IncludeExpressionVisitor 派生自ExpressionVisitor这个实例,你可以在这里找到它,这个实例其实非常简单,或许用在这里有些有过度设计的感觉,但我喜欢在这里采用正确的是寂寞死来实现这个功能。
public class IncludeExpressionVisitor : ExpressionVisitor { private string _navigationProperty = null; public IncludeExpressionVisitor(Expression expr) { base.Visit(expr); } public string NavigationProperty { get { return _navigationProperty; } } protected override Expression VisitMemberAccess( MemberExpression m ) { PropertyInfo pinfo = m.Member as PropertyInfo; if (pinfo == null) throw new Exception( "You can only include Properties"); if (m.Expression.NodeType != ExpressionType.Parameter) throw new Exception( "You can only include Properties of the Expression Parameter"); _navigationProperty = pinfo.Name; return m; } protected override Expression Visit(Expression exp) { if (exp == null) return exp; switch (exp.NodeType) { case ExpressionType.MemberAccess: return this.VisitMemberAccess( (MemberExpression)exp ); case ExpressionType.Lambda: return this.VisitLambda((LambdaExpression)exp); default: throw new InvalidOperationException( "Unsupported Expression"); } } }
从上面的代码中你可以看到,vistor仅仅是起了一个约束的作用,它仅仅能够识别LambdaExpressions和MemberExpressions。
当使用MemberExpression的时候,visitor将确认进入的成员是一个属性(Property),并把这个成员直接绑定到其参数中(Parameter)中(比如p.Property可以,但是p.Property.SubProperty不可以)如果出现这种情况,visitor将会记录下NavigationProperty的名字。
3)当我们知道NavigationProperty的名字,IncludeStrategy.Include 方法会创建一个SubInclude<T> 对象,它将用来注册NavigationProperty,并为串联多个sub-includes提供相关的机制。
类SubInclude<T>的代码如下:
public class SubInclude<TNavProp> where TNavProp : class, IEntityWithRelationships { private Action<string> _callBack; private string[] _paths; internal SubInclude(Action<string> callBack, params string[] path) { _callBack = callBack; _paths = path; _callBack(string.Join(".", _paths)); } public SubInclude<TNextNavProp> Include<TNextNavProp>( Expression<Func<TNavProp, TNextNavProp>> expr ) where TNextNavProp : class, IEntityWithRelationships { string[] allpaths = _paths.Append( new IncludeExpressionVisitor(expr).NavigationProperty ); return new SubInclude<TNextNavProp>(_callBack, allpaths); } public SubInclude<TNextNavProp> Include<TNextNavProp>( Expression<Func<TNavProp, EntityCollection<TNextNavProp>>> expr ) where TNextNavProp : class, IEntityWithRelationships { string[] allpaths = _paths.Append( new IncludeExpressionVisitor(expr).NavigationProperty ); return new SubInclude<TNextNavProp>(_callBack, allpaths); } }
4) 现在还遗留的一点东西是一个用于附加其他元素到array数组中的扩展方法,代码如下:
public static T[] Append<T>(this T[] initial, T additional) { List<T> list = new List<T>(initial); list.Add(additional); return list.ToArray(); }
有了上面的代码,你就可以以你自己的方式非常容易的实现与预先加载,你所需要做的就是继承类IncludeStrategy<T>.
但是要注意的是,这个方案只是我的个人方案,并不是微软的官方解决方案,它没有经过严格测试。你可愿意在这里下载到完整代码。