基於Abp的WebApi容器

 簡述 對 Abp的動態web api的改造過程

註冊

1. 首先經過反射將《服務類型》經過ApiControllerBuilder 構建成成 DynamicApiControllerInfo
2. 在DynamicApiControllerInfo中同時構建DynamicApiActionInfo
3. Ioc注入DynamicApiController<TService> Tservice就是最開始的《服務類型》
3. 最後將DynamicApiControllerInfo添加到DynamicApiControllerManager,經過ServiceName緩存web

執行

1. AbpHttpControllerSelector 經過路由獲取出「service」 這個參數即ServiceName
2. 經過ServiceName從DynamicApiControllerManager中獲取DynamicApiControllerInfo 的信息
3. 將DynamicApiControllerInfo 放入HttpControllerDescriptor.Properties中,返回DynamicHttpControllerDescriptor給MVC處理流程
4. AbpControllerActivator 經過DynamicApiControllerInfor中的ControllerType激活Controller
5. AbpApiControllerActionSelector 獲取HttpControllerDescriptor.Properties中的將DynamicApiControllerInfo 信息
6. AbpApiControllerActionSelector 經過 路由的{action}參數獲取 方法名
7. AbpApiControllerActionSelector 在 DynamicApiControllerInfor經過方法名獲取DynamicApiActionInfo 的信息
8. 最後返回DyanamicHttpActionDescriptor 給MVC處理流程api

改造

Action執行的改造

實際在Abp中 DyanamicHttpActionDescriptor 的 ExecuteAsync 中實際是經過AOP攔截實現的.這裏我作了修改數組

首先將DynamicController改成組合的方式注入IService來做爲代理對象以下圖緩存

DynamicController

而後執行的時候採用獲取IDynamicApiController 的ProxyObject 來使用反射執行架構

執行

 

其中因爲MVC並無放出ReflectedHttpActionDescriptor.ActionExecutor 這個類型,因此用了點技巧。併發

支持重載

1. 首先在 DynamicApiControllerInfo 中增長屬性 FullNameActions 類型和Actions 一致
2. 而後再初始化的時候同時初始化FullNameActions ,Action的key是Name,FullNameActions 是Method.ToString()[這種包含的信息更多,可做爲惟一標識]
3. 最後在客戶端調用的時候放到Header便可區分,實現函數重載app

支持多個複雜參數

在ParameterBindingRules 中添加規則框架

 //增長服務中多個參數的狀況
            ApiGlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, descriptor =>
            {
                if (descriptor.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get) ||
                    descriptor.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Delete))
                    return null;
                if (descriptor.ActionDescriptor.GetParameters().Count(item => !item.ParameterType.IsSimpleUnderlyingType()) < 2)
                    return null;
                if (descriptor.ParameterType.IsSimpleUnderlyingType())
                    return null;
                if (descriptor.ParameterType.GetCustomAttribute(typeof(ParameterBindingAttribute)) != null)
                    return null;
                var config = descriptor.Configuration;
                IEnumerable<MediaTypeFormatter> formatters = config.Formatters;
                var validators = config.Services.GetBodyModelValidator();
                return new MultiPostParameterBinding(descriptor, formatters, validators);
            });

  

2. 在MultiPostParameterBinding 代碼以下async

 

public class MultiPostParameterBinding : FormatterParameterBinding
    {
        // Magic key to pass cancellation token through the request property bag to maintain backward compat.
        private const string CancellationTokenKey = "MS_FormatterParameterBinding_CancellationToken";

        public MultiPostParameterBinding(HttpParameterDescriptor descriptor, IEnumerable<MediaTypeFormatter> formatters,
            IBodyModelValidator bodyModelValidator) :
            base(descriptor, formatters, bodyModelValidator)
        {
        }

        public override bool WillReadBody => false;

        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
            HttpActionContext actionContext,
            CancellationToken cancellationToken)
        {
            var paramFromBody = Descriptor;
            var type = paramFromBody.ParameterType;
            var request = actionContext.ControllerContext.Request;
            IFormatterLogger formatterLogger =
                new ModelStateFormatterLogger(actionContext.ModelState, paramFromBody.ParameterName);
            var task = ExecuteBindingAsyncCore(metadataProvider, actionContext, paramFromBody, type, request,
                formatterLogger, cancellationToken);
            return task;
        }

        // Perf-sensitive - keeping the async method as small as possible
        private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider,
            HttpActionContext actionContext,
            HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request,
            IFormatterLogger formatterLogger,
            CancellationToken cancellationToken)
        {
            // pass the cancellation token through the request as we cannot call the ReadContentAsync overload that takes
            // CancellationToken for backword compatibility reasons.
            request.Properties[CancellationTokenKey] = cancellationToken;

            //todo 這裏若是隻是服務端使用須要要構造一個匿名對象去接受數據

            Dictionary<string, object> allModels;
            if (actionContext.ActionArguments.ContainsKey("MultiDictionary"))
            {
                allModels = actionContext.ActionArguments["MultiDictionary"] as Dictionary<string, object>;
            }
            else
            {
                allModels = await ReadContentAsync(request, typeof(Dictionary<string, object>), Formatters,
                        formatterLogger, cancellationToken)
                    as Dictionary<string, object>;
                actionContext.ActionArguments["MultiDictionary"] = allModels;
            }
            if (allModels != null)
            {
                var model = JsonConvert.DeserializeObject(allModels[paramFromBody.ParameterName].ToString(), type);
                actionContext.ActionArguments[paramFromBody.ParameterName] = model;
                // validate the object graph.
                // null indicates we want no body parameter validation
                if (BodyModelValidator != null)
                    BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);
            }
        }
    }

  

原理實際是若是有兩個複雜類型User user和Company company,那麼客戶端須要傳入的是一個字典有兩個key,user和company,分別對應兩個參數便可ide

客戶端代理

我這裏使用Abp並非爲了基於界面作,而是爲了作服務化,客戶端經過接口,而後走http請求,最後由服務端的服務實現去執行結果最後返回,有點像之前webservice不過更加輕量級。
先來看看實現,至於做用會在後面的文章中陸續分享。

RealProxy

動態代理/真實代理,大部分狀況下用來做爲aop的實現,例如,日誌,事務等,這裏咱們直接用來代理髮送Http請求。實現以下 

public class ClientProxy : RealProxy, IRemotingTypeInfo
    {
        private readonly Type _proxyType;
        private readonly IActionInvoker _actionInvoker;

        private List<string> _unProxyMethods = new List<string>
        {
            "InitContext",
            "Dispose",
        };

        public ClientProxy(Type proxyType, IActionInvoker actionInvoker) :
            base(proxyType)
        {
            _proxyType = proxyType;
            _actionInvoker = actionInvoker;
        }

        public bool CanCastTo(Type fromType, object o)
        {
            return fromType == _proxyType || fromType.IsAssignableFrom(_proxyType);
        }

        public string TypeName { get { return _proxyType.Name; } set { } }


        private static ConcurrentDictionary<Type, List<MethodInfo>> _typeMethodCache = new ConcurrentDictionary<Type, List<MethodInfo>>();
        private List<MethodInfo> GetMethods(Type type)
        {
            return _typeMethodCache.GetOrAdd(type, item =>
            {
                List<MethodInfo> methods = new List<MethodInfo>(type.GetMethods());
                foreach (Type interf in type.GetInterfaces())
                {
                    foreach (MethodInfo method in interf.GetMethods())
                        if (!methods.Contains(method))
                            methods.Add(method);
                }
                return methods;

            });
        }


        public override IMessage Invoke(IMessage msg)
        {
            // Convert to a MethodCallMessage
            IMethodCallMessage methodMessage = new MethodCallMessageWrapper((IMethodCallMessage)msg);
            var methodInfo = GetMethods(_proxyType).FirstOrDefault(item => item.ToString() == methodMessage.MethodBase.ToString());
            //var argumentTypes = TypeUtil.GetArgTypes(methodMessage.Args);
            //var methodInfo = _proxyType.GetMethod(methodMessage.MethodName, argumentTypes) ?? methodMessage.MethodBase as MethodInfo;
            object objReturnValue = null;
            if (methodMessage.MethodName.Equals("GetType") && (methodMessage.ArgCount == 0))
            {
                objReturnValue = _proxyType;
            }
            else if (methodInfo != null)
            {
                if (methodInfo.Name.Equals("Equals")
                    || methodInfo.Name.Equals("GetHashCode")
                    || methodInfo.Name.Equals("ToString")
                    || methodInfo.Name.Equals("GetType"))
                {

                    throw CoralException.ThrowException<ClientProxyErrorCode>(item => item.UnValideMethod);
                }
                if (_unProxyMethods.All(item => item != methodInfo.Name))
                {
                    objReturnValue = _actionInvoker.Invoke(_proxyType, methodInfo, methodMessage.Args);
                }
            }
            // Create the return message (ReturnMessage)
            return new ReturnMessage(objReturnValue, methodMessage.Args, methodMessage.ArgCount,
                methodMessage.LogicalCallContext, methodMessage);
        }

 IActionInvoker 

方法調用者這裏抽象成接口是由於我有兩個實現,一個是基於WebApi一個是基於Hession,之後還可能有其餘的,這樣代理的邏輯能夠複用,只是實現不一樣的請求轉發就能夠了。以RestActionInvoker爲例(就是webApi)

public class RestActionInvoker : IActionInvoker
    {
        private readonly string _host;
        private readonly string _preFixString;

        public RestActionInvoker(string host, string preFixString = "")
        {
            _host = host;
            _preFixString = string.IsNullOrEmpty(preFixString) ? "api" : preFixString;
        }

        /// <summary>
        /// 執行方法
        /// </summary>
        /// <param name="proxyType"></param>
        /// <param name="methodInfo"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public object Invoke(Type proxyType, MethodInfo methodInfo, params object[] parameters)
        {
            var url = TypeUtil.GetUrl(proxyType, methodInfo, _preFixString);
            var requestType = GetRequestType(methodInfo);
            ResultMessage result;
            switch (requestType)
            {
                case RequestType.Get:
                    {
                        var getParam = PrepareGetParams(methodInfo, parameters);
                        result = AppComminutService.Get<ResultMessage>(_host, url, getParam, PrepareHeader(methodInfo));
                        break;

                    }
                case RequestType.Delete:
                    {
                        var delParams = PrepareGetParams(methodInfo, parameters);
                        result = AppComminutService.Delete<ResultMessage>(_host, url, delParams, PrepareHeader(methodInfo));
                        break;

                    }
                case RequestType.Post:
                    {
                        var bodyParam = PrepareBodyParams(methodInfo, parameters);
                        var simpaleParam = PrepareSampleParams(methodInfo, parameters);
                        url = AppendGetParamToUrl(url, simpaleParam);
                        result = AppComminutService.Post<object, ResultMessage>(_host, url, bodyParam.Count == 1 ? bodyParam.First().Value : bodyParam, PrepareHeader(methodInfo));
                        break;

                    }
                case RequestType.Put:
                    {
                        var simpaleParam = PrepareSampleParams(methodInfo, parameters);
                        url = AppendGetParamToUrl(url, simpaleParam);
                        var putParam = PrepareBodyParams(methodInfo, parameters);
                        result = AppComminutService.Put<object, ResultMessage>(_host, url, putParam.Count == 1 ? putParam.First().Value : putParam, PrepareHeader(methodInfo));
                        break;
                    }
                default:
                    throw new ArgumentOutOfRangeException();
            }

            if (result.State == ResultState.Success)
            {
                if (result.Data != null)
                {
                    try
                    {
                        return JsonConvert.DeserializeObject(result.Data.ToString(), methodInfo.ReturnType);
                    }
                    catch
                    {
                        return result.Data;
                    }
                }
                return null;
            }

            throw CoralException.ThrowException<ClientProxyErrorCode>(item => item.UnknowError, result.Message);
        }


        private RequestType GetRequestType(MethodInfo methodInfo)
        {
            if (methodInfo.GetCustomAttribute(typeof(HttpGetAttribute)) != null)
                return RequestType.Get;
            else if (methodInfo.GetCustomAttribute(typeof(HttpDeleteAttribute)) != null)
                return RequestType.Delete;
            else if (methodInfo.GetCustomAttribute(typeof(HttpPostAttribute)) != null)
                return RequestType.Post;
            else if (methodInfo.GetCustomAttribute(typeof(HttpPutAttribute)) != null)
                return RequestType.Put;
            else if (methodInfo.Name.StartsWith("Get") && methodInfo.GetParameters().All(item => item.ParameterType.IsSimpleUnderlyingType()))
                return RequestType.Get;
            else if (methodInfo.Name.StartsWith("Delete") && methodInfo.GetParameters().All(item => item.ParameterType.IsSimpleUnderlyingType()))
                return RequestType.Delete;
            else if (methodInfo.Name.StartsWith("Remove") && methodInfo.GetParameters().All(item => item.ParameterType.IsSimpleUnderlyingType()))
                return RequestType.Delete;
            else if (methodInfo.Name.StartsWith("Update") && methodInfo.GetParameters().Any(item => !item.ParameterType.IsSimpleUnderlyingType()))
                return RequestType.Put;
            else if (methodInfo.Name.StartsWith("Modify") && methodInfo.GetParameters().Any(item => !item.ParameterType.IsSimpleUnderlyingType()))
                return RequestType.Put;
            return RequestType.Post;
        }

        /// <summary>
        /// 準備Header的數據
        /// </summary>
        /// <returns></returns>
        private Dictionary<string, string> PrepareHeader(MethodInfo methodInfo)
        {
            var header = new Dictionary<string, string>();

            if (UserContext.Current != null && UserContext.Current.User != null)
            {
                header.Add("UserId", UserContext.Current.User.Id.ToString());
                header.Add("UserName", HttpUtility.UrlEncode(UserContext.Current.User.Name));
                header.Add("UserToken", UserContext.Current.User.Token);
                header.Add("UserAccount", HttpUtility.UrlEncode(UserContext.Current.User.Account));

                LoggerFactory.Instance.Info($"{methodInfo}存在認證信息,線程{Thread.CurrentThread.ManagedThreadId}");
                LoggerFactory.Instance.Info($"用戶信息爲:{JsonConvert.SerializeObject(header)}");

            }
            else
            {
                header.Add("IsAnonymous", "true");
                LoggerFactory.Instance.Info($"{methodInfo}不存在認證信息,線程{Thread.CurrentThread.ManagedThreadId}");
            }
            if (SessionContext.Current != null)
                header.Add("SessionId", HttpUtility.UrlEncode(SessionContext.Current.SessionKey));
            if (PageContext.Current != null)
                header.Add("ConnectionId", HttpUtility.UrlEncode(PageContext.Current.PageKey));

            if (methodInfo.DeclaringType != null && methodInfo.DeclaringType.GetMethods().Count(item => item.Name == methodInfo.Name) > 1)
                header.Add("ActionInfo", CoralSecurity.DesEncrypt(methodInfo.ToString()));

            return header;
        }

        /// <summary>
        /// 準備Url的請求數據
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        private Dictionary<string, string> PrepareGetParams(MethodInfo methodInfo, params object[] parameters)
        {
            var paramInfos = methodInfo.GetParameters();
            var dict = new Dictionary<string, string>();

            for (int i = 0; i < paramInfos.Length; i++)
            {
                //todo 這裏要支持嵌套
                dict.Add(paramInfos[i].Name, parameters[i]?.ToString() ?? string.Empty);
            }
            return dict;
        }

        /// <summary>
        /// 準備Body的參數
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        private Dictionary<string, object> PrepareBodyParams(MethodInfo methodInfo, params object[] parameters)
        {

            var paramInfos = methodInfo.GetParameters();
            var dict = new Dictionary<string, object>();


            for (var i = 0; i < paramInfos.Length; i++)
            {
                var item = paramInfos[i];
                if (item.ParameterType.IsSimpleType())
                    continue;
                dict.Add(item.Name, parameters[i]);
            }
            return dict;
        }

        /// <summary>
        /// 準備Url的參數
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        private Dictionary<string, string> PrepareSampleParams(MethodInfo methodInfo, params object[] parameters)
        {
            var paramInfos = methodInfo.GetParameters();
            var dict = new Dictionary<string, string>();

            for (var i = 0; i < paramInfos.Length; i++)
            {
                var item = paramInfos[i];
                if (!item.ParameterType.IsSimpleType())
                    continue;
                dict.Add(item.Name, parameters[i]?.ToString() ?? string.Empty);
            }
            return dict;
        }

        /// <summary>
        /// 拼接url參數
        /// </summary>
        /// <param name="url"></param>
        /// <param name="dict"></param>
        /// <returns></returns>
        private string AppendGetParamToUrl(string url, Dictionary<string, string> dict)
        {
            if (dict == null || dict.Count == 0)
                return url;

            if (url.Contains("?"))
                url += "&";
            else
                url += "?";
            url += string.Join("&", dict.Select(item => $"{item.Key}={item.Value ?? string.Empty}"));
            return url;
        }
    }


    internal enum RequestType
    {
        Get = 0,
        Post,
        Put,
        Delete,
    }

  

重點在於要結合服務端的實現考慮怎麼獲得請求,參數組織,認證信息組織等等, 代碼邏輯應該還算清晰,能夠從Invoke 方法開始看起來

客戶端代理注入

注入代碼比較簡單,就是掃描全部的接口,而後利用動態代理注入Ioc容器便可

 //代理工廠代碼以下
   public class ProxyFactory
    {

        public  static TService GetService<TService>(ProxyType proxyType,string host,string preFixedString)
        {
            switch (proxyType)
            {
                case ProxyType.Rest:
                    return (TService)new Core.ClientProxy(typeof(TService), new RestActionInvoker(host, preFixedString)).GetTransparentProxy();
                case ProxyType.Hessian:
                    return (TService)new Core.ClientProxy(typeof(TService), new HessianActionInvoker(host)).GetTransparentProxy();
                default:
                    return (TService)new Core.ClientProxy(typeof(TService), new RestActionInvoker(host, preFixedString)).GetTransparentProxy();
            }
        }

        public static object GetService(Type serviceType, ProxyType proxyType, string host, string preFixedString)
        {
            switch (proxyType)
            {
                case ProxyType.Rest:
                    return new Core.ClientProxy(serviceType, new RestActionInvoker(host, preFixedString)).GetTransparentProxy();
                case ProxyType.Hessian:
                    return new Core.ClientProxy(serviceType, new HessianActionInvoker(host)).GetTransparentProxy();
                default:
                    return new Core.ClientProxy(serviceType, new RestActionInvoker(host, preFixedString)).GetTransparentProxy();
            }
        }
    }


    public enum ProxyType
    {
        Rest = 0,
        Hessian,
    }
    
    //利用Ioc注入代碼以下

 MetaDataManager.Type.GetAll().ForEach(type =>
            {
                if (!type.IsInterface)
                    return;
                if (type.GetInterface(typeof(IService).Name) == null)
                    return;
                if (type.GetGenericArguments().Length != 0)
                    return;
                //todo 這裏最好放在特性裏面判斷
                if (type.GetCustomAttribute(typeof(InjectAttribute)) != null)
                    return;
                //Debug.WriteLine("客戶端註冊類型{0}", type);


                var obj = ProxyFactory.GetService(type, ProxyType.Rest, "http://localhost:28135", "apiservice");
                UnityService.RegisterInstance(type, obj);
                //var obj = ProxyFactory.GetService(type, ProxyType.Hessian, "http://localhost:28135", "");
                //UnityService.RegisterInstance(type, obj);
                //var interfaces = type.GetInterfaces();
                //LinqExtensions.ForEach(interfaces, item =>
                //{
                //    Debug.WriteLine(type.FullName + "-" + item.FullName);

                //    UnityService.RegisterType(item, type);
                //});
            });

  

應用

 一個簡單的Demo

工程結構以下

結構

ClientProxy是客戶端測試工程 ClientModule中作掃描注入

ServiceContractTest是契約:包含服務接口和DTO

ServiceHost :包含Abp的註冊DynamicController和契約的實現

調用代碼以下:

ITestService service = UnityService.Resolve<ITestService>();
var items = service.GetNames("123");

 

實現和契約比較簡單就不貼代碼了,這一篇貼了不少代碼了。冏!_! 

Api容器

有上面測試工程的結構之後,順勢就能夠想象到一個Api容器的概念:

經過將Client和ServiceHost邏輯放到框架層面,客戶端只需引用這兩個dll,客戶端只引用契約。

服務端將契約放到bin下面的Module(或者其餘目錄) 經過反射加載程序集便可。

總體來講Host能夠爲幾種形態,IIs/WindowsServer/Console/....不管哪一種只要放入dll便可實現Host

 

DDD和模塊化

DDD典型六邊形架構中,保證應用層和領域層的內聚性。簡單來講如下幾個dll:
1. Contract.XXX
2. App.XXX
3. Domain.XXX
4. Repository.XXX

以下幾個dll組織成一個內聚的BC也能夠理解爲模塊,這樣你可讓任意一個模塊切換部署方式IIS/Service/Console等。業務更加脫離於技術

架構的合與分

最開始的單體架構中若是咱們以DDD的方式來組織業務,如上所述那幾個dll是一組業務/模塊/bc,那麼咱們多組業務在最開始業務複雜度不高的狀況下以單體應用的方式來組成解決方案。

到後來隨着業務複雜度增大,或者併發性能的因素影響須要對應用進行切割,只須要將解決方案進行從新組織,引入代理和Api容器便可實現幾乎零修改的方式將一個單體的應用分割到不一樣的進程中進行調用。

微服務業務治理

1. 假設咱們把一個Contract當作一個業務模塊也就是一個服務
2. 將Contract上傳到nuget中
3. 擴展nuget功能,提取Contract的註釋做爲元數據持久化
4. 提供搜索功能,搜索關鍵字便可獲取相關的服務。而後下載dll實現

這裏經過擴展nuget把全部的服務進行集中管理,能夠進行分組,至關於構建了一個業務的倉庫,全部的業務爲都進行集中的存放,這樣若是要引用某項服務只須要獲取相關dll結合客戶端代理便可開始開發對應的功能。

微服務技術

1. 由於開發人員接觸到純粹的業務,並且屏蔽掉了技術細節,那麼架構上就留出了迭代的空間
2. 若是某個服務作集羣,那麼能夠經過某個Contract和服務註冊發現結合修改客戶端代理實現。等等這些架構上的工做均可以循序漸進的進行,而且業務上能夠實現無影響的遷移


總結

基於這個Api容器和客戶端代理的方式結合DDD可讓業務和技術並行幾乎無影響分別進行迭代,避免由於技術架構的修改致使大量的業務代碼的修改。

基於這個Api容器,本身後面作一些組件的時候也相對容易。下一篇文章將介紹一個做業調度模塊的設計,就會用到這

相關文章
相關標籤/搜索