ADO.NET Data Services – IUpdatable on LINQ to SQL

I made an attempt at implementing IUpdatable on the current (i.e. VS 2008 Sp1 B1) bits of ADO.NET Data Services.

I struggled a little bit with this. When you produce a data service you provide a class which is derived from;

DataService<T>

and T might be your own custom type or it’s more likely to be a class derived from DataContext ( LINQ to SQL ) or ObjectContext ( LINQ to Entities ).

The framework reflects over the type that you provide as T in order to find public properties of type IQueryable<> and it can expose those ( if you tell it to ) as entity sets available over your service.

If T is not an ObjectContext then the framework also looks to T to implement IUpdatable if you want to do read/write access to your data.

It’s extremely likely that if you’re working with LINQ to SQL or LINQ to Entities then your type T will be the type that is generated from the tooling for you because that type already comes pre-populated with lots of IQueryable<> public properties nicely generated for you which means you do ( very ) little work to expose those entity sets using the framework.

So, if you’re using LINQ to SQL against say the Northwind database then you typically end up with some;

class NorthwindDataContext : DataContext

and that’s all great. However, if I want to write a re-usable implementation of IUpdatable then it means that I have to somehow magically implement it on NorthwindDataContext or a class derived from NorthwindDataContext and that’s not so easy when that class is (re)generated from a tool.

If I don’t implement the interface on NorthwindDataContext or a class derived from it then I have to somehow provide a type that has all those nice IQueryable<> public properties on it and that’s not easy given that you can’t plug in to the LINQ to SQL code generation process and any manual option (i.e. ask the developer to copy them :-)) really is a non-starter.

Some options I considered;

1) Implement some LinqToSqlDataContext<T> : T, IUpdatable where T : DataContext

For me, this would be ideal but C# doesn’t let you derive a generic type from a generic type parameter. Shame. I’d like to see this in a future version of the language as it seems perfectly reasonable to me.

2) Implement some LinqToSqlDataService<T> : DataService<T>

This initially looks attractive because there’s a virtual method to override on DataService<T> which is called CreateDataSource and returns T. I figured that what I could do is to use a similar trick that Matt Warren used over here in order to return some kind of remoting proxy from my CreateDataSource which pretended that T implemented IUpdatable when in fact it doesn’t.

However, that didn’t work. It would work fine if the communication with the type T by the framework was all through interfaces but it’s not. The communication is through the class itself and, as far as I know, you can only intercept calls on interfaces so this trick didn’t help me.

3) Simply implement IUpdatable on the class T itself. The class T that comes from the LINQ to SQL tooling is a partial class so this is very much do-able and it’s relatively simple. It’s not perfect though because there are places in the implementation where I want to divorce the data-providing-type T from the implementation of IUpdatable and I can’t if I use this mechanism.

4) Implement IUpdatable on a class that I then ask the developer to derive from T manually themselves. That is, provide code which wouldn’t compile until the developer plugs in the name of their data-providing class T as a base class to my class. This is a little bit like the template that Visual Studio emits for a Data Services web service in that it has a big comment in it saying /* INSERT YOUR TYPE NAME HERE */ so I could do that too.

 

In the end, I went with (4). I don’t feel particularly good about it – it feels like I’ve been boxed into a corner that I can only get out of by doing something that I don’t really want to do and I think there’s perhaps some need for separation between the data-providing-class and the IUpdatable interface. They seem to be tied together in a way that makes IUpdatable a bit tricky to implement. It might be nice to see the DataService<T> offer you another way of providing the IUpdatable implementation so that you could split the two things out.

Completing part (4) above, I end up with this – note the general comment and also the comment in method ClearChanges which I found difficult to implement given everything above and the available methods on DataContext so I called an internal method to do what I want.

 

/// <summary>
/// Note: This is pure sample stuff and I haven't tested it too much - that
/// responsibility falls to you if you take it and use it. Let me know if
/// you do find problems and I'll update the post.
/// </summary>
/// 
public class UpdatableDataContext : 
  /* TODO: Insert your LINQ to SQL DataContext-derived type here */, 
  IUpdatable
{
  public void AddReferenceToCollection(object targetResource, string propertyName,
    object resourceToBeAdded)
  {
    Type t = targetResource.GetType();

    PropertyInfo collectionProperty = GetPropertyInfoForType(t, propertyName, false);

    object collection = collectionProperty.GetValue(targetResource, null);

    collection.GetType().InvokeMember("Add",
      BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod,
      null, collection, new object[] { resourceToBeAdded });
  }
  public void ClearChanges()
  {
    // This method is somewhat "tricky"
    // 1) We want to use a partial class because we want to pick up the
    //    public IQueryable<T> properties that have been generated by the tooling.
    // 2) We need to have the same class implement IUpdatable as the class that
    //    we provide to DataService<T>.
    // 3) There is no public method on DataContext that will clear everything down 
    //    without disposing but there is an internal method, ClearCache() which 
    //    looks perfect.
    // 4) Ideally here we'd get rid of the DataContext and start again but we can't
    //    because this code _is_ part of DataContext and (1) to (3) above seem to
    //    box us in.
    
    // Hence...
    MethodInfo mi = this.GetType().GetMethod("ClearCache",
      BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod);

    mi.Invoke(this, null);
  }
  public object CreateResource(string containerName, string fullTypeName)
  {
    Type t = Type.GetType(fullTypeName);

    ITable table = GetTableForType(t);

    object value = Construct(t);

    table.InsertOnSubmit(value);

    return (value);
  }
  public void DeleteResource(object targetResource)
  {
    ITable table = this.GetTable(targetResource.GetType());

    if (table == null)
    {
      throw new DataServiceException("Failed to locate table for resource");
    }
    table.DeleteOnSubmit(targetResource);
  }
  public object GetResource(IQueryable query, string fullTypeName)
  {
    object result = null;

    foreach (object item in query)
    {
      if (result != null)
      {
        throw new DataServiceException("A single resource is expected");
      }
      result = item;
    }
    if (result == null)
    {
      throw new DataServiceException(404, "Resource not found");
    }
    if (fullTypeName != null)
    {
      if (result.GetType().FullName != fullTypeName)
      {
        throw new DataServiceException("Resource type mismatch");
      }
    }
    return (result);
  }

  public object GetValue(object targetResource, string propertyName)
  {
    Type t = targetResource.GetType();

    PropertyInfo pi = GetPropertyInfoForType(t, propertyName, false);

    object value = null;

    try
    {
      value = pi.GetValue(targetResource, null);
    }
    catch (Exception ex)
    {
      throw new DataServiceException(string.Format(
        "Failed getting property {0} value", propertyName), ex);
    }
    return (value);
  }

  public void RemoveReferenceFromCollection(object targetResource, string propertyName,
    object resourceToBeRemoved)
  {
    Type t = targetResource.GetType();

    PropertyInfo collectionProperty = GetPropertyInfoForType(t, propertyName, false);

    object collection = collectionProperty.GetValue(targetResource, null);

    collection.GetType().InvokeMember("Remove",
      BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod,
      null, collection, new object[] { resourceToBeRemoved });
  }
  public object ReplaceResource(IQueryable query, string fullTypeName)
  {
    // Not called in Beta 1.
    throw new NotImplementedException();
  }
  public object ResolveResource(object resource)
  {
    return (resource);
  }
  public void SaveChanges()
  {
    base.SubmitChanges();
  }
  public void SetReference(object targetResource, string propertyName, object propertyValue)
  {
    this.SetValue(targetResource, propertyName, propertyValue);
  }
  public void SetValue(object targetResource, string propertyName, object propertyValue)
  {
    Type t = targetResource.GetType();

    PropertyInfo pi = GetPropertyInfoForType(t, propertyName, true);

    try
    {
      pi.SetValue(targetResource, propertyValue, null);
    }
    catch (Exception ex)
    {
      throw new DataServiceException(
        string.Format("Error setting property {0} to {1}", propertyName, propertyValue),
        ex);
    }
  }
  private PropertyInfo GetPropertyInfoForType(Type t, string propertyName,
    bool setter)
  {
    PropertyInfo pi = null;

    try
    {
      BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
      flags |= setter ? BindingFlags.SetProperty : BindingFlags.GetProperty;

      pi = t.GetProperty(propertyName, flags);

      if (pi == null)
      {
        throw new DataServiceException(string.Format("Failed to find property {0} on type {1}",
          propertyName, t.Name));
      }
    }
    catch (Exception exception)
    {
      throw new DataServiceException(
        string.Format("Error finding property {0}", propertyName),
        exception);
    }
    return (pi);
  }
  private ITable GetTableForType(Type t)
  {
    ITable table = this.GetTable(t);

    if (table == null)
    {
      throw new DataServiceException(
        string.Format("No table found for type {0}", t.Name));
    }
    return (table);
  }
  private static object Construct(Type t)
  {
    ConstructorInfo ci = t.GetConstructor(Type.EmptyTypes);

    if (ci == null)
    {
      throw new DataServiceException(
        string.Format("No default ctor found for type {0}", t.Name));
    }
    return (ci.Invoke(null));
  }
}

You would then use this in your service by doing something like;

public class UpdatableDataContext : 
  NorthwindDataContext, 
  IUpdatable
{

 

and then using it as;

public class Service : DataService<UpdatableDataContext>
{
  public static void InitializeService(IDataServiceConfiguration config)
  {
    config.SetEntitySetAccessRule("*", EntitySetRights.All);
    config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
  }
}

Note the comments in the source – it’s received little testing so no doubt has bugs. Feel free to pass them back to me and also let me know if you’ve got a smarter way of implementing this as I’d have liked to end up with something a little more “slick” here.