Creating OData Entries in WCF Data Services

Open Data Protocol
(OData) is built on Web standards like HTTP and AtomPub. Creating data on an
OData Endpoint is simply an HTTP POST that contains an AtomPub payload. Leveraging
WCF HTTP
capabilities, WCF Data Services implements OData for .NET code. To fully exploit OData with
WCF Data Services there are classes and interfaces a .NET app must instantiate
or implement. Using a sample WCF Data Services application, this article
explains how to implement data creation on an OData Endpoint.

OData Operations

A complete review of the OData specification is beyond the
scope of this article, however, there are some key OData conventions essential
to understanding WCF Data Services. You’ll find a more complete introduction in
this article: WCF
Data Services Providers
.

OData builds on Web Standards. HTTP provides the data transport
and Operation commands. An OData service hosts an HTTP Endpoint. Clients
consuming the service create an HTTP request consisting of a payload, OData HTTP
message headers, and an HTTP operation. HTTP GET returns an OData collection,
single item, or item property. HTTP POST creates a new item on an OData
Service. AtomPub or JSON define the data payload. OData follows an AtomPub XML
schema or JSON convention. In the specification an Entry refers to the data
payload.

The following is an AtomPub create Entry request that comes
from the Odata specification web site located here http://www.odata.org/developers/protocols/operations#CreatingnewEntries.

POST /OData/OData.svc/Categories HTTP/1.1 Host: services.odata.org
  DataServiceVersion: 1.0 MaxDataServiceVersion: 2.0 accept: application/atom+xml
  content-type: application/atom+xml Content-Length: 634
<?xml version="1.0" encoding="utf-8"?>
<entry xmlns_d="http://schemas.microsoft.com/ado/2007/08/dataservices"
    xmlns_m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
    >
  <title type="text"></title>
  <updated>2010-02-27T21:36:47Z</updated>
  <author>
    <name />
  </author>
  <category term="DataServiceProviderDemo.Category"
      scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
  <content type="application/xml">
    <m:properties>
      <d:ID>10</d:ID>
      <d:Name>Clothing</d:Name>
    </m:properties>
  </content>
</entry>

Header information specifies the content source type and the
desired content response type. “atom+xml” refers to AtomPub format. A
developer can mix source and response format type. The bulk of the message is
inside an “entry” tag. A “content” section inside the entry defines the entry
payload.

Anything that can generate a message like the one above and
work over HTTP can work with an OData Endpoint. This encompasses a broad range
of options including JavaScript.

JavaScript POST

JavaScript running inside a browser is one common HTTP
client. The following code creates a new TestItem on a WCF Data Service
application that will be defined later in the article.

<div id='result'> </div>

<script type="text/javascript">

    var xml = '';
    xml = xml + '<entry xmlns_d="http://schemas.microsoft.com/ado/2007/08/dataservices" ';
    xml = xml + 'xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" ';
    xml = xml + 'xmlns="http://www.w3.org/2005/Atom"> ';
    xml = xml + '<title type="text" /> '
    xml = xml +  '<updated>2011-10-19T01:10:56Z</updated> ';
    xml = xml + '<author>  <name />   </author> ';
    xml = xml + '<category term="Test.OData.Server.TestItem" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> ';
    xml = xml + '<content type="application/xml"> ';
    xml = xml + '<m:properties> ';
    xml = xml +  '<d:Id>Three</d:Id> ';
    xml = xml +  '<d:Payload>This is Three</d:Payload> ';
    xml = xml +   '</m:properties> ';
    xml = xml +  '</content></entry> ';

    http = new XMLHttpRequest();

    http.open("POST", "http://localhost:8000/Items", false);
    http.setRequestHeader("content-type", "application/atom+xml");
    http.setRequestHeader("accept", "application/atom+xml");
    http.setRequestHeader("charset", "utf-8");
    http.send(xml);
    doc = http.responseXML;

    res = document.getElementById("result");

    res.innerHTML = doc.xml.toString();
</script>

XMLHttpRequest is accessible to any JavaScript inside a
browser. “open” defines the operation, Endpoint address, and determines whether
the operation should be synchronous or asynchronous. Send transmits the request
to the Endpoint and “responseXML” retrieves the response message. As stated
earlier, source type and desired response type are stored in the HTTP header
information. “content-type” stores the source type and “accept” stores the
desired response type.

Though the example uses XML; typically, JavaScript would
want to use a JSON source and response type.

The response result is written to a DIV inside the HTML
page.

In order to expose an Endpoint; a WCF Data Service needs to
setup a host.

WCF Data Services Hosting

As stated earlier WCF Data Services builds on existing WCF
HTTP capabilities. WCF Offers two hosting models; a self-hosting model and an
IIS hosted model. The following code creates a self-hosted WCF Data Services
host.

[System.ServiceModel.ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class TestItemsDataService : DataService<TestItems>
{
    public static void InitializeService(IDataServiceConfiguration
                                config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
        config.UseVerboseErrors = true;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Type serviceType = typeof(TestItemsDataService);
        Uri baseAddress = new Uri("http://localhost:8000");
        Uri[] baseAddresses = new Uri[] { baseAddress };

        DataServiceHost host = new DataServiceHost(
            serviceType,
            baseAddresses);

        host.Open();
}

TestItemsDataService inherits from
DataService<TestItems> generic class. Most of the WCF Data Service
sample is contained in the TestItems class. Testitems code follows.

public class TestItems : IUpdatable
{
static Dictionary<string,TestItem> _items = null;

static TestItems()
{
    _items = new Dictionary<string,TestItem>();
    _items.Add("One", new TestItem() { Id = "One", Payload = "This is One" });
    _items.Add("Two", new TestItem() { Id = "Two", Payload = "This is Two" });
}

public IQueryable<TestItem> Items
{
    get { return _items.Values.AsQueryable(); }
}
}

The IUpdatable implementation will be explained later in the
article. TestItems exposes an Items collection. Clients performing a GET can
read from the collection. Data behind the collection is stored in a Dictionary
variable. Items property contains TestItem class instances. The TestItem class
definition follows.

[DataServiceKey("Id")]
public class TestItem
{
    public TestItem()
    {
        Id = "defaultId";
        Payload = "defaultPayload";
 
    }
 
    public string Id { get; set; }
    public string Payload { get; set; }
 
}
 

WCF Data Services requires a “uniqueness” or key property
for any class exposed in a collection. Id is the key value in the example. DataServiceKey
attribute defines the key property. OData clients can query for values in a
collection with a GET and the appropriate URI. The URI below will retrieve the
entry matching the “One” Id in the collection.

http://localhost:8000/Items(‘One’)/

Though an OData Query property is not required for a create
entry example; it’s difficult to demonstrate an add without a Query. TestItems
implements adding an entry in the IUpdatable methods required by the
IUpdateable interface.

IUpdateable

Following are TestItems IUpdateable implementations.

public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
{
    Trace.WriteLine("AddReferenceToCollection " + targetResource.ToString() + " " + propertyName + " " + resourceToBeAdded.ToString());
 
    throw new NotImplementedException();
}
 
public void ClearChanges()
{
    throw new NotImplementedException();
}
 
public object CreateResource(string containerName, string fullTypeName)
{
    Trace.WriteLine("CreateResource " + fullTypeName);
 
    return new TestItem();
}
 
public void DeleteResource(object targetResource)
{
    throw new NotImplementedException();
}
 
public object GetResource(IQueryable query, string fullTypeName)
{
    Trace.WriteLine("GetResource " + fullTypeName);
 
    return _items[fullTypeName];
}
 
public object GetValue(object targetResource, string propertyName)
{
    Trace.WriteLine("GetValue " + propertyName);
 
    var test = (TestItem)targetResource;
 
    if (propertyName == "Id") { return test.Id; }
    else
    { return test.Payload; }
}
 
public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
{
    throw new NotImplementedException();
}
 
public object ResetResource(object resource)
{
    throw new NotImplementedException();
}
 
public object ResolveResource(object resource)
{
    Trace.WriteLine("ResolveResource");
    return resource;
}
 
public void SaveChanges()
{
    Trace.WriteLine("Called SaveChanges");
    //Completed setting properties save it.
}
 
public void SetReference(object targetResource, string propertyName, object propertyValue)
{
    Trace.WriteLine("SetReference");
}
 
public void SetValue(object targetResource, string propertyName, object propertyValue)
{
    Trace.WriteLine("SetValue " + propertyName + " == " + propertyValue.ToString());
 
    var test = (TestItem)targetResource;
 
    if (propertyName == "Id") //Once you have the key you can add it here
    {
        test.Id = propertyValue.ToString();
 
        if (!(_items.ContainsKey(test.Id)))//not already there
        {_items.Add(test.Id, test);}
    }
    else
    { test.Payload = propertyValue.ToString(); }
 
}
 
 

CreateResource is the first method invoked when the WCF Data
Services receives the POST entry from the JavaScript client. Parameters are
pulled from the entry message. The method returns an instance based on the
entry. More sophisticated services with many collections would look at the
parameter values to determine which class to create. Reflection could also have
been used here to create an instance from the fullTypeName parameter.

WCF Data Service plumbing routes the new object to the SetValue
method. SetValue is invoked for each property defined in the entry payload. Since
the Id did not exist in the CreateResource, adding to the underlying Dictionary
happens here.

Once all properties are set with SetValue, the infrastructure
invokes “SaveChanges”. Typically a class would do an internal save to the
underlying collection data source. During the SaveChange invocation
ResolveResources is called. Documentation is a bit unclear about how a
developer should implement this method. Shawn Wildermuth’s article, Implementing
IUpdatable (Part 3)
, introduces the idea
that CreateResource may not return the complete class, but rather an identifier
for the class instance. ResolveResource would then return the true class based
on the identifier. Since the CreateResource returns the class, this method
returns what was passed to it.

Dictionary would not be a good Service data structure choice.
Services typically have multiple clients and Dictionary is not concurrency
friendly.

Conclusion

WCF Data Services provides the plumbing to surface OData
from .NET code. OData utilizes HTTP operations to do Entry creates, reads,
updates, and deletes. Aside from hosting code in a DataService a developer must
implement the IUpdateable interface.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read