.Net 5 Entity Framework Generic Any Query

145 Views Asked by At

As I am a lazy dog I would like to implement a generic UniqueValidator for .net FluentValidation. The goal is simple: have a validator to which I pass the model, the Expression to get the Property / Field that has to be unique and run a EF Any query. That would avoid to write a dumb class every single time one have to verify the unicity of a value in DB.

I came up after few tweaks to what seems to me a fair solution to avoid passing and invoking a precompiled Lambda to EF query translator which of course would result in a expression could not be translated exception.

Here what I have implemented:

public class UniqueValidator : IUniqueValidator
{
    private ApplicationContext _dbContext;

    public UniqueValidator(ApplicationContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<bool> IsUnique<T>(T model, Expression<Func<T, string>> expression, CancellationToken cancellationToken) where T : class
    {
        // Getting the value to check for unicity:
        Func<T, string> method = expression.Compile(true);
        var value = method(model);

        // For GetDbSet<T>() test purpose, run perfectly:
        bool test = await GetDbSet<T>().OfType<BL.Driver>().AnyAsync(d => d.Email == "[email protected]");


        // Building Linq expression
        var binaryExpression = Expression.Equal(expression.Body, Expression.Constant(value));
        var pe = new ParameterExpression[] { Expression.Parameter(typeof(T)) };

        var anyExpression = Expression.Lambda<Func<T, bool>>(binaryExpression, pe);

        return !(await GetDbSet<T>().AnyAsync(anyExpression));
    }

    private DbSet<T> GetDbSet<T>() where T : class
    {
        return (DbSet<T>)typeof(ApplicationContext)
            .GetProperties()
            .FirstOrDefault(p => p.PropertyType == typeof(DbSet<T>))
            .GetValue(_dbContext);
    }
}

Here is how the validator is used:

RuleFor(d => d)
    .MustAsync((driver, cancellationToken) => {
        return uv.IsUnique(driver, d => d.Email, cancellationToken);
    });

Unfortunately this throw a very cumbersome and not helpful exception:

System.InvalidOperationException: The LINQ expression 'DbSet<Driver>() .Any(d => d.Email == "[email protected]")' could not be translated...

Note: in the UniqueValidator implementation I added a line to test this very same query that is described in the exception and it runs perfectly, just to discard any doubt on the query validity.

I imagine the problem is with the translation of expression.Body but can't see any reason why nor how to workaround it. Any help would be appreciated.

1

There are 1 best solutions below

0
xanatos On BEST ANSWER

You must reuse the original parameter of the expression, or you must use an expression replacer:

var pe = new ParameterExpression[] { Expression.Parameter(typeof(T)) };

change to

var pe = expression.Parameters[0];