Rails dynamic finders for .NET 4.0
Ruby on Rails allows you to use ‘dynamic’ finders to query the database. This is actually a feature from ActiveRecord to dynamicly use methods which will represent where clauses on the database.
Some examples:
User.find(:first, :conditions => ["name = ?", name]) User.find_by_name(name) User.find(:all, :conditions => ["city = ?", city]) User.find_all_by_city(city) User.find(:all, :conditions => ["street = ? AND city IN (?)", street, cities]) User.find_all_by_street_and_city(street, cities)
With .NET 4.0 we have dynamics of our own so I thought why not recreate this feature…
By creating an extension method for IEnumerable
public static class DynamicExtensions
{
public static dynamic AsDynamic<T>(this IEnumerable<T> source)
{
return new DynamicEnumerable<T>(source);
}
public static dynamic AsDynamic<T>(this IQueryable<T> source)
{
return new DynamicQueryable<T>(source);
}
}
The Dynamic classes will inherit from DynamicObject to give basic dynamics support.
sealed class DynamicQueryable<T> : DynamicObject
{
private readonly IQueryable<T> source;
public DynamicQueryable(IQueryable<T> source)
{
this.source = source;
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
var match = methodMatcher.Match(binder.Name);
if (match.Success)
{
var properties = match.Groups[2].Value.Split(new[] { "And" }, StringSplitOptions.RemoveEmptyEntries);
var predicate = BuildExpression<T>(properties, args);
if (match.Groups[1].Success)
result = source.Where(predicate);
else
result = source.FirstOrDefault(predicate);
return true;
}
return base.TryInvokeMember(binder, args, out result);
}
}
With the only missing part the BuildExpression method which will create the expression tree:
private static Expression<Func<T, bool>> BuildExpression<T>(string[] properties, object[] args)
{
if (properties.Length < 1)
throw new InvalidOperationException("Need to specify at least one property.");
if (args.Length != properties.Length)
throw new InvalidOperationException("Method expects " + properties.Length + " parameters and only got " + args.Length + " values.");
var param = Expression.Parameter(typeof(T), "p");
var body = Expression.Equal(Expression.Property(param, properties[0]), Expression.Constant(args[0]));
for (var i = 1; i < properties.Length; i++)
body = Expression.AndAlso(body, Expression.Equal(Expression.Property(param, properties[i]), Expression.Constant(args[i])));
return Expression.Lambda<Func<T, bool>>(body, param);
}
And the only difference with the DynamicEnumerable class is that the expression will be compiled to use for the Where/FirstOrDefault.
Full source at pastebin.