我的开发环境是.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。我了解到的信息大部分来自搜索引擎和官方文档。预先感谢。
使用以下方法。
.AddControllers(options =>
{
// options.ModelBindingMessageProvider.Set...
})
看来传递无效参数导致的JSON反序列化异常只能在客户端消除。到目前为止,我似乎还没有找到此异常的本地化,但目前对我来说并不是很重要。 感谢@XinranShen 帮助我指明了正确的方向。
我也发现自己处于同样的位置,试图本地化 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 中进行处理。
所以,你能做的最好的事情就是:
对于第一个变体,我们重建 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!);
})
在这两种情况下,您都可以将错误消息替换为通用错误消息,但不幸的是,除了从原始错误消息中解析它们之外,无法检索原始参数(例如有问题的字段名称或错误原因) .