Calling Service Operations from the WCF Data Services Client

There seems to be a bit of confusion around the support for and usage of OData service operations in the WCF Data Services client library. Service operations are exposed in the data service metadata returned by an OData service. (If you have no idea what I am talking about, read the topic Service Operations [WCF Data Services].) For example, the Netflix OData service exposes several service operations, exposed as FunctionImport elements, which you can see in the service’s EDMX document. Because of this, you would think that the OData tools, such as Add Service Reference, would be able to turn those FunctionImport elements into methods in the generated data service container (which inherits from DataServiceContext). That is not unreasonable to assume, since EntitySet elements are turned into typed DataServiceQuery<T> properties of the data service container class (let’s just call it “context” from now on).

Not getting these nice service operation-based methods on the context is basically the limitation of using service operations in the WCF Data Services client. (This means that you need to use URIs.) Beyond that, you can use the Execute<T> method on the context to call any service operation on your data service, as long as you know the URI. For example, the following client code calls a service operation named GetOrdersByCity, which takes a string parameter of “city” and returns an IQueryable<Order>:

// Define the service operation query parameter.
string city = "London";

// Define the query URI to access the service operation with specific
// query options relative to the service URI.
string queryString = string.Format("GetOrdersByCity?city='{0}'", city)
+ "&$orderby=ShippedDate desc"
+ "&$expand=Order_Details";

// Create the DataServiceContext using the service URI.
NorthwindEntities context = new NorthwindEntities(svcUri2);

try
{
// Execute the service operation that returns all orders for the specified city.
var results = context.Execute<Order>(new Uri(queryString, UriKind.Relative));

    // Write out order information.
foreach (Order o in results)
{
Console.WriteLine(string.Format("Order ID: {0}", o.OrderID));

        foreach (Order_Detail item in o.Order_Details)
{
Console.WriteLine(String.Format("\tItem: {0}, quantity: {1}",
item.ProductID, item.Quantity));
}
}
}
catch (DataServiceQueryException ex)
{
QueryOperationResponse response = ex.Response;

    Console.WriteLine(response.Error.Message);
}

Notice that not only did I get a collection of materialized Order objects returned from this execution, but because the operation returns an IQueryable<T>, I was also able to compose against the service operation in the query URI. Pretty cool, right. Just have to know the URI.

As Shayne points out in his post Service Operations and the WCF Data Services Client, you can also use the CreateQuery<T> method to call service operations, but there are some limitations. Here is the same query as before but using CreateQuery<T> instead of Execute<T>:

// Define the service operation query parameter.
string city = "London";

// Create the DataServiceContext using the service URI.
NorthwindEntities context = new NorthwindEntities(svcUri2);

// Use the CreateQuery method to create a query that accessess
// the service operation passing a single parameter.
var query = context.CreateQuery<Order>("GetOrdersByCity")
.AddQueryOption("city", string.Format("'{0}'", city)).Expand("Order_Details");

try
{
// The query is executed during enumeration.
foreach (Order o in query)
{
Console.WriteLine(string.Format("Order ID: {0}", o.OrderID));

        foreach (Order_Detail item in o.Order_Details)
{
Console.WriteLine(String.Format("\tItem: {0}, quantity: {1}",
item.ProductID, item.Quantity));
}
}
}
catch (DataServiceQueryException ex)
{
QueryOperationResponse response = ex.Response;

    Console.WriteLine(response.Error.Message);

Back to Execute<T>...here’s an example of calling a service operation that only returns a single Order entity (which you can't do using CreateQuery<T>):

// Define the query URI to access the service operation,
// relative to the service URI.
string queryString = "GetNewestOrder";

// Create the DataServiceContext using the service URI.
NorthwindEntities context = new NorthwindEntities(svcUri2);

try
{
// Execute a service operation that returns only the newest single order.
Order order
= (context.Execute<Order>(new Uri(queryString, UriKind.Relative)))
.FirstOrDefault();

    // Write out order information.
Console.WriteLine(string.Format("Order ID: {0}", order.OrderID));
Console.WriteLine(string.Format("Order date: {0}", order.OrderDate));
}
catch (DataServiceQueryException ex)
{
QueryOperationResponse response = ex.Response;

    Console.WriteLine(response.Error.Message);
}

Notice that I needed to call the FirstOrDefault() method to get the execution to return my single Order object (otherwise it doesn’t compile), but it works just fine and I get back a nice fully-loaded Order object.

And we can call a service operation that doesn’t even return an entity, it returns an integer value:

// Define the query URI to access the service operation,
// relative to the service URI.
string queryString = "CountOpenOrders";

// Create the DataServiceContext using the service URI.
NorthwindEntities context = new NorthwindEntities(svcUri2);

try
{
// Execute a service operation that returns the integer
// count of open orders.
int numOrders
= (context.Execute<int>(new Uri(queryString, UriKind.Relative)))
.FirstOrDefault();

    // Write out the number of open orders.
Console.WriteLine(string.Format("Open orders as of {0}: {1}",
DateTime.Today.Date, numOrders));
}
catch (DataServiceQueryException ex)
{
QueryOperationResponse response = ex.Response;

    Console.WriteLine(response.Error.Message);
}

Again, I had to use the FirstOrDefault method (I couldn't think of a good excuse to return an IEnumerable<int> from my Northwind data).

And what about methods that return void? Maybe not the best design on the service end, but we can still do it with our client:

// Define the query URI to access the service operation,
// relative to the service URI.
string queryString = "ReturnsNoData";

// Create the DataServiceContext using the service URI.
NorthwindEntities context = new NorthwindEntities(svcUri2);

try
{
// Execute a service operation that returns void.
string empty
= (context.Execute<string>(new Uri(queryString, UriKind.Relative)))
.FirstOrDefault();
}
catch (DataServiceQueryException ex)
{
QueryOperationResponse response = ex.Response;

    Console.WriteLine(response.Error.Message);
}

Of course, the only proof we have that the void method worked is that no error was returned.

Hopefully, this will convince folks that they can still use the WCF Data Services client library to call service operations, and not resort to that messy WebRequest code to do it. I will also likely add a new service operation topic to the client library documentation, which certainly won’t hurt.

Cheers,

Glenn Gailey