5. abp集成asp.net core

2023-10-19 19:35:34 浏览数 (2)

一、前言

参照前篇《4. abp中的asp.net core模块剖析》,首先放张图,这也是asp.net core框架上MVC模块的扩展点

二、abp的mvc对象
AbpAspNetCoreMvcOptions类

从这个类的名称来看,这个是abp框架里面的asp.net core配置mvc选项类,是abp对asp.net core mvc的封装。源码如下:

代码语言:javascript复制
public class AbpAspNetCoreMvcOptions
{
    public ConventionalControllerOptions ConventionalControllers { get; }

    public AbpAspNetCoreMvcOptions()
    {
        ConventionalControllers = new ConventionalControllerOptions();
    }
}

这个类只有一个默认构造函数,用于实例化一个名为ConventionalControllerOptions的类,从名称来看(得益于变量和类的命名规范化)这是Controller的规约配置。

ConventionalControllerOptions类

该类源码如下:

代码语言:javascript复制
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类

这个规约控制器的配置如下:

代码语言:javascript复制
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的定义:

代码语言:javascript复制
 public interface IApplicationService : 
        IRemoteService
{
}

再来看看Initialize方法:

代码语言:javascript复制
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中封装了这么一个规约类,源码如下:

代码语言:javascript复制
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接口的定义:

代码语言:javascript复制
public interface IAbpServiceConvention : IApplicationModelConvention
{
}

可以看到这个接口是继承自aspnet core的IApplicationModelConvention。这个接口有一个Apply方法,该方法,可以简单的理解为应用规约替换默认的应用模型。源码如下:

代码语言:javascript复制
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方法上,顾名思义,就是应用于控制器。先看看这个方法:

代码语言:javascript复制
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方法

源码如下:

代码语言:javascript复制
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的呢?

代码语言:javascript复制
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源码:

代码语言:javascript复制
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方法:

代码语言:javascript复制
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,这个是按照约定来的选择的,如下:

代码语言:javascript复制
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的两个重要属性。路由模板的计算规则如下:

代码语言:javascript复制
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结尾且仅有一个参数,那么路由就是:

代码语言:javascript复制
api/app/xxx/{id}/{action}
或
api/app/xxx/{action}/{id}

构造完url之后就去实例化RouteAttribute特性,构造路由:

代码语言:javascript复制
return new AttributeRouteModel(
    new RouteAttribute(
        CalculateRouteTemplate(rootPath, controllerName, action, httpMethod, configuration)
    )
);

如果没有按照abp的action命名约束命名,并标记了HTTPMethod特性,那么就会调用aspnet core默认的路由,源码如下:

代码语言:javascript复制
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本身的参数绑定机制:

代码语言:javascript复制
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流程便大致的分析完了。

0 人点赞