[WEB API]ModelState.IsValid忽略型別的檢查錯誤

[WEB API]ModelState.IsValid忽略型別的檢查錯誤

前言

之前有寫一篇文章是在說明如何用ActionFilter來為全局註冊一個ModelState.IsValid的方式,但是在公司的老專案發生了一個問題,因為Web Api在Int或DateTime如果傳空值的話會自動幫忙設預設值,但是在ModelState.IsValid的時候,卻會出現型別上的錯誤,這就造成了老專案非常大的困擾?接下來就簡單說明一下實際情況。

模擬情境

我先定義一個非常簡單的Model,內容如下

 public class DemoModel                             
 {                                                  
     public int Id { get; set; }                    
     [Required]                                     
     public string Name { get; set; }               
                                                    
     public DateTime SelectedDate { get; set; }     
 }                                                  

我新增一個DemoController,來模擬一下各種情境

public class DemoController:ApiController         
{                                                 
    public IHttpActionResult Post(DemoModel model)
    {      
         return Ok();      
    }                                             
}                                                 

接著就是ModelState的ActionFilter的部份(這部份沒說的很完整,請參考https://dotblogs.com.tw/kinanson/2017/05/03/082547)

    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ActionDescriptor.GetCustomAttributes<IgnoreValidateModelAttribute>(false).Any())
            {
                return;
            }

            if (!actionContext.ModelState.IsValid)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);

            }            
           
            base.OnActionExecuting(actionContext);
        }
    }

接著看一下swagger的部份,預設狀況是如下例子,這樣子都是正常的

如果我把id和selectDate都去掉的話也是正常的。

但如果我把id給空值的話就會出錯

selectedDate給空值也會出錯

所以實際狀況是我們只要不設定id或selectedDate就不會出錯了,但是很多時候當我們在送出表單或使用ajax的時候,我們畫面上確實有這個textbox可以填值,或者是hidden的值,但是這些值並非必填的,是可以不需填入的,所以當post送出的時候,這些欄位則會存在,而且會預設就是給空值。

解決方式

把Model改成正確,也就是預設允許可以為null

 public class DemoModel                              
 {                                                   
     public int? Id { get; set; }                    
     [Required]                                      
     public string Name { get; set; }                
                                                     
     public DateTime? SelectedDate { get; set; }     
 }                                                   

但是這種解決方式卻不能解決我的問題,更多這類情境其實還蠻容易發生在web form轉到mvc或web api的時候,因為當你想要翻舊系統的時候,會想要把驗證集中放在Model上面,不過即有好幾仟個類別可能都已定型,甚至有些是還透過map或者直接對應db的狀況,我們不可能去抓出所有會發生錯誤的狀況,一一的去排除掉啊,那我的想法是否能只驗證我有定義的attribute,而忽略掉型別的檢查呢?如果int或datetime給空值的話,也不會出錯呢?其實這個解法是筆者自己寫出來的,google也都沒有相關的解法,可能是普遍大家都不會這樣子幹,但是在筆者目前的情境卻不得不這樣做,所以是否要這樣子做就視各位的情境了。

    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ActionDescriptor.GetCustomAttributes<IgnoreValidateModelAttribute>(false).Any())
            {
                return;
            }

            var httpMethod = actionContext.Request.Method;            
            if (httpMethod == HttpMethod.Get || httpMethod == HttpMethod.Options)
            {
                return;
            }

            var modelResult = new ModelStateDictionary();
            foreach (var item in actionContext.ModelState)
            {
                var errors = item.Value.Errors.FirstOrDefault();
                var hasException = item.Value.Errors.Any(x => x.Exception != null);
                if (hasException)
                {
                    continue;
                }
                modelResult.AddModelError(item.Key, errors.ErrorMessage); //自行新增只有ErrorMessage的部份,而忽略型別轉換的錯誤提示
            }

            if (modelResult.Count > 0)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, modelResult);
            }

            base.OnActionExecuting(actionContext);
        }
    }

以上述做法可以忽略有expection的錯誤提醒,但如果有expection的話那model error就會失效了,因為只要有任何expection的狀況,就會中止這個屬性的檢查,不會再加入任何error,也就是說如果我們設定為required,並且屬性也沒有給nullable的話,而且client端傳來又沒有給值的話,結果我們的model驗證卻沒有回應此欄位必須填值,就會變得非常怪異。

結論

這是筆者為了不得以的情境硬幹出來的,如果有任何更好的解決方法或建議,再請多多指導。