Saturday, 13 December 2008

Astoria and LINQ-to-SQL; associations

We can get now manipulate flat data - but we also need to model associates between data (read: foreign keys in RDBMS terms).

Lets start by modifying our client to pick two employees: we'll add one of the records as a subordinate of the other, verify, and then remove them. Note that the ADO.NET Data Services client code doesn't do much to automate this - as with "UpdateObject" we need to keep nudging it to ask it to mirror the changes we make. We also need to manually load child collections.

Implementing Collection (/Child) Associations

Here's our new client test code:

ctx.LoadProperty(boss, "Employees");

CountSubordinates(bossId);

boss.Employees.Add(grunt);
ctx.AddLink(boss, "Employees", grunt);
ctx.SaveChanges();

CountSubordinates(bossId);

boss.Employees.Remove(grunt);
ctx.DeleteLink(boss, "Employees", grunt);
ctx.SaveChanges();

CountSubordinates(bossId);

Here, the CountSubordinates creates a separate query context to validate the data independently. When we execute this, we immediately get errors in the data-service about "AddReferenceToCollection", then "RemoveReferenceFromCollection". To implement these methods, note that collection associations in LINQ-to-SQL are EntitySet instances, which implement IList:

public static void AddReferenceToCollection(
DataContext context, object targetResource,
string propertyName, object resourceToBeAdded)
{
IList list = (IList) GetValue(context,
targetResource, propertyName);
list.Add(resourceToBeAdded);
}

public static void RemoveReferenceFromCollection(
DataContext context, object targetResource,
string propertyName, object resourceToBeRemoved)
{
IList list = (IList)GetValue(context,
targetResource, propertyName);
list.Remove(resourceToBeRemoved);
}

Here we are using our existing "GetValue" method with a simple cast, then just adding the extra data.

Implementing Single (/Parent) Associations

Individual associations are handled differently to collection associations; this time, we'll set the manager of the subordinate directly on the subordinate:

CountSubordinates(bossId);

grunt.Manager = boss;
ctx.SetLink(grunt, "Manager", grunt.Manager);
ctx.SaveChanges();

CountSubordinates(bossId);

grunt.Manager = null;
ctx.SetLink(grunt, "Manager", grunt.Manager);
ctx.SaveChanges();

CountSubordinates(bossId);

The "SetLink" method reminds the data-context that we care... This time, the data-service breaks on the "SetReference" method. Fortunately, since we are treating resources and instances as interchangeable, this is trivial to implement with our existing "SetValue" method:

public static void SetReference(
DataContext context, object targetResource,
string propertyName, object propertyValue)
{
SetValue(context, targetResource,
propertyName, propertyValue);
}

With this in place, our Manager is updated correctly, which can be validated in the database.

Summary - and Why oh Why...

So; we've got associations working in either direction. But te "SetLink" etc nags at me. It seems very unusual to have to do this so manually. And annoyingly, the standard client-side code:

  • doesn't implement INotifyPropertyChanged - which means the objects don't work well in an "observer" setup
  • doesn't provide an OnPropertyChanged (or similar) partial method - which means we can't conveniently add the missing functionality

If either of these were done, I'm pretty certain we could hook some things together so that you (as the consumer) don't need to do all this work, perhaps using events to talk to the context (rather than referencing the context directly from the object, which has some POCO issues). There are lots of On{Foo}Changed partial methods, but you'd have to do them all manually.

Maybe PostSharp would help here. Or maybe ADO.NET Data Services should include these features; I might look at this later...