Creating a Data Service Provider – Part 3 - IDataServiceMetadataProvider

UPDATE: I’ve made a few updates to the code / write-up to reflect refactors I’ve made as I’ve implemented more of the interfaces.

This is Part 3 of my ongoing series on Creating a Data Service Provider, and in this post we’ll look at how to implement IDataServiceMetadataProvider

IDataServiceMetadataProvider is responsible for describing the shape of the Resources and ResourceSets available from your DataSource (see Part 2).

And what is available from your DataSource is really up to you:

  • It could be completely static. For example if you are exposing data from an off the shelve software package or something where the model is fixed – perhaps Twitter, Stackoverflow or something similar.
  • It could be strongly typed. For example your implementation might reflect over the Properties of the DataSource itself and create ResourceSets for each property that returns IQueryable<T> and ResourceTypes for all Ts. This is essentially the way the built-in Reflection and Entity Framework providers work.
  • It could be completely dynamic. For example you could go off to some database somewhere and read a catalog.

Your options are almost endless.

However whichever option you chose the first step is to create a class that implements the interface.

public class DSPMetadata: IDataServiceMetadataProvider
{

The interface has 6 methods and 5 properties, which sounds terribly complicated until you actually try to implement it. I got my first example working in less than half an hour, and I’m not even a developer anymore!

Here is how I’ve hooked it together:

public abstract class DSPDataService<T>: DataService<T>
{
   IDataServiceMetadataProvider _metadata;

public DSPMetadata()
{
_metadata = GetMetadataProvider(typeof(T));
}

public object GetService(Type serviceType)
{
if (serviceType == typeof(IDataServiceMetadataProvider))
{
return _metadata;
}

}

public abstract IDataServiceMetadataProvider GetMetadataProvider(Type dataSourceType);
}

As you can see this code has an abstract method to get the actual metadata, which subclasses must implement.

We are almost ready to implement the interface, but first lets learn about…

Creating Metadata

If you look at the methods on the interface you will see ResourceTypes, ResourceProperties, ServiceOperations, ResourceSets and ResourceAssociationSets.

These are the public classes in the System.Data.Services assembly that you can construct and manipulate yourself to describe your model.

Essentially these methods just walk over this metadata, exposing different information about the model.

Imagine if you have this CLR class:

public class Product
{
public int ProdKey {get;set;}
public string Name {get;set;}
public Decimal Price {get;set;}
public Decimal Cost {get;set;}
}

And you want to create a ResourceType for Product, this is how you would do it:

var productType = new ResourceType(
typeof(Product), // CLR type backing this Resource
ResourceTypeKind.EntityType, // Entity, ComplexType etc
null, // BaseType
"Namespace", // Namespace
"Product", // Name
false // Abstract?
);
var prodKey = new ResourceProperty(
"ProdKey",
ResourcePropertyKind.Key |
ResourcePropertyKind.Primitive,
ResourceType.GetPrimitiveResourceType(typeof(int))
);
var prodName = new ResourceProperty(
"Name",
ResourcePropertyKind.Primitive,
ResourceType.GetPrimitiveResourceType(typeof(string))
);
var prodPrice = new ResourceProperty(
"Price",
ResourcePropertyKind.Primitive,
ResourceType.GetPrimitiveResourceType(typeof(Decimal))
);
productType.AddProperty(prodKey);
productType.AddProperty(prodName);
productType.AddProperty(prodPrice);

Notice that we aren’t exposing the Cost property, because that is ‘sensitive’ information.

Next to create a ResourceSet called Products to expose our ResourceType, you simply do this:

var productsSet = new ResourceSet("Products", productType);

As you can see building metadata is not hard.

Freezing your metadata

The final step before you can use the Metadata is to freeze it:

productsSet.SetReadonly();
productType.SetReadonly();

This means it can no longer be modified, at least not for the current Request.

NOTE: Data Services will ask for a IDataServiceMetadataProvider implementation for every request so even though you need to Freeze your metadata per-request, it is completely possible to reconstruct the metadata for each request if necessary.

Exposing Metadata via IDataServiceMetadataProvider

Once you’ve built your metadata you just need to expose it via your implementation.

Here is what your implementation might look like if you didn’t support inheritance, relationships or service operations.

public class DSPMetadataProvider
{
    private Dictionary<string, ResourceType> _resourceTypes
= new Dictionary<string, ResourceType>();
private Dictionary<string, ResourceSet> _resourceSets
= new Dictionary<string, ResourceSet>();

    public DSPMetadataProvider(){} 

    public void AddResourceType(ResourceType type)
{
type.SetReadOnly();
_resourceTypes.Add(type.FullName, type);
}

    public void AddResourceSet(ResourceSet set)
{
set.SetReadOnly();
_resourceSets.Add(set.Name, set);
}

    public string ContainerName
{
get { return "Container"; }
}

public string ContainerNamespace
{
get { return "Namespace"; }
}

    public IEnumerable<ResourceType> GetDerivedTypes(
ResourceType resourceType
)
{
// We don't support type inheritance yet
yield break;
}

public ResourceAssociationSet GetResourceAssociationSet(
ResourceSet resourceSet,
ResourceType resourceType,
ResourceProperty resourceProperty)
{
throw new NotImplementedException("No relationships.");
}

public bool HasDerivedTypes(ResourceType resourceType)
{
// We don’t support inheritance yet
return false;
}

    public IEnumerable<ResourceSet> ResourceSets
{
get { return this.resourceSets.Values; }
}

public IEnumerable<ServiceOperation> ServiceOperations
{
// No service operations yet
get { yield break; }
}

public bool TryResolveResourceSet(
string name,
out ResourceSet resourceSet)
{
return resourceSets.TryGetValue(name, out resourceSet);
}

public bool TryResolveResourceType(
string name,
out ResourceType resourceType)
{
return resourceTypes.TryGetValue(name, out resourceType);
}

    public bool TryResolveServiceOperation(
string name,
out ServiceOperation serviceOperation)
{
// No service operations are supported yet
serviceOperation = null;
return false;
}

public IEnumerable<ResourceType> Types
{
get { return this.resourceTypes.Values; }
}
}

The idea here is that your implementation of GetMetadataProvider you do something like this:

public override IDataServiceMetadataProvider GetMetadataProvider(Type dataSourceType)
{
DSPMetadataProvider metadata = new DSPMetadataProvider();
var productType = new ResourceType(
typeof(Product), // CLR type backing this Resource
ResourceTypeKind.EntityType, // Entity, ComplexType etc
null, // BaseType
"Namespace", // Namespace
"Product", // Name
false // Abstract?
);
var prodKey = new ResourceProperty(
"ProdKey",
ResourcePropertyKind.Key |
ResourcePropertyKind.Primitive,
ResourceType.GetPrimitiveResourceType(typeof(int))
);
var prodName = new ResourceProperty(
"Name",
ResourcePropertyKind.Primitive,
ResourceType.GetPrimitiveResourceType(typeof(string))
);
var prodPrice = new ResourceProperty(
"Price",
ResourcePropertyKind.Primitive,
ResourceType.GetPrimitiveResourceType(typeof(Decimal))
);
productType.AddProperty(prodKey);
productType.AddProperty(prodName);
productType.AddProperty(prodPrice);

    metadata.AddResourceType(productType);
metadata.AddResourceSet(
new ResourceSet("Products", productType)
);
return metadata;
}

Notice that as you add ResourceTypes and ResourceSets to the metadata they are indexed and frozen (SetReadOnly) for you.

Of course as you add support for inheritance, relationships and service operations, things get a little bit more complicated. But you can handle each of these methods using LINQ to Objects pretty easily.

Performance Considerations

You might have noticed that there are two kinds of functions/properties on this interface, those that return enumerations and those that try to find a specific instance.

This is for performance reasons.

Most requests to the server use the TryResolveXXX instance methods. This means that even if you have a lot of metadata you can often avoid creating most of it.

The other methods are called much more sparingly: for example when someone gets $metadata or the root service document.

Next time

Now we’ve implemented hooked up our DataService to our Custom DSP, and we’ve implemented IDataServiceMetadataProvider, all we need to do to finish our custom read-only DSP is implement IDataServiceQueryProvider.

We’ll start to cover that in Part 4.