首先需要說明的是這是.net framework的一個組件,而不是針對.net core的。目前工作比較忙,因此.net core的轉換正在編寫過程中,有了實現會第一時間貼出來。
接下來進入正題。對於大型的分層系統,會有一個應用程序層,應用程序層的主要作用是封裝業務領域層的業務邏輯層,並對界面展示層提供服務。界面展示層例如有Web網站、移動應用、WPF等等,例如下圖。
很多情況下,業務領域層中間的業務邏輯層方法和應用服務層的服務接口幾乎是一致的。在業務邏輯方法編寫完成後,編程人員,也會重復性的編寫應用服務層。該層難度不大,但是屬於重復性勞動並且工作量不小。對於一個有敬業精神的程序員來說,問題就來了,寫一大堆不加思考的、工作量大的代碼,還不如寫一個框架自動通過業務邏輯層生成WebApi。
為了簡化編程人員的工作量,減少錯誤的出現,我們編寫了這個框架,就是通過業務邏輯層的方法自動生成應用服務層的服務。
其實ABP也有這個東西,但是發現ABP中的實現與Castle Windsor結合太緊密了,用了些Castle Windsor的特性。我等用Unity作為DI/IoC的無法借鑒。
要了解這個自動生成WebApi的框架,我們得簡要的講解下.net framework下webapi的請求處理過程。
Web API是微軟的主導的一種面向服務的實現方式,已經集成在visual studio的模板中,是一種比較成熟的SOA數據服務方式。Web API的服務提供方式實現過程由三個步驟組成:路由匹配階段;控制器選擇和構建階段;執行器選擇和執行階段。
缺省情況下,一個mvc項目包含webapi,會在app_start目錄下有一個webapiconfig.cs文件。這個文件是webapi路由的設置,我們假設按照controller和action的名稱進行路由,寫法見下(尤其是routetemplate行):
1 public static void Register(HttpConfiguration config) 2 { 3 // Web API 配置和服務 4 5 // Web API 路由 6 config.MapHttpAttributeRoutes(); 7 8 config.Routes.MapHttpRoute( 9 name: "DefaultApi", 10 routeTemplate: "api/{controller}/{action}/{id}", 11 defaults: new { id = RouteParameter.Optional } 12 ); 13 }
系統運行過程中,如果框架發現了URI的一個匹配,它會創建一個包含了每個占位符適用的值的字典集合。鍵是不包含大括號的占位符名稱,例如controller、action。值是提取自URI路徑或者表單提交的數據。該字典被存儲在IHttpRouteData對象中。
在路由匹配階段,"{controller}"和"{action}"占位符會被像其他占位符一樣對待。它們被同其他值一起簡單地存儲在字典中。http://[server]/[appName]/api/user/get/1,路由字典將包含:
控制器選擇和構建階段
在一個路徑匹配路由規則後,可以獲得到Controller和Action,並存放與路由字典中。Web API的消息處理管道由一組HttpMessageHandler經過"首尾相連"而成。WebApi 最終會引導到默認的HttpControllerDispatcher處理,其中HttpControllerDispatcher實現了HttpMessageHandler接口。HttpControllerDispatcher實現了目標HttpController對象的激活、執行。
執行器選擇和調用階段
ApiController是HttpController的基類。ApiController中的ExecuteAsync方法實現了Action的選擇和執行。
至此,整個從路由到執行並返回結果的整個流程就結束了。
上面洋洋灑灑介紹了整個WeApi的路由和執行過程,下面我們就根據這個流程來確定如何加入我們修改的內容,以通過業務邏輯層的方法自動實現WebApi。業務邏輯層是一個個的邏輯實現類,類中包含了一個個的業務邏輯方法,例如UserService類包含了GetUser方法。我們最終的實現就是生成http://[server]/[appName]/User/GetUser的WebApi調用方式。做法是:
在替換之前,我們預先做了部分處理,在所有程序集中查找所有以AppService結尾的邏輯類,並將邏輯類生成為DynamicApiControllerInfo,注冊進DynamicApiControllerManager中,邏輯類中的GetUser等方法生成為DynamicApiActionInfo,存放在DynamicApiControllerInfo的Actions屬性中。該方式可以避免每次查找Controller和Action都要做反射的做法,提高系統執行效率。
這裡,我們約定所有以AppService結尾的邏輯類都要自動生成WebApi,也可以根據情況寫成繼承IApplicationService接口的類自動生成WebApi。
1 public static class DynamicApiBuilder 2 { 3 public static void Build() 4 { 5 IEnumerable<Type> types = ReflectionHelper.GetSubTypes<object>().Where(type => !type.IsAbstract && type.IsPublic && type.FullName.EndsWith("AppService")); 6 7 foreach (Type type in types) 8 { 9 DynamicApiControllerInfo controllerInfo = GenerateApiControllerInfo(type); 10 11 DynamicApiControllerManager.Register(controllerInfo); 12 } 13 } 14 15 private static DynamicApiController GenerateApiController() 16 { 17 DynamicApiController controller = new DynamicApiController(); 18 19 return controller; 20 } 21 22 private static DynamicApiControllerInfo GenerateApiControllerInfo(Type type) 23 { 24 //10位是AppService 25 DynamicApiControllerInfo controllerInfo = new DynamicApiControllerInfo(type); 26 27 foreach (MethodInfo methodInfo in GetMethodsOfType(type)) 28 { 29 DynamicApiActionInfo actionInfo = new DynamicApiActionInfo(methodInfo.Name, GetNormalizedVerb(methodInfo), methodInfo); 30 31 controllerInfo.Actions.Add(actionInfo.ActionName, actionInfo); 32 } 33 34 return controllerInfo; 35 } 36 37 private static IEnumerable<MethodInfo> GetMethodsOfType(Type type) 38 { 39 var allMethods = new List<MethodInfo>(); 40 41 FillMethodsRecursively(type, BindingFlags.Public | BindingFlags.Instance, allMethods); 42 //method.DeclaringType != typeof(ApplicationService) && 43 return allMethods.Where(method => method.DeclaringType != typeof(object) && !IsPropertyAccessor(method)); 44 } 45 46 private static void FillMethodsRecursively(Type type, BindingFlags flags, List<MethodInfo> members) 47 { 48 members.AddRange(type.GetMethods(flags).Where(m => !members.Exists(mm => m.Name == mm.Name))); 49 50 foreach (var interfaceType in type.GetInterfaces()) 51 { 52 FillMethodsRecursively(interfaceType, flags, members); 53 } 54 } 55 56 private static bool IsPropertyAccessor(MethodInfo method) 57 { 58 return method.IsSpecialName && (method.Attributes & MethodAttributes.HideBySig) != 0; 59 } 60 }
接下來實現DynamicApiControllerSelector,替換缺省的DefaultHttpControllerSelector。這個程序的主要做法是從路由信息RouteData中獲取當前的Controller和Action等信息。根據Controller信息從DynamicApiControllerManager中獲取已注冊的DynamicApiControllerInfo,創建Controller的描述類DynamicApiControllerDescriptor。DynamicApiControllerManager就是預處理部分,將DynamicApiControllerInfo注冊的類。
1 public class DynamicApiControllerSelector : DefaultHttpControllerSelector 2 { 3 private readonly HttpConfiguration _Configuration; 4 5 public DynamicApiControllerSelector(HttpConfiguration configuration) 6 : base(configuration) 7 { 8 _Configuration = configuration; 9 } 10 11 public override HttpControllerDescriptor SelectController(HttpRequestMessage request) 12 { 13 if (request == null) 14 { 15 return base.SelectController(null); 16 } 17 18 var routeData = request.GetRouteData(); 19 if (routeData == null) 20 { 21 return base.SelectController(request); 22 } 23 24 //Get Area/Controller/Action from route 25 object objArea; 26 if (!routeData.Values.TryGetValue("area", out objArea)) 27 { 28 return base.SelectController(request); 29 } 30 object objController; 31 if (!routeData.Values.TryGetValue("controller", out objController)) 32 { 33 return base.SelectController(request); 34 } 35 object objAction; 36 if (!routeData.Values.TryGetValue("action", out objAction)) 37 { 38 return base.SelectController(request); 39 } 40 41 string areaName = (string)objArea; 42 string controllerName = (string)objController; 43 string actionName = (string)objAction; 44 45 //Normalize serviceNameWithAction 46 if (actionName.EndsWith("/")) 47 { 48 actionName = actionName.Substring(0, actionName.Length - 1); 49 routeData.Values["action"] = actionName; 50 } 51 52 DynamicApiControllerInfo controllerInfo = DynamicApiControllerManager.Get(areaName, controllerName); 53 54 //Create the controller descriptor 55 var controllerDescriptor = new DynamicApiControllerDescriptor(_Configuration, controllerInfo.AreaName, controllerInfo.ControllerName); 56 controllerDescriptor.Properties["__MicroLibraryDynamicApiControllerInfo"] = controllerInfo; 57 return controllerDescriptor; 58 } 59 }
下一步就是實現DynamicApiActionSelector,替換缺省的ApiControllerActionSelector。DynamicApiActionSelector重寫了SelectAction方法,主要做法是Controller的上下文HttpControllerContext中獲取當前的DynamicApiControllerInfo,創建Action的描述類DynamicApiActionDescriptor。
1 public class DynamicApiActionSelector : ApiControllerActionSelector 2 { 3 public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext) 4 { 5 object controllerInfoObj; 6 if (!controllerContext.ControllerDescriptor.Properties.TryGetValue("__MicroLibraryDynamicApiControllerInfo", out controllerInfoObj)) 7 { 8 return GetDefaultActionDescriptor(controllerContext); 9 } 10 11 //Get controller information which is selected by HttpControllerSelector. 12 var controllerInfo = controllerInfoObj as DynamicApiControllerInfo; 13 MicroLibraryExceptionHelper.IsNull(controllerInfo, this.GetType().FullName, TraceLogType.Error, "DynamicApiControllerInfo in ControllerDescriptor.Properties is not a " + typeof(DynamicApiControllerInfo).FullName + " class."); 14 15 string areaName = (string)controllerContext.RouteData.Values["area"]; 16 string controllerName = (string)controllerContext.RouteData.Values["controller"]; 17 string actionName = (string)controllerContext.RouteData.Values["action"]; 18 19 return GetActionDescriptorByActionName(controllerContext, controllerInfo, actionName); 20 } 21 22 private HttpActionDescriptor GetActionDescriptorByActionName(HttpControllerContext controllerContext, DynamicApiControllerInfo controllerInfo, string actionName) 23 { 24 //Get action information by action name 25 DynamicApiActionInfo actionInfo; 26 MicroLibraryExceptionHelper.FalseThrow(controllerInfo.Actions.TryGetValue(actionName, out actionInfo), this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ControllerName + "中,沒有對應的Action:" + actionName); 27 MicroLibraryExceptionHelper.FalseThrow(actionInfo.Verb.IsEqualTo(controllerContext.Request.Method), this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ControllerName + "中的Action:" + actionName + "的HttpVerb不正確,應該為:" + actionInfo.Verb); 28 29 return new DynamicApiActionDescriptor(controllerContext.ControllerDescriptor, actionInfo.Method); 30 } 31 32 private HttpActionDescriptor GetDefaultActionDescriptor(HttpControllerContext controllerContext) 33 { 34 return base.SelectAction(controllerContext); 35 } 36 }
需要說明的是,我們只是按照ActionName來確定最終的Action。還可以按照HttpVeb來確定Actin的方式,我們沒有使用該方式,因此沒有實現,可以參照如下代碼實現:
1 private HttpVerb GetNormalizedVerb(MethodInfo methodInfo) 2 { 3 if (methodInfo.IsDefined(typeof(HttpGetAttribute))) 4 { 5 return HttpVerb.Get; 6 } 7 8 if (methodInfo.IsDefined(typeof(HttpPostAttribute))) 9 { 10 return HttpVerb.Post; 11 } 12 13 if (methodInfo.IsDefined(typeof(HttpPutAttribute))) 14 { 15 return HttpVerb.Put; 16 } 17 18 if (methodInfo.IsDefined(typeof(HttpDeleteAttribute))) 19 { 20 return HttpVerb.Delete; 21 } 22 23 if (methodInfo.IsDefined(typeof(HttpOptionsAttribute))) 24 { 25 return HttpVerb.Options; 26 } 27 28 if (methodInfo.IsDefined(typeof(HttpHeadAttribute))) 29 { 30 return HttpVerb.Head; 31 } 32 33 return HttpVerb.Get; 34 } 35 36 private HttpActionDescriptor GetActionDescriptorByCurrentHttpVerb(HttpControllerContext controllerContext, DynamicApiControllerInfo controllerInfo) 37 { 38 //Check if there is only one action with the current http verb 39 var actionsByVerb = controllerInfo.Actions.Values.Where(action => action.Verb.IsEqualTo(controllerContext.Request.Method)); 40 41 MicroLibraryExceptionHelper.TrueThrow(actionsByVerb.Count() == 0, this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ServiceName + "中,沒有HttpVerb: " + controllerContext.Request.Method + "的Action"); 42 MicroLibraryExceptionHelper.TrueThrow(actionsByVerb.Count() > 1, this.GetType().FullName, TraceLogType.Error, "在Api Controller:" + controllerInfo.ServiceName + "中,HttpVerb: " + controllerContext.Request.Method + "的Action有多個"); 43 44 //Return the single action by the current http verb 45 return new DynamicApiActionDescriptor(controllerContext.ControllerDescriptor, actionsByVerb.First().Method); 46 }View Code
再下一步就是實現DynamicApiActionInvoker,替換缺省的ApiControllerActionInvoker。DynamicApiActionInvoker實現了InvokeActionAsync方法,主要做法是獲取Controller和Action的描述類DynamicApiControllerDescriptor、DynamicApiActionDescriptor。根據Controller的描述類獲取DynamicApiControllerInfo,進一步獲取ControllerInfo的ServiceType,這就是邏輯類的類型信息。通過我們自己的Ioc管理器IocManager,從DI容器中獲取SerivceType的具體實現obj對象。然後從Action描述中獲取MethodInfo,執行方法。返回結果信息,返回前在頭部增加Access-Control-Allow-Origin等信息。
還有一點說明的是需要在Web.config中進行修改,modules中移除WebDavModule,handlers中移除WebDav和OPTIONSVerbHandler。
1 public class DynamicApiActionInvoker : IHttpActionInvoker 2 { 3 private bool authenticatedFlag = DynamicApiConfiguration.GetConfig().AuthenticatedFlag; 4 5 public Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken) 6 { 7 DynamicApiActionDescriptor actionDescriptor = actionContext.ActionDescriptor as DynamicApiActionDescriptor; 8 DynamicApiControllerDescriptor controllerDescriptor = actionContext.ControllerContext.ControllerDescriptor as DynamicApiControllerDescriptor; 9 10 DynamicApiControllerInfo controllerInfo = controllerDescriptor.Properties["__MicroLibraryDynamicApiControllerInfo"] as DynamicApiControllerInfo; 11 12 object obj = IocManager.Instance.Resolve(controllerInfo.ServiceType); 13 14 MicroLibraryExceptionHelper.FalseThrow(Verify(actionContext), this.GetType().FullName, TraceLogType.Error, "Token驗證不通過!"); 15 16 //todo: 異常處理以及參數順序問題 17 object result = actionDescriptor.MethodInfo.Invoke(obj, actionContext.ActionArguments.Where(kvp => !string.Equals(kvp.Key, "sign", StringComparison.OrdinalIgnoreCase) || !string.Equals(kvp.Key, "appKey", StringComparison.OrdinalIgnoreCase)).Select(kvp => kvp.Value).ToArray()); 18 19 return Task.Run<HttpResponseMessage>(() => 20 { 21 string strResult; 22 if (result is String || result is Char) 23 { 24 strResult = obj.ToString(); 25 } 26 else 27 { 28 strResult = SerializerHelper.ToJson(result); 29 } 30 31 var response = new HttpResponseMessage() 32 { 33 Content = new StringContent(strResult, Encoding.GetEncoding("UTF-8"), "application/json") 34 }; 35 36 //需要在web.config中,system.webServer---modules---remove name="WebDavModule" 37 //---handlers---remove name="WebDav"和remove name="OPTIONSVerbHandler" 38 response.Headers.Add("Access-Control-Allow-Origin", "*"); 39 response.Headers.Add("Access-Control-Allow-Headers", "X-Requested-With"); 40 if (actionContext.Request.Method.Method == "Options") 41 { 42 response.Headers.Add("Access-Control-Allow-Methods", "*"); 43 response.Headers.Add("Access-Control-Allow-Headers", "*"); 44 } 45 return response; 46 }); 47 } 48 49 private bool Verify(HttpActionContext actionContext) 50 { 51 if (!authenticatedFlag) return true; 52 53 var attrs = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(); 54 if (attrs.Any()) return true; 55 56 string appKey = (string)actionContext.ActionArguments["appCode"]; 57 58 return DynamicApiHelper.ValidAppSecretSign(actionContext.ActionArguments, appKey); 59 } 60 }
大家可能還注意到我們有Verify方法。這個方法是判斷對WebApi是否有權限訪問。在這種Rest形式的服務中,服務接口對外部暴露出來,因此對用戶調用的授權尤為重要。在Dynamic WebAPI的安全實現過程中,使用了JWT(JsonWebToken)技術,這裡就不做贅述了。