Тип возвращаемого типа generic с ограничением типа в LINQ to Entities (EF4.1)

У меня есть простой метод расширения для фильтрации LINQ IQueryable по тегам. Я использую это с LINQ to Entities с интерфейсом:

public interface ITaggable
{
    ICollection<Tag> Tags { get; } 
}

Не работает следующее, возвращая IQueryable<ITaggable> вместо IQueryable<T>:

public static IQueryable<T> WhereTagged<T>(this IQueryable<T> set, string tag) where T:ITaggable
    {
        return set.Where(s=>s.Tags.Any(t=>t.Name.ToLower() == tag.ToLower()));
    }

Это приводит к исключению LINQ to Entities cast:

"Невозможно ввести тип 'ReleaseGateway.Models.Product' для ввода типа 'ReleaseGateway.Models.ITaggable. LINQ to Entities поддерживает только литье Entity Data Model примитивных типов." (System.NotSupportedException) Исключение System.NotSupportedException catch: "Невозможно ввести тип" Project.Models.Product "для ввода типа 'Project.Models.ITaggable. LINQ to Entities поддерживает только литье Entity Data Model примитивные типы."

Он работает без ограничения, подобного этому, но я должен явно объявить тип T в моем коде приложения:

public static IQueryable<T> WhereTagged<T>(this IQueryable<ITaggable> set, string tag)
{
    return set.Where(s=>s.Tags.Any(t=>t.Name.ToLower() == tag.ToLower())).Cast<T>();
}

Вопрос: Почему ограничение типа приводит к типу возврата? Могу ли я переписать это, чтобы воспользоваться выводом типа из вызывающего абонента метода расширения?

Ответ 1

Я искал тот же ответ и не удовлетворен синтаксической чистотой предоставленных ответов, я продолжал искать и находил этот пост.

ТЛ; дг; - добавить класс к вашим ограничениям, и он работает.

LINQ to Entities поддерживает только листинг EDM примитивных или перечисляемых типов с интерфейсом IEntity

public static IQueryable<T> WhereTagged<T>(this IQueryable<T> set, string tag)
    where T: class, ITaggable

Ответ 2

Я подозреваю, что проблема возникает из вызова s.Tags. Поскольку s является Product, но вы вызываете ITaggable.Tags, выражение, которое генерируется, выглядит более похоже:

set.Where(s=>((ITaggable)s).Tags.Any(...))

Это просто путает Entity Framework. Попробуйте следующее:

((IQueryable<ITaggable>)set)
    .Where(s=>s.Tags.Any(t=>t.Name.ToLower() == tag.ToLower()))
    .Cast<T>();

Так как IQueryable является ковариантным интерфейсом, это будет рассматривать набор как IQueryable<ITaggable>, который должен работать, поскольку ваш второй пример в основном делает точно то же самое.

Ответ 3

Вы никогда не показываете, где это используется. Я думаю, что вы уже передаете метод IQueryable<ITaggable> в первую очередь.

Доказательство концепции https://ideone.com/W8c66

using System;
using System.Linq;
using System.Collections.Generic;

public class Program
{
    public interface ITaggable {}

    public struct TagStruct : ITaggable {}
    public class  TagObject : ITaggable {}

    public static IEnumerable<T> DoSomething<T>(IEnumerable<T> input) 
        where T: ITaggable
    {
        foreach (var i in input) yield return i;
    }

    public static void Main(string[] args)
    {
        var structs = new [] { new TagStruct() };
        var objects = new [] { new TagObject() };

        Console.WriteLine(DoSomething(structs).First().GetType());
        Console.WriteLine(DoSomething(objects).First().GetType());               
    }
}

Выход

Program+TagStruct
Program+TagObject

Таким образом, он возвращает тип ввода, а не ограниченный интерфейс.

Не удивительно, что было бы результатом, если бы DoSometing требовал два интерфейса?

    public static IEnumerable<T> DoSomething<T>(IEnumerable<T> input) 
        where T: ITaggable, ISerializable

??

Ответ 4

Вам не нужно, чтобы Cast в конце был указан dlev.

Я предполагаю, что класс продукта реализует ITaggable? Я думаю, что удаление Cast поможет решить проблему.