如何本地化Json反序列化的异常消息?请求接口时传递了无效参数?

问题描述 投票:0回答:2

我的开发环境是.Net7 WebApi(开箱即用)

下面是相关代码。 DataAnnotations 我已经实现了本地化。

using System.ComponentModel.DataAnnotations;

namespace WebApi.Dtos
{
    public class UserRegistrationDto
    {
        [Required(ErrorMessage = "UserNameRequiredError")]
        [MinLength(6, ErrorMessage = "UserNameMinLengthError")]
        [MaxLength(30, ErrorMessage = "UserNameMaxLengthError")]
        public required string UserName { get; set; }

        [Required(ErrorMessage = "PasswordRequiredError")]
        public required string Password { get; set; }
    }
}
[HttpPost]
public async Task<IActionResult> RegisterUser([FromBody] UserRegistrationDto userRegistration)
{
    return Ok(1);
    // IdentityResult userResult = await _userManager.CreateAsync(new IdentityUser { UserName = userRegistration.UserName }, userRegistration.Password);

    // return userResult.Succeeded ? StatusCode(201) : BadRequest(userResult);
}

当请求正文为无效 JSON 时。

curl -X 'POST' \
  'https://localhost:7177/Authenticate/RegisterUser' \
  -H 'accept: */*' \
  -H 'Api-Version: 1.0' \
  -H 'Content-Type: application/json' \
  -d '{}'
{
    "$": [
        "JSON deserialization for type 'WebApi.Dtos.UserRegistrationDto' was missing required properties, including the following: userName, password"
    ],
    "userRegistration": [
        "The userRegistration field is required."
    ]
}

当请求正文为空时。

curl -X 'POST' \
  'https://localhost:7177/Authenticate/RegisterUser' \
  -H 'accept: */*' \
  -H 'Api-Version: 1.0' \
  -H 'Content-Type: application/json' \
  -d ''
{
    "": [
        "The userRegistration field is required."
    ]
}

绑定DTO之前抛出异常信息,这个异常信息可以本地化吗?如果没有的话,是否可以捕获这些信息进行二次处理,比如返回固定的JSON格式?

我在Program.cs入口文件中尝试过这个,但并不理想。

.ConfigureApiBehaviorOptions(options =>
{
    options.SuppressModelStateInvalidFilter = false;
    options.InvalidModelStateResponseFactory = context =>
    {
        bool knownExceptions = context.ModelState.Values.SelectMany(x => x.Errors).Where(x => x.Exception is JsonException || (x.Exception is null && String.IsNullOrWhiteSpace(x.ErrorMessage) == false)).Count() > 0;
        if (knownExceptions)
        {
            return new BadRequestObjectResult(new { state = false, message = localizer["InvalidParameterError"].Value });
        }
        // ...
        return new BadRequestObjectResult(context.ModelState);
    };
})

我也尝试过这个方法,但是无法捕获单独绑定DTO时失败的异常信息。它们会像上面的写法一样和ErrorMessage异常信息一起出现在DTO中。

.AddControllers(options =>
{
    // options.Filters.Add(...);
    // options.ModelBindingMessageProvider // This doesn't work either, it seems to support [FromForm]
})

回到主题,能本地化吗?或者代码有问题。我不久前刚刚学习.Net。我了解到的信息大部分来自搜索引擎和官方文档。预先感谢。

exception localization asp.net-core-webapi model-binding json-deserialization
2个回答
0
投票

使用以下方法。

.AddControllers(options =>
{
    // options.ModelBindingMessageProvider.Set...
})

看来传递无效参数导致的JSON反序列化异常只能在客户端消除。到目前为止,我似乎还没有找到此异常的本地化,但目前对我来说并不是很重要。 感谢@XinranShen 帮助我指明了正确的方向。


0
投票

我也发现自己处于同样的位置,试图本地化 API 中的所有错误消息,所以我进行了更深入的挖掘。

“类型‘[Type]’的 JSON 反序列化缺少必需的属性,包括以下内容:[Properties]”消息是由反序列化期间在包 System.Text.Json 内引发的 JsonException 导致的 (ThrowHelper.Serialization) .ThrowJsonException_JsonRequiredPropertyMissing(..) ).

不幸的是,该消息的来源是硬编码的,无法更改。此外,使用此 ThrowHelper 方法(ReadStackFrame、ObjectDefaultConverter 等)的类/结构大多是内部的,无法使用依赖注入进行替换,因此无法影响此异常的创建(除了编写自己的 JSON 反序列化,即是)。

该异常最终在 Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter 的 ReadRequestBodyAsync 方法中进行处理,并将其添加到 ModelState 中。 ModelState 随后在 DefaultProblemDetailsFactory 中进行处理。

所以,你能做的最好的事情就是:

  1. 替换 ProblemDetailsFactory 并替换 ModelState 中的消息或
  2. 替换InputFormatter并用你自己的消息抛出另一个JsonException。

对于第一个变体,我们重建 DefaultProblemDetailsFactory 并将其添加到依赖项注入中:

自定义问题详细信息Factory.cs:

/// <inheritdoc /> public sealed class CustomProblemDetailsFactory : ProblemDetailsFactory { private readonly ApiBehaviorOptions _options; private readonly Action<ProblemDetailsContext>? _configure; public CustomProblemDetailsFactory( IOptions<ApiBehaviorOptions> options, IOptions<ProblemDetailsOptions>? problemDetailsOptions = null) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _configure = problemDetailsOptions?.Value?.CustomizeProblemDetails; } /// <inheritdoc /> public override ProblemDetails CreateProblemDetails( HttpContext httpContext, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null) { statusCode ??= 500; var problemDetails = new ProblemDetails { Status = statusCode, Title = title, Type = type, Detail = detail, Instance = instance, }; ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); return problemDetails; } /// <inheritdoc /> public override ValidationProblemDetails CreateValidationProblemDetails( HttpContext httpContext, ModelStateDictionary modelStateDictionary, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null) { ArgumentNullException.ThrowIfNull(modelStateDictionary); statusCode ??= 400; if (modelStateDictionary.ContainsKey("$")) { modelStateDictionary.Remove("$"); modelStateDictionary.TryAddModelError("$", "My error message"); } var problemDetails = new ValidationProblemDetails(modelStateDictionary) { Title = Resources.ErrorMessages.ValidationProblemTitle, Status = statusCode, Type = type, Detail = detail, Instance = instance, }; if (title != null) { problemDetails.Title = title; } ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); return problemDetails; } private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) { problemDetails.Status ??= statusCode; if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) { problemDetails.Title ??= clientErrorData.Title; problemDetails.Type ??= clientErrorData.Link; } var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; if (traceId != null) { problemDetails.Extensions["traceId"] = traceId; } _configure?.Invoke(new() { HttpContext = httpContext!, ProblemDetails = problemDetails }); } }
在您的 Statup/Program.cs 中:

services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>(); services.AddProblemDetails();
错误消息的替换发生在这些行中:

if (modelStateDictionary.ContainsKey("$")) { modelStateDictionary.Remove("$"); modelStateDictionary.TryAddModelError("$", "My error message"); }
对于第 2 个变体

,我们可以将 SystemTextJsonInputFormatter 替换为我们自己的,并自行处理异常。

MyJsonInputFormatter.cs:

public class MyJsonInputFormatter : TextInputFormatter { private readonly SystemTextJsonInputFormatter _baseFormatter; public MyJsonInputFormatter(SystemTextJsonInputFormatter baseFormatter) { _baseFormatter = baseFormatter; foreach (var mediaType in _baseFormatter.SupportedMediaTypes) { SupportedMediaTypes.Add(mediaType); } foreach (var encoding in _baseFormatter.SupportedEncodings) { SupportedEncodings.Add(encoding); } } public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(encoding); var httpContext = context.HttpContext; var (inputStream, usesTranscodingStream) = GetInputStream(httpContext, encoding); try { await JsonSerializer.DeserializeAsync(inputStream, context.ModelType, _baseFormatter.SerializerOptions); } catch (JsonException jsonException) { var path = jsonException.Path ?? string.Empty; var modelStateException = WrapExceptionForModelState(jsonException); context.ModelState.TryAddModelError(path, modelStateException, context.Metadata); return InputFormatterResult.Failure(); } return await _baseFormatter.ReadRequestBodyAsync(context, encoding); } public override IReadOnlyList<string>? GetSupportedContentTypes(string contentType, Type objectType) { return _baseFormatter.GetSupportedContentTypes(contentType, objectType); } private Exception WrapExceptionForModelState(JsonException jsonException) { string message = "My error message"; return new InputFormatterException(message, jsonException); } private static (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding encoding) { if (encoding.CodePage == Encoding.UTF8.CodePage) { return (httpContext.Request.Body, false); } var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true); return (inputStream, true); } }

在Startup/Program.cs中,我们搜索原来的SystemTextJsonInputFormatter并替换为我们自己的:

services.AddControllers(options => { int index; for (index = 0; index < options.InputFormatters.Count; index++) { if (options.InputFormatters[index] is SystemTextJsonInputFormatter) { break; } } if (index >= options.InputFormatters.Count) { throw new ArgumentException($"{nameof(SystemTextJsonInputFormatter)} not found"); } var originalFormatter = options.InputFormatters[index] as SystemTextJsonInputFormatter; options.InputFormatters[index] = new MyJsonInputFormatter(originalFormatter!); })

在这两种情况下,您都可以将错误消息替换为通用错误消息,但不幸的是,除了从原始错误消息中解析它们之外,无法检索原始参数(例如有问题的字段名称或错误原因) .

© www.soinside.com 2019 - 2024. All rights reserved.