简体   繁体   中英

LINQ: SQL optimised query for basic generic types

I'm trying to get LINQ to generate SQL optimised code for comparing basic generic types.

In short: I want the code below to be SQL optimised, eg generate the SQL query select * from X where X_ID == id .

public async Task GetByIdAsync(long id)
{
    var entity = await DB.Set<TEntity>().SingleOrDefaultAsync(i => i.Id == id);
}

The problem is that this does not happen, because i.Id is a generic type (see below), so LINQ does not know how to handle it.

Full demonstration code, with errors in comments.

public interface IBaseEntity<TIdType>
{
    TIdType Id { get; set; }
}

public class TestClass<TEntity, TIdType>
    where TEntity : class, IBaseEntity<TIdType>
    // TIdType will be a basic numeric type like short and int.
    where TIdType : struct, IConvertible
{
    public async Task GetByIdAsync(long id)
    {
        // This is the best solution I could find for comparing basic generic types.
        // However, it is not SQL optimised, so it will be evaluated in code.
        var entity = await DB.Set<TEntity>().SingleOrDefaultAsync(i => i.Id.ToInt64(NumberFormatInfo.CurrentInfo) == id);
    }

    public async Task CompileErrorAsync(TIdType id)
    {
        // Error: Operator '==' cannot be applied to operands of type 'TIdType' and 'TIdType'
        var entity = await DB.Set<TEntity>().SingleOrDefaultAsync(i => i.Id == id);

        // Using "dynamic" also won't work. Error: An expression tree may not contain a dynamic operation.
    }

    public async Task RuntimeFailureAsync(long id)
    {
        // Runtime warning: Possible unintended use of method Equals(object) for arguments of different types ... This comparison will always return 'false'.
        var entity = await DB.Set<TEntity>().SingleOrDefaultAsync(i => id.Equals(i.Id));

        // Runtime warning: Possible unintended use of method Equals(object) for arguments of different types in expression 'i.MC_CODEID.Equals(Convert(__id_0, Object))'. This comparison will always return 'false'.
        var entity = await DB.Set<TEntity>().SingleOrDefaultAsync(i => i.Id.Equals(id));

        // CompareTo() also fails: System.ArgumentException: Object must be of type Int64.
    }
}

EDIT : This question has two valid answers, one where you manually build the expression tree , and another where every child class has an override to handle it's own unique case. My final solution borrowed from both.

Here you have to use Expression Tree generation to make query work as expected. Simplified your class:

public class TestClass<TEntity, TIdType>
    where TEntity : class, IBaseEntity<TIdType>
    // TIdType will be a basic numeric type like short and int.
    where TIdType : struct, IConvertible
{
    public async Task<TEntity> GetByIdAsync(long id)
    {
        var entity = await GetFilteredById(id).SingleOrDefaultAsync();
        return entity;
    }

    private IQueryable<TEntity> GetFilteredById(long id)
    {
        var query = DB.Set<TEntity>().AsQueryable();

        var param = Expression.Parameter(typeof(TEntity), "e");
        var prop = Expression.PropertyOrField(param, nameof(IBaseEntity<TIdType>.Id));
        Expression idExpr;

        if (prop.Type != typeof(long))
            idExpr = Expression.Constant(Convert.ChangeType(id, prop.Type));
        else
            idExpr = Expression.Constant(id);

        var predicateLambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(prop, idExpr), param);
        return query.Where(predicateLambda);
    }
}

Running with the idea of overloads and expressions (from the comments), this is more or less what I came up with.

public abstract class TestClass<TEntity, TIdType, TModel>
    where TEntity : class, IBaseEntity<TIdType>
    where TIdType : struct, IConvertible // I probably don't need this line any more.
    where TModel : class IBaseModel<TIdType>
{
    protected abstract Expression<Func<TEntity, bool>> GetSingleEntityWhereClause(TModel model);

    public async Task TestMethodAsync(TModel model)
    {
        var test = await DB.Set<TEntity>().SingleOrDefaultAsync(GetSingleEntityWhereClause(model));
    }
}

public class SampleImplementation : TestClass<Sample, short, SampleModel>
{
    protected override Expression<Func<Sample, bool>> GetSingleEntityWhereClause(SampleModel model)
    {
        return p => p.Id == model.Id;
    }
}

And then, if I want to implement this for a specific type.

public abstract class TestClass<TEntity, TIdType, TModel>
    // ...
{
    // ...

    public async Task GetByIdAsync(long id)
    {
        var model = Activator.CreateInstance<TModel>();
        model.Id = GetIdTypeValue(id);
        var entity = await DB.Set<TEntity>().SingleOrDefaultAsync(GetSingleEntityWhereClause(model));
    }

    private static TIdType GetIdTypeValue(object id)
    {
        var type = typeof(TIdType);

        if (type == typeof(short))
        {
            return (dynamic)Convert.ToInt16(id);
        }
        else if (type == typeof(int))
        {
            return (dynamic)Convert.ToInt32(id);
        }
        else
        {
            throw new Exception(string.Format("Unknown ID type \"{0}\"", type));
        }
    }
}

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