More on the New OData T4 Template: Service Operations

I’ve long missed support for calling service operations by using the proxy client code-generated by WCF Data Services, and I’ve described some workarounds in the post Calling Service Operations from the WCF Data Services Client. This is why I was excited to discover that the new T4 template, which I introduced in my previous post New and Improved T4 Template for OData Client and Local Database, now supports calling service operations as a first class behavior.

For example, the following T4-generated method on the NorthwindEntities context calls a GetOrdersByCity GET service operation:

 public global::System.Data.Services.Client.DataServiceQuery<Order> 
    GetOrdersByCity(global::System.String city)
{ 
    return this.CreateQuery<Order>("GetOrdersByCity")
        .AddQueryOption("city" ,"'"+city+"'");
}

And, here’s another generated method that returns a collection of strings:

 public  System.Collections.Generic.IEnumerable<string> GetCustomerNames()
{ 
    return (System.Collections.Generic.IEnumerable<string>)
        this.Execute<string>(new global::System.Uri("GetCustomerNames",
            global::System.UriKind.Relative),
            Microsoft.Data.OData.HttpMethod.Get,false);
}

Note that these methods use essentially the same techniques described in Calling Service Operations from the WCF Data Services Client.

Before Getting Started with the T4 Template

Before we get started, I should point out that there is currently a bug ( which I reported) in line 868, which can be fixed by changing this line of code as follows:

 parameters=string.Concat(parameters,"global::System." + GetNameFromFullName(p.Type.FullName()));

(You might also want to do a global replace of "refrence" with "reference"--if you care about such things.)

Also, remember that this T4 template requires the current Microsoft WCF Data Services October 2011 CTP release for the upcoming OData release. To install this new T4 template into your project:

  1. Make sure that you have NuGet installed. You can install it from here: https://nuget.org/.
  2. If you haven’t already done so, use the Add Service Reference tool Visual Studio to add a reference to the OData service.
    (The template needs the service.edmx file generated by the tool).
  3. In your project, use the NuGet Package Manager Console to download and install the ODataT4-CS package:
    PM> Install-Package ODataT4-CS
  4. Open the Reference.tt template file and edit the line 868 to fix the bug described above. 
  5. In the Reference.tt template file, change the value of the MetadataFilepath property in the TransformContext constructor to the location of the .edmx file generated by the service reference and update the Namespace property to a namespace that doesn’t collide with the one generated by the service reference.

Now let’s compare the ease of using the client proxy generated by this new T4 template against the examples from the topic Calling Service Operations (WCF Data Services).

Calling a Service Operation that Returns a Collection of Entities

For example, here’s the previously difficult to compose URI-based query to call the GetOrdersByCity operation:

 string queryString = string.Format("GetOrdersByCity?city='{0}'", city)
    + "&$orderby=ShippedDate desc"
    + "&$expand=Order_Details";

var results = context.Execute<Order>(
    new Uri(queryString, UriKind.Relative));

With the new template, this becomes a much nicer, LINQ query:

 var results = from o in context.GetOrdersByCity(city)
                   .Expand("Order_Details")
              orderby o.ShippedDate descending
              select o;

Because this service operation returns an IQueryable<T> collection of entities, it can be further composed against and you get support for all the nice LINQ operations. And, as you would expect with DataServiceQuery<T>, the request to the service operation is made when the result is assigned or enumerated.

Calling a Service Operation that Returns a Single Entity

The benefits of this new client support for service operations are even more evident when calling a service operation that returns a single entity, which used to look like this:

 string queryString = "GetNewestOrder";
Order order = (context.Execute<Order>(
              new Uri(queryString, UriKind.Relative)))
              .FirstOrDefault();

Which can now be simplified to this one clean line of code:

 Order order = context.GetNewestOrder();

Note that in this case, the request is sent when the method is called.

Calling a Service Operation by using POST

It was a pet peeve of many folks that you couldn’t call a POST service operation from the client, even though it’s perfectly legal in OData. In this new version, you can now call DataServiceContext.Execute() and select the request type (GET or POST). This means that the new T4 template enables you to call POST service operations directly from methods:

 // Call a POST service operation that returns a collection of customer names.
IEnumerable<string> customerNames = context.GetCustomerNamesPost();

(Please don’t give me grief over having a POST operation named Get…it’s just for demo purposes.) As before, you can only upload data by using parameters, which is still an OData requirement.

The Remaining Examples

Here’s the new versions of the remaining service operation examples (called synchronously) by using the new service operation methods on the client. In a way, they become almost trivial.

Calling a Service Operation that Returns a Collection of Primitive Types
 var customerNames = context.GetCustomerNames();
Calling a Service Operation that Returns a Single Primitive Type
 int numOrders = context.CountOpenOrders();
Calling a Service Operation that Returns Void
 context.ReturnsNoData();

Calling a Service Operation Asynchronously

OK, so it’s now super easy to synchronously call service operations using this new template-generated proxy client, but what about asynchronous calls? Well, there is a way to do this with the generated proxy, but it only works for service operations that return a collection of entities. In this case, it works because the service operation execution is represented as a DataServiceQuery<T> instance, which has its own async execution methods. For this kind of service operation, you can make the execution work asynchronously, like this:

 public static void GetOrdersByCityAsync()
{
    // Define the service operation query parameter.
    string city = "London";            

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

    // Define a query for orders based on calling GetOrdersByCity.
    var query = (from o in context.GetOrdersByCity(city)
                          .Expand("Order_Details")
                 orderby o.ShippedDate descending
                 select o) as DataServiceQuery<Order>;

     // Asynchronously execute the service operation that returns 
     // all orders for the specified city. 
     query.BeginExecute(OnAsyncExecutionComplete, query);
 }

private static void OnAsyncExecutionComplete(IAsyncResult result)
{
    NorthwindEntities context = new NorthwindEntities(svcUri);
    
    // Get the context back from the stored state.
    var query = result.AsyncState as DataServiceQuery<Order>;            
    
    try
    {
        // Complete the exection and write out the results.
        foreach (Order o in query.EndExecute(result))
        {
            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);
    }
}

Since the other types of execution rely on calling the synchronous DataServiceContext.Execute() method, I’m not sure how I would make this work with the existing APIs—I need to think more about it.

Service Operations Won’t Work on Asynchronous Clients

And don’t even bother trying to make any of this work with the Windows Phone client. First, there isn’t currently a Windows Phone client available that supports OData v3. (In fact, I removed the service operations code altogether from my hybrid T4 template, just to prevent any errors.) While the October CTP did include a Silverlight client, there is a bigger problem. Under the covers, most of the generated methods call DataServiceContext.Execute(), which is a synchronous method that doesn’t exist in the asynchronous clients. This means lots of errors should you encounter a service operation when using the template in a Silverlight app. The problem is that if you leave the T4 template code in place for a Silverlight client, when it comes across a service operation that requires the use of DataServiceContext.Execute(), which doesn’t exist in the Silverlight client, you will get a compiler error.

Just to be clear…on these async clients, only generated service operations methods are broken. The rest of the T4 template code works fine.

I’ve been providing feedback to the author of this excellent new template, so I have high hopes that much of this can be addressed.

Cheers,

Glenn Gailey