Building a Func<TEntity, bool> delegate from an anonymous object

The background to this code is that I’m getting rid of Entity Framework – it’s too slow and IQueryable is so cool but ends up sucking by slowly bleeding into the code base and thereby becoming difficult to performance tune.

Thanks, as always, go to my friend .NETJunkie for his remarkable post on the query pattern that has led me to this design pattern. I have my own variation on the theme that I will post at some point in time … but I digress …

I’m migrating all my code to use the query pattern by implementing IQueryHandler<> and will use Dapper to access the database. I will be implementing local caching using a simple decorator over the relevant IQueryHandler<>‘s and that is the point of this post.

Dapper takes a query and an anonymous object. What I plan to do is intercept calls to Dapper and check the local cache for the entit(y/ies) being requested before going to the database. To start with the cache will be a simple List<TEntity>. To find something in a list we use Linq. To use Linq I need to convert each anonymous object into a delegate that can be passed into the Where extension method of List<TEntity> (a Func<TEntity, bool>). For now I only plan to support ==[&& ==[&& ==[...]]] queries.

Dapper is a simple, super fast, data mapper implementation.

var dog = connection.Query<Dog>("select Id = @Id", new { Id = 1 });

To the code.

The standard abstractions (the delegate builder is a standard query handler)

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> 
    where TQuery : IQuery<TResult>
{
    TResult Execute(TQuery query);
}

The generic parameter object for the EqualsDelegateBuilder query handler

public class EqualsDelegate<TEntity> : IQuery<Func<TEntity, bool>>
{
    public object query { get; set; }
}

And the query handler that will create a Func<TEntity, bool> from an anonymous object

public class EqualsDelegateBuilder<TEntity> : 
    IQueryHandler<EqualsDelegate<TEntity>, Func<TEntity, bool>>
{
    public Func<TEntity, bool> Execute(EqualsDelegate<TEntity> query)
    {
        object o = query.query;
        var type = o.GetType();
        var props = type.GetProperties()
            .Where(p => p.GetIndexParameters().Length == 0).ToArray();

        ParameterExpression pe = Expression.Parameter(typeof(TEntity), "x");
        Expression expression = 
            CreatePropertiesEqualValuesExpression<TEntity>(pe, props, o);

        var lambda = Expression.Lambda<Func<TEntity, bool>>(expression, pe);

        return lambda.Compile();
    }

    private Expression CreatePropertiesEqualValuesExpression<TEntity>(
        ParameterExpression pe,
        PropertyInfo[] props,
        object o)
    {
        Expression result = null;

        foreach (var prop in props)
        {
            Expression expression = 
                CreatePropertyEqualsValueExpression<TEntity>(pe, prop, o);
            result = result == null
                ? expression
                : Expression.AndAlso(result, expression);
        }

        return result;
    }

    private Expression CreatePropertyEqualsValueExpression<TEntity>(
        ParameterExpression pe,
        PropertyInfo prop,
        object o)
    {
        Expression left = 
            Expression.Property(pe, typeof(TEntity).GetProperty(prop.Name));
        Expression right;
        if (typeof(TEntity).GetProperty(prop.Name).PropertyType == typeof(long))
        {
            right = Expression.Constant(
                Convert.ToInt64(prop.GetValue(o, null)),
                typeof(TEntity).GetProperty(prop.Name).PropertyType);
        }
        else
        {
            right = Expression.Constant(prop.GetValue(o, null), prop.PropertyType);
        }

        return Expression.Equal(left, right);;
    }
}

This is the test data

private readonly DateTime date = DateTime.Parse("31 jul 2014");
private readonly Guid uid = Guid.NewGuid();

public class DataEntity
{
    public long Id { get; set; }
    public string Name { get; set; }
    public DateTime Date { get; set; }
    public Guid Uid { get; set; }
}

private IEnumerable<DataEntity> Entities()
{
    return new DataEntity[]
    {
        new DataEntity{ Id = 1, Name = "name", Date = this.date, Uid = Guid.NewGuid() },
        new DataEntity{ Id = 2, Name = "stuff", Date = this.date.AddDays(1), Uid = this.uid },
        new DataEntity{ Id = 3, Name = "also", Date = this.date.AddDays(2), Uid = this.uid },
        new DataEntity{ Id = 4, Name = "else", Date = this.date, Uid = Guid.NewGuid() },
        new DataEntity{ Id = 5, Name = "name", Date = this.date, Uid = Guid.NewGuid() },
    };
}

The methods that call the delegate builder and filter the data

public Func<TEntity, bool> BuildDelegate<TEntity>(object o)
{
    var handler = new EqualsDelegateBuilder<TEntity>();
    var query = new EqualsDelegate<TEntity>
    {
        query = o
    };

    return handler.Execute(query);
}

public DataEntity Get(object o)
{
    return this.GetAll(o).FirstOrDefault();
}

public IEnumerable<DataEntity> GetAll(object o)
{
    return this.Entities().Where(this.BuildDelegate<DataEntity>(o)).ToList();
}

And the tests

[Fact]
public void Get_ById_Succeeds()
{
    var result = this.Get(new { Id = 1 });
    Assert.NotNull(result);
}

[Fact]
public void Get_ByName_Succeeds()
{
    var result = this.GetAll(new { Name = "name" });
    Assert.Equal(2, result.Count());
}

[Fact]
public void Get_ByDate_Succeeds()
{
    var result = this.GetAll(new { Date = this.date });
    Assert.Equal(3, result.Count());
}

[Fact]
public void Get_ByGuid_Succeeds()
{
    var result = this.GetAll(new { Uid = this.uid });
    Assert.Equal(2, result.Count());
}

[Fact]
public void Get_ByMultipleProperties_Succeeds()
{
    var result = this.GetAll(new { Id = 2, Uid = this.uid });
    Assert.Equal(1, result.Count());
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.