Uploading Data to a Service Operation

When you upload data using HTTP, you typically include the data being uploaded in the body of the POST request. However, service operations in the Open Data Protocol (OData) work a bit differently, in that input to a service operation may only be passed to the service operation by using parameters. Consider a service operation that takes an entity as an input, creates a property-wise clone of this entity in the data source, and returns the cloned entity. This scenario requires us to actually upload entity data (and not just something simple, like an integer). In this this post, I will work through just this sort of service operation.

GET Versus POST

Web programmers know that, in HTTP, the GET method is used to request data from the Web server and the POST method is used to submit data, so uploading data by using a POST makes sense. The OData spec also seems to come down in favor of POST for uploading data to a service operation:

“Server implementations should only allow invocation of Service Operations through GET when the operations will not cause side-effects in the system.”

It seems to me that scenarios where you are uploading data to a service operation, there is likely going to be chance of creating a “side-effect” in the system, which I take to mean something that affects the results of queries, such as CUD operation—the stuff for which one typically uses POST. However, as I will demonstrate, we can’t actually use POST requests as OData wants us to because of limitations in our client libraries. The primary limitation is that the WCF Data Services client (designed specifically for OData) doesn’t support sending POST requests to OData service operations, as I described in my previous post Calling Service Operations Using POST. This pretty much leaves us with using GET to upload data.

Use Parameters to Pass Data

Because, OData requires that data sent to service operation be included as parameters in the query URI, the data service ignores any data in the body of the request. This is the case with either GET or POST requests. Not only that, WCF Data Services provides us no way to even get to this message body data should you choose to send it, short of implementing the IDispatchMessageInspector interface to intercept the message before it gets handed off to the data service. That means that if we want to pass data to a service operation, we need to do so by supplying this data as parameters to the service operation, as prescribed by OData.

Serialize Non-Primitive Types

Passing data to a service operation works great for things like integers, strings, and Booleans, since service operation parameters must be primitive type data (as per spec). But as we have already illustrated, we might need to supply more complex data types to your service operation, such as entities or graphs of entities. Consider the following service operation CloneCustomer, which clones a client-supplied Customer entity and then returns the new entity:

[WebGet]
public Customer CloneCustomer(string serializedCustomer)
{
NorthwindEntities context = this.CurrentDataSource;

XmlSerializer xmlSerializer =
new System.Xml.Serialization.XmlSerializer(typeof(Customer));

    TextReader reader = new StringReader(serializedCustomer);

    // Get a customer created with a property-wise clone
// of the supplied entity, with a new ID.
Customer clone = CloneCustomer(xmlSerializer.Deserialize(reader) as Customer);

    try
{
// Note that this bypasses the service ops restrictions.
context.AddToCustomers(clone);
context.SaveChanges();
}
catch (Exception ex)
{
throw new DataServiceException(
"The Customer could not be cloned.", ex.GetBaseException());
}
return clone;
}

Security note: Using a service operation to upload entity data bypasses the built-in entity set access rules, which are used to restrict the ability of clients to do things like insert data. Because an operation like this one may act on the data source directly, you must make sure to implement your own authorization checks on the operation and not rely on entity set or service operation access settings in the data service configuration.

Notice that when the client calls this service operation, the Customer entity to clone is passed to the method as a string, which is an XML serialized representation of the Customer entity object. In this example, a client that can call CloneCustomer is able to insert new entities into the data source, which is often a restricted privilege).

Batch Requests with Large Parameters

You can see that this serialization of uploaded data can easily end up creating some very long URIs that are going to get truncated by most Web servers, which limit request URIs. This is the main reason why we must use the WCF Data Services client (with its lack of POST support) instead of something like HttpWebRequest. The batching functionality provided by OData lets us “package” multiple requests (or in this case just one request), which may have long URIs, into a single request to the  $batch endpoint of the data service. This batching functionality is only available to us by using an OData-aware client.

The following code on the client uses an XmlSerializer to serialize a Customer object, which is supplied to the CloneCustomer service operation by using a typed DataServiceRequest sent in a batch:

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

Customer clonedCustomer = null;

// Get a Customer entity.
var customer = (from cust in context.Customers
select cust).FirstOrDefault();

// Serialize the Customer object.
XmlSerializer xmlSerializer =
new System.Xml.Serialization.XmlSerializer(typeof(Customer));
TextWriter writer = new StringWriter();
xmlSerializer.Serialize(writer, customer);
var serializedCustomer = writer.ToString();

// Define the URI-based data service request.
DataServiceRequest<Customer> request =
new DataServiceRequest<Customer>(new
Uri(string.Format("CloneCustomer?serializedCustomer='{0}'",
serializedCustomer), UriKind.Relative));

// Batch the request so we don't have trouble with the long URI.
DataServiceRequest[] batchRequest = new DataServiceRequest[] { request };

// Define a QueryOperationResponse.
QueryOperationResponse response;

try
{
// Execute the batch query and get the response--
// there should be only one.
response = context.ExecuteBatch(batchRequest)
.FirstOrDefault() as QueryOperationResponse;

    if (response != null)
{
// Get the returned customer from the QueryOperationResponse,
// which is tracked by the DataServiceContext.
clonedCustomer = response.OfType().FirstOrDefault();

// Do something with the cloned customer.
clonedCustomer.ContactName = "Joe Contact Name"; context.SaveChanges();
}
else
{
throw new ApplicationException("Unexpected response type.");
}
}
catch (DataServiceQueryException ex)
{
QueryOperationResponse error = ex.Response;

    Console.WriteLine(error.Error.Message);
}
catch (DataServiceRequestException ex)
{
Console.WriteLine(ex.GetBaseException());
}

And here’s what the batched HTTP request looks like that gets sent to to the CloneCustomer operation. The batch request is a POST to the $batch endpoint, and the body of the request contains the GET request with a very long URI that is the serialized representation of a customer object:

POST https://myserver/Northwind/Northwind.svc/$batch HTTP/1.1
User-Agent: Microsoft ADO.NET Data Services
DataServiceVersion: 1.0;NetFx
MaxDataServiceVersion: 2.0;NetFx
Accept: application/atom+xml,application/xml
Accept-Charset: UTF-8
Content-Type: multipart/mixed; boundary=batch_11d9a5ac-1e92-446d-b6da-22d7e1bbba53
Host: myserver
Content-Length: 1067
Expect: 100-continue

--batch_11d9a5ac-1e92-446d-b6da-22d7e1bbba53
Content-Type: application/http
Content-Transfer-Encoding: binary

GET https://myserver/Northwind/Northwind.svc/CloneCustomer?serializedCustomer='%3C?xml%20version=%221.0%22%20encoding=%22utf-16%22?%3E%0D%0A%3CCustomer%20xmlns:xsd=%22https://www.w3.org/2001/XMLSchema%22%20xmlns:xsi=%22https://www.w3.org/2001/XMLSchema-instance%22%3E%0D%0A%20%20%3CCustomerID%3EAAAAA%3C/CustomerID%3E%0D%0A%20%20%3CCompanyName%3EAlfreds%20Futterkiste%3C/CompanyName%3E%0D%0A%20%20%3CContactName%3EPeter%20Franken%3C/ContactName%3E%0D%0A%20%20%3CContactTitle%3EMarketing%20Manager%3C/ContactTitle%3E%0D%0A%20%20%3CAddress%3EObere%20Str.%2057%3C/Address%3E%0D%0A%20%20%3CCity%3EBerlin%3C/City%3E%0D%0A%20%20%3CPostalCode%3E12209%3C/PostalCode%3E%0D%0A%20%20%3CCountry%3EGermany%3C/Country%3E%0D%0A%20%20%3CPhone%3E089-0877310%3C/Phone%3E%0D%0A%20%20%3CFax%3E089-0877451%3C/Fax%3E%0D%0A%20%20%3COrders%20/%3E%0D%0A%20%20%3CCustomerDemographics%20/%3E%0D%0A%3C/Customer%3E' HTTP/1.1

--batch_11d9a5ac-1e92-446d-b6da-22d7e1bbba53--

Note that the client correctly encodes the serialized entity XML in the URI. The service operation returns the cloned entity in a response body, which is also batched (because the request was batched). We get the first QueryOperationResponse in the batch response, which contains the cloned customer returned by the service operation. This object is already attached to the DataServiceContext (which happened on materialization), so we can make immediate updates and send them back to the data service.

Glenn Gailey
WCF Data Services User Education
Microsoft Corporation