Creating a Data Service Provider – Part 7 – Update
In Parts 1 thru 6 we created a custom Read/Only provider over an in memory list of Products.
Now it’s time to add update support.
To do that we need to add an implementation of IDataServiceUpdateProvider .
But first lets talk about the…
Implications of Batching
The IDataServiceUpdateProvider interface is designed to support Batching, allowing customers to update many Resources in one transaction.
Indeed because the interface itself is low level – there is a API call needed to update every individual property – even just updating a single resource can result in a batch of API calls that should happen atomically: if an API call fails we need to rollback / abort whatever happened earlier.
All this means you can expect to see a number of calls to methods like IDataServiceUpdateProvider.SetValue(..), before each SaveChanges() call.
This seems simple enough, but it has profound implications on how you implement the interface.
You can’t just blindly apply the requested change as it is made. You need a way of recording and then committing or aborting all requested changes as a group.
If your data is in a database this typically means recording a series of commands and issuing them in a single transaction. Which something like the Entity Framework does for you automatically.
If however your data is memory, like the sample we’ve been building, you basically have to record intent, and apply that intent only when SaveChanges() is called.
This is why our implementation creates and records Actions whenever one of these low level changes is made…
Making our DSPContext Updatable.
Another prerequisite to implementing IDataServiceUpdateProvider is having a data source that is updatable.
You can do this in a million different ways, but for the purpose of illustration I’ve decided to add some new abstract methods, to our DSPContent class, for Creating Resources, Adding Resources, Deleting Resources and Saving Changes, like this:
public abstract object CreateResource(ResourceType resourceType);
public abstract void AddResource(ResourceType resourceType,
object resource);
public abstract void DeleteResource(object resource);
public abstract void SaveChanges();
Then in our derived ProductsContext we implement these new methods like this:
public override object CreateResource(ResourceType resourceType)
{
if (resourceType.InstanceType == typeof(Product))
{
return new Product();
}
throw new NotSupportedException(
string.Format("{0} not found", resourceType.FullName)
);
}
public override void AddResource(
ResourceType resourceType,
object resource)
{
if (resourceType.InstanceType == typeof(Product))
{
Product p = resource as Product;
if (p != null){
Products.Add(p);
return;
}
}
throw new NotSupportedException("Type not found");
}
public override void DeleteResource(
object resource)
{
if (resource.GetType() == typeof(Product))
{
Products.Remove(resource as Product);
return;
}
throw new NotSupportedException("Type not found");
}
public override void SaveChanges()
{
var prodKey = Products.Max(p => p.ProdKey);
foreach (var prod in Products.Where(p => p.ProdKey == 0))
prod.ProdKey = ++prodKey;
}
As you can see each of these methods are pretty simple, except for SaveChanges which mimics a database Identity column by assigning every new Product (ProdKey == 0) a Unique sequential ID.
Which means we are finally ready to…
Implement IDataServiceUpdateProvider
Our implementation is called DSPUpdateProvider and looks like this:
public class DSPUpdateProvider<T>: IDataServiceUpdateProvider
where T: DSPContext
As you can see it is a generic class where T is the type of our DataSource.
The IDataServiceUpdateProvider interface doesn’t have any methods to give us the DSPContext - so we’ll need to get it via the constructor somehow – remember we are in charge of constructing the provider through our implementation of IServiceProvider. :)
Metadata would be handy too, so the DSPUpdateProvider constructor takes both metadata and query providers as arguments:
public DSPUpdateProvider(
IDataServiceMetadataProvider metadata,
DSPQueryProvider<T> query)
{
_metadata = metadata;
_query = query;
_actions = new List<Action>();
}
The _actions list is there to batch a list of actions to invoke when IDataServiceUpdateProvider.SaveChanges() is called.
As you remember we can’t ‘really’ update our Data Source until Data Services tells us to.
Getting the DataSource
Next we are going to need a way to get to the DataSource i.e. the class that derives from DSPContext. So we need something like this:
public T GetContext()
{
return (_query.CurrentDataSource as T);
}
Which simply gets the CurrentDataSource from the IDataServiceQueryProvider, and casts it to T which, if you remember, is a class derived from DSPContext.
Creating Resources
Next whenever someone does an insert (aka POST) Data Services needs a way of creating the object backing the Resource – CreateResource – our implementation looks like this:
public object CreateResource(
string containerName,
string fullTypeName)
{
ResourceType type = null;
if (_metadata.TryResolveResourceType(fullTypeName, out type))
{
var context = GetContext();
var resource = context.CreateResource(type);
_actions.Add(
() => context.AddResource(type, resource)
);
return resource;
}
throw new Exception(
string.Format("Type {0} not found", fullTypeName)
);
}
Here we try to create an instance of the CLR type used for a particular ResourceType.
Notice that we create a Resource, but we don’t Add it immediately to our Data Source, instead we create and queue an action that will add the resource to the Data Source when invoked.
Getting a Resource
When Data Services tries to Update or Delete a Resource, it first needs to retrieve it (or a representation of it). Which is where IDataServiceUpdateProvider.GetResource(..) comes in.
Data Services passes an IQueryable that when invoked should return exactly one resource.
You might ask why Data Services doesn’t just get the resource itself?
Well the reason is that this method provides and extra layer of indirection which allows you to return something that ‘represents’ the resource, aka a proxy.
You could for example use this ‘proxy’ to record changes and then committing them in batch when SaveChanges() is called, thereby avoid Actions altogether.
But in our implementation we’ve chosen the Action approach, so we can simply return the resource directly.
So we get the resource, check that there is only one matching resource and if the ResourceType is known – which it isn’t for deletes – we also check that the resource has the expected CLR type.
public object GetResource(IQueryable query, string fullTypeName)
{
var enumerator = query.GetEnumerator();
if (!enumerator.MoveNext())
throw new Exception("Resource not found");
var resource = enumerator.Current;
if (enumerator.MoveNext())
throw new Exception("Resource not uniquely identified");
if (fullTypeName != null)
{
ResourceType type = null;
if (!_metadata.TryResolveResourceType(
fullTypeName, out type))
throw new Exception("ResourceType not found");
if (!type.InstanceType.IsAssignableFrom(resource.GetType()))
throw new Exception("Unexpected resource type");
}
return resource;
}
Closely related is the IDataServiceUpdateProvider.ResolveResource(..) method.
When you implement this method you are passed whatever your returned from GetResource(..) – which remember might have been a proxy – and are expected to return the real resource.
Because we don’t use proxies, our implementation is essentially a no-op:
public object ResolveResource(object resource)
{
return resource;
}
Updating Property Values
Once Data Services has the resource (or proxy for the resource), it will start updating it, by calling IDataServicesUpdateProvider.SetValue(..) to update each property in turn.
By now you know the drill, we are going to record an action to set the property value rather than setting it immediately.
public void SetValue(
object targetResource,
string propertyName,
object propertyValue)
{
// TODO: add some asserts!!!
_actions.Add(
() => ReallySetValue(
targetResource,
propertyName,
propertyValue)
);
}
public void ReallySetValue(
object targetResource,
string propertyName,
object propertyValue)
{
targetResource
.GetType()
.GetProperties()
.Single(p => p.Name == propertyName)
.GetSetMethod()
.Invoke(targetResource, new[] { propertyValue });
}
As you can see the ReallySetValue (sorry for the unimaginative name) uses reflection to set the property.
You might be tempted to remove this Reflection code to can speed this code up, but before you do all that work remember the real latency is related to OData / Atom serialization and deserialization and network latency, so it might not actually be worth the effort.
Getting Property Values
Occasionally during updates Data Services needs to Get the value of a property, if so it will use the IDataServiceUpdateProvider.GetValue(..) method. Because this GetValue is non-mutating we don’t can just go and get the value directly using a little reflection code:
public object GetValue(object targetResource, string propertyName)
{
var value = targetResource
.GetType()
.GetProperties()
.Single(p => p.Name == propertyName)
.GetGetMethod()
.Invoke(targetResource, new object[] { });
return value;
}
Deleting Resources
Next we need to handle Deletes. This code is pretty self explanatory, we record an action to delete the resource later.
public void DeleteResource(object targetResource)
{
_actions.Add(() =>
GetContext().DeleteResource(targetResource)
);
}
Resetting Resources
By default there is no need to reset a resource, but a client can request this, for example by using:
SaveChanges(SaveChangesOptions.ReplaceOnUpdate);
If this happens Data Services will ask your IDataServiceUpdateProvider implementation to reset all the non-key properties of the Resource by calling ResetResource(..).
Here's our implementation:
public object ResetResource(object resource)
{
_actions.Add(() => ReallyResetResource(resource));
return resource;
}
Which uses this to do the actual work:
public void ReallyResetResource(object resource)
{
// Create an new 'blank' instance of the resource
var clrType = resource.GetType();
ResourceType resourceType =
_metadata.Types.Single(t => t.InstanceType == clrType);
var resetTemplate = GetContext().CreateResource(resourceType);
// Copy non-key property values from the 'blank' resource
foreach (var prop in resourceType
.Properties
.Where(p => (p.Kind & ResourcePropertyKind.Key)
!= ResourcePropertyKind.Key))
{
// Obviously for perf reasons you could might want to
// cache the result of these reflection calls.
var clrProp = clrType
.GetProperties()
.Single(p => p.Name == prop.Name);
var defaultPropValue = clrProp
.GetGetMethod()
.Invoke(resetTemplate, new object[] { });
clrProp
.GetSetMethod()
.Invoke(resource, new object[] { defaultPropValue });
}
}
As you can see the approach used to reset non-key properties, is to create an new ‘blank’ resource, and copy property values across.
Saving Changes
Finally if everything goes according to plan, once all the property updates etc have been queued up you will see a request to IDataServiceUpdateProvider.SaveChanges().
Because of how we’ve implemented things, this one is trivial for us:
public void SaveChanges()
{
foreach (var a in _actions)
a();
GetContext().SaveChanges();
}
We simply invoke all the queued actions, and then ask the DSPContext to Save its changes, which if you remember, simply ensures that each new Product has a new ProdKey.
If anything goes wrong you’ll get a ClearChanges() call, which is even simplier:
public void ClearChanges()
{
_actions.Clear();
}
And now we are done implementing IDataServiceUpdateProvider .
Yippee!
Missing methods
But wait a second … you might have noticed that we haven’t implemented a number of the methods on the interface:
SetReference(..)
AddReferenceToCollection(..)
RemoveReferenceToCollection(..)
SetConcurrencyValues(..)
That’s because we don’t have any Relationships or ETags, so these methods will never be called, meaning we can safely ignore them for now.
Don’t worry though I’ll cover them in a future post.
Hooking it all together
The last step is to modify our IServiceProvider implementation to construct our DSPUpdateProvider and returned it whenever Data Services asked for the IDataServiceUpdateProvider interface.
Summary
The key to implementing IDataServiceUpdateProvider is realizing that you need to be able to delay execution. You can do that using either Proxy objects or Actions, like the above implementation.
Once you know how you are going to handle the atomicity requirements, and you understand what each method is supposed to do, it is actually a pretty mechanical exercise of going through an implementing each method in turn.
Next up we’ll look at adding relationships.
Comments
- Anonymous
February 19, 2010
Hello.I am trying to implement my custom data service provider to allow it to work with Nhibernate entities. When I started to implement IDataServiceUpdateProvider I noticed that it inherits from IUpdateble, which has to be implemented also by DataSource (DSPContext in your samples). So i tried just to add implementation of IDataServiceUpdateProvider on my DataSource to avoid implementing IUpdateble twice (in Data source and in class, which implements IDataServiceUpdateProvider) and it worked perfectly. So it looks like you don't need to implement IDataServiceUpdateProvider in a separate class and return that class on IServiceUpdateProvider.GetService.BTW, thanks for great posts. For now they are the only documentation on how to make your own data service provider. - Anonymous
June 24, 2010
chinese versionwww.cnblogs.com/.../DSP8.html - Anonymous
August 02, 2010
I want to Add new Service Operations to my the codeCan any one explain me the steps to create custom Service Operations.