Creating a Data Service Provider – Part 8 - Relationships
In parts 1 thru 7 we made a Read / Write typed data service provider, although it was a little simplistic because it didn’t have relationships.
So lets rectify that right now by adding a relationship to make this more 'real-world'
Changes to our Classes
If you remember from Part 3 we had just a Product class, so lets add a Category and a one to many relationship between the two:
public class Category
{
private List<Product> _products = new List<Product>();
public int ID { get; set; }
public string Name { get; set; }
public List<Product> Products { get { return _products; } }
}
public class Product
{
public int ProdKey { get; set; }
public string Name { get; set; }
public Decimal Price { get; set; }
public Decimal Cost { get; set; }
public Category Category { get; set; }
}
Metadata Changes
Building relationships through metadata.
Now that our classes are ready, the next step is to tell Data Services about Category, and the relationships between then two classes:
Most of this is simple, except perhaps the bit that actually adds the relationship.
You start by creating the ‘navigation’ properties:
var productType = …; // Product and its primitive properties
var categoryType = …; // Category and its primitive properties
// Tell Data Services about product.Category
var prodCategory = new ResourceProperty(
"Category",
ResourcePropertyKind.ResourceReference,
categoryType
);
productType.AddProperty(prodCategory);
// Tell Data Services about category.Products
var catProducts = new ResourceProperty(
"Products",
ResourcePropertyKind.ResourceSetReference,
productType
);
categoryType.AddProperty(catProducts);
Because of the way the API works – you need the target ResourceType to create a property – both Resource Types need to be at least partially created before you can create and add the navigation properties.
This essentially means you need to do two passes over each ResourceType: One to create the type and its primitive properties and another to add it’s navigation properties.
You should also notice that product.Category is a ResourceReference, because there is just one category, whereas category.Products is a ResourceSetReference, because it is a collection.
Next we need a ResourceSet for both Category and Product:
var productsSet = new ResourceSet("Products", productType);
var categoriesSet = new ResourceSet("Categories", categoryType);
Once you done that we need to create an AssociationSet that links those two navigation properties and resource sets together:
ResourceAssociationSet productCategoryAssociationSet = new ResourceAssociationSet(
"ProductCategory",
new ResourceAssociationSetEnd(
productsSet,
productType,
prodCategory
),
new ResourceAssociationSetEnd(
categoriesSet,
categoryType,
catProducts
)
);
i.e. this is the bit that tells Data Services that setting:
product.Category = category;
is equivalent to:
category.Products.Add(product);
Next we store a pointer from these properties back to the ResourceAssociationSet using the CustomState property.
prodCategory.CustomState = productCategoryAssociationSet;
catProducts.CustomState = productCategoryAssociationSet;
If you remember the good old days of VB forms development CustomState will remind you of your old friend the Tag property. You can use it for all sorts of tricks.
Actually storing the ResourceAssociationSet in the ResourceProperty.CustomState is not strictly necessary, but it will make our lives easier, and our code quicker, as you’ll see very soon.
Finally we need to tell the DSPMetadataProvider, about both ResourceTypes, both ResourceSets and the ResourceAssociationSet:
metadata.AddResourceType(productType);
metadata.AddResourceSet(productsSet);
metadata.AddResourceType(categoryType);
metadata.AddResourceSet(categoriesSet);
metadata.AddAssociationSet(productCategoryAssociationSet);
IDataServiceMetadataProvider Changes
For this to work we need to make some changes to our DSPMetadataProvider to actually support Relationships:
First we need a place to hold AssociationSets:
List<ResourceAssociationSet> _associationSets
= new List<ResourceAssociationSet>();
Then we need a way to register new AssociationsSets:
public void AddAssociationSet(ResourceAssociationSet aset)
{
_associationSets.Add(aset);
}
And finally we need to update our implementation of IDataServiceMetadataProvider.GetResourceAssociationSet(..) like this:
public virtual ResourceAssociationSet GetResourceAssociationSet(
ResourceSet resourceSet,
ResourceType resourceType,
ResourceProperty resourceProperty
)
{
return resourceProperty.CustomState as ResourceAssociationSet
}
See I told you putting the ResourceAssociationSet in CustomState was going to come in handy!
The alternative is to write a LINQ query over the _associationSets list, using the method arguments in the predicate, but that is a little more complicated and a lot slower.
Metadata should be working by now…
By this point both the service document: ~/Sample.svc and metadata: ~/Sample.svc/$metadata should be working.
Next we move on to getting query and update working, and to do that we so we need to update our Data Source, and our implementation of IDataServiceQueryProvider.
Query Changes
Updating our DataSource
This just involves adding a List for my categories to ProductsContext, and updating the existing methods to expose Categories too:
public class ProductsContext: DSPContext
{
private static List<Product> _products
= new List<Product>();
private static List<Category> _categories
= new List<Category>();
public override IQueryable GetQueryable(
ResourceSet resourceSet)
{
if (resourceSet.Name == "Products")
return Products.AsQueryable();
else if (resourceSet.Name == "Categories")
return Categories.AsQueryable();
throw new NotSupportedException(
string.Format("{0} not found", resourceSet.Name));
}
public static List<Product> Products{
get { return _products; }
}
public static List<Category> Categories {
get { return _categories; }
}
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;
}
}
else if (resourceType.InstanceType == typeof(Category))
{
Category c = resource as Category;
if (c != null)
{
Categories.Add(c);
return;
}
}
throw new NotSupportedException(
string.Format("{0} not found", resourceType.FullName));
}
public override void DeleteResource(object resource)
{
if (resource.GetType() == typeof(Product))
{
Products.Remove(resource as Product);
return;
}
else if (resource.GetType() == typeof(Category))
{
Categories.Remove(resource as Category);
return;
}
throw new NotSupportedException("Resource Not Found");
}
public override object CreateResource(
ResourceType resourceType)
{
if (resourceType.InstanceType == typeof(Product))
return new Product();
else if (resourceType.InstanceType == typeof(Category))
return new Category();
throw new NotSupportedException(
string.Format("{0} not found", resourceType.FullName));
}
public override void SaveChanges()
{
var prodKey = Products.Max(p => p.ProdKey);
foreach (var prod in Products.Where(p => p.ProdKey == 0))
prod.ProdKey = ++prodKey;
var catKey = Categories.Max(p => p.ID);
foreach (var cat in Categories.Where(c => c.ID == 0))
cat.ID = ++catKey;
}
}
As you can see this is mostly just a mechanical modification of the code we already had for Products. If I was doing this for real, I’d be looking are refactoring this code, to make adding another type a more pain free.
But I’ll leave that as an exercise for you guys.
The only thing remaining to get query working is to actually have some data:
protected override ProductsContext CreateDataSource()
{
if (ProductsContext.Products.Count == 0)
{
var bovril = new Product
{
ProdKey = 1,
Name = "Bovril",
Cost = 4.35M,
Price = 6.49M
};
var marmite = new Product
{
ProdKey = 2,
Name = "Marmite",
Cost = 4.97M,
Price = 7.21M
};
var food = new Category
{
ID = 1,
Name = "Food"
};
food.Products.Add(bovril);
bovril.Category = food;
food.Products.Add(marmite);
marmite.Category = food;
ProductsContext.Categories.Add(food);
ProductsContext.Products.Add(bovril);
ProductsContext.Products.Add(marmite);
}
return base.CreateDataSource();
}
Notice that I am building the relationships in both directions, sometimes this logic might be built into the classes themselves – it is what the Entity Framework calls relationship fix-up – if so this would be superfluous.
Either way if you try running your code at this point you should be able to get both query AND relationship traversal working.
i.e. getting the Category for a particular Product:
and getting the Products in a particular Category:
Its also interesting to notice that we didn’t need to make any changes to our implementation of IDataServiceQueryProvider to get Query working.
Update Changes
Our last challenge is getting Update working.
Which basically means implementing these three methods that we didn’t need last time:
public void SetReference(
object targetResource,
string propertyName,
object propertyValue)
{
throw new NotImplementedException();
}
public void AddReferenceToCollection(
object targetResource,
string propertyName,
object resourceToBeAdded)
{
throw new NotImplementedException();
}
public void RemoveReferenceFromCollection(
object targetResource,
string propertyName,
object resourceToBeRemoved)
{
throw new NotImplementedException();
}
Remember (from part 7) that our Update Providers need to delay doing updates until IDataServiceUpdateProvider.SaveChanges() is called, so we record our intent to SetReference as an action.
To make this easy we’ll create methods that do the real work and call theme inside actions we can invoke later:
public void SetReference(
object targetResource,
string propertyName,
object propertyValue)
{
_actions.Add(() => ReallySetReference(
targetResource,
propertyName,
propertyValue));
}
public void AddReferenceToCollection(
object targetResource,
string propertyName,
object resourceToBeAdded)
{
_actions.Add(() => ReallyAddReferenceToCollection(
targetResource,
propertyName,
resourceToBeAdded));
}
public void RemoveReferenceFromCollection(
object targetResource,
string propertyName,
object resourceToBeRemoved)
{
_actions.Add(() => ReallyRemoveReferenceFromCollection(
targetResource,
propertyName,
resourceToBeRemoved));
}
So what do those ReallyXXX() methods look like?
Well something to bear in mind here is that our classes are pretty naive – they don’t do any relationship fix-up – yet the relationships themselves are bi-directional, so our code needs to keep both ends of the relationship in sync.
This is what ReallySetReference(..) looks like:
public void ReallySetReference(
object targetResource,
string propertyName,
object propertyValue)
{
// Get the resource type.
var targetType = targetResource.GetType();
// Figure out the other end...
var targetResourceType =
_metadata.Types.Single(t => t.InstanceType==targetType);
var targetResourceTypeProperty = targetResourceType
.Properties
.Single(p => p.Name == propertyName);
var associationSet = targetResourceTypeProperty
.CustomState as ResourceAssociationSet
// Get the relatedEnd
var relatedAssociationSetEnd = associationSet.End1
.ResourceProperty == targetResourceTypeProperty ?
associationSet.End2 : associationSet.End1;
var relatedResourceTypeProperty = relatedAssociationSetEnd
.ResourceProperty;
// Get the relatedType and all Properties
Type relatedType = relatedAssociationSetEnd
.ResourceType
.InstanceType;
var targetTypeProperty = targetType
.GetProperties()
.Single(p => p.Name == propertyName);
var relatedTypeProperty =
relatedResourceTypeProperty == null ?
null :
relatedType.GetProperties()
.Single(p => p.Name == relatedResourceTypeProperty.Name);
// We need to fix up the other end
// but only if this is a two way relationship
if (relatedResourceTypeProperty != null)
{
// Get the other end and clears its
// relationship to targetResource
object originalValue = targetTypeProperty
.GetPropertyValueFromTarget(targetResource);
if (originalValue != null)
{
if (relatedAssociationSetEnd.ResourceProperty.Kind ==
ResourcePropertyKind.ResourceReference)
{
// the other end is a reference so we check
// that it is pointing to the targetResource
// before nulling out.
var backPointer =
relatedTypeProperty
.GetPropertyValueFromTarget(originalValue);
if (object.ReferenceEquals(
backPointer,
targetResource)
)
relatedTypeProperty.SetPropertyValueOnTarget(
originalValue, null);
}
else if (relatedAssociationSetEnd
.ResourceProperty.Kind ==
ResourcePropertyKind.ResourceSetReference)
{
// the other end is a collection
// (i.e. a List in our implementation) so we can
// safely remove targetResource without checking
// whether it is in that list or not
(relatedTypeProperty
.GetPropertyValueFromTarget(originalValue)
as IList
).Remove(targetResource);
}
}
// Add the relationship to the other end...
if (propertyValue != null)
{
if (relatedAssociationSetEnd.ResourceProperty.Kind ==
ResourcePropertyKind.ResourceReference)
{
relatedTypeProperty.SetPropertyValueOnTarget(
propertyValue, targetResource);
}
else if (relatedAssociationSetEnd
.ResourceProperty.Kind ==
ResourcePropertyKind.ResourceSetReference)
{
(relatedTypeProperty
.GetPropertyValueFromTarget(propertyValue)
as IList
).Add(targetResource);
}
}
}
// actually set the reference !
targetTypeProperty.SetPropertyValueOnTarget(
targetResource, propertyValue
);
}
As you can see this is pretty complicated, but if you look closely you'll notice most of the complexity comes from the fix-up logic. Indeed if the Product and Category classes did their own fix-up logic this would suffice:
public void ReallySetReference(
object targetResource,
string propertyName,
object propertyValue)
{
// Get the resource type.
var targetType = targetResource.GetType();
var targetTypeProperty = targetType
.GetProperties()
.Single(p => p.Name == propertyName);
// actually set the reference !
targetTypeProperty.SetPropertyValueOnTarget(
targetResource, propertyValue
);
}
As you can see that is much better, so if you know the classes your DSP exposes do their own fixup remember to take advantage of that.
In the above code I’m also using a couple of extension methods to make this code ‘easier’ (a relative term) to read:
public static void SetPropertyValueOnTarget(
this PropertyInfo property,
object target,
object value)
{
property.GetSetMethod()
.Invoke(target,new object[] { value });
}
public static object GetPropertyValueFromTarget(
this PropertyInfo property,
object target)
{
return property.GetGetMethod()
.Invoke(target, new object[] { });
}
The next step is implementing both ReallyRemoveReferenceFromCollection(..) and ReallyAddReferenceToCollection(..).
But I’ll leave that to you the astute reader, because it is more about implementing fixup than implementing a DSP at this point.
When you’ve got all those methods implemented you should have a working Strongly Typed Read/Write Data Service with Relationships
So congratulations are in order!!
Next up we’ll look at doing this all over again, this time un-typed.
Along the way you are going to learn some nifty IQueryable tricks :)
Comments
- Anonymous
May 05, 2010
The CreateDataSource method shown above fills the CurrentDataSource, and in nearly all examples of working with custom data types, the data is static (e.g. could also come from a cache). But where is the opportunity to fill the data source if it's not "in memory"? What if the data source is the database, and you don't have EF or L2S? Which method do you target to pass in an ID so that the Data source can be built - presumably every caller wouldn't be building the entire data source each time? For example, using a Service Operation, one can pass a parameter that queries the datasource, but in that case the datasource is already built and lives in memory. What if we don't want to hold it in-memory? - Anonymous
June 24, 2010
chinese version:www.cnblogs.com/.../DSP9.html