Search code examples
entity-frameworkodataasp.net-web-api2

Nested change sets in a batch payload are not supported : Odata - asp.net web api 2


I have Batch route enabled in config, please refer below code.

namespace WebAPI {
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Services.Replace(typeof(IExceptionHandler), new CustomExceptionHandler());

            config.MapODataServiceRoute(routeName: "OData",
                routePrefix: "",
                model: APIConfiguration.GetModel(),
                batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
        }
} } 

I am using below code to use http client to post a batch.

Dim batchContent As New MultipartContent("mixed", "--testDataBoundary---")

'Parent
Dim RqP As New HttpRequestMessage(HttpMethod.Put, String.Format("{0}Projects({1})/", APIURI, projectRequest.project.ProjectID))
RqP.Content = New StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(projectRequest.project, microsoftDateFormatSettings))
Dim ProjectContent As New HttpMessageContent(RqP)
If ProjectContent.Headers.Contains("Content-Type") Then ProjectContent.Headers.Remove("Content-Type")
ProjectContent.Headers.Add("Content-Type", "multipart/mixed; boundary=--testPTDataBoundary---")
'ProjectContent.Headers.Add("Content-Type", "application/http")
ProjectContent.Headers.Add("Content-Transfer-Encoding", "binary")

If ProjectContent.Headers.Contains("contentTypeMime-Part") Then ProjectContent.Headers.Remove("contentTypeMime-Part")
'ProjectContent.Headers.Add("contentTypeMime-Part", "Content-Type:application/http;")
ProjectContent.Headers.Add("contentTypeMime-Part", "Content-Type:multipart/mixed;")

batchContent.Add(ProjectContent)

'Child1
Dim report As Report = projectRequest.project.Reports.Take(1).First()
Dim RqR As New HttpRequestMessage(HttpMethod.Put, String.Format("{0}Reports({1})/", APIURI, report.ReportID))
RqR.Content = New StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(report, microsoftDateFormatSettings))
Dim ReportContent As New HttpMessageContent(RqR)
If ReportContent.Headers.Contains("Content-Type") Then ReportContent.Headers.Remove("Content-Type")
ReportContent.Headers.Add("Content-Type", "multipart/mixed; boundary=--testPTDataBoundary---")
'ReportContent.Headers.Add("Content-Type", "application/http")
ReportContent.Headers.Add("Content-Transfer-Encoding", "binary")

If ReportContent.Headers.Contains("contentTypeMime-Part") Then ReportContent.Headers.Remove("contentTypeMime-Part")
'ReportContent.Headers.Add("contentTypeMime-Part", "Content-Type:application/http;")
ReportContent.Headers.Add("contentTypeMime-Part", "Content-Type:multipart/mixed;")

batchContent.Add(ReportContent)


Dim batchRequest As HttpRequestMessage = New HttpRequestMessage(HttpMethod.Post, APIURI& "/$batch/")
batchRequest.Content = batchContent

Dim APIResponseBatch = Await Client.SendAsync(batchRequest)
Dim streamProvider = APIResponseBatch.Content.ReadAsMultipartAsync().Result()

For Each cnt As HttpContent In streamProvider.Contents
  lblErrorMsg.Text &= cnt.ReadAsStringAsync().Result
Next

Initially I was getting following exception and then I added contentTypeMime-Part to each request of the batch.

A missing or invalid 'Content-Transfer-Encoding' header was found. The 'Content-Transfer-Encoding' header must be specified for each batch operation, and its value must be 'binary'

I am able to club multiple GETs in one Batch and data is being fetched for all requests. But for post (Parent and Children in my case) I am getting following exeption;

Nested change sets in a batch payload are not supported.

Is this limitation of oData using asp.net web API2 or I am missing something here?


Solution

  • I have finally resolved the issue.

    There were following errors in sequences given below and I continued to add the header and finally I was able to two PUT request in Batch.

    Error 1

    The "Content-Type’ header value "application/http; msgtype=request’ is invalid. When this is the start of the change set, the value must be "multipart/mixed'; otherwise it must be "application/http’.

    Error 2

    The content type "multipart/mixed’ specifies a batch payload; however, the payload either does not include a batch boundary or includes more than one boundary. In OData, batch payload content types must specify exactly one batch boundary in the "boundary’ parameter of the content type.

    Error 3

    Nested change sets in a batch payload are not supported.

    Error 3 took most of the time to find out this URL from MSDN, which speaks about adding following Header Content Types

    client-request-id

    return-client-request-id

    Content-ID

    DataServiceVersion

    After adding above four types to header Error #3 was gone and my Batch was working.

    The URL from MSDN says;

    Add Tasks requests in Batch service support the following concepts:

    Each request must contain a unique Content-ID MIME part header. This header is returned with the result of the operation in the response, and can be used to match an individual request with its response.

    The Batch service returns a HTTP Status Code 400 (Bad Request) if any request contains an invalid set of headers or contains operations which are not supported in the batch.

    The Batch service returns a HTTP Status Code 202 (Accepted) for a valid Add Tasks request. The server will then stream the results of individual operations.

    The Batch service can re-order the responses of these Add Task requests. The Content-Id MIME part header needs to be used by the Client to match the request corresponding to the response. The response contains the results and error information for each operation.If the server times out or the connection is closed during an Add Tasks request, the request may have been partially or fully processed, or not at all. In such cases, the user should re-issue the request. Note that it is up to the users to correctly deal with failures when re-issuing a request. For example, the users should use the same task names during retry, so that if the prior operation succeeded, the retry will not create extra tasks unexpectedly.

    An Add Tasks request can include at most 100 operations.

    All the tasks in an Add Tasks request must belong to the same workitem and

    Code

    Private Async Function SaveProjectAsBatch() As Threading.Tasks.Task
            Using Client As New HttpClient()
                Dim APIhostUri As String = "http://localhost:2025/"
                Dim APIResponse As HttpResponseMessage
                Client.BaseAddress = New Uri(APIhostUri)
                Dim content As HttpContent
    
                'If date serialization is required              
                Dim microsoftDateFormatSettings As JsonSerializerSettings = New JsonSerializerSettings With {
                                    .DateFormatHandling = DateFormatHandling.IsoDateFormat,
                                    .DateParseHandling = DateParseHandling.DateTimeOffset, .DateTimeZoneHandling = DateTimeZoneHandling.Utc
                                    }
    
                ' Batch Process
                Dim boundary As String = "--changeset_ProjectData"
                Dim batchContent As New MultipartContent("mixed", "--batch" & boundary)
    
                'request 1
                Dim RqP As New HttpRequestMessage(HttpMethod.Put, "Url1(key)/")
                RqP.Content = New StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(RequestContent, microsoftDateFormatSettings))
                If RqP.Content.Headers.Contains("Content-Type") Then RqP.Content.Headers.Remove("Content-Type")
                RqP.Content.Headers.Add("Content-Type", "application/json")
                Dim Request1 As New HttpMessageContent(RqP)
                If Request1.Headers.Contains("Content-Type") Then Request1.Headers.Remove("Content-Type")
                Request1.Headers.Add("Content-Type", "application/http")
                Request1.Headers.Add("Content-Transfer-Encoding", "binary")
                Request1.Headers.Add("client-request-id", "12345678")
                Request1.Headers.Add("return-client-request-id", "True")
                Request1.Headers.Add("Content-ID", "1")
                Request1.Headers.Add("DataServiceVersion", "3.0")
                If Request1.Headers.Contains("contentTypeMime-Part") Then Request1.Headers.Remove("contentTypeMime-Part")
                Request1.Headers.Add("contentTypeMime-Part", "Content-Type:application/http;")
    
                batchContent.Add(Request1)
    
                'request 2
                Dim RqR As New HttpRequestMessage(HttpMethod.Put, "Url2(Key)/")
                RqP.Content = New StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(Request1Content, microsoftDateFormatSettings))
                If RqR.Content.Headers.Contains("Content-Type") Then RqR.Content.Headers.Remove("Content-Type")
                RqR.Content.Headers.Add("Content-Type", "application/json")
                Dim Request2 As New HttpMessageContent(RqR)
    
                If Request2.Headers.Contains("Content-Type") Then Request2.Headers.Remove("Content-Type")
                Request2.Headers.Add("Content-Type", "application/http")
                Request2.Headers.Add("Content-Transfer-Encoding", "binary")
                Request2.Headers.Add("client-request-id", "99999")
                Request2.Headers.Add("return-client-request-id", "True")
                Request2.Headers.Add("Content-ID", "2")
                Request2.Headers.Add("DataServiceVersion", "3.0")
    
                If Request2.Headers.Contains("contentTypeMime-Part") Then Request2.Headers.Remove("contentTypeMime-Part")
                Request2.Headers.Add("contentTypeMime-Part", "Content-Type:application/http;")
                batchContent.Add(Request2)
    
    
                Dim batchRequest As HttpRequestMessage = New HttpRequestMessage(HttpMethod.Post, APIhostUri & "/$batch/")
                batchRequest.Content = batchContent
    
    
                Dim APIResponseBatch = Await Client.SendAsync(batchRequest)
                Dim streamProvider = APIResponseBatch.Content.ReadAsMultipartAsync().Result()
    
                For Each cnt As HttpContent In streamProvider.Contents
                    Console.WriteLine(cnt.ReadAsStringAsync().Result)
                Next
    
            End Using
        End Function