Table of Contents
What is this Article About?
This is not your typical “expose an entity framework
model as a WCF Data Service” kind of article; in fact I deliberately omitted Entity
Framework from this discussion. Now don’t get me wrong, I have nothing against
EF, but you will find a lot of great articles on how to set up a WCF Data
Service using EF as data source.
The discussion is around OData v3 and WCF Data Services
5.0 and 5.2. Of course there is no way I cover all new features or every API
available, nor do I need so; I will point you to the resources where you can
get all information you need. Instead my aim in this article is to discuss some
of the topics that usually remain oblivious to the typical WCF Data Services
developer.
What are OData, Atom, and AtomPub?
The Open Data Protocol (OData) is a protocol which standardizes
the exposure and consumption of data. In times where data is being exposed at high
rates and where consumers connect to more and more data endpoints, it’s
important for clients to access these endpoints in a common way.
OData builds on web standards such as Http, Atom, and JSON
to provide REST-based access to these endpoints. Data is exposed as entities
where each entity can be treated as an Http resource which makes it subject to
CRUD (create, read, update, delete) operations.
So how is OData related to Atom and AtomPub?
Atom is way to expose feeds much the same way RSS does. If
you are wondering what are the differences between Atom and RSS you can check
this site (http://en.wikipedia.org/wiki/Atom_(standard)#Atom_compared_to_RSS_2.0)
Atom by itself allows only feed exposure. If you want to
publish data, AtomPub (Atom publishing) provides this ability. AtomPub uses
Http verbs GET, POST, PUT, and DELETE to enable data publishing.
OData adds a set of extensions on top of AtomPub to
enable more advanced and smart operations such as data retrieval filtration and
typed values definition. For example, below are two queries that are made
possible through the power of OData:
- http://server/service.svc/entity/$count
- http://server/service.svc/entity?$filter=(entityID add 4) eq 8
REST vs. SOAP: The Design Decisions
You must have expected a REST vs. SOAP section in an OData
article, haven’t you? After all the resources around this topic are really
scarce. Ok, you got the joke.
If you search online for REST vs. SOAP you will undoubtedly
get a lot of valuable resources. So instead of addressing the topic in a rather
abstract approach, I will show you real world decisions I took at work while
evaluating whether to go for REST or SOAP.
So I work in the e-government sector (why I mentioned
this will become clear shortly). When I first joined my current job, the first
mission was creating an integration service bus that would eliminate any
point-to-point integrations; which was the predominant case back then. This bus
was also required to handle B2B communications with other e-government agencies
over the internet. Examine the below diagram, and let’s see how each
requirements was mapped into a (high level) design decision to whether I should
use SOAP or REST-based services:
- 1, 1.1, and 1.2: we had an initial list of governmental agencies that we needed to allow them to query our services for data retrieval. In addition, we knew that over time the list of agencies will increase. So we did not want to have an ever expanding list of endpoints that we share with our partners. Moreover, due to the crucial nature of government operations, we wanted the minimum amount of downtime required if we ever wanted to edit our schema internally. So Our best bet was to have some sort of addressing mechanism where all agencies get the same endpoint with a fixed schema that nearly never chances, yet based on some content on each request we would route the request into internally hosted services. This scenario is best handled using SOAP WS-Addressing extension. So we ended up implementing a WCF service configured to use WS-Addressing. (Note: when using WS-Addressing, there are some security roadblocks that you will hit, but that outside the scope of this discussion)
- 2: Another requirement was mutual authentication for every service that wanted to call into our service bus. Of course that’s very typical in government communications. REST-based services are typically secured via SSL. For any form of custom client certificate authentication and digital signatures, you’d be better of using WS-Security which we ended up using via the power of WCF.
- 3: internally we have a BPM engine that requests data from a data store. Now since the communication is internal, we are relaxed about the security requirements. We just need username/password authentication and no encryption or signature is required. REST-based services in this case are the best bet due to its simplicity. These services we ended up building using WCF Data Services, were easily consumed by the BPM front-end which is heavily JQuery-based which made REST/JSON combination a perfect candidate. One more important thing was the size of payload. Using REST and JSON, the payload returned was in much reduced size when compared to the size of SOAP/XML payload.
- 4: Finally, we needed transaction support from the BPM to the called service. This required WS-Transaction SOAP extension which made SOAP a must. REST based services do not support transactions. If you want to read more about transactions, I advise you to check my article: http://www.codeproject.com/Articles/35087/Truly-Understanding-NET-Transactions-and-WCF-Imple
As you can see from
the above discussion, the answer to the question “SOAP vs. REST” is the famous
one: “it depends”. Hopefully the above scenarios will provide you a decent
starting point to making similar decisions. Personally, I would go with REST
simplicity whenever I can; sometimes however, it’s just that you cannot.
First Look at OData Queries
The best way to have a first look of OData in action is
using some demo services offered by the OData web site. Browse to http://www.odata.org/ecosystem and
scroll down to section “Sample Services”. Select the Read-Only OData Sample
Service and click Browse.
Note:
the services offered in the OData web site might change over time, but
basically the techniques you will learn here will stay mostly the same. Only
entity names might change.
Now take your first
look on an OData service and let’s examine some important observations:
- First notice that the service URL ends with “.svc” extension. This indicates that it’s a WCF service; more specifically, that’s a WCF Data Service – which we will discuss in detail later.
- Also notice that the data is published via the Atom feed
- Also notice that what you got is the first level of the set of entities offered by the service
- For each entity you get a href attribute telling you how to further query sub entities and related entities
What points 3 and 4 confirm is that true to the REST
promise, each resource (entity) is being defined as a URI which is easily
consumable.
Now let’s play a bit with this service. Say you
want to query all the Products offered. Here is how you do this:
Again, I will discuss some important highlighted items:
- The URL is pointing to http://services.odata.org/OData/OData.svc/Products. Recall that “href=Products” from our first query informed me how to query all the products.
- Each product is represented with an entry, and each entry has an id which is the URI of each product in the set of products. For example the entity id of the first product is http://services.odata.org/OData/OData.svc/Products(0). Where the value “0” is the entity primary key discussed in the next point.
- The primary key of the product entity is specified in the ID element. This key uniquely identifies an entity and in some cases it could be a composite key; for example it could have been “/Products(0,1)” if products would allow composite primary keys.
- There are relationships for the Product entity with other entities; namely Category and Supplier. To see the Category of Product of key 0 you would use “Products(0)/Category” and to examine the Suppliers of the same Product you would use “Products(0)/Supplier”.
- The properties of each product are listed in the properties element.
Here are some more queries:<o:p>
<o:p>
<o:p>
Keywords
You can use keywords to perform some advanced query
operations.<o:p>
For example, for client tools wanting to consume the
OData service, the metadata can be exposed using: http://services.odata.org/OData/OData.svc/$metadata.
The “metadata” keyword generates the metadata that can then be used by proxy
generation tools.
<o:p>
Another example is the “value” keyword which strips the
XML surrounding a property value and returns the value in its raw format. For
example, http://services.odata.org/OData/OData.svc/Products(1)/Price
returns the price wrapped with Atom XML:
Using the “value” keyword returns the raw format:
<o:p>
Another useful keyword is the “filter” keyword. For
example the following query returns all products with price = 2.5: http://services.odata.org/OData/OData.svc/Products?$filter=Price%20eq%202.5 (note: the %20 encoding is added by the browser)
The following query http://services.odata.org/OData/OData.svc/Products?$filter=Price%20gt%202.5&$orderby=Price%20desc&$top=2
couples a combination of keywords:
- the “filter” keyword uses
the “greater than” operator to filter products of a price > 2.5
- the keyword “orderby”
orders the prices by descending values
- the keyword “top” returns
only the top 2 of the returned product set
Formatting
Up until this point, we have been querying the service
using the Atom XML format. The problem of Atom is the size of the response as
its wrapped with XML tags which increases the entire payload size.
<o:p>
Using the “format” keyword however you can return data
using the JavaScript Object Notation (JSON) format:
However, what you have seen above is the default “verbose
JSON”. It’s called verbose because the size of the JSON returned is quite large
(although much less than Atom).
A new feature in OData V3 is the “light JSON” which
strips unwanted information from the verbose JSON and presents a much reduced
size one. If you examine again the verbose JSON data you will see a lot of
information concerning OData. The new light JSON strips this data off and only
presents a single metadata URI which consuming clients can then further query
if they do care about the OData stripped information.<o:p>
<o:p>
You can specify if you want verbose or light JSON in the
request header by using the “DataServiceVersion” header. If you use fiddler to
examine a request to an OData service using the $format=JSON keyword, you will
see the “DataServiceVersion: 1.0” response header:
<o:p>
You can then use Fiddler composer to compose a custom
request for a light JSON using the following two headers:
Note that in the Accept header we supplied two sets:
- application/json;odata=light;q=1:
to enable JSON light format
- application/json;odata=verbose;q=0.5:
a fall back option to verbose JSON if the client does not support JSON light.
Now back to the OData service from the http://services.odata.org website. The
light JSON can be reached using an experimental link as the feature is not yet
supported by all clients. Navigate to http://services.odata.org/Experimental/OData/OData.svc/Products?$filter=Price%20gt%202.5&$orderby=Price%20desc&$top=2&$format=json
to see the light JSON and compare its size to the verbose one:
So when to use ATOM
and XML vs. JSON formatting? The answer is usually governed by the type of
client consuming an OData service. JSON is typically ideal for clients running
at user agents such as Ajax and Jquery based clients. ATOM XML is usually more
appropriate for server side applications calling into OData services.
OData Clients
OData clients are the programs that can read and parse
data returned by an OData service. Some of these clients can handle both ATOM
and JSON formats while others can understand a single format.
One client we have already seen in action is the browser.
As you’ve seen before, I have used the browser to download data from an OData
service in both ATOM and JSON formats. Browsers however are unlikely to be
useful in real world applications where clients typically need to present data
in more user-friendly format which typically means in a strongly types manner,
as well as performing further processing and undertaking business decisions
based on this data.
<o:p>
This section shows other types of clients.
Excel 2013 PowerPivot<o:p>
Excel has an add-on called PowerPivot that is cabamle of
reading ATOM feeds form an OData service. Frist locate the PowerPivot add-on in
Excel and do the following:
<o:p>
In the resulting window, point to http://services.odata.org/Experimental/OData/OData.svc/Products,
test the connection and finish the wizard. This will import the products from
the service and lay them out in a human readable form:
<o:p>
As far as I know, PowerPivot cannot read JSON formatted
data.
Visual Studio 2012<o:p>
We’ll spend much more time within Visual Studio 2012 later
on, but for now let’s take a glance at how VS can consume an OData Service.
VS 2012 includes the WCF Data Service client library
which enables generation of a strongly-typed client proxy through the familiar
Add Service Reference wizard.
Launch VS 2012, create a new project (it can be Console
or Web) and invoke the Add Service Reference wizard. Point the wizard to the
URL of our demo service http://services.odata.org/OData/OData.svc/.
You will get the list of top-level entities as shown below:
Click OK to close the wizard.
<o:p>
To inspect the strongly-types client proxy, select Show
All Files from the Solution Explorer toolbar. Then open Reference.cs file. This
file exposes the entities and their relationships as strongly-typed classes and
OO relationships. You will also notice that the proxy has auto generated
methods to perform updates.
Other Clients<o:p>
You can see a full list of OData clients here: http://www.odata.org/ecosystem#consumers
Working with OData via WCF Data Services
Creating a WCF Data Service with POCOs
There are multiple ways to expose data in an OData
service. The below are methods available to you using the .NET Framework:
- WCF Data Service with Entity
Framework as a data source
- WCF Data Service with LINQ
to SQL as a data source
- WCF Data Service with custom
.NET objects (POCOs)
- WCF RIA Service
- WCF Data Service with a
custom provider if all the above options are not applicable
Plain Old CLR Objects (POCOs) refer to objects that obey
the Persistence Ignorance design principal which states that objects do not
contain any persistence logic. In plain English, these are the CLR objects that
you create using simple OO mechanisms.
<o:p>
Let’s start by creating a WCF Data Service exposing 2 CLR
objects. Using VS 2012 create a new ASP.NET web application. Add a reference to
“System.Data”. Create a new class and add the following code inside it:
- The DataServiceKey attribute
specifies the ID of each entity to be exposed via the data service
- The DataServiceEntity
actually marks the class as an entity
- There is a composition
relationship between Course and Student entities, where each student has
multiple courses
- Class MyDataSource acts as
the container of my entities as note that it exposes the entities as IQueryable.
This is required so that WCF Data Service can identify and expose classes as
entities.
Now add a new item of
type WCF Data Service to the project. Once you do you will get the default
template of a WCF Data Service class:
Let’s examine this class:
- It inherits from class DataService
- We need to supply the data
source class in the place of “/* TODO: put your data source class name here */”.
In our case that’ll be class “MyDataSource” which holds the IQueryable
collections
- We need to set access rules
for the entities in our service. For each entity set we can specify whether we
want to allow reads, updates or both. In our case, we want to allow all
operations on all entities. So this will read “config.SetEntitySetAccessRule("*",
EntitySetRights.All);”.
- Similarly we can set the
access rules for any service operations we might have defined. Service
operations will be discussed later.
- Finally we can set the
protocol version of OData which as mentioned before is in V3 at the time of
this writing.
So the class will look like this after modification:
<o:p>
Now run the application, and just like that you have
created a WCF Data Service:
Just as we have done before, you can start querying the
service and examining the relation between the entities. Here some query
examples:<o:p>
Behind the Scenes
<o:p>
So obviously a WCF Data Service is actually a WCF service
afterall. However, taking a look at the web.config file of the sample we have
just created shows nothing realted to WCF except a blank system.serviceModel
section:
<o:p>
As you can see there are no services nor ednpoints
defined. WCF Data Services uses default configurations to do its job and these
are configurations are hidden from you. So what are these configurations? The
best way is to show you directly. I have edited the web.config file to read as
follows:
<o:p>
I have defined a service names “MyDataService” which – as
you recall – in the name of the WCF Data Service I created in the previous
example.
Now to the creame of WCF: the endpoint. If you have any
previous experience in WCF you know that an endpoint is identified via an
address, binding, and contract. Here I have supplied these values.
The binding is webHttpBinding which is WCF binding used
for REST operations<o:p>
I specified REST as my address, so from now on to query
my service I will use http://localhost:19865/MyDataService.svc/REST/
to query my service instead of http://localhost:19865/MyDataService.svc/
which was used before.
<o:p>
Finally, what about the contract
System.Data.Services.IRequestHandler? A contract is the interface that a
service implements. In our case the service is MyDataService which to remind
you is defined as follows:
So this begs the
question: why the endpoint contract is not DataService instead of
System.Data.Services.IRequestHandler? To answer this question you need to
understand what IRequestHandler is. In order to so I will use reflector to show
you what’s inside this interface:
<o:p>
Now examining the contents of IRequestHandler shows this:
The handler is itself defined as a service contract. It
contains one generic method which accepts a generic message stream and returns
a Message type which is WCF’s generic message representation. This method is
defined is tagged as a WCF operation and is tagged with WebInvokeAttribute
which accepts any UriTemplate and any Method. If you’re new to .NET REST
programming you will be like: what the heck did he just say?
Ok so prior to WCF Data Services, if you wanted to create
REST-based WCF services you would have to use REST starter kit. When doing so
you have to define by hand all what WCF Data Service abstracts away. One of the
things you had to do is to tag any operation that you want to expose via REST
by one of two attributes:
- WebGetAttribute: used to
expose an operation using Get Http verb. Can be used only to retrieve data.
- WebInvokeAttribute: used to
expose an operation via any required Http verb. It defaults to POST but using
the Method parameter you can specify any required verb. So WebInvokeAttribute
can be used for both data updates and retrieval (note: it’s only a common
practice to use WebGet for retrieves and WebInvoke for updated to stay
consistent with Http specification, however, when WebInvoke(Method=”GET”) is
used the metadata generated mimics those of WebGet)
<o:p>
<o:p>
UriTemplate allows you to specify the REST URI for a
method. Recall that in REST, each resource can be specified using a unique URI.
UriTemplate is the way to do so for a particular WCF operation – which in this
case is the resource.
One final thing; in reflector examine the Derived Types
of the handler:
You will notice that DataService<T> (i.e.
DataService of generic type T) implements the interface. And recall one more
time that our service “MyDataService” inherits from DataService<T>.
<o:p>
So to summarize, IRequestHandler is WCF Data Service’s
generic handler for all operations defined within the service. This abstracts
away the need for us to define what we used to define in the REST starter kit
by hand.
Finally, browse the service using the new URL:
You can now navigate the service as usual, for example:<o:p>
Enabling JSON Format through WCF Message Inspector
When I showed you the demo service provided by the
odata.org web site, I was able to use the “format” keyword to display data
using JSON format instead of the default ATOM format. You might be tempted then
to apply the same technique on our created service, for example:<o:p>
http://localhost:19865/MyDataService.svc/REST/Students?&$format=json
<o:p>
However, doing so will not give you want you’d expected.
Instead you will get:
The reason you got this response is that out of the box
(at the time of this writing), WCF Data Services does not provide a way to implicitly
tell your service to expose data using JSON format.
What we need is a way to intercept client requests and explicitly
set the Accept header to “application/json” so WCF Data Service runtime knows
to generate JSON responses instead of ATOM ones. Basically there are two ways
to do so, either via Http Module or WCF Message Inspectors. I prefer the second
approach.
To fully understand WCF channel layer and extension
points I advise you to read my post here:<o:p>
http://thedotnethub.blogspot.com/2012/01/creating-soaprest-based-wcf-service.html
<o:p>
In a nutshell, WCF provides extension points where you
can intercept requests both at client and service sides. In this example I will
create an extension point at the service side to add the required JSON header.
Step1: Create the Message Inspector and Behavior
Extension<o:p>
Here is the code:
public class JSONMessageInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
{
HttpRequestMessageProperty requestMessage = request.Properties["httpRequest"] as HttpRequestMessageProperty;
UriTemplateMatch jsonMatch = (UriTemplateMatch)request.Properties["UriTemplateMatchResults"];
if (jsonMatch.QueryParameters["$format"] != null && jsonMatch.QueryParameters["$format"].ToLower() == "json")
{
jsonMatch.QueryParameters.Remove("$format");
requestMessage.Headers["Accept"] = "application/json;odata=light,application/json;odata=verbose";
} }
public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { }
}
public class JSONEndpointBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { }
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { }
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new JSONMessageInspector());
}
public void Validate(ServiceEndpoint endpoint) { }
}
public class JSONBehaviorExtensionElement : BehaviorExtensionElement
{
public JSONBehaviorExtensionElement() { }
public override Type BehaviorType
{
get { return typeof(JSONEndpointBehavior); }
}
protected override object CreateBehavior()
{ return new JSONEndpointBehavior(); }
}
The AfterReceiveRequest method contains the important
logic. It inspects the incoming request and checks if $format is set to “json”.
If so it forces the accept header to “application/json” to instruct WCF to
return JSON formatted response, and it removes $format from the list of query
parameters in order to prevent the “The query parameter '$format' begins with a
system-reserved '$' character but is not recognized” problem described earlier
from appearing.
<o:p>
Again, refer to my post to understand the mechanics
behind this as I want to keep the article focused on OData rather than WCF
architecture.
Step2: Register the
Extension point in web.config
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="messageInspector" type="JSONBehaviorExtensionElement, TestOData, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
<behaviors>
<endpointBehaviors>
<behavior name="JSONBehavior">
<messageInspector/>
<webHttp helpEnabled="true" />
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service name="MyDataService">
<endpoint binding="webHttpBinding" contract="System.Data.Services.IRequestHandler" address="REST" behaviorConfiguration="JSONBehavior"></endpoint>
</service>
</services>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
</system.serviceModel>
Now run the application and browse to http://localhost:19865/MyDataService.svc/REST/Students.
You will see an ATOM formatted response:
<o:p>
<o:p>
Now supply instruct the application into using the JSON
format by using http://localhost:19865/MyDataService.svc/REST/Students?&$format=json.
You will get the JSON formatted response:
What about JSON light?
As explained before, JSON light is a new direction by OData
in place of verbose JSON. Review section “Formatting” for more details.<o:p>
Back to the previous section, and there is one bit of
detail that I missed. Examine this line again:<o:p>
requestMessage.Headers["Accept"] =
"application/json;odata=light,application/json;odata=verbose";
Why did not I simply use: requestMessage.Headers["Accept"]
= "application/json”?
The answer resides in class MyDataService.svc.cs, in the
following line:<o:p>
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V3;<o:p> </o:p>
V3 indicates that I am using OData V3 which defaults to
JSON light when setting Accept header to “application/json”. To see this in
action, edit the accept header in the message inspector to read:<o:p>
requestMessage.Headers["Accept"] =
"application/json";
<o:p>
Now browse to http://localhost:19865/MyDataService.svc/REST/Students?&$format=json:
The reason I get “Unsupported media type requested” is
that my client (WCF Client Library client 5.0) does not yet support JSON light.<o:p>
Now edit the MxProtocolVersion in MyDataService.svc.cs to
read:<o:p>
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V3;
<o:p>
Browse again to http://localhost:19865/MyDataService.svc/REST/Students?&$format=json:
This time JSON format was rendered because we are using
OData V2 which defaults to verbose JSON.
<o:p>
This is why when using DataServiceProtocolVersion.V3, I
set the accept header to "application/json;odata=light,application/json;odata=verbose";
where verbose JSON acts a fall back option in case the client does not yet
support JSON light.
JSON Light in WCF Data Services 5.2.0
The latest version of Data Services 5.2.0 (both service
and client) are released just couple of days ago. Let’s use NuGet to install
both packages:
Version 5.2.0 contains full support for JSON light. As
you recall from the previous section “What about JSON light?”, back then I used
WCF Data Services client V5.0 which had no support for JSON light. I then used
a custom Http header to support verbose JSON as a fall back option when the
client does not support JSON light.
<o:p>
Now after updating to V5.2.0 lets remove the message
inspector:
<o:p>
Now browse to http://localhost/TestOdata/MyDataService.svc/REST/Courses?&$format=json.
You will see JSON light returned without even emitting a custom header:
<o:p>
This makes JSON light a first class citizen in V5.2.0.
Extending Data Service with Service Operations and Interceptors
As we have seen, WCF Data Services exposes data as entities
for clients. However, sometimes you want to expose data shaped by come custom
logic; for example an entity which is the result of a merge of other entities.
Service operations allow you to expose custom operations to your clients.<o:p>
You might also want to intercept requests and perform
some logic prior to these requests being services by WCF Data Service.
Interceptors come into play in this scenario.
Service Operations<o:p>
<o:p>
To add a service operation to our example, simply add the
following method to class MyDataService:
[WebGet]
public IQueryable<Student> GetCustomLogicStudents()
{
return (new List<Student>()
{
new Student { NationalID = "01", Name = "Mohamad", Courses = null}
}).AsQueryable();
}
Note that you need to apply the WebGetAttribute to this operation.
Similarly you could’ve applied WeInvokeAttribute for service operations which
perform updates (see the “Behind the Scenes” section for more information about
WebGet and WebInvoke).
You also need to access to the new service operation as
follows:<o:p>
config.SetServiceOperationAccessRule("GetCustomLogicStudents",
ServiceOperationRights.All);
<o:p>
Now browse to http://localhost:19865/MyDataService.svc/REST/GetCustomLogicStudents
to see the service operation in action.
Query Interceptors<o:p>
To add a query interceptor to prevent course of id 1 to be
returned, add the following method to class MyDataService:
[QueryInterceptor("Courses")]
public
Expression<Func<Course, bool>> CoursesQuery()
{
return
p => p.CourseID != "1";
}
<o:p>
<o:p>
<o:p>
<o:p>
Now if you query
for http://localhost:19865/MyDataService.svc/REST/Courses,
you will see that course of id 1 will not be returned.
Metadata, EDM, and CSDL
Back in the “Keywords” section I mentioned that you can
use “$metadata” to generate the metadata of a WCF Data Service. Let’s do that
for our sample service:
The Schema element under the Edmx and DataServices
elements is the Conceptual Schema Definition Language (CSDL) specification.
CSDL defines the data model of OData which is none other than the Entity Data
Model (EDM) of Microsoft which is a derivative of the Entity Relationship
model. This exposes CSDL data as a set of entities and associations
(relationships) between them; which we have been working with since the start
of the article.<o:p>
Yep I know what you’re thinking, that’s the same edmx of
Entity Framework; however, this has nothing to do with Entity Framework in this
context.<o:p>
It’s the CSDL that VS 2012 Add Service Reference dialog
uses to generate the “Reference.cs” file which is the strongly typed client
proxy.
<o:p>
Let’s go ahead and use VS to add a service reference to
our sample service. Using CSDL the dialog was able to strongly-type identify
the classes that should be generated:
If you examine the generated Referece.cs file you will
see that the class generated is of name “MyDataService” which is the value of the
“Namespace” attribute of the CSDL:
Concurrency Updates with ETAG
Consider this scenario: a client application is calling into
our WCF Data Service. It first retrieves a certain Course object, updates it,
and then submit the changes back to the service. Now assume in between the
moments the client retrieves the Course object and just before it submits the
changes, the Course object back at the service was updated. This causes a
concurrency issue because the client when performing the update possibly took
the decision based on the last Course object it got from the service; however,
this object has changed without the client ever knowing it. This can cause all
sorts of business issues.<o:p>
There are two ways you deal with concurrency; either be
optimistic or pessimistic about it. Being optimistic is like saying “ok let’s
not bother with concurrency for now, let the client update the Customer object
and if something wring happens then we worry about it”. Being pessimistic
however is being overly protective, it’s like saying “better safe than sorry. I
want to prevent the client from updating the Course object if the value was
changed back at the service”.<o:p>
ETAGs provide the mechanism to check for concurrency issues
when you want to being pessimistic about it.<o:p>
In order to see this in action, there are couple of
updates that I will make to our Course class. Here is the full code:
<o:p>
<o:p>
[DataServiceKey("CourseID")]
[DataServiceEntity]
[ETag("CourseNum")]
public class Course
{
public decimal CourseID { get; set; }
public int CourseNum { get; set; }
public static List<Course> GetCourses()
{
return (new List<Course>()
{
new Course() { CourseID=1, CourseNum=2},
new Course() { CourseID=2, CourseNum=1}
});
}
}
public class MyDataSource:IUpdatable
{
static List<Course> courses;
static List<Student> students;
static MyDataSource()
{
courses = Course.GetCourses();
students = Student.GetStudents();
}
public IQueryable<Course> Courses
{
get { return (courses.AsQueryable()); }
}
public IQueryable<Student> Students
{
get { return (students.AsQueryable()); }
}
#region IUpdatable
public object CreateResource(string containerName, string fullTypeName)
{
throw new NotImplementedException();
}
public void DeleteResource(object targetResource)
{
throw new NotImplementedException();
}
public object GetResource(IQueryable query, string fullTypeName)
{
object result = null;
var enumerator = query.GetEnumerator();
while (enumerator.MoveNext())
{
if (enumerator.Current != null)
{
result = enumerator.Current;
break;
}
}
if (fullTypeName != null && !fullTypeName.Equals(result.GetType().FullName))
{
throw new DataServiceException();
}
object resultClone = result;
return resultClone;
}
public object GetValue(object targetResource, string propertyName)
{
var targetType = targetResource.GetType();
var targetProperty = targetType.GetProperty(propertyName);
return targetProperty.GetValue(targetResource, null);
}
public void SetValue(object targetResource,
string propertyName, object propertyValue)
{
Type targetType = targetResource.GetType();
PropertyInfo property = targetType.GetProperty(propertyName);
property.SetValue(targetResource, propertyValue, null);
}
public object ResolveResource(object resource)
{
return resource;
}
public void SaveChanges()
{
}
public void SetReference(object targetResource,
string propertyName, object propertyValue)
{
throw new NotImplementedException();
}
public object ResetResource(object resource)
{
throw new NotImplementedException();
}
public void ClearChanges()
{
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();
}
#endregion
}
First thing to notice is that I decorated class Course
with an ETAG attribute and I set CourseNum as the value to monitor (more about this
in a moment).<o:p>
Next I made my custom data source implement IUpdatable.
This is required for my data source to accept updates from clients (if the
client only wanted to query my service, then no updates on my data source would
have been required).
Now create a Console application and a service reference
to service:<o:p>
http://localhost/TestOdata/MyDataService.svc/REST/
<o:p>
Write the following code:
MyDataSource proxy = new MyDataSource(new Uri("http://localhost/TestOdata/MyDataService.svc/REST/"));
Course course = (from c in proxy.Courses
where c.CourseID == 1
select c).First();
course.CourseID = 2;
proxy.UpdateObject(course);
DataServiceResponse res = proxy.SaveChanges();
The code first retrieves
a certain Course object, updates it, and send the updates back to the service.
Run the client and examine Fiddler:
Here you see that in the request the “If-Match” header is
saying that it will check if the ETAG value is 2. The response actually comes
with ETAG value of 2, so the invocation succeeds.<o:p>
Now let’s do a trick. Go back to the code of class
MyDataSource and uncomment the following line:<o:p>
//((Course)resultClone).CourseNum = 100;<o:p>
In this line I am hardcoding a change to the CourseNum
property (since I do not have a persistent medium such as EF). This way, the object
that the client will try to update will not be the same as that it first retrieved.
<o:p>
Run the client again. Examine Fiddler:
<o:p>
This time the invocation fails with an exception “the
etag value in the request header does not match with the current etag value of
the object”.
Resources
http://www.odata.org/<o:p>
WCF Web Http Programming Model:<o:p>
http://msdn.microsoft.com/en-us/library/bb412169.aspx
WCF Data Services 5.0 on MSDN<o:p>
http://msdn.microsoft.com/en-us/library/hh487257(v=vs.103).aspx<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>
<o:p>