简体   繁体   中英

Cast a IQueryable type to interface in Linq to Entities

I have the following method in my generic class:

// This is the class declaration
public abstract class BaseService<TEntity, TKey> : IBaseService<TEntity, TKey> where TEntity : class, IEntity<TKey>

// The Method
public IQueryable<TEntity> GetActive()
{
    if (typeof(IActivable).IsAssignableFrom(typeof(TEntity)))
    {
        return this.repository.Get().Cast<IActivable>()
            .Where(q => q.Active)
            .Cast<TEntity>();
    }
    else
    {
        return this.Get();
    }
}

This is the interface:

public interface IActivable
{
    bool Active { get; set; }
}

Basically, TEntity is an Entity (POCO) class, that can implement IActivable if they have Active property. I want to the method to return all records that have Active value true. However, I have this error:

Unable to cast the type 'WebTest.Models.Entities.Product' to type 'Data.IActivable'. LINQ to Entities only supports casting EDM primitive or enumeration types.

I understand why this error occurs. But the articles on SO does not have any valid solution for my case. Is it achievable with Cast , or any other way? Note: I do not want to convert to IEnumerable , I want to keep IQueryable .

The EF expression parser will work without casting, however you won't be able to compile the C# code without the casting (C# will complain that it doesn't know that TEntity has an Active property). The solution is: cast for the c# compiler and not cast for the EF expression parser.

So if you are sure (you are checking it in the if , so you are) that the object implements IActivable , you can create the expression with the casting (for compiling) and then remove the castings in runtime (which are unnecessary) for EF. For your particular case:

public IQueryable<TEntity> GetActive()
{
  if (typeof(IActivable).IsAssignableFrom(typeof(TEntity)))
  {
    Expression<Func<TEntity, bool>> getActive = x => ((IActivable)x).Active;
    getActive = (Expression<Func<TEntity, bool>>)RemoveCastsVisitor.Visit(getActive);
    return this.repository.Get().Where(getActive);
  }
  else
  {
    return this.Get();
  }
}

The expression visitor is implemented like this:

internal class RemoveCastsVisitor : ExpressionVisitor
{
  private static readonly ExpressionVisitor Default = new RemoveCastsVisitor();

  private RemoveCastsVisitor()
  {
  }

  public new static Expression Visit(Expression node)
  {
    return Default.Visit(node);
  }

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

It just checks if a casting is necessary: if the actual value already implements the type it's casting to, it'll just remove the conversion from the expression, and EF will pick it up correctly.

The trick is to cast the whole IQueryable<TEntity> to IQueryable<IActivable> instead of the first cast:

if (typeof(IActivable).IsAssignableFrom(typeof(TEntity)))
{
    return ((IQueryable<IActivable>)(this.repository.Get()))
        .Where(q => q.Active)
        .Cast<TEntity>();
}

Currently I have an alternative is to use Extension method. However the downside is that my IBaseService cannot declare the GetActive method because the concrete classes do not actually implement it.

public static class BaseServiceExtension
{

    public static IQueryable<TEntity> GetActive<TEntity, TKey>(this IBaseService<TEntity, TKey> service) 
        where TEntity : class, IEntity<TKey>, IActivable
    {
        return service.Get().Where(q => q.Active);
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM