一、前言

参照前篇《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流程便大致的分析完了。

5. abp集成asp.net core的更多相关文章

  1. 从零搭建一个IdentityServer——集成Asp.net core Identity

    前面的文章使用Asp.net core 5.0以及IdentityServer4搭建了一个基础的验证服务器,并实现了基于客户端证书的Oauth2.0授权流程,以及通过access token访问被保护 ...

  2. Dapr 运用之集成 Asp.Net Core Grpc 调用篇

    前置条件: <Dapr 运用> 改造 ProductService 以提供 gRPC 服务 从 NuGet 或程序包管理控制台安装 gRPC 服务必须的包 Grpc.AspNetCore ...

  3. ABP vue+asp.net core yarn serve报 Cannot find module 'typescript/package.json错误

    abp的前端在node install 安装完成了相关的依赖包,直接yarn serve运行相关服务的时候报"Cannot find module 'typescript/package.j ...

  4. ABP官方文档翻译 6.2.1 ASP.NET Core集成

    ASP.NET Core 介绍 迁移到ASP.NET Core? 启动模板 配置 启动类 模块配置 控制器 应用服务作为控制器 过滤器 授权过滤器 审计Action过滤器 校验过滤器 工作单元Acti ...

  5. 学习ABP ASP.NET Core with Angular 环境问题

    1. 前言 最近学习ABP架构 搭建ASP.NET Core with Angular遇到了些问题,折腾了一个礼拜最终在今天解决了,想想这个过程的痛苦就想利用博客记录下来.其实一直想写博客,但因为 时 ...

  6. Azure DevOps+Docker+Asp.NET Core 实现CI/CD(二.创建CI持续集成管道)

    前言 本文主要是讲解如何使用Azure DevOps+Docker 来实现持续集成Asp.NET Core项目(当然 也可以是任意项目). 上一篇: Azure DevOps+Docker+Asp.N ...

  7. ASP.NET Core 的 `Core` 有几种写法?

    一.概述 本文将会根据情况持续更新. 作为一个 Framework,ASP.NET Core 提供了诸多的扩展点.使用内置的组件和默认的配置通常就能够满足部分需求,当需要扩展的时就需要先去找出这些扩展 ...

  8. ASP.NET Core & Docker 实战经验分享

    一.前言 最近一直在研究和实践ASP.NET Core.Docker.持续集成.在ASP.NET Core 和 Dcoker结合下遇到了一些坑,在此记录和分享,希望对大家有一些帮助. 二.中间镜像 我 ...

  9. Azure DevOps+Docker+Asp.NET Core 实现CI/CD(一 .简介与创建自己的代理池)

    前言 本文主要是讲解如何使用Azure DevOps+Docker 来实现持续集成Asp.NET Core项目(当然 也可以是任意项目). 打算用三个篇幅来记录完整的全过程 觉得有帮助的朋友~可以左上 ...

随机推荐

  1. 浅谈.NET中的反射

    一.概述  1.通过反射可以提供类型信息,从而使得我们开发人员在运行时能够利用这些信息构造和使用对象 2.反射机制允许程序在执行过程中动态地添加各种功能 二.运行时类型标识 1.运行时类型标志(RTT ...

  2. Linux -- 进程管理之僵尸进程

    UNIX 存在一种机制:在每个进程退出的同时,操作系统释放该进程所有资源,但仍然保留一定的信息(PID / Status / runtime),直到父进程执行 wait() / waitpid(),以 ...

  3. Android Studio 2.2 NDK开发环境搭建

    转载请标明出处:http://blog.csdn.net/shensky711/article/details/52763192 本文出自: [HansChen的博客] Android应用程序使用ND ...

  4. 利用keytool、openssl生成证书文件

    转载请标明出处:http://blog.csdn.net/shensky711/article/details/52225073 本文出自: [HansChen的博客] 用openssl指令逐步生成各 ...

  5. 解决WebUploader 上传按钮按F12 才行的问题

    遇到了 WebUploader 插件的上传按钮点击无效(此时鼠标在按钮任何位置时,按钮都没变化).按F12 之后才有反应(此时鼠标在按钮任何位置时,按钮颜色都会变深) 的问题,网上查到一些答案,找到了 ...

  6. Linux发展史及安装

    操作系统 什么是操作系统 人与硬件的中介/桥梁 操作系统的组成  操作系统可以理解为一个鸡蛋 蛋黄    内核                   Linux内核  托瓦斯  鸡蛋清  命令解释器(s ...

  7. Docker部署Mysql集群

    单节点数据库的弊病 大型互联网程序用户群体庞大,所以架构必须要特殊设计 单节点的数据库无法满足性能上的要求 单节点的数据库没有冗余设计,无法满足高可用 单节点MySQL的性能瓶领颈 2016年春节微信 ...

  8. 基于springboot的web项目最佳实践

    springboot 可以说是现在做javaweb开发最火的技术,我在基于springboot搭建项目的过程中,踩过不少坑,发现整合框架时并非仅仅引入starter 那么简单. 要做到简单,易用,扩展 ...

  9. Python面试的一些心得,与Python练习题分享

    关于基础 项目打算招聘一个自动化运维,主要需求是python.Linux与shell脚本能力.但面试几天发现一些问题: 简历虚假 这个不管哪行,简历含水量大都是普遍存在的,看简历犀利的一比,一面是能力 ...

  10. JGit----将 Git 嵌入你的应用

    如果你想在一个 Java 程序中使用 Git ,有一个功能齐全的 Git 库,那就是 JGit . JGit 是一个用 Java 写成的功能相对健全的 Git 的实现,它在 Java 社区中被广泛使用 ...