5. abp集成asp.net core


一、前言

參照前篇《4. abp中的asp.net core模塊剖析》,首先放張圖,這也是asp.net core框架上MVC模塊的擴展點

img

二、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流程便大致的分析完了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM