Creating OData Entries in WCF Data Services

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >

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"
    xmlns="http://www.w3.org/2005/Atom">
  <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.



About the Author

Jeffrey Juday

Jeff is a software developer specializing in enterprise application integration solutions utilizing BizTalk, SharePoint, WCF, WF, and SQL Server. Jeff has been developing software with Microsoft tools for more than 15 years in a variety of industries including: military, manufacturing, financial services, management consulting, and computer security. Jeff is a Microsoft BizTalk MVP. Jeff spends his spare time with his wife Sherrill and daughter Alexandra.

Related Articles

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • As all sorts of data becomes available for storage, analysis and retrieval - so called 'Big Data' - there are potentially huge benefits, but equally huge challenges...
  • The agile organization needs knowledge to act on, quickly and effectively. Though many organizations are clamouring for "Big Data", not nearly as many know what to do with it...
  • Cloud-based integration solutions can be confusing. Adding to the confusion are the multiple ways IT departments can deliver such integration...

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date