In his
great blog post about NuGet dependency monitor Mauricio Scheffer devoted a whole section to WCF Data Services criticism. And it’s well deserved criticism. Anyone who used WCF Data Services client for anything more than a simple demo knows what Mauricio is talking about. It’s been about three years since I was exposed to WCF Data Services, and I remember how pleased I was at first to discover that I could send LINQ expressions to WCF Data Services client that was converting them to OData HTTP requests. But I also remember my disappointment over later discoveries: I couldn’t write LINQ expressions the way I was used to when working with SQL databases. I had to carefully think about the LINQ expression syntax, and test, test, test. They all compiled but I never knew for sure if they are not going to explode at runtime, either on a client or a server side.
Read last section of Mauricio Scheffer’s blog post, and you will understand how fragile WCF Data Services LINQ expressions are. If you want to project query result to a single column, you can’t just write “x.Id” – you need to write “new { x.Id }”. Negate a boolean expression – and you will receive a cryptic error message. Swap Where and Select clauses – and previously working statement no longer works. And all these failures have nothing to do with OData protocol restrictions (such as no support for JOINs). They are all limitations of WCF Data Services LINQ expression parser – mostly on a client side, sometimes on a server side.
My own dissatisfaction with WCF Data Services LINQ support was so big that I even wrote my own OData client. It was originally packaged as a Simple.Data OData adapter, adding to a collection of adapters for an excellent micro-ORM developed by Mark Rendle. However, Simple.Data API is modeled around relational databases thus leaving out some less common OData scenarios, in addition I wanted my client to support Mono and non-Windows mobile platforms. So I extracted from Simple.Data OData adapter a platform independent core and packaged it as Simple.OData.Client (internally used by Simple.Data OData adapter). And of course when I read Mauricio’s post, I was immediately provoked to test on Simple.OData.Client commands that WCF Data Services client failed to execute.
Since you are reading this blog post, you may guess that I was pleased with results. Not only Simple.OData.Client supports features not implemented in WCF Data Services, it offers syntax alternatives, and I will further present variations of its fluent interface: typed and dynamic. If you’re already using Simple.Data, you may stick to more familiar Simple.Data OData adapter, although Simple.OData.Client supports more advanced OData protocol features and is packaged as portable class library that can be used on non-Windows platforms, such as iOS and Android. It also gives a choice of either typed or dynamic API (so it effectively offers multiple API flavors). I will show examples using both typed and dynamic Simple.OData.Client syntax, but every example will be originated with its WCF Data Services counterpart. And you can judge yourself which API works better.
Libraries used in comparison
WCF Data Services client
Visual Studio has built-in support for generating OData client access code: simply choose “Add Service Reference…” from project context menu and enter a URL of the existing OData service. You will get proxy code that contains entities (OData service resources), and you will be able to manipulate them using Entity Framework-alike API. Well, as long as you manage to get LINQ statements right. In order to get most recent WCF Data Services client components, install them from NuGet.
Simple.OData.Client
Simple.OData.Client (also available from NuGet) is a portable class library that once has been extracted from Simple.Data OData adapter. It has its own Wiki pages at GitHub, but I believe the easiest way to understand its features is to check its tests. As I mentioned earlier, it can target various platforms including .NET 4.x, Windows Store, Silverlight 5, Windows Phone 8, iOS and Android. If you need to consume OData feeds consisting of interlinked resource sets I believe it provides the simplest yet powerful API with typed and dynamic flavors.
Retrieving data
In .NET world data sources are expected to have LINQ providers, and sometimes LINQ providers are written even for databases that don’t expose its data in tabular or relational form. So it was quite logical for Microsoft developers to offer a LINQ provider for clients of its OData protocol, and this provder has grown from a project “Astoria” to become WCF Data Services. They deliberately made client interface look like to LINQ to Entities, so a simple LINQ statement is indistinguishable from a LINQ to Entities call:
var product = ctx.Products.Where(x => x.ProductName ==
"Chai").Single();
I am using here fluent LINQ syntax, but of course all these statements can be
written using query comprehension. The variable “ctx” represents the data
context that is responsbile for translating commands to HTTP requests
and sending them to an OData service.
As I already mentioned, Simple.OData.Client offers several syntax flavors, we
will focus on two of them: a typed and a dynamic ones. A typed request to an
OData service will be like like this:
var product = client.For<Products>().Filter(x => x.ProductName ==
"Chai").FindEntry();
And here’s a dynamic one:
var x = ODataDynamic.Expression;
var product =
client.For(x.Products).Filter(x.ProductName == "Chai").FindEntry();
Note that when using dynamic API you need first to define an instance of a
dynamic expression object (“x”) that can be used for the rest of the code block
(even in multiple calls) in forms of various expressions that are evaluated at
runtime and converted to parts of OData commands.
Now that we have a basic idea of how to query OData services using different
OData client libraries, let me guide you through some specific examples and show
why WCF Data Services client may lose its attractiveness.
Reason 1: projection to a single column
WCF Data Services
This is a widely used scenario, therefore it comes as a complete surprise that it’s not supported by WCF Data Services. Here’s an example:
var productIDs = ctx.Products.Select(x => x.ProductID);
If you run it it will fail with the following error:
System.NotSupportedException : Individual properties can only be selected from a single resource or as part of a type. Specify a key predicate to restrict the entity set to a single instance or project the property into a named or anonymous type.
“As part of a type”? Let’s select it as a part of an anonymous type. Then it will work:
var productIDs = ctx.Products.Select(x => new { x.ProductID });
Like Mauricio Scheffer, I don’t see any logic in this restriction. LINQ supports single column projections, WCF Data Services server side supports single column projections. This is a pure client side LINQ expression handler limitation which should have been fixed long ago.
Simple.OData.Client (typed)
Here’s the code using that uses typed entities:
var productIDs = client.For<Products>().Select(x => x.ProductID).FindEntries();
Simple.OData.Client (dynamic)
Finally dynamic version of the code:
IEnumerable<dynamic> productIDs = client.For(x.Products).Select(x.ProductID).FindEntries();
Note that we can use “Products” instead of “Product”. Like Simple.Data, Simple.OData.Client has a simple built-in word matcher that accepts both singular, multiple and underscore-separated names and tries to find the matching metadata element.
Reason 2: reduntant query conditions
This is perhaps not a common scenario, however it may occur when LINQ expression is built in a loop, and it’s a perfectly valid LINQ expression. It just contains redundant clause.
WCF Data Services
This code will fail:
var product = ctx.Products.Where(x => x.ProductID == 1 && x.ProductID == 1).Single();
System.NotSupportedException : Multiple key predicates cannot be specified for the same entity set.
Sure you can’t look up an entity using multiple keys. But if the second key clause is identical to the first one, why not simple optimize the expression and remove redundancy? Simple.OData.Client performs it fine.
Simple.OData.Client (typed)
var product = client.For<Product>().Where(x => x.ProductID == 1 && x.ProductID == 1).FindEntry();
Simple.OData.Client (dynamic)
var product = client.For(x.Products).Select(x.ProductID == 1 && x.ProductID == 1).FindEntry();
Reason 3: deceiving LINQ extension methods
WCF Data Services
Let’s have a look at the statement similar to the one used in the previous example:
var product = ctx.Products.Where(x => x.ProductID == 1).Single();
If you have ReSharper installed on your machine, you should a green wavy line under that statement. Hover the mouse, and ReSharper will display a tooltip suggesting replacing Where and Single clauses with just one call: "Replace with single call to Single(...)".
This is a natural code simplification supported by most LINQ providers. Unfortunately LINQ to OData provider doesn’t support such syntax optimization, and if you press Alt+Enter to accept the suggestion and replace the code above with the command
var product = ctx.Products.Single(x => x.ProductID == 1);
… you will get the following error:
System.NotSupportedException : The method 'Single' is not supported.
Too bad. Single, First, Last, SingleOrDefault etc. – all these LINQ extension methods must be applied to the result collection after the query execution completes, you can not send them as a part of a server request. To make the matter worse, you won’t get any warning if you use one of these statements as a server request – quite opposite, as we just saw, you will get a ReSharper notice if you don’t use them. The error will happen later, at runtime.
Simple.OData.Client use different methods to retrieve single and multiple results to ensure server-side result optimization:
Simple.OData.Client (typed)
var products = client.For<Product>().Where(x => x.ProductID == 1).FindEntry();
var products = client.For<Product>().Where(x => x.ProductID == 1).FindEntries();
Simple.OData.Client (dynamic)
var product = client.For(x.Products).Where(x.ProductID == 1).FindEntries();
var product = client.For(x.Products).Where(x.ProductID == 1).FindEntry();
When retrieving multiple results, you can of course append Single/First/Last LINQ extension methods to the statements as you would do when using LINQ provider. This statements will be applied to the result collection after it has been retrieved from the server.
Reason 4: swapping command clauses
There should be no reason why you couldn’t swap Where and Select clauses? Let’s try.
WCF Data Services
var productID = ctx.Products.Where(x => x.ProductID != 1).Select(x => new { ProductID }).First();
The statement above works fine (note that we had to use anonymous type in the Select clause, otherwise it would have failed). Now we’ll apply Where to the projection:
var productID = ctx.Products.Select(x => new { ProductID }).Where(x => x.ProductID != 1).First();
ReSharper immediately comes with the suggestion to merge Where and First into a single clause, but we know it’s going to fail. Unfortunately the whole statement is going to fail anyway:
System.NotSupportedException : The filter query option cannot be specified after the select query option.
Simple.OData.Client doesn’t have this problem:
Simple.OData.Client (typed)
var productID = client.For<Product>().Where(x => x.ProductID != 1).Select(x => x.ProductID).FindEntry();
var productID = client.For<Product.Select(x => x.ProductID)>().Where(x => x.ProductID != 1).FindEntry();
Simple.OData.Client (dynamic)
var productID = client.For(x.Products).Where(x.ProductID != 1).Select(x.ProductID).FindEntry();
var productID = client.For(x.Products.Select(x.ProductID)).Where(x.ProductID != 1).FindEntry();
Reason 5. Expanding related entities
Although OData protocol does not support joins, it has a notion of related entities that can be navigated and expanded. So if Products entities has columns CategoryID referring to Categories entities and there is a Products.Category association defined in service metadata, it should be possible to navigate to Categories from Products via Category association.
WCF Data Services
WCF Data Services LINQ provider has a custom method Expand that can be used for eager loading of related entities. So if we want to expand products with associated categories we might use the following statement:
var products = ctx.Products.Where(x => x.ProductName == "Chai").Expand(x => x.Category);
Nope! It doesn’t even compile. And the reasons for this is custom Expand method applied after Where clause. “Where” is a standard extension method that returns IQueryable, but Expand is defined on DataServiceQuery class that is a part of WCF Data Services, so it can only be applied to generated proxy collection classes (“Products”, “Categories” etc.), and can’t be interchanged with standard LINQ extension methods.
Again, we are facing a problem with swapped LINQ command clauses, now due to use of custom method. Here’s the statement that works:
var products = ctx.Products.Expand(x => x.Category).Where(x => x.ProductName == "Chai");
But what if we want to add a projection clause to this statement and only select the category? Let’s try:
var categories = ctx.Products.Expand(x => x.Category).Where(x => x.ProductName == "Chai").Select(x => x.Category);
Hmm, an error:
System.NotSupportedException : Can only specify query options (orderby, where, take, skip) after last navigation.
Maybe this is because we didn’t use anonymous type in the Select clause? Alright, another attempt:
var categories = ctx.Products.Expand(x => x.Category).Where(x => x.ProductName == "Chai").Select(x => new { x.Category });
Oops, this time the error message is different:
System.NotSupportedException : Cannot create projection while there is an explicit expansion specified on the same query.
But at least it gives us some clue. It’s not permitted to combine Expand and Select clauses. Out of my understanding, but let’s drop Expand then:
var categories = ctx.Products.Where(x => x.ProductName == "Chai").Select(x => new { x.Category });
It worked now, but you just saw how fragile the WCF Data Services LINQ provider. You just have to keep on trying different syntax variations until you find something that won’t fail at runtime.
For eager loading of relations Simple.OData.Client defines a method Expand that we can freely swap with other methods of its fluent interface, including filtering and projection:
Simple.OData.Client (typed)
var product = client.For<Products>().Expand(x => x.Category).Filter(x => x.ProductName == "Chai").FindEntry();
var product = client.For<Products>().Filter(x => x.ProductName == "Chai").Expand(x => x.Category).FindEntry();
var product = client.For<Products>().Expand(x => x.Category).Filter(x => x.ProductName == "Chai")
.Select(x => new { x.ProductID, x.Category.CategoryID }).FindEntry();
Simple.OData.Client (dynamic)
var product = client.For(x.Products).Expand(x.Category).Filter(x.ProductName == "Chai").FindEntry();
var product = client.For(x.Products).Filter(x.ProductName == "Chai").Expand(x.Category).FindEntry();
var product = client.For(x.Products).Expand(x.Category).Filter(x.ProductName == "Chai")
.Select(x.ProductID, x.Category.CategoryID).FindEntry();
Reason 6: navigating to a related entity
In addition to expanding relations OData protocol supports navigation to related entities, so it should be possible to bypass the parent entry and go directly to its relations. There is a constraint on search conditions: the parent entry should be looked up by its key, so the navigation is performed from a single entry.
WCF Data Services
WCF Data Services doesn’t support navigation in a strict sense, but as we saw in the previous section, it’s possible to simulate navigation using projection on a property that represents the relation:
var categories = ctx.Products.Where(x => x.ProductID == 1).Select(x => new { x.Category });
This is not a real navigation to a related entity for two reasons. First, the statement returns a collection of anonymous types, not category objects, so we need to access the actual Category object through a Category property of the anonymous type. Second, the statement generates a different OData command: filter instead of a key lookup. So in principle you can retrieve categories for multiple products, for example by issuing the following command:
var categories = ctx.Products.Where(x => x.ProductName != 1).Select(x => new { x.Category });
Unlike WCF Data Services client, Simple.OData.Client has a proper support for navigation using method NavigateTo:
Simple.OData.Client (typed)
var category = client.For<Product>().Where(x => x.ProductName == "Chai")
.NavigateTo<Category>().FindEntry();
Simple.OData.Client (dynamic)
var category = client.For(x.Product).Where(x.ProductName == "Chai")
.NavigateTo(x.Category).FindEntry();
Reason 7: chained navigation
WCF Data Services
Due to lack of proper navigation support WCF Data Services client can’t be used to fetch results of multi-level navigation. You can only retrieve the information that belongs to an immediate descendant, an attempt to reach deeper levels won’t work:
var product = ctx.Products.Where(x => x.ProductID == 1)
.Select(x => new { x.Category, x.Category.Products })
The above statement will succeed, but product.Category.Products will contain no elements. For the same reason the following statements will both fail with “sequence contains no elements” exception:
var employee = ctx.Employees.Where(x => x.EmployeeID == 14)
.Select(x => new { x.Superior })
.Select(x => new { x.Superior.Superior }).First();
var employees = ctx.Employees.Where(x => x.EmployeeID == 14)
.Select(x => new { x.Superior })
.Select(x => new { x.Superior.Subordinates });
Simple.OData.Client is another story: it fully supports navigation, so all the above scenarios can be easily expressed using both typed and dynamic syntax:
Simple.OData.Client (typed)
var products = client.For<Products>().Filter(x => x.ProductID == 1)
.NavigateTo(x => x.Category)
.NavigateTo(x => x.Products)
.FindEntries();
We can perform deeper navigation, navigating from an employee to a superior of superior:
var employee = client.For<Employees>().Filter(x => x.EmployeeID == 14)
.NavigateTo(x => x.Superior)
.NavigateTo(x => x.Superior)
.FindEntry();
Or we can fetch multiple results if we request subordinates of the superior:
var employees = client.For<Employees>().Filter(x => x.EmployeeID == 14)
.NavigateTo(x => x.Superior)
.NavigateTo(x => x.Subordinates)
.FindEntries();
Simple.OData.Client (dynamic) Here are similar statements for dynamic API:
var products = client.For(x.Products).Filter(x.ProductID == 1)
.NavigateTo(x.Category)
.NavigateTo(x.Products)
.FindEntries();
var employee = client.For(x.Employees).Filter(x.EmployeeID == 14)
.NavigateTo(x.Superior)
.NavigateTo(x.Superior)
.FindEntry();
var employees = client.For(x.Employees).Filter(x.EmployeeID == 14)
.NavigateTo(x.Superior)
.NavigateTo(x.Subordinates)
.FindEntries();
Reason 8: selecting a derived type entity
Let’s have a brief look at rather advanced (and rarely used in OData services) scenario: subclassing OData resource types. Personally I don’t fancy bringing OOP to RESTful (or REST-alike) Web services, but since OData protocol has support for base and derived resource types, it’s fair to examine how our libraries deal with it.
So far I have been using classical Northwind model, but it doesn’t have derived classes. So I extended it with a Transport and Ship entity type: Ship subclasses Transport and add ShipName property. TransportType property on a base class is used as a type discriminator.
WCF Data Services
Selecting a Transport instance without regards to its type is straightforward:
var transport = ctx.Transport.First();
Next is to select only ships:
var ship = ctx.Transport.Where(x => x is Ships).First();
To gain access to ShipName property we need to cast the result:
var shipName = ctx.Transport.Where(x => x is Ships).First() as Ships;
What if we want to search by ship name? This is also possible:
var ship = ctx.Transport.Where(x => x is Ships && (x as Ships).ShipName == "Titanic").First();
From the last example you may imagine that you can cast the results to the derived type using Select clause, so you will get back properly typed results:
var ships = ctx.Transport.Where(x => x is Ships).Select(x => x as Ships);
But this won’t work:
System.NotSupportedException : Unsupported expression '(x As Ships)' in 'Select' method. Expression cannot end with TypeAs.
You always obtain results as a base type and will need to cast them to a derived type on a client side. However, comparing to problems we experienced with other queries, derived type management in WCF Data Services works almost as expected. But let’s check how it is implemented in Simple.OData.Client, and I believe it will become clear which syntax is more intuitive.
Simple.OData.Client (typed)
Simple.OData.Client has a generic method As<T> that redefines the type of entities being processed. Once the cast is performed, all subsequent calls on the fluent chain will be made using that type:
var transports = client.For<Transport>().FindEntries();
var ships = client.For<Transport>().As<Ships>().FindEntries();
var ship = client.For<Transport>().As<Ships>().Filter(x => x.ShipName == "Titanic").FindEntry();
Simple.OData.Client (dynamic)
In a similar manner, dynamic Simple.OData.Client API has a method As, it’s just that this method is not generic and the derived entity type is evaluated based on the dynamic expression parameter:
var transports = client.For(x.Transport).FindEntries();
var ships = client.For(x.Transport).As(x.Ships).FindEntries();
var ship = client.For(x.Transport).As(x.Ships).Filter(x.ShipName == "Titanic").FindEntry();
Modifying data
WCF Data Services client use same methods as Entity Framework to add, update and delete OData resources, it also has a notion of data context, so all operations are queued until the call to SaveChanges and only then are sent to an OData service. Simple.OData.Client doesn’t have a concept of data context (although multiple operations can be packed in batches according to OData protocol), but eventually data modification methods provided by both libraries are mapped to equivalent HTTP requests.
Here’s an example of creating an entry using WCF Data Services client:
var product = new Products { ProductName = "Milk" };
ctx.AddToProducts(product);
ctx.SaveChanges();
And here’s Simple.OData.Client counterparts:
var product = client.For<Products>().Set(new { ProductName = "Milk" }).InsertEntry();
or
var product = client.For<Products>().Set(new Products { ProductName = "Milk" }).InsertEntry();
or
var product = client.For(x.Products).Set(x.ProductName = "Milk").InsertEntry();
The important difference is that in WCF Data Services API a central role is given to the context. It’s a container, a stack of operations and entities. Whatever you intend to do must go via the context, it needs to track all entities prior them to be sent to the server, and it’s developer’s responsibility to ensure the entities are tracked. This is what makes it quite different from Entity Framework that is capable of auto-tracking the affected entities. As long as you are dealing with isolated entities, it shouldn’t be inconvenient. But relationship management has some pitfalls. Let’s have a look at some of them and compare with how this can be done using Simple.OData.Client.
Reason 9: Assigning a linked entity
WCF Data Services
The following code looks reasonable except that it doesn’t work:
var category = ctx.Categories.Where(x => x.CategoryName == "Beverages").Single();
var product = new Products { ProductName = "Chai", Category = category };
ctx.AddToProducts(product);
ctx.SaveChanges();
The code doesn’t fail and the product is created, but it is not linked to a category. Unlike Entity Framework that detects assignments of association properties, the context (“ctx”) needs to be explicitly notified about the intention to link the product to the category. So we need to modify the code, and if you search for available options you will find several choices: AddLink, AddRelatedObject and SetLink. Which one is the right in every particular scenario depends on relation cardinality. In the example above SetLink is the right one, so the code that works looks like this:
var category = ctx.Categories.Where(x => x.CategoryName == "Beverages").Single();
var product = new Products { ProductName = "Chai", Category = category };
ctx.AddToProducts(product);
ctx.SetLink(product, "Category", category);
ctx.SaveChanges();
There are several reasons why I dislike such approach:
- A library could have been smarter to figure out that the object has an association property assigned and take the burden of additional linking code from developers;
- The service metadata has all information about relation cardinality, so there is no need to force developers to be explicit about what kind of relation they are establishing;
- Use of magic strings sucks. WCF Data Services uses lambda expressions in other methods, it should have allowed lambdas here too.
Now let’s see how linking is managed by Simple.OData.Client
Simple.OData.Client (typed)
var category = client.For<Categories>().Filter(x => x.CategoryName == "Beverages").FindEntry();
var product = client.For<Products>().Set(new { ProductName = "Chai", Category = category }).InsertEntry();
Couldn’t be easier, could it? And here’s a dynamic version:
Simple.OData.Client (dynamic)
var category = client.For(x.Categories).Filter(x.CategoryName == "Beverages").FindEntry();
var product = client.(x.Products).Set(new { ProductName = "Chai", Category = category }).InsertEntry();
Reason 10: Detaching a linked entity
WCF Data Services
I guess you are already prepared that detaching a link won’t be easy, and I can assure you it won’t be. In fact it was even more difficult than I thought. No need to say that the following code won’t work:
var product = ctx.Products.Where(x => x.ProductName == "Chai").Single();
product.Category = null;
ctx.UpdateObject(product);
ctx.SaveChanges();
The code doesn’t fail, it just doesn’t affect anything. Again, due to the unaware (or should I say ignorant) context. But having learned about SetLink method in the previous section we might try that:
var product = ctx.Products.Where(x => x.ProductName == "Chai").Single();
ctx.SetLink(product, "Category", null);
ctx.UpdateObject(product);
ctx.SaveChanges();
Nope. Doesn’t work either. The WCF Data Services context doesn’t treat a null association object as an intention to detach the link. But there is a DetachLink method, let’s try that instead:
ctx.DetachLink(product, "Category", category);
No, the relationship is still there. Perhaps DeleteLink?
ctx.DeleteLink(product, "Category", category);
This time the code fails at runtime:
System.InvalidOperationException : AddLink and DeleteLink methods only work when the sourceProperty is a collection.
Let’s change it then so the link is deleted from a category:
ctx.DeleteLink(product, "Category", category);
No, if I reload the product it still shows the linked category. But apparently this is due to the internal cache in the context object. If I clear the context and reload the product the link will disappear. So after some frustrations here is the code that works:
var category = ctx.Categories.Where(x => x.CategoryName == "Beverages").Single();
var product = ctx.Products.Where(x => x.ProductName == "Chai").Single();
ctx.DeleteLink(category, "Products", product);
ctx.UpdateObject(product);
ctx.SaveChanges();
ctx.DetachProduct();
Well, you can judge yourself how intuitive the above code is. And now how the same thing can be achieved using Simple.OData.Client.
Simple.OData.Client (typed)
var product = client.For<Products>().Filter(x => x.ProductName == "Chai").FindEntry();
var category = client.For<Categories>().Filter(x => x.CategoryName == "Beverages").FindEntry();
client.For<Products>().Key(product.ProductID).UnlinkEntry(x => x.Category);
Note use of Key clause. I could use Filter clause instead as long as the filter condition represented a key lookup.
Simple.OData.Client (dynamic)
var product = client.For(x.Products).Filter(x.ProductName == "Chai").FindEntry();
var category = client.For(x.Categories).Filter(x.CategoryName == "Beverages").FindEntry();
client.For(x.Products).Key(product.ProductID).UnlinkEntry(x.Category);
An alternative to using UnlinkEntry method is to set the Category property to null and call UpdateEntry.
Reason 11: Modifying multiple entries
Neither Entity Framework nor WCF Data Services client has built-in support for update of multiple entries based on search condition. OData protocol doesn’t offer this feature either – you have to fetch query results (or at least its key values) and then go through them and update one after another. But this is not uncommon scenario when you need to change or delete all entities matching certain criteria, so Simple.OData.Client offers this functionality.
WCF Data Services
As I said, WCF Data Services client doesn’t expose a method to modify or delete multiple results of a query execution in a single class, so the you will have to do something like this:
foreach (var product in ctx.Products.Where(x => x.ProductName.StartsWith("A"))
{
ctx.DeleteObject(product);
}
ctx.SaveChanges();
Simple.OData.Client offers methods UpdateEntries and DeleteEntries that takes care of querying the OData service and traversing the result set. Below are the examples.
Simple.OData.Client (typed)
client.For<Products>().Filter(x => x.ProductName.StartsWith("A")).DeleteEntries();
Simple.OData.Client (dynamic)
client.For(x.Products).Filter(x.ProductName.StartsWith("A")).DeleteEntries();
Final thoughts
Reason 12: REST is not SOAP
We have gone through several examples that demonstrate obstacles of reading and updating OData feeds with WCF Data Services clients. However there is one more, on a conceptual level. Microsoft has chosen to present OData services to Visual Studio users as if it were old fashioned SOAP services described with its WSDL files. I believe that majority of developers who are first introduced to OData services when they choose “Add Service Reference” from a project menu don’t even realize that they are exposed to REST communication. From within Visual Studio it all looks like a traditional WCF service with a contract and data types. Sure, OData resource types may be looked upon as classes with DataContract attribute, and HTTP verbs GET, POST, PUT/MERGE and DELETE are similar to WCF service operations. But analogy with WCF, SOAP and event Entity Framework is misleading. I’d rather see better exposure of HTTP verbs, so developers have a clear picture of what operations are safe, idempotent or neither of them.
While Simple.OData.Client API doesn’t include methods named after HTTP verbs, it has methods with straightforward mapping to them. FindEntries and FindEntry correspond to GET, they retrieve either all or first matching result. InsertEntry, UpdateEntry and DeleteEntry are mapped to POST, PUT/MERGE and DELETE respectively. LinkEntry and UnlinkEntry provide higher abstraction over calls HTTP verbs to establish or remove relationships between entities. These methods (all named <Verb>Entry or <Verb>Entries) are always final clauses in the chain of Simple.OData.Client fluent interface calls. All but last elements in the call chain incrementally build an OData request, and the final call to <Verb>Entry invokes the corresponding HTTP command. I believe that such API gives a better understanding of underlying HTTP communication and help developers make efficient use of OData protocol.