📜 ⬆️ ⬇️

The critical difference between compiling the expression tree in Visual Studio 2015

After switching to Visual Studio 2015, I encountered an unpleasant and rather critical difference in compiling the same code with the old studio version and the new one.

Its essence is that when generating the expression tree, Visual Studio 2015 generates a little differently the result, namely, inserts the Convert () operand to explicitly cast types that can in general be implicitly cited.

This does not always happen, but under certain conditions. Here is an example of such a code:

public interface IEntity { string Id { get; set; } } public class Customer : IEntity { public string Id { get; set; } public string Name { get; set; } } public class MyDbContext : DbContext { public string FindById<TEntity>(string id) where TEntity: class, IEntity { return DbSet<TEntity>().FirstOrDefault(x => x.Id == "id"); } } 

VS'13 generates an expression tree for a query of the following form:
')
 .Lambda #Lambda1<System.Func`2[Sam.DbContext.Customer,System.Boolean]>(Sam.DbContext.Customer $x) { $x.Id == "id" } 

VS'15 generates a tree of expressions a little different:

 .Lambda #Lambda1<System.Func`2[Sam.DbContext.Customer,System.Boolean]>(Sam.DbContext.Customer $x) { ((Sam.DbContext.IEntity)$x).Id == "id" } 

As we can see, the operation of explicitly converting an object into an interface is inserted. When you try to perform such a request, the EntityFramework (version 6.1.3) subsoil throws an exception:

  Unable to cast the type 
   'Sam.DbContext.Customer' to type 
   'Sam.DbContext.IEntity'. 
   LINQ to Entities only supports casting EDM primitive or enumeration types.

Google’s digging on this issue led to the Roslyn project's bug tracker, where in the end it all came down to waiting for EF7, where this bug was fixed (in beta8, it’s kind of fixed). As I understood, no one will change this behavior in Roslyn (the bug was registered on August 10 - now it's almost November and I have Update 1 CTP).

What to do if you have “old” projects on EF6, and you want to work on VS'15? In addition, the case is not limited to one EF. I also received a similar error when working with C * LINQ (Cassandra).

As a crutch, so far I have thought of only removing additional explicit type conversions from the constructed expression before passing them to the query:

 using System; using System.Linq.Expressions; using System.Reflection; namespace Sam.Extensions.Expressions { /// <summary> /// Removes unnessesary Convert() operands (inserted by Roslyn, but not supported by EF). /// </summary> public class SimplifyExpression : ExpressionVisitor { /// <summary> /// Simplificates the <paramref name="sourceExpression"/>. /// </summary> /// <param name="sourceExpression">Source expression</param> public static Expression Execute(Expression sourceExpression) { return new SimplifyExpression().Visit(sourceExpression); } /// <summary> /// Creates and simplificates the <paramref name="predicateExpression"/>. /// Can be used when building EnituyFramework LINQ. /// <example> /// var res = Db.Set{TEntity}().First(SimplifyExpression.Predicate{TEntity}(x => x.Id == id)); /// </example> /// </summary> /// <typeparam name="T">Predicate source type.</typeparam> /// <param name="predicateExpression">Predicate expression</param> public static Expression<Func<T, bool>> Predicate<T>(Expression<Func<T, bool>> predicateExpression) { return predicateExpression.Simplify(); } protected override Expression VisitUnary(UnaryExpression node) { // Replace explicit Convert to implicit one. (Except converion to Object). if (node.Type != typeof(object) && node.Type.IsAssignableFrom(node.Operand.Type)) return Visit(node.Operand); return base.VisitUnary(node); } } 

Now the above query to EF can be rewritten as follows:

  return DbSet<TEntity>().FirstOrDefault(SimplifyExpression.Predicate<TEntity>(x => x.Id == "id")); 

Maybe someone will come in handy, and maybe someone will advise something better.

UPD: As prompted by mayorovp, it is enough to rewrite the original request a bit more complicated:
 public class MyDbContext : DbContext { public string FindById<TEntity>(string id) where TEntity: class, IEntity { return (TEntity)((IQueryable<IEntity>)(DbSet<TEntity>())).FirstOrDefault(x => x.Id == "id"); } } 

With this option, everything works.

Source: https://habr.com/ru/post/269139/


All Articles