Partager via


Tip 35 – How to write OfTypeOnly()

If you write a LINQ to Entities query like this:

var results = from c in ctx.Vehicles.OfType<Car>()
select c;

It will bring back, Cars and any type that derives from Car, like say SportCar or SUV.

If you just want Cars and you don’t want derived types like SportCars or SUVs in LINQ to Objects you would write something like this:

var results = from c in vehiclesCollection
where c.GetType() == typeof(Car)
select c;

But unfortunately LINQ to Entities doesn’t know how to translate this.

NOTE:
In Entity SQL this is actually pretty easy. The OFTYPE(collection, [ONLY] type) function will exclude derived types if you include the optional ONLY keyword.

For example this Entity SQL:

SELECT VALUE(C)
FROM Container.Vehicles AS C
WHERE C IS OF(ONLY Model.Car)

will only return Cars: entities derived from Car, like say SUVs, will be filtered out.

Now about six months back, in Tip 5, I showed you a work-around. You basically had to write something like this:

var results = from c in ctx.Vehicles.OfType<Car>()
where !(c is SUV) && !(c is SportsCar)
select c;

But this solutions is cumbersome and error prone, so I decided I wanted a better solution.

You should be able to write something like this:

var results = from c in ctx.Vehicles.OfTypeOnly<Car>()
select c;

Now under the hood this method has to:

  1. Take the source ObjectQuery and call OfType<Car>() to get an ObjectQuery<Car>()
  2. Figure out which EntityTypes derive from Car
  3. Construct an LambdaExpression to exclude each of those Derived types from the result.
  4. Take the ObjectQuery<Car> and call Where(Expression<Func<Car,bool>>) on it using the LamdaExpression

So lets look at what the code looks like.

This is the function that pulls everything together:

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>>
);
}

As you can see we are using an extension method on MetadataWorkspace called GetCSpaceEntityType() that takes a CLR type and returns the corresponding EntityType.

It looks like this:

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;
}

If this method looks familiars, it is, I introduced it in Tip 13. In fact this method is a very handy for your EF toolbox.

Once we have the EntityType we can look for derived EntityTypes, which is where the GetImmediateDescendants() method comes in. It looks like this:

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;
}
}

NOTE: I’m only interested in immediate descendants because when the immediate descendants are filtered out, their descendants will also get filtered out.

Next we need to get the CLR types for each of those EntityTypes. To do this I have a function that uses the EF metadata to find the CLR typename for each EntityType, which looks like this:

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");
}

You can then compose this method with some code to get the CLR type for a particular typename.

Now writing something fool proof can get complicated, but in my case I’m just assuming all the Types are in the same assembly as TEntity. Which makes things very easy:

// Get the CLRTypes.
Type[] clrTypes = subTypes
.Select(st => wkspace.GetClrTypeName(st))
.Select(tn => elementType.Assembly.GetType(tn))
.ToArray();

… and I’m pretty sure you can figure out how to make this a little more robust if necessary :)

At this point we leave the EF metadata APIs behind and move over to the Expression APIs,

Gulp!

Actually its a lot easier that I thought it was going to be.

We just need a lambda expression that will filter out all the derived CLR types. The equivalent of this:

(TEntity entity) => !(entity is TSubType1) && !(entity is TSubType2)

So I added this method, the first parameter is the type of the lambda parameter, and then you pass is all the types you want to exclude:

public static LambdaExpression GetIsNotOfTypePredicate(
Type parameterType,
params Type[] clrTypes)
{
ParameterExpression predicateParam =
Expression.Parameter(parameterType, "parameter");

return Expression.Lambda(
predicateParam.IsNot(clrTypes),
predicateParam
);
}

As you can see this creates a parameter, and then calls another extension method to create the AndAlso expression needed:

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;
}

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.

NOTE: You may have noticed that this code is going to produce a deep AndAlso graph. I think this is probably fine, but if you have a particularly wide type hierarchy you might want to look at rewriting this query to balance the graph.

So by now we have a way to create a LambdaExpression that does the necessary filtering, all we need is cast it to Expression<Func<TEntity, bool>> and pass it to the Where(..) extension method, like this:

var lambda = GetIsNotOfTypePredicate(elementType, clrTypes);
return filter.Where(
lambda as Expression<Func<TEntity, bool>>
);

And we’re done!

Now I’m the first to admit this isn’t exactly ‘easy peasy lemon squeezy’, but I enjoyed developing this solution, it forced me to learn a little more about Expressions and EF metadata APIs.

Hopefully you’ve found it interesting too.

EntityFrameworkTips.cs

Comments

  • Anonymous
    September 17, 2009
    This is not the first time I've wished L2E knew how to translate GetType(). It would make a lot of things easier, especially this!
  • Anonymous
    September 19, 2009
    Interesting but ... arguably in this case you should instead make Car an abstract base class (ABC) and have concrete classes like Sedan and StationWagon and then query only on the concrete classes.Wikipedia: "Many authors argue that classes should be leaf classes (have no subtypes), or else be abstract."
  • Anonymous
    September 19, 2009
    Ian, You're probably right that if you find yourself needing to do this you have some more fundamental problem. But unfortunately I've found myself in this situation quite a few times :(Alex
  • Anonymous
    March 15, 2010
    Oh.its very usefull, but if I have the supper type in relation(Include) how can I filter it.