ASP.NET MVC Core 3.0 - 为什么来自主体的 API 请求不断返回 !ModelState.IsValid?

Rey*_*ldi 2 c# asp.net-web-api asp.net-core

我目前正在使用ASP.NET MVC Core 3.0创建一个 API 项目。我成功发送了一个没有参数的 POST 请求。但是目前我在尝试通过 Postman 发送带有 JSON 参数的 POST 请求时遇到问题,总是收到无效请求,如下所示。

在此处输入图片说明

请注意,key查询字符串中还有param 用于使用我创建的中间件授权请求。这部分没有问题。

这是控制器的代码:

[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}!");
    }
}
Run Code Online (Sandbox Code Playgroud)

奇怪的是,我已经创建并使用了类 UserRequest 作为参数输入,如下所示:

public class UserRequest
{
    public string Id { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

这是我的Startup.cs设置,我已经添加AddNewtonsoftJson以启用 JSON 序列化程序输入:

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*/
    ...
}
Run Code Online (Sandbox Code Playgroud)

到目前为止,这是我的尝试:

  1. 添加[BindProperties]UserRequest课堂上。仍然返回相同的错误。
  2. [FromBody]在控制器参数上删除。仍然返回相同的错误。
  3. 重命名idId遵循UserRequest类内部的命名。仍然返回相同的错误。
  4. 在 上添加了此代码Startup.cs,这将执行return BadRequest(new ApiResponse(400, "Model state is not valid."));

    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    })
    
    Run Code Online (Sandbox Code Playgroud)
  5. 删除了此代码 Startup.cs

    .AddNewtonsoftJson(opt => opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore)
    
    Run Code Online (Sandbox Code Playgroud)

    它会返回这个:

    {
        "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."
            ]
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

任何帮助将不胜感激。

2019 年 12 月 11 日更新:以下是我处理 API 密钥请求的方式:

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);
}
Run Code Online (Sandbox Code Playgroud)

Sim*_*Ged 8

正如评论中所讨论的,您的日志记录中间件导致了问题。当您读取请求正文或响应正文时,您需要重置流以便其他中间件可以读取它(在本例中为 JsonSerializer)。

在您的日志中间件中,您将有一个类似的调用:

var body = await new StreamReader(request.Body).ReadToEndAsync();
Run Code Online (Sandbox Code Playgroud)

在该方法返回之前,您需要重置该流:

request.Body.Seek(0, SeekOrigin.Begin);
Run Code Online (Sandbox Code Playgroud)

这与响应代码相同,例如

response.Body.Seek(0, SeekOrigin.Begin);
Run Code Online (Sandbox Code Playgroud)

编辑

根据评论中的要求,这里是中间件代码可能是什么的示例:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

重置Body流位置的代码需要在调用之前出现_next(context)