2

So we have a middleware that takes the response body of our API responses and wraps it in an ApiResult class under a "data" property.

    namespace Web.Api.ApiResult
{
    public class ApiResultMiddleware
    {
        private readonly RequestDelegate next;

        public ApiResultMiddleware(RequestDelegate next)
        {
            this.next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            var originalBody = context.Response.Body;
            var responseBody = string.Empty;
            try
            {
                CsWebException exception = null;

                using (var stream = new MemoryStream())
                {
                    context.Response.Body = stream;

                    try
                    {
                        await next.Invoke(context);
                    }
                    catch (CsWebException e)
                    {
                        exception = e;
                    }

                    stream.Position = 0;
                    responseBody = new StreamReader(stream).ReadToEnd();
                }

                object result = null;

                if (exception != null)
                {
                    result = new ApiResultResponse(null)
                    {
                        ErrorCode = exception.ErrorCode,
                        ErrorData = exception.ErrorData,
                    };
                    context.Response.StatusCode = (int)ApiResultHttpStatusCodeConverter.ConvertToHttpStatusCode(exception.ErrorCode);
                }
                else
                {
                    var data = JsonConvert.DeserializeObject(responseBody);
                    result = new ApiResultResponse(data);
                }

                var buffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
                    result,
                    new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
                if (context.Response.StatusCode != StatusCodes.Status204NoContent)
                {
                    using (var output = new MemoryStream(buffer))
                    {
                        var test = JsonConvert.DeserializeObject<ApiResultResponse>(new StreamReader(output).ReadToEnd());
                        await output.CopyToAsync(originalBody);
                    }
                }
            }
            catch (JsonReaderException)
            {
                var result = new ApiResultResponse(responseBody);
                var buffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
                    result,
                    new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
                using (var output = new MemoryStream(buffer))
                {
                    await output.CopyToAsync(originalBody);
                }
            }
            finally
            {
                context.Response.Body = originalBody;
            }
        }
    }

    public static class ApiResultMiddlewareExtensions
    {
        public static IApplicationBuilder UseApiResultMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ApiResultMiddleware>();
        }
    }
}

It worked perfectly in .NET core 2.2 and even after migrating to 3.1 and using the build in Sytem.Text.Json but because we need newtonsoft in our patch endpoints. By adding it with

services.AddControllers().AddNewtonsoftJson();

the returned JSON gets cut somewhere in the middle. I added the test variable and deserialized the JSON just before writing it and it looks fine but when parsing i in our front end it's not.

An example is where the json beeing written to the body looks like:

"{\"errorCode\":0,\"errorData\":null,\"data\":{\"previousWorkingDayWorkOrders\":[],\"nextWorkingDayWorkOrders\":[],\"todaysWorkOrders\":[],\"nonInvoicedWorkOrders\":[{\"workOrderId\":1232753.0,\"employeeNumber\":5000000037.0,\"employeeName\":\"VERKADM, VERKADM\",\"vehicleRegistrationNumber\":\"PXG948\",\"dealerOrderNumber\":null,\"bookingNumber\":null,\"preplannedDate\":null,\"workOrderStatus\":7,\"shortageIndicator\":true,\"customerWaiting\":false,\"vehicleDescriptionShort\":\"Volvo V40 Cross Country\",\"vehicleModelYear\":2018,\"colorDescription\":\"Blå\",\"fuelDescription\":\"Diesel\",\"email\":null,\"customer\":{\"customerNumber\":null,\"name\":\"Volvo Bil I Göteborg AB\",\"telephone\":null,\"email\":null,\"customerType\":1},\"mainPayerCustomerType\":1,\"notes\":null,\"vehicleIdentificationNumber\":\"YV1MZ79L0J2139968\"}],\"webWorkOrders\":[]}}"

When receiving the same response in postman it can't be parsed to json because it is cut off and in plan text it looks like:

{"errorCode":0,"errorData":null,"data":{"previousWorkingDayWorkOrders":[],"nextWorkingDayWorkOrders":[],"todaysWorkOrders":[{"workOrderId":1229253.0,"employeeNumber":5000000037.0,"employeeName":"VERKADM, VERKADM","vehicleRegistrationNumber":"PXG948","dealerOrderNumber":null,"bookingNumber":"349","preplannedDate":"2020-02-06T07:00:00","workOrderStatus":5,"shortageIndicator":true,"customerWaiting":false,"vehicleDescriptionShort":"Volvo V40 Cross Country","vehicleModelYear":2018,"colorDescription":"Blå","fuelDescription":"Diesel","email":null,"customer":{"customerNumber":null,"name":"Volvo Bil I Göteborg AB","telephone":null,"email":null,"customerType":1},"mainPayerCustomerType":1,"notes":null,"vehicleIdentificationNumber":"YV1MZ79L0J2139968"}],"nonInvoicedWorkOrders":[{"workOrderId":1232753.0,"employeeNumber":5000000037.0,"employeeName":"VERKADM, VERKADM","vehicleRegistrationNumber":"PXG948","dealerOrderNumber":null,"bookingNumber":null,"preplannedDate":null,"workOrderStatus":7,"shortageIndicator":true,"customerWaiting":false,"vehicleDescriptionShort":"Volvo V40 Cross Country","vehicleModelYear":2018,"colorDescription":"Blå","fuelDescription":"Diesel","email":null,"customer":{"customerNumber":null,"name":"Volvo Bil I Göteborg AB","telephone":null,"email":null,"customerType":1},"mainPayerCustomerType":1,"notes":null,"vehicleIdentificationNumber":"Y

Any idea of why this is not working?

Edit: I should also point out that by removing the middleware app.UseApiResultMiddleware(); everything workes fine but we still want to wrap our responses

Edit 2. I managed to solve this thanks to dbc's response. By setting the length of response content to the length of the buffer it works perfectly.

context.Response.ContentLength = buffer.Length;

What puzzles me is that fact that is was working without setting the length with System.Text.Json and when we used .Net core 2.2

Mithax
  • 21
  • 2
  • welcome! have you tried flushing the response stream? – Daniel A. White Feb 06 '20 at 15:20
  • Thanks. I have tried flushing it but that does not seem to be the issue. – Mithax Feb 06 '20 at 15:30
  • 1
    Just a quick note, you do know that `{{}}` is invalid as json? – Mark Davies Feb 06 '20 at 15:48
  • your json is not valid – Jonathan Alfaro Feb 06 '20 at 17:21
  • Sorry, the json i posted was actually the parsed value of the actual json to an anonymous object parsed in the test variable in the code. I updated to post with the serialized json – Mithax Feb 07 '20 at 08:21
  • The buffer part should be our main focus. Can you play around with Encoder instead of Encoding ? It's recommended for large stream (I know it's not the case here but maybe the Encoding.Utf8.GetBytes() doesn't behave as you expected.) – Alexandre Nourissier Feb 07 '20 at 09:42
  • I have no idea what's going on here. Usually truncated JSON indicates an exception was thrown partway through writing the response, but here it might mean that `await next.Invoke(context);` didn't actually serialize the complete result into the stream; possibly the response is being chunked? See e.g. [Disable chunking in Asp.Net Core](https://stackoverflow.com/q/37966039). – dbc Feb 08 '20 at 17:13
  • But maybe you could consider switching to a different solution -- namely a `JsonConverter` that wraps the root object in a `{"data" : ...}` container? Simply add the converter to the `JsonSerializerSettings` in `AddNewtonsoftJson()` and you should get the same result without the complexity of what you're doing now. See https://dotnetfiddle.net/eGK1en for a mockup fiddle. – dbc Feb 08 '20 at 17:15
  • This was resolved thanks to the thread linked by dbc. Thanks – Mithax Feb 11 '20 at 14:40
  • @Mithax - Then do you need an answer for this, or should it be closed as a duplicate of [Disable chunking in Asp.Net Core](https://stackoverflow.com/q/37966039)? – dbc Feb 17 '20 at 19:56

3 Answers3

2

Other possibility, you did not follow the migration guide to .NET Core 3.0.

Use the Microsoft.AspNetCore.Mvc.NewtonsoftJson package instead of the Newtonsoft.Json package.

  • Yes we did follow the migration guide and we are using the Microsoft.AspNetCore.Mvc.NewtonsoftJson. The parsing of the object works if i remove the line: app.UseApiResultMiddleware();. but then the responses don't get wrapped in out ApiResultResponse class – Mithax Feb 07 '20 at 08:06
0

You added NewtonsoftJson with

services.AddControllers().AddNewtonsoftJson();

but I am not sure that middlewares will be able to access it (never tried this service registration method before).

Did you try to add it using the old one ?

services.AddMvc().AddNewtonsoftJson();

  • 1
    I actually replaced that when migrating. Since 3.0 there's new options for adding MvC scenarios. You can find more info about it here:https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio#mvc-service-registration – Mithax Feb 07 '20 at 08:01
0

I am getting same Issue. but I have got success after adding below lines in middleware service.

 string plainBodyText = await new StreamReader(context.response.Body).ReadToEndAsync();    
context.Response.ContentLength = plainBodyText.Length;