天天看點

關于ASP.NET WEB API(OWIN WEBAPI)的幾個編碼最佳實踐總結

近期工作比較忙,确實沒太多精力與時間寫博文,博文寫得少,但并不代表沒有研究與總結,也不會停止我繼續分享的節奏,最多有可能發博文間隔時間稍長一點。廢話不多說,直接上幹貨,雖不是主流的ASP.NET CORE但ASP.NET WEB API仍然有很多地方在用【而且核心思路與.NET CORE其實都是一樣的】,而且如下也适用于OWIN WEBAPI方式。

  1. 全局異常捕獲處理幾種方式
具體可參見: ASP.NET Web API 中的錯誤處理
  1. 自定義異常過濾器(ExceptionFilterAttribute) :當控制器方法引發不是帶有 httpresponseexception異常的任何未經處理的異常時,将執行異常篩選器,僅處理與特定操作或控制器相關的子集未處理異常的最簡單解決方案
    /// <summary>
        /// 統一全局異常過濾處理
        /// author:zuowenjun
        /// </summary>
        public class GlobalExceptionFilterAttribute : ExceptionFilterAttribute
        {
    		private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
    		
            public override void OnException(HttpActionExecutedContext actionExecutedContext)
            {
                var actionContext = actionExecutedContext.ActionContext;
                string actionInfo = $"{actionContext.ActionDescriptor.ControllerDescriptor.ControllerName}.{actionContext.ActionDescriptor.ActionName}";
                logger.Error(context.Exception, $"url:{actionContext.Request.RequestUri}({actionInfo}),request body:{actionContext.Request.Content.ReadAsStringAsync().Result},error:{context.Exception.Message}.");
      
                
                //為便于服務之間追蹤分析,嘗試把入參的TraceId繼續傳遞給響應出參
                string traceId = null;
                if (actionContext.ActionArguments?.Count > 0)
                {
                    var traceObj = actionContext.ActionArguments?.Values.Where(a => a is ITraceable).FirstOrDefault() as ITraceable;
                    traceId = traceObj?.TraceId;
                }
    
    
                var apiResponse = new ApiResponse() { Code = 400, Msg = $"{actionInfo} Error:{context.Exception.Message}", TraceId = traceId };
    
                actionExecutedContext.Response = actionExecutedContext.ActionContext.Request.CreateResponse(HttpStatusCode.OK, apiResponse, "application/json");
    
            }
        }
               
  2. 自定義異常處理器(ExceptionHandler):自定義 Web API 捕獲的未處理異常的所有可能響應的解決方案
    /// <summary>
        /// 全局異常處理器
        /// author:zuowenjun
        /// </summary>
        public class GlobalExceptionHandler : ExceptionHandler
        {
            private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
    
            public override void Handle(ExceptionHandlerContext context)
            {
                var actionContext = context.ExceptionContext.ActionContext;
                string actionInfo = $"{actionContext.ActionDescriptor.ControllerDescriptor.ControllerName}.{actionContext.ActionDescriptor.ActionName}";
                logger.Error(context.Exception, $"url:{actionContext.Request.RequestUri}({actionInfo}),request body:{actionContext.Request.Content.ReadAsStringAsync().Result},error:{context.Exception.Message}.");
    
                //為便于服務之間追蹤分析,嘗試把入參的TraceId繼續傳遞給響應出參
                string traceId = null;
                if (actionContext.ActionArguments?.Count > 0)
                {
                    var traceObj = actionContext.ActionArguments?.Values.Where(a => a is ITraceable).FirstOrDefault() as ITraceable;
                    traceId = traceObj?.TraceId;
                }
                var apiResponse = new ApiResponse() { Code = 400, Msg = $"{actionInfo} Error:{context.Exception.Message}", TraceId = traceId };
                context.Result = new ResponseMessageResult(context.ExceptionContext.Request.CreateResponse(HttpStatusCode.OK, apiResponse));
            }
        }
               
  3. 自定義委托處理程式中間件(DelegatingHandler):在請求響應管道中進行攔截判斷,用于捕獲并處理HttpError類異常。【如果想記錄完整的異常資訊,可以自定義多個繼承自ExceptionLogger】
    /// <summary>
        /// 全局異常處理管道中間件(将除GlobalExceptionHandler未能捕獲的HttpError異常進行額外封裝統一傳回)
        /// author:zuowenjun
        /// </summary>
        public class GlobalExceptionDelegatingHandler : DelegatingHandler
        {
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            {
                return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>(respTask =>
                {
                    HttpResponseMessage response = respTask.Result;
                    HttpError httpError;
                    if (response.TryGetContentValue(out httpError) && httpError != null)
                    {
                        var apiResponse = new ApiResponse<HttpError>()
                        {
                            Code = (int)response.StatusCode,
                            Msg = $"{response.RequestMessage.RequestUri} Error:{httpError.Message}",
                            Data = httpError,
                            TraceId = GetTraceId(response.RequestMessage.Content)
                        };
                        response.StatusCode = HttpStatusCode.OK;
                        response.Content = response.RequestMessage.CreateResponse(HttpStatusCode.OK, apiResponse).Content;
                    }
    
                    return response;
    
                });
            }
    
            private string GetTraceId(HttpContent httpContent)
            {
                string traceId = null;
                if (httpContent is ObjectContent)
                {
                    var contentValue = (httpContent as ObjectContent).Value;
                    if (contentValue != null && contentValue is ITraceable)
                    {
                        traceId = (contentValue as ITraceable).TraceId;
                    }
                }
                else
                {
                    string contentStr = httpContent.ReadAsStringAsync().Result;
                    if (contentStr.IndexOf("traceId", StringComparison.OrdinalIgnoreCase) >= 0)
                    {
                        var traceObj = httpContent.ReadAsAsync<ITraceable>().Result;
                        traceId = traceObj.TraceId;
                    }
                }
    
                return traceId;
            }
        }
               

    Web Api Config入口方法(OWIN 是startup Configuration方法)注冊上述相關的自定義的各類中間件:

    【注:建議一般隻需要同時注冊GlobalExceptionDelegatingHandler、GlobalExceptionHandler即可】

    config.MessageHandlers.Insert(0, new GlobalExceptionDelegatingHandler());
                config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
                config.Filters.Add(new GlobalExceptionFilterAttribute());
               

如下是自定義的一個可追蹤接口,用于需要API入參透傳TraceId(Model類實作該接口即代表可傳TraceId),我這裡是為了便于外部接口傳TraceId友善,才放到封包中,其實也可以放在請求頭中

public interface ITraceable
    {
        string TraceId { get; set; }
    }
           
  1. ACTION方法入參驗證的幾種方式
    1. 在Model類對應的屬性上增加添加相關驗證特性
      一般是指System.ComponentModel.DataAnnotations命名空間下的相關驗證特性,如: PhoneAttribute、EmailAddressAttribute 、EnumDataTypeAttribute 、FileExtensionsAttribute 、MaxLengthAttribute 、MinLengthAttribute 、RangeAttribute 、RegularExpressionAttribute 、RequiredAttribute 、StringLengthAttribute 、UrlAttribute 、CompareAttribute 、CustomValidationAttribute【這個支援指定自定義類名及驗證方法,采取約定驗證方法簽名,第一個參數為具體驗證的對象類型(不限定,依實際情況指定),第二個則為ValidationContext類型】
    2. 讓Model類實作IValidatableObject接口并實作Validate方法,同時需注冊自定義的ValidatableObjectAdapter對象,以便實作對IValidatableObject接口的支援,這種方式的好處是可以實作集中驗證(當然也可以使用上述方法1中的CustomValidationAttribute,并标注到要驗證的Model類上,且指定目前Model類的某個符合驗證方法即可)
      public class CustomModelValidator : ModelValidator
          {
              public CustomModelValidator(IEnumerable<ModelValidatorProvider> modelValidatorProviders) : base(modelValidatorProviders)
              {
      
              }
      
      
              public override IEnumerable<ModelValidationResult> Validate(ModelMetadata metadata, object container)
              {
                  if (metadata.IsComplexType && metadata.Model == null)
                  {
                      return new List<ModelValidationResult> { new ModelValidationResult { MemberName = metadata.GetDisplayName(), Message = "請求參數對象不能為空。" } };
                  }
      
                  if (typeof(IValidatableObject).IsAssignableFrom(metadata.ModelType))
                  {
                      var validationResult = (metadata.Model as IValidatableObject).Validate(new ValidationContext(metadata.Model));
                      if (validationResult != null)
                      {
                          var modelValidationResults = new List<ModelValidationResult>();
                          foreach (var result in validationResult)
                          {
                              modelValidationResults.Add(new ModelValidationResult
                              {
                                  MemberName = string.Join(",", result.MemberNames),
                                  Message = result.ErrorMessage
                              });
                          }
      
                          return modelValidationResults;
                      }
                      return null;
                  }
      
                  return GetModelValidator(ValidatorProviders).Validate(metadata, container);
              }
          }
                 
      除了定義上述的CustomModelValidator以便支援對IValidatableObject的驗證外,還必需注冊加入到驗證上下文集合中才能生效,仍然是在WEB API的config入口方法(OWIN 是startup Configuration方法),類似如下:
      RegisterDefaultValidatableObjectAdapter(config);
      
       private void RegisterDefaultValidatableObjectAdapter(HttpConfiguration config)
              {
                  IEnumerable<ModelValidatorProvider> modelValidatorProviders = config.Services.GetModelValidatorProviders();
      
                  DataAnnotationsModelValidatorProvider provider = (DataAnnotationsModelValidatorProvider)
                          modelValidatorProviders.Single(x => x is DataAnnotationsModelValidatorProvider);
      
                  provider.RegisterDefaultValidatableObjectAdapter(typeof(CustomModelValidator));
              }
                 
  2. 出入參JSON格式及日期處理
    1. 大小寫及強制僅支援JSON互動方式(以便符合目前通行的JSON規範,若不加則是大寫)
      config.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
                  
      config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                 
    2. 日期格式化(如果不設定,則預設序列化後為全球通用時,不便于閱讀)
      config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Local;
                  config.Formatters.JsonFormatter.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
                 
    3. 自定義類型轉換(通過自定義實作JsonConverter抽象類,可靈活定義序列化與反序化的行為),例如:
      //在入口方法加入自定義的轉換器類
      config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new BoolenJsonConverter());
      
      public class BoolenJsonConverter : JsonConverter
          {
              public override bool CanConvert(Type objectType)
              {
                  return typeof(bool).IsAssignableFrom(objectType);
              }
      
              public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
              {
                  if (reader.TokenType == JsonToken.Boolean)
                  {
                      return Convert.ToBoolean(reader.Value);
                  }
      
                  throw new JsonSerializationException($"{reader.Path} value: {reader.Value} can not convert to Boolean");
              }
      
              public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
              {
                  if (value == null)
                  {
                      writer.WriteNull();
                      return;
                  }
                  if (value is bool)
                  {
                      writer.WriteValue(value.ToString().ToLower());
                      return;
                  }
      
                  throw new JsonSerializationException($"Expected Boolean object value: {value}");
              }
          }
                 

後面計劃将分享關于OAuth2.0原生實作及借助相關架構實作(JAVA與C#語言),敬請期待!

溫馨提示:我的分享不局限于某一種程式設計語言,更多的是分享經驗、技巧及原理,讓更多的人受益,少走彎路!