英文:
What is the best way to identify that a model binding has failed in ASP.NET Core 6.0+
问题
I have gone through the MSDN documentation:
I tried creating a scenario where value sent from the swagger to the API, failed to bind to the model, that is expected on the server. Here is the code of the scenario:
OrderController.cs
[HttpPost]
public async Task<IActionResult> CreateAsync(OrderViewModel viewModel)
{
//map and add this model to the db
//and return a 201 status code
}
And the input I sent from the swagger:
{
null
}
This led to the model-binding failure, and I have a result filter where I am handling this situation as follows: FailedValidationResultFilter
public class FailedValidationResultFilter : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
//When model-binding fails
var hasModelBindingFailed = context.ModelState.Any(pair => String.IsNullOrEmpty(pair.Key));
if (hasModelBindingFailed)
{
//do something when model-binding fails.
//and return BadRequestObjectResult
}
//When validation attributes fails
var invalidParams = new Dictionary<String, String[]>(context.ModelState.Count);
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var modelErrors = keyModelStatePair.Value.Errors;
if (modelErrors is not null && modelErrors.Count > 0)
{
var errorMessages = modelErrors.Select(error => error.ErrorMessage).ToArray();
invalidParams.Add(key, errorMessages);
}
}
var problemDetails = new ProblemDetails
{
Type = "123",
Title = "Invalid parameters",
Status = StatusCodes.Status400BadRequest,
Detail = "Your request parameters didn't validate.",
Instance = ""
};
problemDetails.Extensions.Add(nameof(invalidParams), invalidParams);
context.Result = new BadRequestObjectResult(problemDetails);
}
}
What I have observed while debugging is this, that whenever model-binding fails for this input, it returns 2 key value pair:
{ "", "Some error message" }
{ "viewModel", "Again some error message" }
So, I am checking if there is a model-state with an empty key, if it is then there is a model-binding error. And I am not sure why, but it just doesn't feel like the right approach to find if model-binding has failed.
Question: what is the correct way to identify if model binding has failed? What could be another input type that can be passed which leads to failure in model-binding and then in the filter, the first property may not be blank/empty as I am expecting it to be?
英文:
I have gone through the MSDN documentation:
I tried creating a scenario where value sent from the swagger to the API, failed to bind to the model, that is expected on the server. Here is the code of the scenario:
OrderController.cs
[HttpPost]
public async Task<IActionResult> CreateAsync(OrderViewModel viewModel)
{
//map and add this model to the db
//and return a 201 status code
}
And the input I sent from the swagger:
{
null
}
This led to the model-binding failure, and I have a result filter where I am handling this situation as follows: FailedValidationResultFilter
public class FailedValidationResultFilter : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
//When model-binding fails
var hasModelBindingFailed = context.ModelState.Any(pair => String.IsNullOrEmpty(pair.Key));
if (hasModelBindingFailed)
{
//do something when model-binding fails.
//and return BadRequestObjectResult
}
//When validation attributes fails
var invalidParams = new Dictionary<String, String[]>(context.ModelState.Count);
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var modelErrors = keyModelStatePair.Value.Errors;
if (modelErrors is not null && modelErrors.Count > 0)
{
var errorMessages = modelErrors.Select(error => error.ErrorMessage).ToArray();
invalidParams.Add(key, errorMessages);
}
}
var problemDetails = new ProblemDetails
{
Type = "123",
Title = "Invalid parameters",
Status = StatusCodes.Status400BadRequest,
Detail = "Your request parameters didn't validate.",
Instance = ""
};
problemDetails.Extensions.Add(nameof(invalidParams), invalidParams);
context.Result = new BadRequestObjectResult(problemDetails);
}
}
What I have observed while debugging is this, that whenever model-binding fails for this input, it returns 2 key value pair:
{ "", "Some error message" }
{ "viewModel", "Again some error message" }
So, I am checking if their is a model-state with an empty key, if it is then there is a model-binding error. And I am not sure why, but it just doesn't feel like the right approach to find if model-binding has failed.
Question: what is the correct way to identify if model binding has failed? What could be another input type that can be passed which leads to failure in model-binding and then in the filter, the first property may not be blank/empty as I am expecting it to be?
答案1
得分: 0
ModelState.key是无效的属性名称。您发送了null
,因此属性名称为空。但在我的测试中,键是“$”。
您可以尝试以下代码。
ViewModel.cs
public class ViewModel
{
public int Number { get; set; }
}
ProblemDetails.cs
public class ProblemDetails
{
public string Type { get; set; }
public string Title { get; set; }
public int Status { get; set; }
public string Detail { get; set; }
public string Instance { get; set; }
public Dictionary<string, string> Extensions { get; set; }
}
Program.cs
builder.Services.Configure<ApiBehaviorOptions>(options
=> options.SuppressModelStateInvalidFilter = true);
Controller
[HttpPost("test")]
public IActionResult test(ViewModel viewModel)
{
if (!ModelState.IsValid)
{
var problemDetails = new ProblemDetails
{
Type = "123",
Title = "Invalid parameters",
Status = StatusCodes.Status400BadRequest,
Detail = "Your request parameters didn't validate.",
Instance = "",
Extensions = new Dictionary<string, string>()
};
foreach (var b in ModelState)
{
problemDetails.Extensions.Add(b.Key, b.Value.Errors.FirstOrDefault().ErrorMessage);
}
return Ok(problemDetails);
}
return Ok(viewModel);
}
Test
您可以将代码放入ActionFilter中。
ValidationFilterAttribute.cs
public class ValidationFilterAttribute : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var problemDetails = new ProblemDetails
{
Type = "123",
Title = "Invalid parameters",
Status = StatusCodes.Status400BadRequest,
Detail = "Your request parameters didn't validate.",
Instance = "",
Extensions = new Dictionary<string, string>()
};
foreach (var b in context.ModelState)
{
problemDetails.Extensions.Add(b.Key, b.Value.Errors.FirstOrDefault().ErrorMessage);
}
context.Result = new ObjectResult(problemDetails);
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
program.cs
builder.Services.AddScoped<ValidationFilterAttribute>();
builder.Services.Configure<ApiBehaviorOptions>(options
=> options.SuppressModelStateInvalidFilter = true);
Controller
[HttpPost("test")]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public IActionResult test(ViewModel viewModel)
{
return Ok(viewModel);
}
Result filter vs Action filter https://stackoverflow.com/questions/59454368/actionfilter-and-resultfilter-different-and-examples
1: https://i.stack.imgur.com/Glwcv.png
2: https://i.stack.imgur.com/rjmXK.png
英文:
ModelState.key is the invalid property name. You send null
so the property name is empty. But in my test, the key is a "$"<br>
You can try the following code.<br>
ViewModel.cs
public class ViewModel
{
public int Number { get; set; }
}
ProblemDetails.cs
public class ProblemDetails
{
public string Type { get; set; }
public string Title { get; set; }
public int Status { get; set; }
public string Detail { get; set; }
public string Instance { get; set; }
public Dictionary<string, string> Extensions { get; set; }
}
Program.cs
builder.Services.Configure<ApiBehaviorOptions>(options
=> options.SuppressModelStateInvalidFilter = true);
Controller
[HttpPost("test")]
public IActionResult test(ViewModel viewModel)
{
if (!ModelState.IsValid)
{
var problemDetails = new ProblemDetails
{
Type = "123",
Title = "Invalid parameters",
Status = StatusCodes.Status400BadRequest,
Detail = "Your request parameters didn't validate.",
Instance = "",
Extensions = new Dictionary<string, string>()
};
foreach (var b in ModelState)
{
problemDetails.Extensions.Add(b.Key, b.Value.Errors.FirstOrDefault().ErrorMessage);
}
return Ok(problemDetails);
}
return Ok(viewModel);
}
You can put the codes in ActionFilter.<br>
ValidationFilterAttribute.cs
public class ValidationFilterAttribute : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var problemDetails = new ProblemDetails
{
Type = "123",
Title = "Invalid parameters",
Status = StatusCodes.Status400BadRequest,
Detail = "Your request parameters didn't validate.",
Instance = "",
Extensions = new Dictionary<string, string>()
};
foreach (var b in context.ModelState)
{
problemDetails.Extensions.Add(b.Key, b.Value.Errors.FirstOrDefault().ErrorMessage);
}
context.Result = new ObjectResult(problemDetails);
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
program.cs
builder.Services.AddScoped<ValidationFilterAttribute>();
builder.Services.Configure<ApiBehaviorOptions>(options
=> options.SuppressModelStateInvalidFilter = true);
Controller
[HttpPost("test")]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public IActionResult test(ViewModel viewModel)
{
return Ok(viewModel);
}
Result filter vs Action filter https://stackoverflow.com/questions/59454368/actionfilter-and-resultfilter-different-and-examples
答案2
得分: 0
以下是翻译好的部分:
"After doing much test and trial, I hope I have the correct answer. So, let's begin."
在经过了许多测试和尝试之后,我希望我有正确的答案。所以,让我们开始吧。
<h1>Scenario One</h1>
<h1>场景一</h1>
When Request Payload is
当请求负载为
null
When we send this payload in the request, the model-validation fails generating 2 keys (one of them is an empty string) with the following error-messages:
当我们将这个负载发送到请求中时,模型验证失败,生成2个键(其中一个是空字符串),并显示以下错误消息:
Key | Error Message |
---|---|
A non-empty request body is required. | |
viewModel | The viewModel field is required. |
需要非空的请求体。 | |
viewModel | 必须填写viewModel字段。 |
<h1>Scenario Two</h1>
<h1>场景二</h1>
When Request Payload is
当请求负载为
{
null
}
In this case these 2 keys are generated:
在这种情况下,生成了这2个键:
Key | Error Message |
---|---|
$ | 'n' is an invalid start of a property name. Expected a '"'. Path: $ |
viewModel | The viewModel field is required. |
$ | 'n' 是无效的属性名称起始。期望是 '"‘。路径:$ |
viewModel | 必须填写viewModel字段。 |
Now, I have used the following piece of code to handle both the scenarios:
现在,我已经使用了以下代码片段来处理这两种情况:
//When model-binding fails because input is an invalid JSON
//当模型绑定失败因为输入是无效的 JSON 时
if (modelStateDictionary.Any(pair => pair.Key == DollarSign || String.IsNullOrEmpty(pair.Key)))
{
problemDetails.Detail = RequestFailedModelBinding;
context.Result = GetBadRequestObjectResult(problemDetails);
return;
}
Complete code:
完整的代码:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApi.ErrorResponse.ViaFilterAndMiddleware.ViewModels;
using static Microsoft.AspNetCore.Http.StatusCodes;
// 省略部分代码...
/// <summary>
/// Creates <see cref="BadRequestObjectResult"/> instance.
/// The content-type is set to: 'application/problem+json'
/// </summary>
/// <param name="problemDetails">The problem details instance.</param>
/// <returns>The bad request object result instance.</returns>
private static BadRequestObjectResult GetBadRequestObjectResult(ProblemDetails problemDetails)
{
var result = new BadRequestObjectResult(problemDetails);
result.ContentTypes.Clear();
result.ContentTypes.Add(MediaTypeApplicationProblemJson);
return result;
}
// 省略部分代码...
这些是你要求的翻译部分,没有包括其他内容。
英文:
After doing much test and trial, I hope I have the correct answer. So, let's begin.
<h1>Scenario One</h1>
When Request Payload is
null
When we send this payload in the request, the model-validation fails generating 2 keys (one of them is an empty string) with the following error-messages:
Key | Error Message |
---|---|
A non-empty request body is required. | |
viewModel | The viewModel field is required. |
<h1>Scenario Two</h1>
When Request Payload is
{
null
}
In this case these 2 keys are generated:
Key | Error Message |
---|---|
$ | 'n' is an invalid start of a property name. Expected a '"'. Path: $ | LineNumber: 1 | BytePositionInLine: 2. |
viewModel | The viewModel field is required. |
Now, I have used the following piece of code to handle both the scenarios:
//When model-binding fails because input is an invalid JSON
if (modelStateDictionary.Any(pair => pair.Key == DollarSign || String.IsNullOrEmpty(pair.Key)))
{
problemDetails.Detail = RequestFailedModelBinding;
context.Result = GetBadRequestObjectResult(problemDetails);
return;
}
Complete code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApi.ErrorResponse.ViaFilterAndMiddleware.ViewModels;
using static Microsoft.AspNetCore.Http.StatusCodes;
public class MyModelValidationResultFilter : IResultFilter
{
#region Private Constants
private const Char Dot = '.';
private const String DollarSign = "$";
private const String InvalidParameters = "Invalid parameters.";
private const String RequestFailedModelBinding = "Your request failed model-binding.";
private const String RequestPropertyFailedModelBinding = "Your request failed model-binding: '{0}'.";
private const String RequestParametersDidNotValidate = "Your request parameters did not validate.";
private const String MediaTypeApplicationProblemJson = "application/problem+json";
#endregion Private Constants
/// <summary>
///
/// </summary>
/// <param name="context">The result executed context.</param>
public void OnResultExecuted(ResultExecutedContext context)
{
}
/// <summary>
///
/// </summary>
/// <param name="context">The result executing context.</param>
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.ModelState.IsValid)
return;
var modelStateDictionary = context.ModelState;
var problemDetails = new ProblemDetails
{
Title = InvalidParameters,
Status = Status400BadRequest
};
//When model-binding fails because input is an invalid JSON
if (modelStateDictionary.Any(pair => pair.Key == DollarSign || String.IsNullOrEmpty(pair.Key)))
{
problemDetails.Detail = RequestFailedModelBinding;
context.Result = GetBadRequestObjectResult(problemDetails);
return;
}
//When a specific property-binding fails
var keyValuePair = modelStateDictionary.FirstOrDefault(pair => pair.Key.Contains("$."));
if (keyValuePair.Key is not null)
{
var propertyName = keyValuePair.Key.Split(Dot)[1];
problemDetails.Detail =
String.IsNullOrEmpty(propertyName) ? RequestFailedModelBinding : String.Format(RequestPropertyFailedModelBinding, propertyName);
context.Result = GetBadRequestObjectResult(problemDetails);
return;
}
//When one of the input parameters failed model-validation
var invalidParams = new List<InvalidParam>(modelStateDictionary.Count);
foreach (var keyModelStatePair in modelStateDictionary)
{
var key = keyModelStatePair.Key;
var modelErrors = keyModelStatePair.Value.Errors;
if (modelErrors is not null && modelErrors.Count > 0)
{
IEnumerable<InvalidParam> invalidParam;
if (modelErrors.Count == 1)
{
invalidParam = modelErrors.Select(error => new InvalidParam(keyModelStatePair.Key, new[] { error.ErrorMessage }));
}
else
{
var errorMessages = new String[modelErrors.Count];
for (var i = 0; i < modelErrors.Count; i++)
{
errorMessages[i] = modelErrors[i].ErrorMessage;
}
invalidParam = modelErrors.Select(error => new InvalidParam(keyModelStatePair.Key, errorMessages));
}
invalidParams.AddRange(invalidParam);
}
}
problemDetails.Detail = RequestParametersDidNotValidate;
problemDetails.Extensions[nameof(invalidParams)] = invalidParams;
context.Result = GetBadRequestObjectResult(problemDetails);
}
/// <summary>
/// Creates <see cref="BadRequestObjectResult"/> instance.
/// The content-type is set to: 'application/problem+json'
/// </summary>
/// <param name="problemDetails">The problem details instance.</param>
/// <returns>The bad request object result instance.</returns>
private static BadRequestObjectResult GetBadRequestObjectResult(ProblemDetails problemDetails)
{
var result = new BadRequestObjectResult(problemDetails);
result.ContentTypes.Clear();
result.ContentTypes.Add(MediaTypeApplicationProblemJson);
return result;
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论