Quantcast
Channel: ASP.NET MVC / Web API / Web Pages
Viewing all articles
Browse latest Browse all 7925

Updated Wiki: Web API Request Batching

$
0
0

Overview

Request batching is a useful way of minimizing the number of messages that are passed between the client and the server. This reduces network traffic and provides a smoother, less chatty user interface. This feature will enable Web API users to submit multiple service calls in a single HTTP request.

Design

BatchHandler

This is a custom HttpMessageHandler that will be used to handle the batch requests.The BatchHandler takes two arguments in the constructor: an HttpServer and an IBatchProcessor.

  • HttpServer will be used to process the individual batch requests.
  • IBatchProcessor will be used to parse the request into individual batch requests and submit them to the HttpServer.
namespace System.Web.Http.Batch
{
    publicclass BatchHandler : HttpMessageHandler
    {
        public BatchHandler(HttpServer httpServer, IBatchProcessor batchProcessor);

        public IBatchProcessor BatchProcessor { get; }

        protectedoverride Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
    }
}

IBatchProcessor

This is an abstraction for processing the batch requests.

namespace System.Web.Http.Batch
{
    publicinterface IBatchProcessor
    {
        Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage request, HttpMessageInvoker invoker, CancellationToken cancellationToken);
    }
}

An implementation of IBatchProcessor will do the following:

  • Parse the incoming request into batch requests
  • Execute the batch requests
  • Build the batch response

The idea is to have different IBatchProcessor implementations that understand different batch request/response formats and know how to process them.

Out of the box, we’re providing the DefaultBatchProcessor for Web API Batching

namespace System.Web.Http.Batch
{
    publicclass DefaultBatchProcessor : IBatchProcessor
    {
        publicbool OrderedExecution { get; set; }

        publicvirtual HttpResponseMessage CreateResponseMessage(IList<HttpResponseMessage> responses, HttpRequestMessage request);
        publicvirtual Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage request, HttpMessageInvoker invoker, CancellationToken cancellationToken);
        publicvirtual Task<IList<HttpResponseMessage>> ExecuteRequestMessagesAsync(IEnumerable<HttpRequestMessage> requests, HttpMessageInvoker invoker, CancellationToken cancellationToken);
        publicvirtual Task<IList<HttpRequestMessage>> ParseBatchRequests(HttpRequestMessage request);
        protectedvirtualvoid ValidateRequest(HttpRequestMessage request);
    }
}

And ODataBatchProcessor/ODataUnbufferedBatchProcessor for OData Batching.

namespace System.Web.Http.OData.Batch
{
    publicabstractclass ODataBatchProcessorBase : IBatchProcessor
    {
        public Uri BaseUri { get; set; }
        public ODataMessageQuotas MessageQuotas { get; }

        publicvirtual HttpResponseMessage CreateResponseMessage(IEnumerable<ODataResponseItem> responseMessages, HttpRequestMessage request);
        publicabstract Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage request, HttpMessageInvoker invoker, CancellationToken cancellationToken);
        protectedstatic Uri GetRequestBaseUri(HttpRequestMessage request);
        protectedvirtualvoid ValidateRequest(HttpRequestMessage request);
    }

    publicclass ODataBatchProcessor : ODataBatchProcessorBase
    {
        publicoverride Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage request, HttpMessageInvoker invoker, CancellationToken cancellationToken);
        publicvirtual Task<IList<ODataResponseItem>> ExecuteRequestMessagesAsync(IEnumerable<ODataRequestItem> requests, HttpMessageInvoker invoker, CancellationToken cancellationToken);
        publicvirtual Task<IList<ODataRequestItem>> ParseBatchRequests(HttpRequestMessage request);
    }

    publicclass ODataUnbufferedBatchProcessor : ODataBatchProcessorBase
    {
        publicoverride Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage request, HttpMessageInvoker invoker, CancellationToken cancellationToken);
        publicvirtual Task<ODataResponseItem> ExecuteRequestAsync(ODataBatchReader batchReader, Guid batchId, HttpMessageInvoker invoker, CancellationToken cancellationToken);
    }
}

Web API Batching

The DefaultBatchProcessor supports the following formats. It basically wraps the request/response messages in a multipart content.

Request Format

POST http://localhost:8080/api/$batch HTTP/1.1
Content-Type: multipart/mixed; boundary="91731eeb-d443-4aa6-9816-560a8aca66b1"
Host: localhost:8080
Content-Length: 390
Expect: 100-continue
Connection: Keep-Alive

--91731eeb-d443-4aa6-9816-560a8aca66b1
Content-Type: application/http; msgtype=request

POST /api/values HTTP/1.1
Host: localhost:8080
Content-Type: application/json; charset=utf-8

"my value"
--91731eeb-d443-4aa6-9816-560a8aca66b1
Content-Type: application/http; msgtype=request

GET /api/values HTTP/1.1
Host: localhost:8080


--91731eeb-d443-4aa6-9816-560a8aca66b1--

Response Format

HTTP/1.1 200 OK
Content-Length: 333
Content-Type: multipart/mixed; boundary="5b2a806d-4040-43f0-8f04-7d4c86793fa7"
Server: Microsoft-HTTPAPI/2.0
Date: Mon, 08 Apr 2013 19:00:26 GMT

--5b2a806d-4040-43f0-8f04-7d4c86793fa7
Content-Type: application/http; msgtype=response

HTTP/1.1 202 Accepted


--5b2a806d-4040-43f0-8f04-7d4c86793fa7
Content-Type: application/http; msgtype=response

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

["my value"]
--5b2a806d-4040-43f0-8f04-7d4c86793fa7--

The request can be built using the HttpClient library.

privatestaticvoid CallApiBatch()
{
    string baseAddress = "http://localhost:8080";
    var client = new HttpClient();

    var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch");
    var batchContent = new MultipartContent("mixed");
    batchRequest.Content = batchContent;
    batchContent.Add(new HttpMessageContent(new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/values")
    {
        Content = new ObjectContent<string>("my value", new JsonMediaTypeFormatter())
    }));
    batchContent.Add(new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/values")));
    
    HttpResponseMessage batchResponse = client.SendAsync(batchRequest).Result;

    MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result;
    foreach (var content in streamProvider.Contents)
    {
        HttpResponseMessage response = content.ReadAsHttpResponseMessageAsync().Result;
    }
}

OData Batching

The ODataBatchProcessor/ODataUnbufferedBatchProcessor supports the following formats which are defined by http://www.odata.org/documentation/odata-v3-documentation/batch-processing/.

Request Format

POST /service/$batch HTTP/1.1 
Host: host 
Content-Type: multipart/mixed; boundary=batch_36522ad7-fc75-4b56-8c71-56071383e77b 

--batch_36522ad7-fc75-4b56-8c71-56071383e77b 
Content-Type: multipart/mixed; boundary=changeset_77162fcd-b8da-41ac-a9f8-9357efbbd621 
Content-Length: ###       

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621) 
Content-Type: application/http 
Content-Transfer-Encoding: binary 
Content-ID: 1 

POST /service/Customers HTTP/1.1 
Host: host  
Content-Type: application/atom+xml;type=entry 
Content-Length: ### 

<AtomPubrepresentationofanewCustomer> 

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621) 
Content-Type: application/http 
Content-Transfer-Encoding: binary 

POST $1/Orders HTTP/1.1 
Host: host 
Content-Type: application/atom+xml;type=entry 
Content-Length: ### 

<AtomPubrepresentationofanewOrder> 

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621)-- 
--batch(36522ad7-fc75-4b56-8c71-56071383e77b)--

Response Format

HTTP/1.1 202 Accepted
DataServiceVersion: 1.0
Content-Length: ####
Content-Type: multipart/mixed; boundary=batch(36522ad7-fc75-4b56-8c71-56071383e77b)

--batch(36522ad7-fc75-4b56-8c71-56071383e77b)
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 Ok
Content-Type: application/atom+xml;type=entry
Content-Length: ###

<AtomPubrepresentationoftheCustomerentitywithEntityKeyALFKI>

--batch(36522ad7-fc75-4b56-8c71-56071383e77b)
Content-Type: multipart/mixed; boundary=changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621)
Content-Length: ###      

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621)
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 201 Created
Content-Type: application/atom+xml;type=entry
Location: http://host/service.svc/Customer('POIUY')
Content-Length: ###

<AtomPubrepresentationofanewCustomerentity>

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621)
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 204 No Content
Host: host

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621)--

--batch(36522ad7-fc75-4b56-8c71-56071383e77b)
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 404 Not Found
Content-Type: application/xml
Content-Length: ###

<Errormessage>
--batch(36522ad7-fc75-4b56-8c71-56071383e77b)--

The request can be built using “Add Service Reference” (from WCF Data Services) as well as 3rd party JavaScript libraries such as datajs.

WCF Data Services client

privatestaticvoid CallODataBatch()
{
    string baseAddress = "http://localhost:8080/odata";
    Container container = new Container(new Uri(baseAddress));

    Random rand = new Random();
    int id = rand.Next();
    var customer = new Customer { ID = id, Name = "User"+ id };
    var order = new Order { ID = id, Amount = id + 10 };

    // Batch operation.
    container.AddToCustomers(customer);
    container.AddToOrders(order);
    container.AddLink(customer, "Orders", order);

    var batchResponse = container.SaveChanges(SaveChangesOptions.Batch);

    foreach (var response in batchResponse)
    {
        Console.WriteLine(response.StatusCode);
        Console.WriteLine(response.Headers);
    }
}

datajs client

OData.request({
    requestUri: "/odata/$batch",
    method: "POST",
    data: {
        __batchRequests: [
            { __changeRequests: [
                { requestUri: "/odata/Customers", method: "POST", data: customer }
            ] },
            { requestUri: "/odata/Customers", method: "GET" }
        ]
    }
}, function (data, response) {
    //success handler
}, function () {
    alert("request failed");
}, OData.batchHandler);

Scenarios

Web API Batching

Registering default batch endpoint

You can use MapBatchRoute, which is an HttpRouteCollection extension method, to create a batch endpoint.

config.Routes.MapBatchRoute("apiBatch", "api/batch", GlobalConfiguration.DefaultServer);

Under the hood it just uses MapHttpRoute.

config.Routes.MapHttpRoute("apiBatch", "api/batch", null, null, new BatchHandler(GlobalConfiguration.DefaultServer, new DefaultBatchProcessor()));

Ordered Execution

By default each individual batch request is executed asynchronously. Meaning there’s no guarantee that the first batch request will finish executing before kicking off the second one. So if you have a scenario where you want to get the results after all the posts, you can enable the OrderedExecution which will process the requests in order.

var batchProcessor = new DefaultBatchProcessor();
batchProcessor.OrderedExecution = true;
config.Routes.MapBatchRoute("apiBatch", "api/batch", new BatchHandler(GlobalConfiguration.DefaultServer, batchProcessor));

OData Batching

Registering OData batch endpoint

You can use MapODataBatchRoute, which is an HttpRouteCollection extension method, to create an OData batch endpoint.

config.Routes.MapODataBatchRoute("odataBatch", "odata/$batch", GlobalConfiguration.DefaultServer);

Under the hood it just uses MapHttpRoute.

config.Routes.MapHttpRoute("odataBatch", "odata/$batch", null, null, new BatchHandler(GlobalConfiguration.DefaultServer, new ODataBatchProcessor()));

Configuring Batch Quotas

Work in progress…

var odataBatchProcessor = new ODataBatchProcessor();
odataBatchProcessor.MessageQuotas.MaxOperationsPerChangeset = 10;
odataBatchProcessor.MessageQuotas.MaxPartsPerBatch = 50;
config.Routes.MapBatchRoute("odataBatch", "odata/$batch", new BatchHandler(GlobalConfiguration.DefaultServer, odataBatchProcessor));

Order of Execution

According to the OData spec, the operation/ChangeSet within a batch request is executed in ordered manner. Operations within the ChangeSet can be executed regardless of the order but our implementation will execute them in order for simplicity.

Transaction

Work in progress…

Custom Batching

Work in progress…


Viewing all articles
Browse latest Browse all 7925

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>