Model Validation in ASP.NET Web API nested model validation 如果post方法的request body包含了安全检查所需要的security hash 扩展阅读 ActionFilter发生在controller阶段(delegating handler在http message handler阶段)

https://docs.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api

When a client sends data to your web API, often you want to validate the data before doing any processing. This article shows how to annotate your models, use the annotations for data validation, and handle validation errors in your web API.

Data Annotations

In ASP.NET Web API, you can use attributes from the System.ComponentModel.DataAnnotations namespace to set validation rules for properties on your model. Consider the following model:

using System.ComponentModel.DataAnnotations;

namespace MyApi.Models
{
    public class Product
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public decimal Price { get; set; }
        [Range(0, 999)]
        public double Weight { get; set; }
    }
}

If you have used model validation in ASP.NET MVC, this should look familiar. The Required attribute says that the Name property must not be null. The Range attribute says that Weight must be between zero and 999.

需要注意的是public class RequiredAttribute : ValidationAttribute和public class RangeAttribute : ValidationAttribute,这2个attribute都是继承自ValidationAttribute

Suppose that a client sends a POST request with the following JSON representation:

{ "Id":4, "Price":2.99, "Weight":5 }

You can see that the client did not include the Name property, which is marked as required. When Web API converts the JSON into a Product instance, it validates the Product against the validation attributes. In your controller action, you can check whether the model is valid:

using MyApi.Models;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace MyApi.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Post(Product product)
        {
            if (ModelState.IsValid)
            {
                // Do something with the product (not shown).

                return new HttpResponseMessage(HttpStatusCode.OK);
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }
    }
}

Model validation does not guarantee that client data is safe. Additional validation might be needed in other layers of the application. (For example, the data layer might enforce foreign key constraints.) The tutorial Using Web API with Entity Framework explores some of these issues.

"Under-Posting": Under-posting happens when the client leaves out some properties. For example, suppose the client sends the following:

{"Id":4, "Name":"Gizmo"}

Here, the client did not specify values for Price or Weight. The JSON formatter assigns a default value of zero to the missing properties.

The model state is valid, because zero is a valid value for these properties. Whether this is a problem depends on your scenario. For example, in an update operation, you might want to distinguish between "zero" and "not set." To force clients to set a value, make the property nullable and set the Required attribute:

[Required]
public decimal? Price { get; set; }

"Over-Posting": A client can also send more data than you expected. For example:

{"Id":4, "Name":"Gizmo", "Color":"Blue"}

Here, the JSON includes a property ("Color") that does not exist in the Product model. In this case, the JSON formatter simply ignores this value. (The XML formatter does the same.) Over-posting causes problems if your model has properties that you intended to be read-only. For example:

public class UserProfile
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    public bool IsAdmin { get; set; }  // uh-oh!
}

You don't want users to update the IsAdmin property and elevate themselves to administrators! The safest strategy is to use a model class that exactly matches what the client is allowed to send:

public class UserProfileDTO
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    // Leave out "IsAdmin"
}

Brad Wilson's blog post "Input Validation vs. Model Validation in ASP.NET MVC" has a good discussion of under-posting and over-posting. Although the post is about ASP.NET MVC 2, the issues are still relevant to Web API.

Handling Validation Errors

Web API does not automatically return an error to the client when validation fails. It is up to the controller action to check the model state and respond appropriately.

You can also create an action filter to check the model state before the controller action is invoked. The following code shows an example:

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;

namespace MyApi.Filters
{
    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }
}

If model validation fails, this filter returns an HTTP response that contains the validation errors. In that case, the controller action is not invoked.

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Tue, 16 Jul 2013 21:02:29 GMT
Content-Length: 331

{
  "Message": "The request is invalid.",
  "ModelState": {
    "product": [
      "Required property 'Name' not found in JSON. Path '', line 1, position 17."
    ],
    "product.Name": [
      "The Name field is required."
    ],
    "product.Weight": [
      "The field Weight must be between 0 and 999."
    ]
  }
}

To apply this filter to all Web API controllers, add an instance of the filter to the HttpConfiguration.Filters collection during configuration:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ValidateModelAttribute());

        // ...
    }
}

需要注意的是,这个例子针对的是Dictionary处理。web api本身默认支持了netsted model validation,如果property是一个普通的class类型,比如Student。

web api默认会去检查,这个Student类中标记了Validation Attribute的Property的。唯一需要记住的是,即使某一个方法GetAll不需要使用传递的安全检查参数,也需要将参数传递过去。否则无法触发model validation的检查。

public class Student
{
      [Required]
       public int Id {get;set;}

      [Required]
       public string Name {get;set;}
}

https://*.com/questions/19448662/asp-net-web-api-nested-model-validation

 There are 2 ways you can setup validation of the Dictionary Values. If you don't care about getting all the errors but just the first one encountered you can use a custom validation attribute.

public class Foo
{
    [Required]
    public string RequiredProperty { get; set; }

    [ValidateDictionary]
    public Dictionary<string, Bar> BarInstance { get; set; }
}

public class Bar
{
    [Required]
    public string BarRequiredProperty { get; set; }
}

public class ValidateDictionaryAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (!IsDictionary(value)) return ValidationResult.Success;

        var results = new List<ValidationResult>();
        var values = (IEnumerable)value.GetType().GetProperty("Values").GetValue(value, null);
        values.OfType<object>().ToList().ForEach(item => Validator.TryValidateObject(item, new ValidationContext(item, null, validationContext.Items), results));
        Validator.TryValidateObject(value, new ValidationContext(value, null, validationContext.Items), results);
        return results.FirstOrDefault() ?? ValidationResult.Success;
    }

    protected bool IsDictionary(object value)
    {
        if (value == null) return false;
        var valueType = value.GetType();
        return valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof (Dictionary<,>);
    }
}

The other way is to create your own Dictionary as an IValidatableObject and do the validation in that. This solution gives you the ability to return all the errors.

public class Foo
{
    [Required]
    public string RequiredProperty { get; set; }

    public ValidatableDictionary<string, Bar> BarInstance { get; set; }
}

public class Bar
{
    [Required]
    public string BarRequiredProperty { get; set; }
}

public class ValidatableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();
        Values.ToList().ForEach(item => Validator.TryValidateObject(item, new ValidationContext(item, null, validationContext.Items), results));
        return results;
    }
}

How do I use IValidatableObject?

First off, thanks to @paper1337 for pointing me to the right resources...I'm not registered so I can't vote him up, please do so if anybody else reads this.

Here's how to accomplish what I was trying to do.

Validatable class:

public class ValidateMe : IValidatableObject
{
    [Required]
    public bool Enable { get; set; }

    [Range(1, 5)]
    public int Prop1 { get; set; }

    [Range(1, 5)]
    public int Prop2 { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();
        if (this.Enable)
        {
            Validator.TryValidateProperty(this.Prop1,
                new ValidationContext(this, null, null) { MemberName = "Prop1" },
                results);
            Validator.TryValidateProperty(this.Prop2,
                new ValidationContext(this, null, null) { MemberName = "Prop2" },
                results);

            // some other random test
            if (this.Prop1 > this.Prop2)
            {
                results.Add(new ValidationResult("Prop1 must be larger than Prop2"));
            }
        }
        return results;
    }
}

Using Validator.TryValidateProperty() will add to the results collection if there are failed validations. If there is not a failed validation then nothing will be add to the result collection which is an indication of success.

Doing the validation:

    public void DoValidation()
    {
        var toValidate = new ValidateMe()
        {
            Enable = true,
            Prop1 = 1,
            Prop2 = 2
        };

        bool validateAllProperties = false;

        var results = new List<ValidationResult>();

        bool isValid = Validator.TryValidateObject(
            toValidate,
            new ValidationContext(toValidate, null, null),
            results,
            validateAllProperties);
    }

It is important to set validateAllProperties to false for this method to work. When validateAllProperties is false only properties with a [Required] attribute are checked. This allows the IValidatableObject.Validate() method handle the conditional validations.

How to get a list of properties with a given attribute?

var props = t.GetProperties().Where(
                prop => Attribute.IsDefined(prop, typeof(MyAttribute)));

This avoids having to materialize any attribute instances (i.e. it is cheaper than GetCustomAttribute[s]().

如果post方法的request body包含了安全检查所需要的security hash

比如,参数构成为security hash,client id,本来是通过继承DelegatingHandler实现自己的MessageHandler,但是这个message handler是在model validation之前执行的。

并且这个message handler中有检查security hash的逻辑,所以,需要把这段逻辑移动到model validation中。

public class ValidateModelAttribute : ActionFilterAttribute
    {
        private RequestAnalyzer requestAnalyzer;

        public ValidateModelAttribute()
        {
            requestAnalyzer = new RequestAnalyzer();
        }

        public override async void OnActionExecuting(HttpActionContext actionContext)
        {
            if (!actionContext.ModelState.IsValid)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
            else//需要注意的是,在else里面处理
            {
                var request = actionContext.Request;
                string requestBody = await request.Content.ReadAsStringAsync();
                HttpResponseMessage responseMessage = new HttpResponseMessage();
                if (request.Method != HttpMethod.Post)
                {
                    responseMessage.StatusCode = HttpStatusCode.MethodNotAllowed;
                }
                else
                {
                    SecurityCheckResult securityCheckResult = requestAnalyzer.SecurityCheck(requestBody);
                    if (!securityCheckResult.Success)
                    {
                        responseMessage.StatusCode = securityCheckResult.StatusCode;
                        responseMessage.Content = securityCheckResult.ErrorMessage;
                    }
                }
                //并且这里只有状态不正常的时候,才设置
                if (!responseMessage.IsSuccessStatusCode)
                {
                    actionContext.Response = responseMessage;
                }
            }
        }
    }

扩展阅读

WEB API 系列(二) Filter的使用以及执行顺序

一、Filter的开发和调用

         在默认的WebApi中,框架提供了三种Filter,他们的功能和运行条件如下表所示:

Filter 类型

实现的接口

描述

Authorization

IAuthorizationFilter

最先运行的Filter,被用作请求权限校验

Action

IActionFilter

在Action运行的前、后运行

Exception

IExceptionFilter

当异常发生的时候运行

二、Filter的执行顺序

    在使用MVC的时候,ActionFilter提供了一个Order属性,用户可以根据这个属性控制Filter的调用顺序,而Web API却不再支持该属性。Web API的Filter有自己的一套调用顺序规则:

    所有Filter根据注册位置的不同拥有三种作用域:Global、Controller、Action:

通过HttpConfiguration类实例下Filters.Add()方法注册的Filter(一般在App_StartWebApiConfig.cs文件中的Register方法中设置)就属于Global作用域;

通过Controller上打的Attribute进行注册的Filter就属于Controller作用域;

通过Action上打的Attribute进行注册的Filter就属于Action作用域;

他们遵循了以下规则:

1、在同一作用域下,AuthorizationFilter最先执行,之后执行ActionFilter

2、对于AuthorizationFilter和ActionFilter.OnActionExcuting来说,如果一个请求的生命周期中有多个Filter的话,执行顺序都是Global->Controller->Action;

3、对于ActionFilter,OnActionExecuting总是先于OnActionExecuted执行;

4、对于ExceptionFilter和ActionFilter.OnActionExcuted而言执行顺序为Action->Controller->Global;

5、对于所有Filter来说,如果阻止了请求:即对Response进行了赋值,则后续的Filter不再执行。

 

ActionFilter发生在controller阶段(delegating handler在http message handler阶段)

https://www.cnblogs.com/chucklu/p/10430831.html

 Model Validation in ASP.NET Web API
nested model validation
如果post方法的request body包含了安全检查所需要的security hash
扩展阅读
ActionFilter发生在controller阶段(delegating handler在http message handler阶段)