我正在将一个应用程序从遗留的asp.net webapi迁移到asp.net核心mvc。我注意到一个问题。对于某些请求,我们在POST正文中发送部分甚至无效的值。而asp.net核心拒绝对其进行反序列化
例如,后模型
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
public enum Category
{
Public,
Personal
}
动作
[HttpPost]
public async Task<Response> Post([FromBody]PostModel model)
=> this.Service.Execute(model);
以下样品请求
POST /endpoint
{
id: 3,
category: "all"
}
ModelState
集合记录了一个错误,指示all
是无效类别,而PostModel
参数model
为null。是否可以禁用这种行为,只尝试绑定帖子正文中所有可能的属性,而忽略无法绑定的属性?这就是在我们的遗留api中为我们所做的,现在,我需要将其移植到各处。
禁用模型验证对我们没有帮助。model
参数仍然为null。
对于FromBody
,它将通过JsonInputFormatter
将request body
绑定到Model
。
对于JsonInputFormatter
,当没有错误时将调用return InputFormatterResult.Success(model)
,当有任何错误时调用return InputFormatterResult.Failure();
。对于return InputFormatterResult.Failure();
,它不会绑定有效的属性。
对于解决方案,您可以实现自定义格式化程序以返回return InputFormatterResult.Success(model)
。
- 基于JsonInputFormatter实现自定义格式化程序
CustomFormatter
-
将
InputFormatterResult.Failure()
替换为InputFormatterResult.Success(model)
。if (!(exception is JsonException || exception is OverflowException)) { var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception); exceptionDispatchInfo.Throw(); } return InputFormatterResult.Success(model);
-
在
Startup.cs
中注入CustomFormatter
services.AddMvc(o => { var serviceProvider = services.BuildServiceProvider(); var customJsonInputFormatter = new CustomFormatter( serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<CustomFormatter>(), serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings, serviceProvider.GetRequiredService<ArrayPool<char>>(), serviceProvider.GetRequiredService<ObjectPoolProvider>(), o, serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value ); o.InputFormatters.Insert(0, customJsonInputFormatter); }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
实际上,您的问题与数据绑定有关,而不是与验证有关,这就是为什么禁用模型验证没有帮助的原因。您可以实现自定义绑定器,并将其配置为手动绑定您的属性,例如:
public class PostModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
string valueFromBody = string.Empty;
using (var sr = new StreamReader(bindingContext.HttpContext.Request.Body))
{
valueFromBody = sr.ReadToEnd();
}
if (string.IsNullOrEmpty(valueFromBody))
{
return Task.CompletedTask;
}
string idString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["id"]).Value);
string categoryString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["category"]).Value);
if (string.IsNullOrEmpty(idString) || !int.TryParse(idString, out int id))
{
return Task.CompletedTask;
}
Category? category = null;
if(Enum.TryParse(categoryString, out Category parsedCategory))
{
category = parsedCategory;
}
bindingContext.Result = ModelBindingResult.Success(new PostModel()
{
Id = id,
Category = category
});
return Task.CompletedTask;
}
}
然后你可以把这个活页夹应用到你的课堂上:
[ModelBinder(BinderType = typeof(PostModelBinder))]
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
或行动:
[HttpPost]
public async Task<Response> Post([ModelBinder(BinderType = typeof(PostModelBinder))][FromBody]PostModel model)
=> this.Service.Execute(model);
或创建CustomModelBinderProvider:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(PostModel))
return new PostModelBinder();
return null;
}
}
并将其注册到Startup类的ConfigureServices方法中:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(
config => config.ModelBinderProviders.Insert(0, new CustomModelBinderProvider())
);
...
}
不可以,因为属性绑定到枚举。如果你真的想成为你发布的内容,那么把模型改为
public class PostModel
{
public int Id { get; set; }
public string Category { get; set; }
}
然后在端点中,将字符串解析为类似的枚举
Enum.TryParse("All", out Category cat);