LINQ to Entities chỉ hỗ trợ truyền kiểu nguyên thủy hoặc kiểu liệt kê EDM với giao diện IEntity


96

Tôi có phương pháp mở rộng chung sau:

public static T GetById<T>(this IQueryable<T> collection, Guid id) 
    where T : IEntity
{
    Expression<Func<T, bool>> predicate = e => e.Id == id;

    T entity;

    // Allow reporting more descriptive error messages.
    try
    {
        entity = collection.SingleOrDefault(predicate);
    }
    catch (Exception ex)
    {
        throw new InvalidOperationException(string.Format(
            "There was an error retrieving an {0} with id {1}. {2}",
            typeof(T).Name, id, ex.Message), ex);
    }

    if (entity == null)
    {
        throw new KeyNotFoundException(string.Format(
            "{0} with id {1} was not found.",
            typeof(T).Name, id));
    }

    return entity;
}

Thật không may, Entity Framework không biết cách xử lý predicatevì C # đã chuyển đổi vị từ thành như sau:

e => ((IEntity)e).Id == id

Entity Framework ném ngoại lệ sau:

Không thể truyền kiểu 'IEntity' thành kiểu 'SomeEntity'. LINQ to Entities chỉ hỗ trợ truyền kiểu nguyên thủy hoặc kiểu liệt kê EDM.

Làm thế nào chúng tôi có thể làm cho Entity Framework hoạt động với IEntitygiao diện của chúng tôi ?

Câu trả lời:


188

Tôi đã có thể giải quyết vấn đề này bằng cách thêm classràng buộc kiểu chung vào phương thức mở rộng. Tuy nhiên, tôi không chắc tại sao nó hoạt động.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}

6
Hoạt động cho tôi quá! Tôi rất thích ai đó có thể giải thích điều này. #linqblackmagic
berko

Bạn có thể vui lòng giải thích làm thế nào bạn thêm ràng buộc này
yrahman

5
Tôi đoán là kiểu lớp được sử dụng chứ không phải kiểu giao diện. EF không biết về kiểu giao diện nên không thể chuyển nó sang SQL. Với ràng buộc lớp, kiểu được suy ra là kiểu DbSet <T> mà EF biết phải làm gì với.
jwize

1
Hoàn hảo, thật tuyệt khi có thể thực hiện các truy vấn dựa trên Giao diện và vẫn duy trì bộ sưu tập dưới dạng IQueryable. Tuy nhiên, hơi khó chịu là về cơ bản không có cách nào nghĩ ra cách khắc phục này, nếu không biết hoạt động bên trong của EF.
Anders

Những gì bạn đang thấy ở đây là giới hạn thời gian của trình biên dịch cho phép trình biên dịch C # xác định rằng T thuộc loại IEntity trong phương thức, do đó có thể xác định rằng bất kỳ cách sử dụng "thứ" IEntity nào là hợp lệ như trong thời gian biên dịch mã MSIL được tạo sẽ tự động thực hiện kiểm tra này cho bạn trước cuộc gọi. Để làm rõ, việc thêm "class" làm ràng buộc kiểu ở đây cho phép collection.FirstOrDefault () chạy chính xác vì nó có khả năng trả về một thể hiện mới của T gọi một ctor mặc định trên kiểu dựa trên lớp.
War

64

Một số giải thích bổ sung liên quan đến class"sửa chữa".

Câu trả lời này cho thấy hai biểu thức khác nhau, một với và một không có where T: classràng buộc. Không có classràng buộc, chúng tôi có:

e => e.Id == id // becomes: Convert(e).Id == id

và với ràng buộc:

e => e.Id == id // becomes: e.Id == id

Hai biểu thức này được xử lý khác nhau bởi khung thực thể. Nhìn vào các nguồn EF 6 , người ta có thể thấy rằng ngoại lệ đến từ đây, hãy xemValidateAndAdjustCastTypes() .

Điều gì xảy ra là EF cố gắng truyền IEntityvào một cái gì đó có ý nghĩa trong thế giới mô hình miền, tuy nhiên nó không thành công trong việc làm như vậy, do đó ngoại lệ được ném ra.

Biểu thức với classràng buộc không chứa Convert()toán tử, ép kiểu không được thử và mọi thứ đều ổn.

Nó vẫn còn là một câu hỏi mở, tại sao LINQ lại xây dựng các biểu thức khác nhau? Tôi hy vọng rằng một số thuật sĩ C # sẽ có thể giải thích điều này.


1
Cảm ơn vì lời giải thích.
Jace Rhea

9
@JonSkeet ai đó đã cố gắng triệu tập trình hướng dẫn C # tại đây. Bạn ở đâu?
Nick N.

23

Entity Framework không hỗ trợ điều này ngay lập tức, nhưng một công cụ ExpressionVisitordịch biểu thức được viết dễ dàng:

private sealed class EntityCastRemoverVisitor : ExpressionVisitor
{
    public static Expression<Func<T, bool>> Convert<T>(
        Expression<Func<T, bool>> predicate)
    {
        var visitor = new EntityCastRemoverVisitor();

        var visitedExpression = visitor.Visit(predicate);

        return (Expression<Func<T, bool>>)visitedExpression;
    }

    protected override Expression VisitUnary(UnaryExpression node)
    {
        if (node.NodeType == ExpressionType.Convert && node.Type == typeof(IEntity))
        {
            return node.Operand;
        }

        return base.VisitUnary(node);
    }
}

Điều duy nhất bạn sẽ phải làm là chuyển đổi vị từ được chuyển vào bằng cách sử dụng biểu thức khách truy cập như sau:

public static T GetById<T>(this IQueryable<T> collection, 
    Expression<Func<T, bool>> predicate, Guid id)
    where T : IEntity
{
    T entity;

    // Add this line!
    predicate = EntityCastRemoverVisitor.Convert(predicate);

    try
    {
        entity = collection.SingleOrDefault(predicate);
    }

    ...
}

Một phương pháp tiếp cận không linh hoạt khác là sử dụng DbSet<T>.Find:

// NOTE: This is an extension method on DbSet<T> instead of IQueryable<T>
public static T GetById<T>(this DbSet<T> collection, Guid id) 
    where T : class, IEntity
{
    T entity;

    // Allow reporting more descriptive error messages.
    try
    {
        entity = collection.Find(id);
    }

    ...
}

1

Tôi đã có cùng một lỗi nhưng một vấn đề tương tự nhưng khác nhau. Tôi đang cố gắng tạo một hàm mở rộng trả về IQueryable nhưng tiêu chí bộ lọc dựa trên lớp cơ sở.

Cuối cùng tôi đã tìm ra giải pháp cho phương thức mở rộng của tôi để gọi .Select (e => e as T) trong đó T là lớp con và e là lớp cơ sở.

chi tiết đầy đủ ở đây: Tạo phần mở rộng IQueryable <T> bằng cách sử dụng lớp cơ sở trong EF

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.