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.