如果你像下面这样书写Linq查询语句:
var results = from c in ctx.Vehicles.OfType<Car>()
select c;
该查询将取回Cars以及所有它的子对象,比如SportCar或者SUV。
如果你只想在LINQ to Object语句中查询Car而不想查询其子类型,你可能需要如下的写法:var results = from c in vehiclesCollection
where c.GetType() == typeof(Car)
select c;
但很不幸的是,EF无法处理上面的写法。
注:
在Entity SQL中,上面的语句其实也可以很容易的实现,只要包含ONLY关键字,函数OFTYPE(collection, [ONLY] type)将会剔除所有的子类型对象。
比如下面的Entity SQL语句:
SELECT VALUE(C)
FROM Container.Vehicles AS C
WHERE C IS OF(ONLY Model.Car)
上面的代码将只返回Cars对象,所有的子对象都将被过滤。
我在前面的杂记5中曾经给出过一个变通方案,当时的方案如下:
var results = from c in ctx.Vehicles.OfType<Car>()
where !(c is SUV) && !(c is SportsCar)
select c;
但是这个方案相对笨重,同时并不能保证完全正确,所以我决定在这里给出一个更好的解决方案。
我所期望的方案要使用户能够像下面这样写这段代码:
var results = from c in ctx.Vehicles.OfTypeOnly<Car>()
select c;
要实现上面的写法,我们需要由下面的几个前提:
下面让我们来看一下如何实现这段代码。
下面的代码就是把所有的函数集成在一起:
public static IQueryable<TEntity> OfTypeOnly<TEntity>(
this ObjectQuery query)
{
query.CheckArgumentNotNull("query");
// Get the C-Space EntityType
var queryable = query as IQueryable;
var wkspace = query.Context.MetadataWorkspace;
var elementType = typeof(TEntity);
// Filter to limit to the DerivedType of interest
IQueryable<TEntity> filter = query.OfType<TEntity>();
// See if there are any derived types of TEntity
EntityType cspaceEntityType =
wkspace.GetCSpaceEntityType(elementType);
if (cspaceEntityType == null)
throw new NotSupportedException("Unable to find C-Space type");
EntityType[] subTypes = wkspace.GetImmediateDescendants(cspaceEntityType).ToArray();
if (subTypes.Length == 0) return filter;
// Get the CLRTypes.
Type[] clrTypes = subTypes
.Select(st => wkspace.GetClrTypeName(st))
.Select(tn => elementType.Assembly.GetType(tn))
.ToArray();
// Need to build the !(a is type1) && !(a is type2) predicate and call it
// via the provider
var lambda = GetIsNotOfTypePredicate(elementType, clrTypes);
return filter.Where(
lambda as Expression<Func<TEntity, bool>>
);
}
从上面的代码中,我们使用了一个MetadataWorkspace的扩展函数叫GetCSpaceEntityType(),该函数使用了CLR类型参数,同时返回相应的实体类型,该函数的实现代码如下:
public static EntityType GetCSpaceEntityType(
this MetadataWorkspace workspace,
Type type)
{
workspace.CheckArgumentNotNull("workspace");
// Make sure the metadata for this assembly is loaded.
workspace.LoadFromAssembly(type.Assembly);
// Try to get the ospace type and if that is found
// look for the cspace type too.
EntityType ospaceEntityType = null;
StructuralType cspaceEntityType = null;
if (workspace.TryGetItem<EntityType>(
type.FullName,
DataSpace.OSpace,
out ospaceEntityType))
{
if (workspace.TryGetEdmSpaceType(
ospaceEntityType,
out cspaceEntityType))
{
return cspaceEntityType as EntityType;
}
}
return null;
}
如果你觉得上面的函数很眼熟,是因为我在我的杂记13中曾介绍过,事实上这个方法是一个非常实用的EF扩展函数,值得你收藏在你的工具箱中。
当我们得到了实体类型以后,我们就可以通过函数GetImmediateDescendants()取得相应的字类型:
public static IEnumerable<EntityType> GetImmediateDescendants(
this MetadataWorkspace workspace,
EntityType entityType)
{
foreach (var dtype in workspace
.GetItemCollection(DataSpace.CSpace)
.GetItems<EntityType>()
.Where(e =>
e.BaseType != null &&
e.BaseType.FullName == entityType.FullName))
{
yield return dtype;
}
}
注: 这里我们只关注直接继承的子类型,它们自身的子类型将随着这些子类型的被过滤而同时被过滤,所以这里我们并不关心。
下一步,我们需要为找到的所有子类型取得它们的CLR类型,这里我们使用了一个EF元数据的方式,通过寻找每一个实体类型的CLR类型名称的方式来取得,代码如下:
public static string GetClrTypeName(
this MetadataWorkspace workspace,
EntityType cspaceEntityType)
{
StructuralType ospaceEntityType = null;
if (workspace.TryGetObjectSpaceType(
cspaceEntityType, out ospaceEntityType))
return ospaceEntityType.FullName;
else
throw new Exception("Couldn’t find CLR type");
}
你可以完善上面的代码以便于为特定的类型名称获取相应的CLR类型,不过为了方便起见,我们假设所有的类型都存在于相同的assembly中,比如TEntity。
// Get the CLRTypes.
Type[] clrTypes = subTypes
.Select(st => wkspace.GetClrTypeName(st))
.Select(tn => elementType.Assembly.GetType(tn))
.ToArray();
通过上面的封装,我们把EF metadata APIs隐藏到了Expression APIs的后面。
事实上,还有一种写法比我想象的情况还要容易一些。
我们只需要使用一句Lambda表达式就可以过滤所有的子CLR类型,对等的写法如下:
(TEntity entity) => !(entity is TSubType1) && !(entity is TSubType2)
所以我增加了下面的这个方法,第一个是Lambda类型的参数,第二个参数是所有你希望过滤的类型。
public static LambdaExpression GetIsNotOfTypePredicate(
Type parameterType,
params Type[] clrTypes)
{
ParameterExpression predicateParam =
Expression.Parameter(parameterType, "parameter");
return Expression.Lambda(
predicateParam.IsNot(clrTypes),
predicateParam
);
}
这里创建了一个参数,然后调用了另一个扩展函数来创建所需要的AndAlso表达式:
public static Expression IsNot(
this ParameterExpression parameter,
params Type[] types)
{
types.CheckArgumentNotNull("types");
types.CheckArrayNotEmpty("types");
Expression merged = parameter.IsNot(types[0]);
for (int i = 1; i < types.Length; i++)
{
merged = Expression.AndAlso(merged,
parameter.IsNot(types[i]));
}
return merged;
}
public static Expression IsNot(
this ParameterExpression parameter,
Type type)
{
type.CheckArgumentNotNull("type");
var parameterIs = Expression.TypeIs(parameter, type);
var parameterIsNot = Expression.Not(parameterIs);
return parameterIsNot;
}
上面代码中的第一个重载便利了所有的类型,从而创建了IsNot表达式(通过调用第二个重载方法),同时把他们和之前创建的表达式进行合并,从而创建了完整的AndAlso表达式。
As you can see the first overload loops over the types and creates an IsNot expression (by calling the second overload) and merges it with the previously created expression, by creating an AndAlso expression.
注: 你可能会注意到上面的方法构筑了一条非常深的AndAlso曲线,我认为这样的实现没有什么问题,但是如果你的特殊类型有非常深的继承关系,你可能需要通过重写这段查询代码以取得相应的平衡。
现在我们可以创建一种Lambda表达式,可以不用写大量的过滤条件,我们所需要做的就是将该表达式转换成Expression<Func<TEntity, bool>>,并将它传递给Where(…)扩咱函数就可以了:
var lambda = GetIsNotOfTypePredicate(elementType, clrTypes);
return filter.Where(
lambda as Expression<Func<TEntity, bool>>
);