Data Transfers With Web API Batching and Protobuf

This article demonstrates how to setup up a Web API service which supports default batching as well as a custom Protobuf batch handler. Protobuf is used because it is the fastest and best serialization library which exists for .NET applications. CRUD operations are supported.

Batching can be used to send multiple requests in a single batch request. This saves HTTP traffic and can be used efficiently for a data synchronization systems.

Code: https://github.com/damienbod/DataTranfersWithWebApiBatching

Standard batching as described on ASP.NET

First a simple batch service is setup using the DefaultHttpBatchHandler class. This works as described in the ASP.NET helps. To use it, just configure it in the Routes in the web API config.

Default setting with JSON

 config.Routes.MapHttpBatchRoute(
  routeName: "WebApiBatch",
  routeTemplate: "api/$batch",
  batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer));

The client can then create a mulitpart request.

using System;
using System.Net.Http;
using System.Net.Http.Formatting;
using Damienbod.Common;

namespace Damienbod.WebAPI.Client
{
    public class DefaultBatchWithJsonAndDefaultBatchHandler
    {
        public static void DoRequest()
        {
            const string baseAddress = "http://localhost:55722";
            var client = new HttpClient();
            var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch")
            {
                Content = new MultipartContent("mixed")
                {
                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/values/2")),
                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/values/3")),
                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/values/4"))
                }
            };

            HttpResponseMessage batchResponse = client.SendAsync(batchRequest).Result;

            MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result;
            foreach (var content in streamProvider.Contents)
            {
                HttpResponseMessage response = content.ReadAsHttpResponseMessageAsync().Result;
                if (response.IsSuccessStatusCode)
                {
                    var p = response.Content.ReadAsAsync<ProtobufModelDto>(new[] { new JsonMediaTypeFormatter() }).Result;
                    Console.WriteLine("{0}\t{1};\t{2}", p.Name, p.StringValue, p.Id);
                }
            }
        }
    }
}

The batch service uses a basic Web API controller for the RESTful service. This is as follows:

using System.Collections.Generic;
using System.Web.Http;
using Damienbod.Common;

namespace Damienbod.WebAPI.Server.Controllers
{
    [RoutePrefix("api/values")]
    public class ValuesController : ApiController
    {
        [HttpGet]
        [Route("{id}")]
        public ProtobufModelDto Get(int id)
        {
            return new ProtobufModelDto() { Id = 1, Name = "HelloWorld", StringValue = "My first Protobuf web api service" };
        }

        [HttpGet]
        [Route("")]
        public IEnumerable<ProtobufModelDto> Get()
        {
            return new ProtobufModelDto[] { new ProtobufModelDto() { Id = 1, Name = "HelloWorld", StringValue = "My first Protobuf web api service" } };
        }

        [HttpPost]
        [Route("")]
        public void Post([FromBody]ProtobufModelDto value)
        {
            var objectToDelete = value;
        }

        [HttpPut]
        [Route("{id}")]
        public void Put(int id, [FromBody]ProtobufModelDto value)
        {
            var objectToDelete = value;
            var idOfDtoToDelete = id;
        }

        [HttpDelete]
        [Route("{id}")]
        public void Delete(int id)
        {
            var idOfDtoToDelete = id;
        }
    }
}

The ProtobufModelDto is used for the service. Protobuf is not required for the above example as it uses JSON for the serialization. If JSON serialization is used, the Protobuf attributes can be removed.

using ProtoBuf;

namespace Damienbod.Common
{
    [ProtoContract]
	public class ProtobufModelDto
	{
            [ProtoMember(1)]
            public int Id { get; set; }
            [ProtoMember(2)]
            public string Name { get; set; }
            [ProtoMember(3)]
            public string StringValue { get; set; }
	}
}

Setting up a Protobuf batch server

To use Protobuf for the batch service, add the webcontrib Protobuf NuGet package to the project.
batchingProtobuf01

The custom batch handler for Protobuf inherits from HttpBatchHandler. This is changed so Protobuf is used for the serialization. This custom batch handler accepts multipart/protobuf content.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Batch;

namespace Damienbod.WebAPI.Server
{
    public class ProtobufBatchHandler : HttpBatchHandler
    {
        private BatchExecutionOrder _executionOrder;
        private const string MultiPartContentSubtype = "mixed";
        private const string ApplicationProtobufSupportedContentType = "application/x-protobuf";

        public ProtobufBatchHandler(HttpServer httpServer) : base(httpServer)
        {
            ExecutionOrder = BatchExecutionOrder.Sequential;
            SupportedContentTypes = new List<string>{ ApplicationProtobufSupportedContentType, "multipart/protobuf" };
        }

         public IList<string> SupportedContentTypes { get; private set; }

        public BatchExecutionOrder ExecutionOrder
        {
            get
            {
                return _executionOrder;
            }
            set
            {
                if (!Enum.IsDefined(typeof (BatchExecutionOrder), value))
                {
                    throw new InvalidEnumArgumentException("value", (int) value, typeof (BatchExecutionOrder));
                }
                _executionOrder = value;
            }
        }

        public override async Task<HttpResponseMessage> ProcessBatchAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (request == null)
            {
               throw new ArgumentNullException("request");
            }

            ValidateRequest(request);

            IList<HttpRequestMessage> subRequests = await ParseBatchRequestsAsync(request, cancellationToken);

            try
            {
                IList<HttpResponseMessage> responses = await ExecuteRequestMessagesAsync(subRequests, cancellationToken);
                return await CreateResponseMessageAsync(responses, request, cancellationToken);
            }
            finally
            {
                foreach (HttpRequestMessage subRequest in subRequests)
                {
                    request.RegisterForDispose(subRequest.GetResourcesForDisposal());
                    request.RegisterForDispose(subRequest);
                }
            }
        }

        public Task<HttpResponseMessage> CreateResponseMessageAsync(IList<HttpResponseMessage> responses, HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (responses == null)
            {
               throw new ArgumentNullException("responses");
            }
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            var batchContent = new MultipartContent(MultiPartContentSubtype);

            foreach (HttpResponseMessage batchResponse in responses)
            {
                batchContent.Add(new HttpMessageContent(batchResponse));
            }

            HttpResponseMessage response = request.CreateResponse();
            response.Content = batchContent;
            return Task.FromResult(response);
        }

        public async Task<IList<HttpResponseMessage>> ExecuteRequestMessagesAsync(
            IEnumerable<HttpRequestMessage> requests, CancellationToken cancellationToken)
        {
            if (requests == null)
            {
                throw new ArgumentNullException("requests");
            }

            var responses = new List<HttpResponseMessage>();

            try
            {
                switch (ExecutionOrder)
                {
                    case BatchExecutionOrder.Sequential:
                        foreach (HttpRequestMessage request in requests)
                        {
                            request.Headers.Add("Accept", ApplicationProtobufSupportedContentType);
                            responses.Add(await Invoker.SendAsync(request, cancellationToken));
                        }
                        break;

                    case BatchExecutionOrder.NonSequential:
                        responses.AddRange(
                            await
                                Task.WhenAll(requests.Select(request => Invoker.SendAsync(request, cancellationToken))));
                        break;
                }
            }
            catch
            {
                foreach (HttpResponseMessage response in responses)
                {
                    if (response != null)
                    {
                        response.Dispose();
                    }
                }
                throw;
            }

            return responses;
        }

        public async Task<IList<HttpRequestMessage>> ParseBatchRequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            var requests = new List<HttpRequestMessage>();
            cancellationToken.ThrowIfCancellationRequested();
            MultipartStreamProvider streamProvider = await request.Content.ReadAsMultipartAsync(cancellationToken);
            foreach (HttpContent httpContent in streamProvider.Contents)
            {
                cancellationToken.ThrowIfCancellationRequested();
                HttpRequestMessage innerRequest = await httpContent.ReadAsHttpRequestMessageAsync(cancellationToken);
                innerRequest.CopyBatchRequestProperties(request);
                requests.Add(innerRequest);
            }
            return requests;
        }

        private void ValidateRequest(HttpRequestMessage request)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            if (request.Content == null)
            {
                throw new HttpResponseException(request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, "BatchRequestMissingContent"));
            }

            MediaTypeHeaderValue contentType = request.Content.Headers.ContentType;
            if (contentType == null)
            {
                throw new HttpResponseException(request.CreateErrorResponse(
                    HttpStatusCode.BadRequest,  "BatchContentTypeMissing"));
            }

            if (!SupportedContentTypes.Contains(contentType.MediaType, StringComparer.OrdinalIgnoreCase))
            {
                throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.BadRequest, string.Format("BatchMediaTypeNotSupported")));
            }
        }
    }
}

Once the custom Protobuf batch handler has been created and added to the project, it can be configured in the global settings.

config.Routes.MapHttpBatchRoute(
                routeName: "WebApiBatch",
                routeTemplate: "api/$batch",
                batchHandler: new ProtobufBatchHandler(GlobalConfiguration.DefaultServer));

The Protobuf batch client code can send GET, POST, DELETE or PUT. These are serialized using Protobuf, sent in a single request, executed on the server and returned to the client, again using Protobuf.

using System;
using System.Net;
using System.Net.Http;
using Damienbod.Common;
using WebApiContrib.Formatting;

namespace Damienbod.WebAPI.Client
{
    public class ProtobufBatch
    {
        public static void DoRequest()
        {
            const string baseAddress = "http://localhost:55722";
            var client = new HttpClient();
            var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch")
            {
                Content = new MultipartContent("protobuf")
                {
                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/values")
                    {
                        Content = new ObjectContent<ProtobufModelDto>(new ProtobufModelDto() { Id = 56, Name = "ClientObjectToCreate", StringValue = "Protobuf object sent in a batch" }, new ProtoBufFormatter())
                    }),

                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/values/2")),

                     new HttpMessageContent(new HttpRequestMessage(HttpMethod.Put, baseAddress + "/api/values/7")
                    {
                        Content = new ObjectContent<ProtobufModelDto>(new ProtobufModelDto() { Id = 56, Name = "ClientObjectToCreate", StringValue = "Protobuf object sent in a batch" }, new ProtoBufFormatter())
                    }),

                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Delete, baseAddress + "/api/values/3")),
                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/values/4"))
                }
            };

            HttpResponseMessage batchResponse = client.SendAsync(batchRequest).Result;

            MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result;
            foreach (var content in streamProvider.Contents)
            {
                HttpResponseMessage response = content.ReadAsHttpResponseMessageAsync().Result;
                if (response.IsSuccessStatusCode)
                {
                    if (response.StatusCode == HttpStatusCode.NoContent)
                    {
                        Console.WriteLine("delete, post or update ok");
                    }
                    else
                    {
                        // Parse the response body. Blocking!
                        var p = response.Content.ReadAsAsync<ProtobufModelDto>(new[] { new ProtoBufFormatter() }).Result;
                        Console.WriteLine("{0}\t{1};\t{2}", p.Name, p.StringValue, p.Id);
                    }
                    
                }
            }
        }
    }
}

Now that the batch service is working for all CRUD operations, a data synchronization system can be built on top of this.

Links:

https://aspnetwebstack.codeplex.com/wikipage?title=Web+API+Request+Batching

http://bradwilson.typepad.com/blog/2012/06/batching-handler-for-web-api.html

http://sravi-kiran.blogspot.in/2014/02/BatchUpdateSupportInAspNetWebApiOData.html

http://sravi-kiran.blogspot.in/2014/02/ConsumingAspNetWebApiODataBatchUpdateFromJavaScript.html?spref=tw

http://blogs.msdn.com/b/webdev/archive/2013/11/01/introducing-batch-support-in-web-api-and-web-api-odata.aspx

http://robtiffany.com/teched-north-america-2014-empower-your-demanding-mobile-line-of-business-apps-with-sqlite-and-offline-data-sync-on-windows-8-1/

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: