I am trying to implement OrderBy
and ThenBy
in a different way to hide lambda expression from OrderBy
and ThenBy
extension methods. These extension methods accept classes which implement IOrderSpecification
:
public class PersonOrderByAgeSpecification : OrderSpecification<Person>
{
public PersonOrderByAgeSpecification(Sort direction= Sort.Ascending) : base(direction)
{
}
public override Expression<Func<Person, IComparable>> AsExpression()
{
return personOrder => personOrder.Age;
}
}
And the usage:
var orderSpecification = new PersonOrderByAgeSpecification(Sort.Ascending);
var sortedPeople= _dbContext.People.OrderBy(orderSpecification);
It works fine when the property type in AsExpression()
is just string. For example:
public override Expression<Func<Person, IComparable>> AsExpression()
{
return personOrder => personOrder.FirstName;
}
Otherwise I would get this error: (Does not work with integer or bool)
InvalidOperationException: Null TypeMapping in Sql Tree Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalSqlTranslatingExpressionVisitor+SqlTypeMappingVerifyingExpressionVisitor.VisitExtension(Expression node)
The source code is available here
I appreciate any help.
First off, you are using preview (beta) software, which is expected to have issues.
But the main problem is that LINQ ordering methods have second generic type argument TKey
, which you are hiding behind IComparable
, which for value types causes a hidden cast inside the expression.
Apart from unnecessary boxing, this is not a problem for LINQ to Objects provider because it simply compiles and executes a delegate from the lambda expression. However other IQueryable
providers usually need to translate the expression to something else (usually SQL). Most of them identify such casts (Expression.Convert
) and remove them during the processing. Apparently EF 3.0 preview you are using doesn't, hence the exception.
You can avoid such issues by eliminating the hidden casts yourself. It's possible to do that with expression manipulation, but the easiest is to introduce the second generic type argument to your base abstract class:
public abstract class OrderSpecification<T, TKey> : IOrderSpecification<T>
and change the abstract method signature to
public abstract Expression<Func<T, TKey>> AsExpression();
The implementation, interface and everything else except the concrete classes will remain as is.
Now all you need is to specify the actual key type in the inherited class and change the AsExpression
override signature. For instance:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
public class PersonAgeOrderSpecification : OrderSpecification<Person, int>
{
public PersonAgeOrderSpecification(Sort direction) : base(direction) { }
public override Expression<Func<Person, int>> AsExpression()
{
return person => person.Age;
}
}
and everything will be fine.