在ASP.NET Core 6.0+中,识别模型绑定失败的最佳方法是什么?

huangapple go评论74阅读模式
英文:

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&lt;IActionResult&gt; 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 =&gt; String.IsNullOrEmpty(pair.Key));
		if (hasModelBindingFailed)
		{
			//do something when model-binding fails.
			//and return BadRequestObjectResult
		}

		//When validation attributes fails
		var invalidParams = new Dictionary&lt;String, String[]&gt;(context.ModelState.Count);
		foreach (var keyModelStatePair in context.ModelState)
		{
			var key = keyModelStatePair.Key;
			var modelErrors = keyModelStatePair.Value.Errors;

			if (modelErrors is not null &amp;&amp; modelErrors.Count &gt; 0)
			{
				var errorMessages = modelErrors.Select(error =&gt; error.ErrorMessage).ToArray();
				invalidParams.Add(key, errorMessages);
			}
		}

		var problemDetails = new ProblemDetails
		{
			Type = &quot;123&quot;,
			Title = &quot;Invalid parameters&quot;,
			Status = StatusCodes.Status400BadRequest,
			Detail = &quot;Your request parameters didn&#39;t validate.&quot;,
			Instance = &quot;&quot;
		};
		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:

  • { &quot;&quot;, &quot;Some error message&quot; }
  • { &quot;viewModel&quot;, &quot;Again some error message&quot; }

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
在ASP.NET Core 6.0+中,识别模型绑定失败的最佳方法是什么?
在ASP.NET Core 6.0+中,识别模型绑定失败的最佳方法是什么?
您可以将代码放入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&lt;string, string&gt; Extensions { get; set; }
    }

Program.cs

builder.Services.Configure&lt;ApiBehaviorOptions&gt;(options
    =&gt; options.SuppressModelStateInvalidFilter = true);

Controller

        [HttpPost(&quot;test&quot;)]
        public IActionResult test(ViewModel viewModel)
        {
            if (!ModelState.IsValid)
            {
                var problemDetails = new ProblemDetails
                {
                    Type = &quot;123&quot;,
                    Title = &quot;Invalid parameters&quot;,
                    Status = StatusCodes.Status400BadRequest,
                    Detail = &quot;Your request parameters didn&#39;t validate.&quot;,
                    Instance = &quot;&quot;,
                    Extensions = new Dictionary&lt;string, string&gt;()
                };
                foreach (var b in ModelState)
                {
                    problemDetails.Extensions.Add(b.Key, b.Value.Errors.FirstOrDefault().ErrorMessage);
                }
                return Ok(problemDetails);
            }
            return Ok(viewModel);           
        }

Test<br>
在ASP.NET Core 6.0+中,识别模型绑定失败的最佳方法是什么?<br>
在ASP.NET Core 6.0+中,识别模型绑定失败的最佳方法是什么?<br>

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 = &quot;123&quot;,
                    Title = &quot;Invalid parameters&quot;,
                    Status = StatusCodes.Status400BadRequest,
                    Detail = &quot;Your request parameters didn&#39;t validate.&quot;,
                    Instance = &quot;&quot;,
                    Extensions = new Dictionary&lt;string, string&gt;()
                };
                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&lt;ValidationFilterAttribute&gt;();
builder.Services.Configure&lt;ApiBehaviorOptions&gt;(options
    =&gt; options.SuppressModelStateInvalidFilter = true);

Controller

        [HttpPost(&quot;test&quot;)]
        [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 =&gt; 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;
// 省略部分代码...

/// &lt;summary&gt;
/// Creates &lt;see cref=&quot;BadRequestObjectResult&quot;/&gt; instance.
/// The content-type is set to: 'application/problem+json'
/// &lt;/summary&gt;
/// &lt;param name=&quot;problemDetails&quot;&gt;The problem details instance.&lt;/param&gt;
/// &lt;returns&gt;The bad request object result instance.&lt;/returns&gt;
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 =&gt; 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 = &#39;.&#39;;
	private const String DollarSign = &quot;$&quot;;
	private const String InvalidParameters = &quot;Invalid parameters.&quot;;
	private const String RequestFailedModelBinding = &quot;Your request failed model-binding.&quot;;
	private const String RequestPropertyFailedModelBinding = &quot;Your request failed model-binding: &#39;{0}&#39;.&quot;;
	private const String RequestParametersDidNotValidate = &quot;Your request parameters did not validate.&quot;;
	private const String MediaTypeApplicationProblemJson = &quot;application/problem+json&quot;;
	#endregion Private Constants

	/// &lt;summary&gt;
	/// 
	/// &lt;/summary&gt;
	/// &lt;param name=&quot;context&quot;&gt;The result executed context.&lt;/param&gt;
	public void OnResultExecuted(ResultExecutedContext context)
	{

	}

	/// &lt;summary&gt;
	/// 
	/// &lt;/summary&gt;
	/// &lt;param name=&quot;context&quot;&gt;The result executing context.&lt;/param&gt;
	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 =&gt; 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 =&gt; pair.Key.Contains(&quot;$.&quot;));
		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&lt;InvalidParam&gt;(modelStateDictionary.Count);
		foreach (var keyModelStatePair in modelStateDictionary)
		{
			var key = keyModelStatePair.Key;
			var modelErrors = keyModelStatePair.Value.Errors;
			if (modelErrors is not null &amp;&amp; modelErrors.Count &gt; 0)
			{
				IEnumerable&lt;InvalidParam&gt; invalidParam;
				if (modelErrors.Count == 1)
				{
					invalidParam = modelErrors.Select(error =&gt; new InvalidParam(keyModelStatePair.Key, new[] { error.ErrorMessage }));
				}
				else
				{
					var errorMessages = new String[modelErrors.Count];
					for (var i = 0; i &lt; modelErrors.Count; i++)
					{
						errorMessages[i] = modelErrors[i].ErrorMessage;
					}

					invalidParam = modelErrors.Select(error =&gt; new InvalidParam(keyModelStatePair.Key, errorMessages));
				}

				invalidParams.AddRange(invalidParam);
			}
		}

		problemDetails.Detail = RequestParametersDidNotValidate;
		problemDetails.Extensions[nameof(invalidParams)] = invalidParams;
		context.Result = GetBadRequestObjectResult(problemDetails);
	}

	/// &lt;summary&gt;
	/// Creates &lt;see cref=&quot;BadRequestObjectResult&quot;/&gt; instance.
	/// The content-type is set to: &#39;application/problem+json&#39;
	/// &lt;/summary&gt;
	/// &lt;param name=&quot;problemDetails&quot;&gt;The problem details instance.&lt;/param&gt;
	/// &lt;returns&gt;The bad request object result instance.&lt;/returns&gt;
	private static BadRequestObjectResult GetBadRequestObjectResult(ProblemDetails problemDetails)
	{
		var result = new BadRequestObjectResult(problemDetails);
		result.ContentTypes.Clear();
		result.ContentTypes.Add(MediaTypeApplicationProblemJson);
		return result;
	}
}

huangapple
  • 本文由 发表于 2023年6月5日 02:57:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/76401993.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定