Dynamic repositories in LightSpeed

Dynamic repositories in LightSpeed

When you’re building a repository, you’ll often find yourself writing a lot of facade methods to support different kinds of query: FindPersonByName, FindPenguinBySpecies, FindDromedaryByHumpSize and so on. This isolates your application code from the specifics of the LINQ queries or Query objects, but at the expense of requiring you to churn out a zillion and one boilerplate methods and their implementations. One way of avoiding this is to use dynamic programming. This allows the application code to call the various FindXByY methods without you having to write those methods.

How does this work? In C# you can use the dynamic keyword to tell the compiler that you want a member call to be looked up at run time, not at compile time:

dynamic repository = new WildlifeRepository();
dynamic adelies = repository.FindPenguinBySpecies("Adelie");

This solves the caller side of the problem: the compiler will no longer barf on the non-existent methods. But without a bit more work this will still fail at runtime because the method doesn’t exist.

Dynamic objects in C#

When a dynamic call is looked up at runtime, .NET — technically, a component called the runtime binder — tries several ways to resolve it. One of those, of course, is to try to match the call to a method defined on the object. Obviously, that won’t for us. Another is to query the object for an interface called IDynamicMetaObjectProvider. If that interface is present, the runtime binder invokes this interface and flings the call information at the resulting ‘metaobject.’

As you might expect from the name, metaobjects are kinda hairy. Fortunately, if you’re writing a standalone repository class, you don’t need to worry about metaobjects directly. Instead, you can inherit from the DynamicObject class, which takes care of all the metamadness and translates it into a bunch of nice simple overridable methods.

Let’s get started. Our repository class will encapsulate a unit of work — we might also use a scope object, but I won’t worry about that here.

public class WildlifeRepository : DynamicObject, IDisposable, IRepository
{
  // Unit of work initialisation omitted for brevity
  private WildlifeUnitOfWork _unitOfWork;
 
  public void Dispose()
  {
    _unitOfWork.Dispose();
  }
 
  public void SaveChanges()
  {
    _unitOfWork.SaveChanges();
  }
}

Handling the dynamic method call

At this point, if I run the test code above, I still get an exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException : ‘LightSpeed.Dynamic.Tests.WildlifeRepository’ does not contain a definition for ‘FindPenguinsBySpecies’. To implement dynamic method calls to the repository, we have to override the TryInvokeMember method. For now, I’ll cheat with the implementation so that you can see the signature of the method and how it fits in:

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
  // You wouldn't really do this
  if (binder.Name == "FindPenguinsBySpecies")
  {
    string species = (string)(args[0]);
    result = _unitOfWork.Penguins
                        .Where(p => p.Species == species)
                        .ToList();
    return true;
  }
  return base.TryInvokeMember(binder, args, out result);
}

Now when I run my test code, even though there still isn’t a method called FindPenguinsBySpecies, it works! If I trace the code, I find that the runtime binder is calling TryInvokeMember, passing in the method name and arguments specified in the dynamic call. Since that name is just a string, I can check if it’s one I recognise, and if so I can unpack the arguments appropriately and do the right thing.

Of course, if I had to implement each dynamic method like this, it would be just as repetitive as grinding through the compile-time implementations! But remember that I have access to the method name. That means I can parse the method name and use that to decide what to do. The method name becomes a specification for me to construct the query on the fly. Here goes:

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
  // Crude parsing for simplicity -- could be made nicer
  if (binder.Name.StartsWith("Find"))
  {
    int byIndex = binder.Name.IndexOf("By");
    if (byIndex >= 0)
    {
      string collectionName = binder.Name.Substring(4, byIndex - 4);  // 4 == "Find".Length
      string attribute = binder.Name.Substring(byIndex + 2);  // 2 == "By".Length
 
      Type entityType = LookupType(collectionName);
      result = _unitOfWork.Find(entityType, Entity.Attribute(attribute) == args[0]);
      return true;
    }
  }
  return base.TryInvokeMember(binder, args, out result);
}

Here I’m looking for method names of the form FindXByY, cracking them open to get X and Y, and then constructing a query using X as the entity type and Y as the attribute to query on. The args array still contains the value to query against. My previous test code still works, of course, but now I can also write:

dynamic spinies = repository.FindHedgehogsByName("Spiny Norman Tebbit");

and that works too, even though I haven’t written any specific support for Hedgehog queries or the Name property.

There are a couple of things going on here that merit a bit more discussion: one a bit tricky, the other one just a part of the LightSpeed API you may not recognise.

What’s that thing we’re passing to Find?

When we come to construct the LightSpeed query, all we have is the name of a property. If you’ve ever tried to build LINQ queries on the fly from property names, you’ll know it’s a pain.

Fortunately, LightSpeed predates LINQ, and has its own query API based around query objects. And the way you build up queries in that ‘core’ API is using… property name strings! Entity.Attribute(“SomeProperty”) == someValue is the ‘core’ way of writing the LINQ where x.SomeProperty == someValue — but since Entity.Attribute takes a string it’s much easier to work with in the dynamic scenario.

How do we know what type to query?

The slightly tricky bit is the LookupType method. I need some way to map strings like “Penguins” and “Hedgehogs” to the Penguin and Hedgehog entity types. Because I have a strong-typed WildlifeUnitOfWork around, I can do this by looking for the query with the appropriate name, and figuring out the entity type of that query:

private Type LookupType(string collectionName)
{
  var queryProperty = _unitOfWork.GetType().GetProperty(collectionName);
  return queryProperty.PropertyType.GetGenericArguments()[0];
}

This won’t work if you don’t have a strong-typed unit of work, for example if you are using dynamic entity types. In that case you could use a dictionary mapping strings to types, or reflect over the assembly, or whatever. LightSpeed doesn’t care how you do it, but it does need the entity type, not just the name.

Multiple query criteria

What if you want to find penguins of a certain species and name? Our existing implementation handles only a single query criterion, but with a bit more parsing we can extend it to an arbitrary number of criteria:

string collectionName = binder.Name.Substring(4, byIndex - 4);
string[] attributes = binder.Name.Substring(byIndex + 2).Split(new string[] { "And" }, StringSplitOptions.None);
 
Type entityType = LookupType(collectionName);
QueryExpression queryExpression = null;
for (int i = 0; i < attributes.Length; ++i)
{
  queryExpression &= Entity.Attribute(attributes[i]) == args[i];
}
result = _unitOfWork.Find(entityType, queryExpression);

With this in place we can now write repository.FindPenguinsBySpeciesAndName("Adelie", "Gloria") and it will work fine.

Needs moar hawtness

This is all very well, but we’re only using one C# 4 feature. If we’re going to achieve fame, fortune and the front page of MSDN, we need to rope in the full circus. Well, how about named arguments? It would be nice to get away from all those Bys and Ands, and of course all that dreary parsing. Let’s allow our users to write this:

dynamic adelies = repository.FindPenguins(species: "Adelie");
dynamic spinies = repository.FindHedgehogs(name: "Spiny Norman Tebbit");
dynamic glorias = repository.FindPenguins(species: "Adelie", name: "Gloria");

The InvokeMemberBinder helpfully provides a list of argument names in the CallInfo.ArgumentNames property. You need to be a bit careful here, because ArgumentNames lists only the names of named arguments. You should also check CallInfo.ArgumentCount in case there are any positional arguments before the named arguments start. In our case, positional arguments make no sense, so we would just report an error if ArgumentCount and NamedArguments.Count didn’t match — but for clarity I’m just going to skip error checking altogether.

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)
    {
      // as before
    }
    else
    {
      string collectionName = binder.Name.Substring(4);
      Type entityType = LookupType(collectionName);
 
      QueryExpression queryExpression = null;
      for (int i = 0; i < binder.CallInfo.ArgumentNames.Count; ++i)
      {
        queryExpression &= Entity.Attribute(binder.CallInfo.ArgumentNames[i]) == args[i];
      }
      result = _unitOfWork.Find(entityType, queryExpression);
      return true;
    }
  }
  return base.TryInvokeMember(binder, args, out result);
}

We’re iterating over the argument names, matching them up with the argument values, and appending them to the query expression. It’s pretty much the same as we did when we parsed the method name, except we’re getting the attribute names from the named argument names instead of the parsed string. And now our FindX(attr: value) test code works just as we’d like.

Review

What have we done?

We’ve inherited from DynamicObject in order to intercept dynamic method calls, and we’ve seen two ways of turning a dynamic method call into a list of attributes and values — first by parsing the method name, then by using named arguments.

We’ve also seen that it’s very easy to turn this list of attributes and values into a LightSpeed query. LightSpeed supports both strong typed queries through LINQ, and weak typed queries through the Query and QueryExpression objects. InvokeMemberBinder hands us strings for the method and argument names, which is almost exactly what the query objects need (the exception is the entity type, which is easy to adapt).

And with these two steps in place, we’ve provided a way for application code to write simple queries as method calls without us needing to anticipate all the queries that might come up, or to slog through acres of repetitive query code.

But wait, there’s more: So far our dynamic API only supports equality comparisons. We’ll crack this limitation in the next exciting episode.

Want to try it yourself? LightSpeed Express Edition is free: grab a copy today and take it for a spin!

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