一、前言

参照前篇《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. centos7关闭默认firewall,启用iptables

    CentOS 7.0默认使用"firewall"防火墙 一:关闭firewall1.直接关闭防火墙systemctl stop firewalld.service 2.禁止fire ...

  2. 自学python day 10 函数的动态参数、命名空间、作用域

    作业提升: s为字符串 s.isalnum() 所有字符都是字母或者数字 s.isalpha() 所有字符都是字母 s.isdigit() 所有字符否是数字 2. for i in range(1,1 ...

  3. Fragment事务管理源码分析

    转载请标明出处:http://blog.csdn.net/shensky711/article/details/53132952 本文出自: [HansChen的博客] 概述 在Fragment使用中 ...

  4. ASP.NET Core 中的 ObjectPool 对象重用(一)

    前言 对象池是一种设计模式,一个对象池包含一组已经初始化过且可以使用的对象,而可以在有需求时创建和销毁对象.池的对象可以从池中取得对象,对其进行操作处理,并在不需要时归还给池子而非直接销毁他,他是一种 ...

  5. ES集群操作原理

    路由 当你索引一个文档,它被存储在单独一个主分片上.Elasticsearch 是如何知道文档属于哪个分片的呢?当你创建一个新文档,它是如何知道是应该存储在分片 1 还是分片 2 上的呢? 进程不能是 ...

  6. 格式化JS代码

    平常在项目中经常会遇到下载别人的js文件都是加密过的,不方便阅读都是一整行, 个人无法进行阅读,浏览器能够识别出来,所以就可以使用浏览器进行格式化js代码: 1.打开浏览器chrome为例,打开使用j ...

  7. [学习笔记] [数据分析] 02、NumPy入门与应用

    01.NumPy基本功能 ※ 数据类型的转换在实际操作过程中很重要!!! ※ ※ ndarray的基本索引与切片 ※ 布尔型数组的长度必须跟被索引的轴长度一致 花式索引是利用“整数数组”进行索引. 整 ...

  8. PAT-2019年秋季考试-甲级

    7-1 Forever (20 分) #include <bits/stdc++.h> using namespace std; int N,K,m,number[10]; multima ...

  9. 通过Javascript 创建POST提交, 无页面刷新下载

    前端准备: //Download the template through "POST" request function getTargertContainer() { var ...

  10. PHP数组总汇

    数组,顾名思义,本质上就是一系列数据的组合.在这个组合中,每个数据都是独立的,可以对每个单独的数据进行分配和读取.PHP对数据的操作能力非常强大,尤其是PHP为程序开发人员提供了大量方便.易懂的数组操 ...