首页 > 解决方案 > ASP.NET MVC Core 3.0 - Why API Request from body keeps returning !ModelState.IsValid?

问题描述

I'm currently using ASP.NET MVC Core 3.0 to create an API project. I was successful to send a POST request without parameter. But currently I'm having a problem when trying to send a POST request with the parameter in JSON via Postman, always getting invalid request as shown below.

enter image description here

Notice that there's also key param in the query string to authorize the request using the middleware I created. This part has no problem.

Here's the code of the controller:

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // POST api/values
    [HttpPost]
    public IActionResult Post([FromBody] UserRequest model)
    {
        if (!ModelState.IsValid)
            return BadRequest(new ApiResponse(400, "Model state is not valid."));

        return Ok($"Hello world, {model.Id}!");
    }
}

The odd thing is, I've already created and used the class UserRequest as a parameter input, as shown below:

public class UserRequest
{
    public string Id { get; set; }
}

Here's my Startup.cs settings, I've already added AddNewtonsoftJson to enable JSON serializer input:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(option => option.EnableEndpointRouting = false)
        .SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
        .AddNewtonsoftJson(opt => opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);

    /*Other API, DB settings and services goes here*/
    ...
}

Here's my attempts so far:

  1. Added [BindProperties] on UserRequest class. Still returning same error.
  2. Removed [FromBody] on the parameter of controller. Still returning same error.
  3. Renamed id to Id to follow the naming inside UserRequest class. Still returning same error.
  4. Added this code on Startup.cs, this will execute return BadRequest(new ApiResponse(400, "Model state is not valid."));:

    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    })
    
  5. Removed this code on Startup.cs

    .AddNewtonsoftJson(opt => opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore)
    

    It will return this instead:

    {
        "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
        "title": "One or more validation errors occurred.",
        "status": 400,
        "traceId": "|f6037d12-44fa46ceaffd3dba.",
        "errors": {
            "$": [
                "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."
            ]
        }
    }
    

Any help would be greatly appreciated.

Updated 12/11/2019: Here's how I handle the API key request:

public async Task Invoke(HttpContext httpContext, IApiKeyService apiKeyService)
{
    var remoteIpAddress = httpContext.Connection.RemoteIpAddress;

    if (httpContext.Request.Path.StartsWithSegments("/api"))
    {
        _logger.LogInformation($"Request from {remoteIpAddress}.");

        var queryString = httpContext.Request.Query;
        queryString.TryGetValue("key", out var keyValue);

        if (keyValue.ToString().Any(char.IsWhiteSpace))
            keyValue = keyValue.ToString().Replace(" ", "+");

        if (httpContext.Request.Method != "POST")
        {
            httpContext.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
            await WriteJsonResponseAsync(httpContext, "Only POST method is allowed.");
            return;
        }

        if (keyValue.Count == 0)
        {
            httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
            await WriteJsonResponseAsync(httpContext, "API Key is missing.");
            return;
        }

        var isKeyValid = await apiKeyService.IsApiKeyValidAsync(keyValue);
        var isKeyActive = await apiKeyService.IsApiKeyActiveAsync(keyValue);

        if (!isKeyValid)
        {
            httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await WriteJsonResponseAsync(httpContext, "Invalid API Key.");
            return;
        }

        if (!isKeyActive)
        {
            httpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
            await WriteJsonResponseAsync(httpContext, "Service is Deactivated.");
            return;
        }
    }
    await _next.Invoke(httpContext);
}

private static async Task WriteJsonResponseAsync(HttpContext httpContext, string message = null)
{
    httpContext.Response.ContentType = "application/json";
    var response = new ApiResponse(httpContext.Response.StatusCode, message);
    var json = JsonConvert.SerializeObject(response);
    await httpContext.Response.WriteAsync(json);
}

标签: c#asp.net-coreasp.net-web-api

解决方案


As discussed in the comments your logging middleware is causing the problem. When you read the request body, or response body, you need to reset the stream so that other middleware can read it (in this case the JsonSerializer).

In your logging middleware you will have a call like:

var body = await new StreamReader(request.Body).ReadToEndAsync();

Before the method returns you need to reset that stream:

request.Body.Seek(0, SeekOrigin.Begin);

This is the same for the response code e.g.

response.Body.Seek(0, SeekOrigin.Begin);

EDIT

As requested in the comments here is an example of what the middleware code might be:

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;

    public LoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableBuffering();
        var body = await new StreamReader(context.Request.Body).ReadToEndAsync();

        // Log the contents of body...

        context.Request.Body.Seek(0, SeekOrigin.Begin);

        await _next(context);
    }
}

The code to reset the Body stream position needs to come before the call to _next(context)


推荐阅读