После перехода на Visual Studio 2015 столкнулся с неприятным и довольно критичным отличием в компиляции одного и того же кода старой версией студии и новой.
Суть его состоит в том, что при генерации дерева выражений Visual Studio 2015 немного по другому генерирует результат, а именно вставляет операнд Convert() для явного приведения типов, которые могут в общем-то приводиться неявно.
Это происходит не всегда, а при определённых условиях. Вот пример такого кода:
VS'13 генерирует дерево выражений для запроса примерно такого вида:
VS'15 генерирует дерево выражений немного другое:
Как мы можем увидеть, вставляется операция явного преобразования объекта в интерфейс. При попытке выполнить такой запрос недра EntityFramework (версия 6.1.3) выдают исключение:
Копания Гугла по этому вопросу привели на баг-трекер проекта Roslyn где в конце концов всё свелось к тому, что нужно ждать EF7, где эту багу пофиксят (в beta8 вроде как уже пофиксили). Как я понял в Roslyn никто данное поведение менять не будет (баг был зарегистрирован 10 августа — сейчас почти ноябрь и у меня стоит Update 1 CTP).
Что же делать если у вас «старые» проекты на EF6, а хочется работать на VS'15? Кроме того, одним EF дело не ограничивается. Подобную ошибку, я так же получил при работе с C*LINQ (Cassandra).
В качестве костыля пока придумал только убирать дополнительные явные преобразования типов из построенного выражения перед передачей их в запрос:
Теперь вышеприведенный запрос к EF можно переписать в таком виде:
Может кому и пригодится, а может кто-то посоветует что-то получше.
UPD: Как подсказал mayorovp достаточно переписать исходный запрос чуть посложнее:
при таком варианте всё работает.
Суть его состоит в том, что при генерации дерева выражений 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");
}
}
при таком варианте всё работает.
ThePretender
У меня в проекте такой код работает нормально:
Что если вместо интерфейса использовать абстрактный класс?
LionSoft
Попробовал для интереса не интерфейс, а базовый класс — та же фигня, только теперь в ошибке написано, что не может привести объект к базовому классу.
Ваш пример также не работает, единственное отличие, я дописал материализацию запроса в массив:
ThePretender
Так, а давайте ради интереса сделаем
LionSoft
Ну, так компилятор не даст сделать — IQueryable не приводится к IQueryable.
Думаю, что ваш код работает, потому что подготовленный IQueryable уходит в ODataProvider,
который дополнительно преобразовывает выражение.
ThePretender
Я хотел сказать
:) DbSet точно должен приводиться к IQueryable.
LionSoft
У нас обоих парсер порезал типизированные генерики )))
В любом случае, явное приведение DbSet к IQueryable ничего не меняет, т.к. FirstOrDefault — это именно метод-расширение интерфейса IQueryable, а не DbSet.
mayorovp
Надо приводить не к IQueryable<TEntity> — это ничего не дает, а к IQueryable<EntityBase>, пользуясь ковариантностью IQueryable<>. В таком случае в лямбде у параметра x будет тип EntityBase — и у компилятора не будет причин явно добавлять преобразование типов.
LionSoft
Я понял.
Гипотеза интересная, но…
выдаёт ту же ошибку (
mayorovp
Вы забыли еще одну пару скобок.
LionSoft
Точно! Как я мог так протупить? )))
ТАК ДЕЙСТВИТЕЛЬНО РАБОТАЕТ!
Что-же получается, можно обойтись и без упрощалки экспрешенов.
Спасибо! [])
ThePretender
Меня очень смущает “LINQ to Entities” в тексте ошибки. У меня такое бывало, когда в блоке using был зарефан только System.Ling. Тогда по факту DbSet неявно приводится к IEnumerable и FirstOrDefault применяется уже к нему, используя механизм Linq to Entities вместо Linq to SQL.
LionSoft
У меня:
и да, я проверял — вызывается метод-расширение именно IQueryable интерфейса. Ну и потом, я же EF и использую, а не Linq2Sql
mayorovp
Вы путаете “LINQ to Entities” с “LINQ to Objects”.