После перехода на Visual Studio 2015 столкнулся с неприятным и довольно критичным отличием в компиляции одного и того же кода старой версией студии и новой.

Суть его состоит в том, что при генерации дерева выражений Visual Studio 2015 немного по другому генерирует результат, а именно вставляет операнд Convert() для явного приведения типов, которые могут в общем-то приводиться неявно.

Это происходит не всегда, а при определённых условиях. Вот пример такого кода:

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 генерирует дерево выражений для запроса примерно такого вида:

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

VS'15 генерирует дерево выражений немного другое:

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

Как мы можем увидеть, вставляется операция явного преобразования объекта в интерфейс. При попытке выполнить такой запрос недра EntityFramework (версия 6.1.3) выдают исключение:

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

Копания Гугла по этому вопросу привели на баг-трекер проекта Roslyn где в конце концов всё свелось к тому, что нужно ждать EF7, где эту багу пофиксят (в beta8 вроде как уже пофиксили). Как я понял в Roslyn никто данное поведение менять не будет (баг был зарегистрирован 10 августа — сейчас почти ноябрь и у меня стоит Update 1 CTP).

Что же делать если у вас «старые» проекты на EF6, а хочется работать на VS'15? Кроме того, одним EF дело не ограничивается. Подобную ошибку, я так же получил при работе с C*LINQ (Cassandra).

В качестве костыля пока придумал только убирать дополнительные явные преобразования типов из построенного выражения перед передачей их в запрос:

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

Теперь вышеприведенный запрос к EF можно переписать в таком виде:

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

Может кому и пригодится, а может кто-то посоветует что-то получше.

UPD: Как подсказал mayorovp достаточно переписать исходный запрос чуть посложнее:
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");
    }
}

при таком варианте всё работает.

Комментарии (13)


  1. ThePretender
    20.10.2015 16:42

    У меня в проекте такой код работает нормально:

    public static IQueryable<T> Range<T>(this IDbSet<T> set, HashSet<long> ids) where T : EntityBase
    {
    	return set.Where(x => ids.Contains(x.Id));
    }
    


    Что если вместо интерфейса использовать абстрактный класс?


    1. LionSoft
      20.10.2015 18:00

      Попробовал для интереса не интерфейс, а базовый класс — та же фигня, только теперь в ошибке написано, что не может привести объект к базовому классу.
      Ваш пример также не работает, единственное отличие, я дописал материализацию запроса в массив:

      public static T[] Range<T>(this IDbSet<T> set, HashSet<long> ids) where T : EntityBase
      {
      	return set.Where(x => ids.Contains(x.Id)).ToArray();
      }
      


      1. ThePretender
        20.10.2015 18:14

        Так, а давайте ради интереса сделаем

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


        1. LionSoft
          20.10.2015 18:36

          Ну, так компилятор не даст сделать — IQueryable не приводится к IQueryable.

          Думаю, что ваш код работает, потому что подготовленный IQueryable уходит в ODataProvider,
          который дополнительно преобразовывает выражение.


          1. ThePretender
            20.10.2015 19:08

            Я хотел сказать

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


            :) DbSet точно должен приводиться к IQueryable.


            1. LionSoft
              21.10.2015 12:29

              У нас обоих парсер порезал типизированные генерики )))
              В любом случае, явное приведение DbSet к IQueryable ничего не меняет, т.к. FirstOrDefault — это именно метод-расширение интерфейса IQueryable, а не DbSet.


              1. mayorovp
                21.10.2015 12:47
                +1

                Надо приводить не к IQueryable<TEntity> — это ничего не дает, а к IQueryable<EntityBase>, пользуясь ковариантностью IQueryable<>. В таком случае в лямбде у параметра x будет тип EntityBase — и у компилятора не будет причин явно добавлять преобразование типов.


                1. LionSoft
                  21.10.2015 13:01

                  Я понял.
                  Гипотеза интересная, но…

                  var res = (IQueryable<IEntityObjectId>)Db.Set<TEntity>().First(x => x.Id == entity.Id);
                  

                  выдаёт ту же ошибку (


                  1. mayorovp
                    21.10.2015 13:13
                    +1

                    Вы забыли еще одну пару скобок.

                    var res = ((IQueryable<IEntityObjectId>)Db.Set<TEntity>()).First(x => x.Id == entity.Id);
                    


                    1. LionSoft
                      21.10.2015 19:30

                      Точно! Как я мог так протупить? )))
                      ТАК ДЕЙСТВИТЕЛЬНО РАБОТАЕТ!
                      Что-же получается, можно обойтись и без упрощалки экспрешенов.
                      Спасибо! [])


              1. ThePretender
                21.10.2015 12:58

                Меня очень смущает “LINQ to Entities” в тексте ошибки. У меня такое бывало, когда в блоке using был зарефан только System.Ling. Тогда по факту DbSet неявно приводится к IEnumerable и FirstOrDefault применяется уже к нему, используя механизм Linq to Entities вместо Linq to SQL.


                1. LionSoft
                  21.10.2015 13:07

                  У меня:

                  using System.Data.Entity;
                  using System.Data.Entity.Infrastructure;
                  using System.Linq;
                  

                  и да, я проверял — вызывается метод-расширение именно IQueryable интерфейса. Ну и потом, я же EF и использую, а не Linq2Sql


                1. mayorovp
                  21.10.2015 13:14

                  Вы путаете “LINQ to Entities” с “LINQ to Objects”.