Fluent Validation 使用ActionFilter來驗證參數

  • 1089
  • 0
  • 2019-07-24

環境配置
Asp.Net core 2.2

Visual Studio 2017

nuget:FluentValidation.AspNetCore 8.4.0版

一般想使用Fluent Validation作傳入參數驗證可以在程式內使用Validate方法
現在為了節省程式碼改用actionfilterattribute的方式,掛在每個action方法上
但是發現每個request進來後都沒進到actionfilter

這個範例的程式碼我是參考軟體主廚的文章
https://dotblogs.com.tw/supershowwei/2016/04/30/005529

在開始之前先說明一下.net core filter的運作方式

黃色是正常處理流程
灰色是異常流程
資料參考https://blog.johnwu.cc/article/ironman-day14-asp-net-core-filters.html

request進來之前會先經過Model Binding
重點就是在這邊
看FluentValidation的文件說明他的驗證方式是依照.Net MVC 模型驗證的方式
所以跑到這一段的時候就會去自訂的Validator執行建構式
但是這邊驗證參數沒過的話不會往下走到action filter 而是會直接回傳錯誤訊息


用程式實際跑一次的流程如下
CustomAuthorizationFilter in. 
CustomResourceFilter in. 
CustomResultFilter in. 
CustomResultFilter out. 
CustomResourceFilter out. 


回傳預設的格式是

{
    "errors": {
        "Amount": [
            "Amount 必須大於 0。"
        ],
        "UnitPrice": [
            "Unit Price 必須大於等於 0。"
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "8000016b-0002-fc00-b63f-84710c7967bb"
}

 

如果沒有特別需求用這樣就可以
如果是像我需要自訂回傳格式就需要讓request進到Actionfilter
 

原本做法是在Startup.cs裡面註冊
 

   services.AddMvc()
                .AddFluentValidation(fvc =>fvc.RegisterValidatorsFromAssemblyContaining<Startup>())
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

這個做法會去找所有繼承AbstractValidator<T>的類別
在這邊就直接註冊

如果想改成手動的方式不走ModelBinding
就要把AddFluentValidation拿掉自己註冊
改成像這樣
 

 services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddTransient<IValidatorFactory, ServiceProviderValidatorFactory>();
            services.AddTransient<IValidator<Order>, OrderValidator>();

2019.07.24補上另一個做法
 

services.AddMvc()
                .AddFluentValidation(fvc => fvc.RegisterValidatorsFromAssemblyContaining<Startup>())
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

          
            services.Configure<ApiBehaviorOptions>(options =>
            {
               options.SuppressModelStateInvalidFilter = true;
            });

這邊意思是Model Binding驗證不過跳400error的話,會不理他繼續往下執行

 

範例ActionFilter
 

public class ValidateRequestParameterAttribute : ActionFilterAttribute
    {
        private string ParameterName;

        private Type ParameterType;

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidateRequestParameterAttribute" /> class.
        /// </summary>
        /// <param name="parameterType">Type of the parameter.</param>
        /// <param name="parameterName">Name of the parameter.</param>
        public ValidateRequestParameterAttribute(Type parameterType,
                                                 string parameterName = "parameter")
        {
            this.ParameterName = parameterName;
            this.ParameterType = parameterType;
        }

        /// <summary>
        /// 所有實作IRequestParameter介面的類別.
        /// </summary>
        private static Type[] _entryAssemblyParameterTypes;
        private static Type[] EntryAssemblyParameterTypes
        {
            get
            {
                if (_entryAssemblyParameterTypes==null || _entryAssemblyParameterTypes.Any().Equals(false))
                {
                    var type = typeof(IRequestParameter);
                    _entryAssemblyParameterTypes = type.Assembly
                                                  .GetTypes()
                                                  .Where(p => type.IsAssignableFrom(p))
                                                  .ToArray();
                }

                return _entryAssemblyParameterTypes;
            }
        }

        /// <summary>
        /// Called when [action executing].
        /// </summary>
        /// <param name="context">The context.</param>
        /// <exception cref="System.NotSupportedException">wrong parameter type</exception>
        /// <inheritdoc />
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            // 找到Controller設定對應的類別
            var typeFullName = this.ParameterType.FullName;
            var requestParameterType = EntryAssemblyParameterTypes.SingleOrDefault
            (
                x => x.FullName.Equals(typeFullName, StringComparison.OrdinalIgnoreCase)
            );

            // 取得傳進來 RequestParameter
            var parameterObject = this.GetParameterFromActionContext(context, this.ParameterName);
            if (parameterObject.GetType() != requestParameterType)
            {
                throw new NotSupportedException("wrong parameter type");
            }
            var validatorFactory = context.HttpContext.RequestServices.GetRequiredService<IValidatorFactory>();

            var validator = validatorFactory.GetValidator(parameterObject.GetType());

            // skip objects with no validators
            if (validator!=null)
            {
                var result = validator.Validate(parameterObject);

                // if there are errors, throw fluent validation exception.
                if (!result.IsValid)
                {
                    throw FluentValidationExtensions.GetFluentValidationException(result.Errors);
                }
            }

            base.OnActionExecuting(context);
        }

        /// <summary>
        /// Gets the parameter from action context.
        /// </summary>
        /// <param name="actionContext">The action context.</param>
        /// <param name="parameterName">對應傳入參數名稱</param>
        /// <returns>System.Object.</returns>
        /// <exception cref="System.ArgumentNullException">actionContext - must input actionContext.</exception>
        /// <exception cref="System.ArgumentException">must input parameter</exception>
        private object GetParameterFromActionContext(ActionExecutingContext actionContext,
                                                     string parameterName)
        {
            if (actionContext==null)
            {
                throw new ArgumentNullException(nameof(actionContext), "must input actionContext.");
            }

            if (actionContext.ActionArguments.Any().Equals(false))
            {
                throw new ArgumentException("must input parameter");
            }

            var actionArguments = actionContext.ActionArguments;
            var parameter = actionArguments[parameterName];

            return parameter;
        }
    }

 

文件:https://fluentvalidation.net/aspnet#injecting-child-validators