Web API : One side object materialization

By Mirek on (tags: Web API, web service, XML, categories: architecture)

Recently I’ve been working on a api project where the requirement was that any data on the api service side is plain xml, stored in database as xml type column. No binding and object materialization is done on the server. However the client has to operate on clr objects. I am going to show you the solution I came to.

I have chosen the Web API 2 as the service architecture, since it perfectly fits when you want to build REST web services. A total starting point and full library for Web API 2 can be found here.

The requirement was that client application can use a web api service for retrieving and posting data and that data should be materialized into a clr object automatically. Let say the data is the following Product class

public class Product
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime? CreatedOn { get; set; }
    public decimal Price { get; set; }
    public ProductCategory Category { get; set; }
public enum ProductCategory

From the other side the requirement was that web service is independent on the data type it operates on. The web api service retrieves and sends  the plain xml content and stores it in a database. Of course the data is somehow versioned so the service can easily validates the xml based on the data version. This is however not a topic for this post.
On the service side we only have an entity like this

public class XMLContentEntity
    public int Id { get; set; }
    public string Version { get; set; }
    [Column(TypeName = "xml")]
    public string Content { get; set; }
    public XElement ContentXml
        get { return XElement.Parse(Content); }
        set { Content = value.ToString(); }

Now the idea is to use xml type as a message content type, do a normal serialization/deserialization on the client side, but catch the message content on the service side before the binders come to an action and tries to materialize the message content. Let’s see how it can look on a ProductController class

   1: public class ProductController : ApiController
   2:     {
   3:         AppContext db = new AppContext();
   5:         public async Task<IHttpActionResult> Get()
   6:         {
   7:             XElement resultList = new XElement("ArrayOfProduct", (await db.XMLEntities.ToListAsync()).Select(c => c.ContentXml).ToArray());
   8:             var response = new HttpResponseMessage(HttpStatusCode.OK);
   9:             response.Content = new StringContent(resultList.ToString(), Encoding.UTF8, "application/xml");
  10:             return ResponseMessage(response);
  11:         }
  13:         [Route(Name = "GetByIdRoute")]
  14:         public async Task<IHttpActionResult> Get(int id)
  15:         {
  16:             var item = await db.XMLEntities.FindAsync(id);
  17:             if (item == null) return NotFound();
  18:             var response = new HttpResponseMessage(HttpStatusCode.OK);
  19:             response.Content = new StringContent(item.Content, Encoding.UTF8, "application/xml");
  20:             return ResponseMessage(response);
  21:         }
  23:         public async Task<IHttpActionResult> Post(HttpRequestMessage request)
  24:         {
  25:             XElement xml = null;
  26:             try
  27:             {
  28:                 xml = XElement.Parse(await request.Content.ReadAsStringAsync());
  29:             }
  30:             catch (Exception)
  31:             {
  32:                 return BadRequest();
  33:             }
  34:             var item = new XMLContentEntity { Version = "1.0", ContentXml = xml };
  35:             db.XMLEntities.Add(item);
  36:             await db.SaveChangesAsync();
  37:             return CreatedAtRoute("GetByIdRoute", new { id = item.Id }, item.ContentXml);
  38:         }
  40:         public async Task<IHttpActionResult> Delete(int id)
  41:         {
  42:             var item = await db.XMLEntities.FindAsync(id);
  43:             if (item == null) return NotFound();
  44:             db.XMLEntities.Remove(item);
  45:             await db.SaveChangesAsync();
  46:             return Ok();
  47:         }
  48:     }

As you can see we accept a HttpRequestMessage and return HttpResponseMessage, which by default are supported by the Web API engine and passed as action parameters, and operate on the content and the headers of the message. For the GET action (lines 5-11) we simply get the xml strings of items, concatenates them and put into an ArrayOfProduct xml node. This is because the xml serializer on the client side expects it to be an array of client side type element (it expects the ArrayOfProduct which is deserialized into a list of products). Then we construct the response message of it and set the content type to application/xml, so the client knows how to deserialize it. Same happens in get by id action. For POST method (lines 23-38) the body of the message is first parsed as xml document and then new xml entity is created and stored in database.

Keep in mind that for the sake of simplicity above implementation lacks the validation and security check, which should be included in production ready services.

Now we can go to the client side. Create new console application, add references to System.Net.Http and System.Net.Http.Formating and paste below code in Program.cs file

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         MainAsync().Wait();
   6:     }
   8:     static async Task MainAsync()
   9:     {
  10:         //Create HttpClient
  11:         var client = new HttpClient();
  12:         client.BaseAddress = new Uri("http://localhost:55274/api/");
  13:         client.DefaultRequestHeaders.Accept.Clear();
  14:         client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
  16:         //Post Product
  17:         var product = new Product { Name = "Test Product", Price = 234.45M, Category = ProductCategory.Car, CreatedOn = DateTime.Now };
  18:         var response = await client.PostAsync("Product", product, new XmlMediaTypeFormatter { UseXmlSerializer = true });
  19:         if (response.IsSuccessStatusCode)
  20:         {
  21:             // Get the URI of the created resource.
  22:             var location = response.Headers.Location;
  23:             product.Id = int.Parse(location.Segments.Last());
  24:         }
  26:         Debug.Assert(product.Id > 0);
  28:         //Get products
  29:         List<Product> products = null;
  30:         response = await client.GetAsync("Product");
  31:         if (response.IsSuccessStatusCode)
  32:         {
  33:             products = await response.Content.ReadAsAsync<List<Product>>(new[] { new XmlMediaTypeFormatter { UseXmlSerializer = true } });
  34:         }
  36:         Debug.Assert(products != null && products.Count > 0);
  37:     }
  38: }
The code should be self explanatory. One note is only required I think. In lines 18 and 33 we must explicitly say that the XmlSerializer should be used. This is because by default the DataContractSerializer is used by HttpClient which adds some special object type related namespaces to the generated xml. Since on the web service side we want to stay as much independent on the model type as possible we want to avoid such namespaces. 

That’s it. The sample solution can be found in the attachement to this post.