簡述 對 Abp的動態web api的改造過程
注冊
1. 首先通過反射將《服務類型》通過ApiControllerBuilder 構建成成 DynamicApiControllerInfo
2. 在DynamicApiControllerInfo中同時構建DynamicApiActionInfo
3. Ioc注入DynamicApiController<TService> Tservice就是最開始的《服務類型》
3. 最后將DynamicApiControllerInfo添加到DynamicApiControllerManager,通過ServiceName緩存
執行
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處理流程
改造
Action執行的改造
實際在Abp中 DyanamicHttpActionDescriptor 的 ExecuteAsync 中實際是通過AOP攔截實現的.這里我做了修改
首先將DynamicController改為組合的方式注入IService來作為代理對象如下圖

然后執行的時候采用獲取IDynamicApiController 的ProxyObject 來使用反射執行

其中由於MVC並沒有放出ReflectedHttpActionDescriptor.ActionExecutor 這個類型,所以用了點技巧。
支持重載
1. 首先在 DynamicApiControllerInfo 中增加屬性 FullNameActions 類型和Actions 一致
2. 然后再初始化的時候同時初始化FullNameActions ,Action的key是Name,FullNameActions 是Method.ToString()[這種包含的信息更多,可作為唯一標識]
3. 最后在客戶端調用的時候放到Header即可區分,實現函數重載
支持多個復雜參數
在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 代碼如下
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,分別對應兩個參數即可
客戶端代理
我這里使用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容器,自己后面做一些組件的時候也相對容易。下一篇文章將介紹一個作業調度模塊的設計,就會用到這
