一、前言
參照前篇《4. abp中的asp.net core模塊剖析》,首先放張圖,這也是asp.net core框架上MVC模塊的擴展點
二、abp的mvc對象
AbpAspNetCoreMvcOptions類
從這個類的名稱來看,這個是abp框架里面的asp.net core配置mvc選項類,是abp對asp.net core mvc的封裝。源碼如下:
public class AbpAspNetCoreMvcOptions
{
public ConventionalControllerOptions ConventionalControllers { get; }
public AbpAspNetCoreMvcOptions()
{
ConventionalControllers = new ConventionalControllerOptions();
}
}
這個類只有一個默認構造函數,用於實例化一個名為ConventionalControllerOptions的類,從名稱來看(得益於變量和類的命名規范化)這是Controller的規約配置。
ConventionalControllerOptions類
該類源碼如下:
public class ConventionalControllerOptions
{
public ConventionalControllerSettingList ConventionalControllerSettings { get; }
public List<Type> FormBodyBindingIgnoredTypes { get; }
public ConventionalControllerOptions()
{
ConventionalControllerSettings = new ConventionalControllerSettingList();
FormBodyBindingIgnoredTypes = new List<Type>
{
typeof(IFormFile)
};
}
public ConventionalControllerOptions Create(Assembly assembly, [CanBeNull] Action<ConventionalControllerSetting> optionsAction = null)
{
var setting = new ConventionalControllerSetting(assembly, ModuleApiDescriptionModel.DefaultRootPath);
optionsAction?.Invoke(setting);
setting.Initialize();
ConventionalControllerSettings.Add(setting);
return this;
}
}
在這里要提下asp.net core的options模式,一般XXXOptions類都會在默認的構造函數中實例化一些對象,Options類的作用就是將一個POCO類注冊到服務容器中,使得我們可以在控制器的構造函數中通過IOptions
獲取到TOptions類的實例。
這個類只有一個Create方法,返回當前TOptions類的實例,當然,在這個方法中構造了規約控制器的配置(ConventionalControllerSetting)<對於這個類的描述請查看第三點>。在這個Create方法中,首先實例化一個ConventionalControllerSetting類,參數就是傳過來的規約控制器所在的程序集以及url路由中默認的根目錄(app)。接下來再調用委托,參數就是前面實例化的ConventionalControllerSetting,然后就是實例化(Initialize)操作,檢索規約控制器集合。
ConventionalControllerSetting類
這個規約控制器的配置如下:
public class ConventionalControllerSetting
{
[NotNull]
public Assembly Assembly { get; }
[NotNull]
public HashSet<Type> ControllerTypes { get; } //TODO: Internal?
[NotNull]
public string RootPath
{
get => _rootPath;
set
{
Check.NotNull(value, nameof(value));
_rootPath = value;
}
}
private string _rootPath;
[CanBeNull]
public Action<ControllerModel> ControllerModelConfigurer { get; set; }
[CanBeNull]
public Func<UrlControllerNameNormalizerContext, string> UrlControllerNameNormalizer { get; set; }
[CanBeNull]
public Func<UrlActionNameNormalizerContext, string> UrlActionNameNormalizer { get; set; }
public Action<ApiVersioningOptions> ApiVersionConfigurer { get; set; }
public ConventionalControllerSetting([NotNull] Assembly assembly, [NotNull] string rootPath)
{
Assembly = assembly;
RootPath = rootPath;
ControllerTypes = new HashSet<Type>();
ApiVersions = new List<ApiVersion>();
}
public void Initialize()
{
var types = Assembly.GetTypes()
.Where(IsRemoteService)
.WhereIf(TypePredicate != null, TypePredicate);
foreach (var type in types)
{
ControllerTypes.Add(type);
}
}
private static bool IsRemoteService(Type type)
{
if (!type.IsPublic || type.IsAbstract || type.IsGenericType)
{
return false;
}
var remoteServiceAttr = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(type);
if (remoteServiceAttr != null && !remoteServiceAttr.IsEnabledFor(type))
{
return false;
}
if (typeof(IRemoteService).IsAssignableFrom(type))
{
return true;
}
return false;
}
}
在這個類中有幾個重要的成員變量,首先是Assembly,這個是規約控制器所在的程序集,abp通過這個程序集去檢索規約控制器;第二個就是ControllerTypes,它用於存儲規約控制器類型,而這些類型就是從Assembly程序集中檢索出來的;最后就是RootPath,它表示默認的根目錄,在abp中是"app"。接下來就是兩個方法了,首先是IsRemoteService,顧名思義就是檢索RemoteService,從代碼來看,主要就是檢索RemoteAttribute和繼承自IRemoteService接口的類,為什么要根據這兩個來檢索呢?很簡單,看看IAppService的定義:
public interface IApplicationService :
IRemoteService
{
}
再來看看Initialize方法:
public void Initialize()
{
var types = Assembly.GetTypes()
.Where(IsRemoteService)
.WhereIf(TypePredicate != null, TypePredicate);
foreach (var type in types)
{
ControllerTypes.Add(type);
}
}
它正是通過調用IsRemoteService方法來檢索規約控制器,然后添加到ControllerTypes中的。
三、abp中的應用模型規約
在最上面的aspnetcore mvc擴展圖中,規約模塊(Convention)可以調換掉mvc框架的默認應用模型(Model),從而自定義的控制器等。abp中封裝了這么一個規約類,源碼如下:
public class AbpServiceConvention : IAbpServiceConvention, ITransientDependency
{
private readonly AbpAspNetCoreMvcOptions _options;
public AbpServiceConvention(IOptions<AbpAspNetCoreMvcOptions> options)
{
_options = options.Value;
}
public void Apply(ApplicationModel application)
{
ApplyForControllers(application);
}
protected virtual void ApplyForControllers(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
var controllerType = controller.ControllerType.AsType();
var configuration = GetControllerSettingOrNull(controllerType);
//TODO: We can remove different behaviour for ImplementsRemoteServiceInterface. If there is a configuration, then it should be applied!
//TODO: But also consider ConventionalControllerSetting.IsRemoteService method too..!
if (ImplementsRemoteServiceInterface(controllerType))
{
controller.ControllerName = controller.ControllerName.RemovePostFix(ApplicationService.CommonPostfixes);
configuration?.ControllerModelConfigurer?.Invoke(controller);
ConfigureRemoteService(controller, configuration);
}
else
{
var remoteServiceAttr = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(controllerType.GetTypeInfo());
if (remoteServiceAttr != null && remoteServiceAttr.IsEnabledFor(controllerType))
{
ConfigureRemoteService(controller, configuration);
}
}
}
}
protected virtual void ConfigureRemoteService(ControllerModel controller, [CanBeNull] ConventionalControllerSetting configuration)
{
ConfigureApiExplorer(controller);
ConfigureSelector(controller, configuration);
ConfigureParameters(controller);
}
}
IAbpServiceConvention接口
看看IAbpServiceConvention接口的定義:
public interface IAbpServiceConvention : IApplicationModelConvention
{
}
可以看到這個接口是繼承自aspnet core的IApplicationModelConvention。這個接口有一個Apply方法,該方法,可以簡單的理解為應用規約替換默認的應用模型。源碼如下:
public interface IApplicationModelConvention
{
//
// 摘要:
// Called to apply the convention to the Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModel.
//
// 參數:
// application:
// The Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModel.
void Apply(ApplicationModel application);
}
AbpServiceConvention類
回到AbpServiceConvention類,這個類的構造函數就是用過Options模式獲取到aspnetcoremvcoption類的實例,主要就是在ApplyForController方法上,顧名思義,就是應用於控制器。先看看這個方法:
protected virtual void ApplyForControllers(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
var controllerType = controller.ControllerType.AsType();
var configuration = GetControllerSettingOrNull(controllerType);
//TODO: We can remove different behaviour for ImplementsRemoteServiceInterface. If there is a configuration, then it should be applied!
//TODO: But also consider ConventionalControllerSetting.IsRemoteService method too..!
if (ImplementsRemoteServiceInterface(controllerType))
{
controller.ControllerName = controller.ControllerName.RemovePostFix(ApplicationService.CommonPostfixes);
configuration?.ControllerModelConfigurer?.Invoke(controller);
ConfigureRemoteService(controller, configuration);
}
else
{
var remoteServiceAttr = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(controllerType.GetTypeInfo());
if (remoteServiceAttr != null && remoteServiceAttr.IsEnabledFor(controllerType))
{
ConfigureRemoteService(controller, configuration);
}
}
}
}
在這個方法里面遍歷應用模型里面的控制器(Controller)集合,根據控制器去檢索規約控制器配置(ConventionalControllerSetting),上面也提到了這個類,就是一些約定的配置,如果我們配置了控制器模型(ConventionModel),那么就會在這里被調用。接下來最重要的就是ConfigureRemoteService方法。
ConfigureRemoteService方法
源碼如下:
protected virtual void ConfigureRemoteService(ControllerModel controller, [CanBeNull] ConventionalControllerSetting configuration)
{
ConfigureApiExplorer(controller);
ConfigureSelector(controller, configuration);
ConfigureParameters(controller);
}
在這里就是為我們的遠程服務也就是XXXAppServices類配置詳細的api信息。首先就是配置ApiExplorer,主要就是開放Api檢索,swagger就是調用這個的。Selector就是配置Api的HTTPMethod和路由模型。Parameters則配置Action的參數,主要就是配置復雜類型的參數。
ConfigureApiExplorer
The ApiExplorer contains functionality for discovering and exposing metadata about your MVC application. 這句話是摘自博客 Introduction to the ApiExplorer in ASP.NET Core。我們翻譯過來就是:ApiExplorer包含發現和公開MVC應用程序元數據的功能。從命名我們也能看出來這用來檢索Api的。abp中是如何處理ApiExplorer的呢?
protected virtual void ConfigureApiExplorer(ControllerModel controller)
{
if (controller.ApiExplorer.GroupName.IsNullOrEmpty())
{
controller.ApiExplorer.GroupName = controller.ControllerName;
}
if (controller.ApiExplorer.IsVisible == null)
{
var controllerType = controller.ControllerType.AsType();
var remoteServiceAtt = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(controllerType.GetTypeInfo());
if (remoteServiceAtt != null)
{
controller.ApiExplorer.IsVisible =
remoteServiceAtt.IsEnabledFor(controllerType) &&
remoteServiceAtt.IsMetadataEnabledFor(controllerType);
}
else
{
controller.ApiExplorer.IsVisible = true;
}
}
foreach (var action in controller.Actions)
{
ConfigureApiExplorer(action);
}
}
protected virtual void ConfigureApiExplorer(ActionModel action)
{
if (action.ApiExplorer.IsVisible == null)
{
var remoteServiceAtt = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(action.ActionMethod);
if (remoteServiceAtt != null)
{
action.ApiExplorer.IsVisible =
remoteServiceAtt.IsEnabledFor(action.ActionMethod) &&
remoteServiceAtt.IsMetadataEnabledFor(action.ActionMethod);
}
}
}
這個方法中並沒有做其余的事情,只是檢索RemoteAttribute,然后去配置ApiExplorerModel類的IsVisible,默認的是true,也就是開放出來,提供檢索。swagger就是通過這個來枚舉api的。
ConfigureSelector
這個比較難理解,先看看aspnet core中的SelectorModel源碼:
public class SelectorModel
{
public SelectorModel();
public SelectorModel(SelectorModel other);
public IList<IActionConstraintMetadata> ActionConstraints { get; }
public AttributeRouteModel AttributeRouteModel { get; set; }
//
// 摘要:
// Gets the Microsoft.AspNetCore.Mvc.ApplicationModels.SelectorModel.EndpointMetadata
// associated with the Microsoft.AspNetCore.Mvc.ApplicationModels.SelectorModel.
public IList<object> EndpointMetadata { get; }
}
分析下這個類,首先是ActionConstrains,這是一個接口其中就有一個實現HttpMethodActionConstraint,這個類就是約束了Action的HTTP類型,也就是平時在action上標記的[HTTPGet],一般標記了此特性,aspnetcore會默認實例化一個SelectorModel對象。然后就是最重要的AttributeRouteModel,這個就是路由特性,即平時在action上標記的[Route("xxx/xxx")],同時也實例化了一個SelectorModel對象。看看ConfigureSelector方法:
protected virtual void ConfigureSelector(ControllerModel controller, [CanBeNull] ConventionalControllerSetting configuration)
{
if (controller.Selectors.Any(selector => selector.AttributeRouteModel != null))
{
return;
}
var rootPath = GetRootPathOrDefault(controller.ControllerType.AsType());
foreach (var action in controller.Actions)
{
ConfigureSelector(rootPath, controller.ControllerName, action, configuration);
}
}
protected virtual void ConfigureSelector(string rootPath, string controllerName, ActionModel action, [CanBeNull] ConventionalControllerSetting configuration)
{
if (!action.Selectors.Any())
{
AddAbpServiceSelector(rootPath, controllerName, action, configuration);
}
else
{
NormalizeSelectorRoutes(rootPath, controllerName, action, configuration);
}
}
protected virtual void AddAbpServiceSelector(string rootPath, string controllerName, ActionModel action, [CanBeNull] ConventionalControllerSetting configuration)
{
var httpMethod = SelectHttpMethod(action, configuration);
var abpServiceSelectorModel = new SelectorModel
{
AttributeRouteModel = CreateAbpServiceAttributeRouteModel(rootPath, controllerName, action, httpMethod, configuration),
ActionConstraints = { new HttpMethodActionConstraint(new[] { httpMethod }) }
};
action.Selectors.Add(abpServiceSelectorModel);
}
如果我們配置了路由特性,那么直接返回,否則,我們首先獲取到默認的根目錄(默認是app)。接下來就去配置abp的Selector,首先是選擇HTTPMethod,這個是按照約定來的選擇的,如下:
public static Dictionary<string, string[]> ConventionalPrefixes { get; set; } = new Dictionary<string, string[]>
{
{"GET", new[] {"GetList", "GetAll", "Get"}},
{"PUT", new[] {"Put", "Update"}},
{"DELETE", new[] {"Delete", "Remove"}},
{"POST", new[] {"Create", "Add", "Insert", "Post"}},
{"PATCH", new[] {"Patch"}}
};
根據Action的名稱來選擇(默認是POST),然后實例化一個HttpMethodActionConstraint類,傳入的參數就是HTTPMethod,這個就是前面說到的SelectorModel,最后就是創建路由模型了,我們會去計算一個路由模板,根據這個模板實例化RouteAttribute,再通過這個去實例化AttributeRouteModel,從而構造了SelectorModel的兩個重要屬性。路由模板的計算規則如下:
protected virtual string CalculateRouteTemplate(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration)
{
var controllerNameInUrl = NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration);
var url = $"api/{rootPath}/{controllerNameInUrl.ToCamelCase()}";
//Add {id} path if needed
if (action.Parameters.Any(p => p.ParameterName == "id"))
{
url += "/{id}";
}
//Add action name if needed
var actionNameInUrl = NormalizeUrlActionName(rootPath, controllerName, action, httpMethod, configuration);
if (!actionNameInUrl.IsNullOrEmpty())
{
url += $"/{actionNameInUrl.ToCamelCase()}";
//Add secondary Id
var secondaryIds = action.Parameters.Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList();
if (secondaryIds.Count == 1)
{
url += $"/{{{secondaryIds[0].ParameterName}}}";
}
}
return url;
}
首先,Abp的動態控制器約束是以AppService、ApplicationService、Service結尾的控制器,在這里要注意兩點,如果action參數是id,或者以id結尾且僅有一個參數,那么路由就是:
api/app/xxx/{id}/{action}
或
api/app/xxx/{action}/{id}
構造完url之后就去實例化RouteAttribute特性,構造路由:
return new AttributeRouteModel(
new RouteAttribute(
CalculateRouteTemplate(rootPath, controllerName, action, httpMethod, configuration)
)
);
如果沒有按照abp的action命名約束命名,並標記了HTTPMethod特性,那么就會調用aspnet core默認的路由,源碼如下:
protected virtual void NormalizeSelectorRoutes(string rootPath, string controllerName, ActionModel action, [CanBeNull] ConventionalControllerSetting configuration)
{
foreach (var selector in action.Selectors)
{
var httpMethod = selector.ActionConstraints
.OfType<HttpMethodActionConstraint>()
.FirstOrDefault()?
.HttpMethods?
.FirstOrDefault();
if (httpMethod == null)
{
httpMethod = SelectHttpMethod(action, configuration);
}
if (selector.AttributeRouteModel == null)
{
selector.AttributeRouteModel = CreateAbpServiceAttributeRouteModel(rootPath, controllerName, action, httpMethod, configuration);
}
if (!selector.ActionConstraints.OfType<HttpMethodActionConstraint>().Any())
{
selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] {httpMethod}));
}
}
}
ConfigureParameters
顧名思義,這是用來配置action的參數,默認是調用aspnetcore mvc本身的參數綁定機制:
protected virtual void ConfigureParameters(ControllerModel controller)
{
/* Default binding system of Asp.Net Core for a parameter
* 1. Form values
* 2. Route values.
* 3. Query string.
*/
foreach (var action in controller.Actions)
{
foreach (var prm in action.Parameters)
{
if (prm.BindingInfo != null)
{
continue;
}
if (!TypeHelper.IsPrimitiveExtended(prm.ParameterInfo.ParameterType))
{
if (CanUseFormBodyBinding(action, prm))
{
prm.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() });
}
}
}
}
}
如此,整個abp集成aspnetcore mvc創建並管理自己的api流程便大致的分析完了。