Adding Multi-Value properties to untyped providers

With the recent release of the WCF Data Services Oct 2010 CTP1 for .NET4 the WCF Data Services now has the ability to expose Multi-Value properties (formerly called “Bags”). These can come very handy when you want to include short collections of primitive or complex types on your entities (or other complex types). In this blog we’ll take a look at what does it take to add the Multi-Value properties to an untyped provider.

Naming note: Sometimes in the CTP1 APIs the Multi-Value properties are still referred to as Bags, or Bag properties. Also the EDM type name is still Bag. I will refer to these as Multi-Value properties in the text, but the code might talk about Bags to be consistent with existing APIs.

Setup

I’m going to be using the untyped read-only provider sample from the OData Provider Toolkit which you can download here: https://www.odata.org/developers/odata-sdk.

You will also need the WCF Data Services Oct 2010 CTP1 for .NET4 as announced on this blog: https://blogs.msdn.com/b/astoriateam/archive/2010/10/26/announcing-wcf-data-services-oct-2010-ctp1-for-net4-amp-sl4.aspx

In your VS 2010 open the Untyped\RO\ODataDemo.sln solution file and let VS convert it to 2010 format and convert all the projects to .NET4.

Since the CTP is a side-by-side release it doesn’t replace the WCF Data Services assemblies, so you will need to update the references. Simply open each of the three projects and remove the references to System.Data.Services and System.Data.Services.Client. Then add references to Microsoft.Data.Services.dll and Microsoft.Data.Services.Client.dll which can be found in your installation folder of the CTP under .NETFramework sub-folder. (You will have to browse for the files as the assemblies are not listed in the framework list).

To verify everything works as expected you should be able to rebuild the solution and run all the tests without issues. You can also browse to the demo service to see it working.

Metadata

In order to expose Multi-Value property, such property must be added to the metadata. To define a Multi-Value property a special Multi-Value resource type is required. This type is used to define the type of each item of the Multi-Value. The class which represents this type is called BagResourceType, it derives from the ResourceType class and it adds a single property ItemType. To create an instance of this type a new method ResourceType.GetBagResourceType was added.

Let’s see how this works on the untyped provider toolkit sample. To add a Multi-Value of complex or primitive type add this method to the DSPMetadata class:

 /// <summary>Adds a bag of complex or primitive items property.</summary>
/// <param name="resourceType">The resource type to add the property to.</param>
/// <param name="name">The name of the property to add.</param>
/// <param name="itemType">The resource type of the item in the bag.</param>
public void AddBagProperty(ResourceType resourceType, string name, ResourceType itemType)
{
    ResourceProperty property = new ResourceProperty(
        name, 
        ResourcePropertyKind.Bag, 
        ResourceType.GetBagResourceType(itemType));
    property.CanReflectOnInstanceTypeProperty = false;
    resourceType.AddProperty(property);
}

Please note that the property must be defined with a special kind Bag as well as have the special BagResourceType type. Also since we’re creating an untyped provider the property is set to not use reflection (as any other property in this provider).

For a Multi-Value of primitive types we can also add a helper method like this:

 /// <summary>Adds a bag of primitive items property.</summary>
/// <param name="resourceType">The resource type to add the property to.</param>
/// <param name="name">The name of the property to add.</param>
/// <param name="itemType">The primitive CLR type of the item in the bag.</param>
public void AddBagProperty(ResourceType resourceType, string name, Type itemType)
{
    ResourceType itemResourceType = ResourceType.GetPrimitiveResourceType(itemType);
    this.AddBagProperty(resourceType, name, itemResourceType);
}

One other thing to note about the Multi-Value properties is their instance type. The InstanceType property of the BagResourceType will always return IEnumerable<T> where T is the InstanceType of the ItemType. So for example a Multi-Value property of strings (primitive type), will have an instance type IEnumerable<string>.

In the untyped provider in the sample all complex types use the same instance type called DSPResource. So a Multi-Value of complex types will have instance type IEnumerable<DSPResource>.

The instance type is important because it’s the type the WCF Data Services will assume as the value of the Multi-Value property. Most importantly the IDataServiceQueryProvider.GetPropertyValue method should return an instance of IEnumerable<T> for a Multi-Value property. Note that other than being IEnumerable<T> there are no other requirements on the value returned. It can be really any instance which implements the required interface. For most cases List<T> works great, but if you have some special requirements, you can implement your own and it will work just fine.

Data

And that leads us to how to specify instance data. Let’s use the sample ODataDemoService for this. In the DemoDSPDataService.svc.cs file there is a method which defines the metadata for the demo service called CreateDSPMetadata. For now, we will tweak the metadata by adding a Tags property to the Product entity. This can be done by adding just a single line like this:

 metadata.AddBagProperty(product, "Tags", typeof(string));

Add this line after the other property additions for the Product entity type.

Now we need to add the instance values for the Tags property. Important thing to note is that Multi-Value properties can not have null value. That means the IEnumerable<T> must never be null (if it is, the service will fail to serialize the results). It is also invalid to have items in the Multi-Value of null value, so the enumeration of the IEnumerable<T> must not return nulls either (if the T actually allows nulls).

The demo service has a method called CreateDataSource which initializes the instance data. So let’s add some instance data for our new Multi-Value property there. Anywhere into the part which sets property values for the productBread add a line like this:

 productBread.SetValue("Tags", new List<string>() { "Bakery", "Food" });

Then add similar lines for the productMilk and productWine as well. If you omit these lines the value of the Multi-Value property will be null and it will cause failures (since nulls are not allowed). It is perfectly valid to add empty lists though.

Result

To run the demo service, one more thing has to be done. The Multi-Value property is a new construct which older clients/servers might not understand so it requires an OData protocol version 3.0. So we need to allow the V3 for our service by changing the InitializeService method to include line like this (replace the line there which sets it to V2):

 config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;

And that’s it. Just build the solution and run the demo service. If you request the product with ID 0 with URL like https://service/DemoDSPDataService.svc/Products(0) in your browser you should see this property in the output:

 <d:Tags m:type="Bag(Edm.String)">
    <d:element>Bakery</d:element>
    <d:element>Food</d:element>
</d:Tags>

This is an XML representation of a Multi-Value of primitive type, the JSON representation is similar and uses JSON array to represent it.

Adding a Multi-Value of complex types is similar to the primitive types, just remember that the T in IEnumerable<T> is the instance type of the complex type, so in this sample provider it would be the DSPResource type.