Enhancing queries in dynamic repositories

I wrote yesterday about using the C# 4 dynamic keyword and the .NET 4 DynamicObject class to implement a dynamic repository, which would allow users to invent their own query methods according to a naming convention and have them work automagically. However, the code I showed only did equality queries. In reality, users often need to be able to do comparison queries.

We can do this by extending the method name parser, for example to support method names such as FindPenguinsByAgeLessThan. This is a very flexible approach, but complicates the parser and leads to some pretty epic method names. Instead, I’m going to show you a technique which is a bit more limited, but reads more nicely. We’ll then merge this with one of yesterday’s examples to get a very flexible and extensible dynamic API.

Abusing named arguments for fun and readability

Here’s what I’d like to be able to write:

dynamic repository = new WildlifeRepository();
var youngsters = repository.FindPenguinsByAge(lessThan: 10);

This is very naughty, of course. This is not how we are meant to use named arguments. And in the general case we are punished for it accordingly:

repository.FindPenguinsByAgeAndHeight(lessThan: 10, lessThan: 70);  // compiler error

You can’t specify the same named argument more than once in the same method call, so this trick really only works with single-criterion methods. But it’s a cool trick all the same, so I’m going to show it anyway.

As we saw yesterday, dynamic methods like FindPenguinsByAge are handled by an override of TryInvokeMember. We already have code in place to parse the method name, and we saw yesterday that we could get named arguments from the InvokeMemberBinder.CallInfo object. So all we need to do is check if our arguments have magic comparison names, and if so send LightSpeed an appropriate comparison expression instead of an equality comparison expression.

Now it turns out that building comparison (predicate) expressions programmatically in LightSpeed is quite boring. LightSpeed much prefers you to use the Entity.Attribute(attr) < value syntax, and let’s face it that’s easier to read than writing PredicateExpression all over the place. So let’s have a helper method to create the right predicate expression in a nice readable way:

private static QueryExpression MakePredicate(string attribute, string opName, object comparand)
{
  var attr = Entity.Attribute(attribute);
  switch (opName.ToUpperInvariant())
  {
    case "LESSTHAN": return attr < comparand;
    case "GREATERTHAN": return attr > comparand;
    case "LIKE": return attr.Like((string)comparand);
    // etc.
    default: return attr == comparand;
  }
}

Now we just need to build our LightSpeed query expression using MakePredicate instead of equality comparisons. But there’s a catch: I want my existing code, without comparison-named arguments, still to work and to count as an equality comparison. A quick way to do this is to stick as many copies of “equal” onto the front of the CallInfo.ArgumentNames collection as there are positional arguments:

int positionalArgCount = binder.CallInfo.ArgumentCount - binder.CallInfo.ArgumentNames.Count;
var opNames = Enumerable.Repeat("equal", positionalArgCount)
                        .Concat(binder.CallInfo.ArgumentNames)
                        .ToList();

Now I have a list of operation names that exactly matches up with the attribute names I have parsed out of the method name, and with the comparand argument values. Let’s feed it all to MakePredicate and build a query:

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
  if (binder.Name.StartsWith("Find"))
  {
    int byIndex = binder.Name.IndexOf("By");
    if (byIndex >= 0)
    {
      string collectionName = binder.Name.Substring(4, byIndex - 4);
      string[] attributes = binder.Name.Substring(byIndex + 2).Split(new string[] { "And" }, StringSplitOptions.None);
      int positionalArgCount = binder.CallInfo.ArgumentCount - binder.CallInfo.ArgumentNames.Count;
      var opNames = Enumerable.Repeat("equal", positionalArgCount)
                              .Concat(binder.CallInfo.ArgumentNames)
                              .ToList();
 
      Type entityType = LookupType(collectionName);
      QueryExpression queryExpression = null;
      for (int i = 0; i < attributes.Length; ++i)
      {
        queryExpression &= MakePredicate(attributes[i], opNames[i], args[i]);  // Note using MakePredicate
      }
      result = _unitOfWork.Find(entityType, queryExpression);
      return true;
    }
    else
    {
      // as before
    }
  }
  return base.TryInvokeMember(binder, args, out result);
}

This is all we need to do. Our existing queries and comparison-named queries now both work. We can even mix them as long as we don’t need to use the same comparison operator more than once.

var adelies = repository.FindPenguinsBySpecies("Adelie");  // implicitly "equal"
var youngsters = repository.FindPenguinsByAge(lessThan: 10);
var youngAdelies = repository.FindPenguinsBySpeciesAndAge("Adelie", lessThan: 10);

Because of the restriction on using the same comparison operator more than once, I’d be a bit doubtful about using this in real code. But it’s an interesting trick all the same.

Let’s build a mini-DSL right here in the argument name!

In fact, the trick can be mixed to good effect with yesterday’s trick of using named parameters to specify attribute names:

var youngsters = repository.FindPenguins(age_lessThan: 10);

This avoids the limitation on using the same comparison operator more than once, because you’d have different property name prefixes in each case. It also allows you to perform multiple comparisons on the same property:

var tweens = repository.FindPenguins(age_greaterThan: 10, age_lessThan: 20);

The implementation is a pretty obvious change from yesterday’s FindX code: instead of using the argument name directly as the attribute name, we parse the argument name into an attribute and comparison operator:

QueryExpression queryExpression = null;
for (int i = 0; i < binder.CallInfo.ArgumentNames.Count; ++i)
{
  string[] bits = binder.CallInfo.ArgumentNames[i].Split('_');
  string opName = "equal";
  if (bits.Length >= 2)
  {
    opName = bits[1];
  }
  queryExpression &= MakePredicate(bits[0], opName, args[i]);
}
result = _unitOfWork.Find(entityType, queryExpression);

Et voila! This is a bit like parsing a method name like FindPenguinsByAgeLessThan as I mentioned at the top, but it splits up the big honkin’ method name into a set of small names which are much easier to parse.

More magic argument names

One final example. Suppose users of the dynamic repository want to be able to perform sorting and paging. You can imagine that method names like FindPenguinsByNameLikeOrderByAgeWithPaging will cause widespread unrest. How about:

repository.FindPenguins(name_like: "%a%", orderBy: "Age", offset: 20, limit: 10);

All we have to do is look out for those magic names and build them into our LightSpeed query. This means we’ll need a Query object instead of just a QueryExpression, but that’s no problem. Here’s the new code:

Query query = new Query(entityType) { Page = Page.Offset(0) };
QueryExpression queryExpression = null;
for (int i = 0; i < binder.CallInfo.ArgumentNames.Count; ++i)
{
  string argName = binder.CallInfo.ArgumentNames[i];
  string[] bits = argName.Split('_');
  string opName = "equal";
  if (bits.Length >= 2)
  {
    argName = bits[0];
    opName = bits[1];
  }
  switch (argName.ToUpperInvariant())
  {
    case "ORDERBY": query.Order = Order.By((string)(args[i])); break;
    case "OFFSET": query.Page = query.Page.AdvanceBy((int)(args[i])); break;
    case "LIMIT": query.Page = query.Page.LimitTo((int)(args[i])); break;
    default: queryExpression &= MakePredicate(argName, opName, args[i]); break;
  }
}
query.QueryExpression = queryExpression;
result = _unitOfWork.Find(query);

Needless to say, you’re not limited to sorting and paging: you can define magic argument names for anything the LightSpeed query object supports (yes, James, even index hints). Of course, there comes a point at which it’s much easier to tell your users just to construct a Query object themselves, with all the Intellisense joy that brings; it’s up to you what you think is worth offering in a dynamic API, but keeping it simple is probably a good idea…

I want one just like that

Want to try it out? LightSpeed Express is free to download, or you can buy the Professional edition from the store. And if you do any cool dynamic stuff with it, let us know!

原文地址:https://www.cnblogs.com/luoyaoquan/p/2035120.html