简介

Panda.DynamicWebApi 是一个动态生成WebApi的组件,生成的API符合Restful风格,受启发于ABP。它可以根据符合条件的类来生成WebApi,由MVC框架直接调用逻辑,无性能问题,完美兼容Swagger来构建API说明文档,与手动编写Controller相比并无区别。

应用场景:DDD架构中的应用逻辑层,可使用本组件来直接生成WebApi,而无需再用Controller来调用。

Asp .Net Core 集成 Panda.DynamicWebApi

(1)新建一个 ASP.NET Core WebApi(或MVC) 项目

(2)通过Nuget安装组件

Install-Package Panda.DynamicWebApi

(3)创建一个类命名为 AppleAppService,实现 IDynamicWebApi 接口,并加入特性 [DynamicWebApi]

[DynamicWebApi]
public class AppleAppService: IDynamicWebApi
{
private static readonly Dictionary<int,string> Apples=new Dictionary<int, string>()
{
[1]="Big Apple",
[2]="Small Apple"
}; /// <summary>
/// Get An Apple.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id:int}")]
public string Get(int id)
{
if (Apples.ContainsKey(id))
{
return Apples[id];
}
else
{
return "No Apple!";
}
} /// <summary>
/// Get All Apple.
/// </summary>
/// <returns></returns>
public IEnumerable<string> Get()
{
return Apples.Values;
} public void Update(UpdateAppleDto dto)
{
if (Apples.ContainsKey(dto.Id))
{
Apples[dto.Id] =dto.Name;
}
} /// <summary>
/// Delete Apple
/// </summary>
/// <param name="id">Apple Id</param>
[HttpDelete("{id:int}")]
public void Delete(int id)
{
if (Apples.ContainsKey(id))
{
Apples.Remove(id);
}
} }

(4)在 Startup 中注册 DynamicWebApi

public void ConfigureServices(IServiceCollection services)
{
// 默认配置
services.AddDynamicWebApi(); // 自定义配置
services.AddDynamicWebApi((options) =>
{
// 指定全局默认的 api 前缀
options.DefaultApiPrefix = "apis"; /**
* 清空API结尾,不删除API结尾;
* 若不清空 CreatUserAsync 将变为 CreateUser
*/
options.RemoveActionPostfixes.Clear(); /**
* 自定义 ActionName 处理函数;
*/
options.GetRestFulActionName = (actionName) => actionName; /**
* 指定程序集 配置 url 前缀为 apis
* 如: http://localhost:8080/apis/User/CreateUser
*/
options.AddAssemblyOptions(this.GetType().Assembly, apiPreFix: "apis"); /**
* 指定程序集 配置所有的api请求方式都为 POST
*/
options.AddAssemblyOptions(this.GetType().Assembly, httpVerb: "POST"); /**
* 指定程序集 配置 url 前缀为 apis, 且所有请求方式都为POST
* 如: http://localhost:8080/apis/User/CreateUser
*/
options.AddAssemblyOptions(this.GetType().Assembly, apiPreFix: "apis", httpVerb: "POST");
});
}

(5)添加 Swagger

            builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo() { Title = "Panda Dynamic WebApi", Version = "v1" }); // TODO:一定要返回true!
options.DocInclusionPredicate((docName, description) => true); var baseDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
var xmlFile = System.AppDomain.CurrentDomain.FriendlyName + ".xml";
var xmlPath = Path.Combine(baseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
});

配置

所有的配置均在对象 DynamicWebApiOptions 中,说明如下:

属性名 是否必须 说明
DefaultHttpVerb 默认值:POST。默认HTTP动词
DefaultAreaName 默认值:空。Area 路由名称
DefaultApiPrefix 默认值:api。API路由前缀
RemoveControllerPostfixes 默认值:AppService/ApplicationService。类名需要移除的后缀
RemoveActionPostfixes 默认值:Async。方法名需要移除的后缀
FormBodyBindingIgnoredTypes 默认值:IFormFile。不通过MVC绑定到参数列表的类型。

原理

什么是POCO Controller?

POCO Controller是 ASP.NET Core 中的一个特性,虽然在2015年刚发布的时候就有这个特性了,可是大多数开发者都只是按原有的方式去写,而没有用到这个特性。其实,如果利用这个特性进行稍微封装后,用在SOA架构中Service层的场景中是极其便利的。这篇文章主要就是说我最近在学习使用开源AOP库AspectCore写WebApi动态代理客户端的时候,实现为普通类无添加WebApi服务的过程。

POCO控制器就是ASP.NET Core项目中所有带有Controller后缀的类、或者标记了[Controller]特性的类,虽然没有像模版项目中那样继承自Controller类,也会被识别为控制器,拥有跟普通控制器一样的功能,像下面这段代码中,两个类都会被识别成控制器:

public class PocoController
{
public IActionResult Index()
{
return new ContentResult() { Content = “Hello from POCO controller!” };
}
}
[Controller]
public class Poco
{
public IActionResult Index()
{
return new ContentResult() { Content = “Hello from POCO controller!” };
}
}

POCO控制器原理

其实,在ASP.NET Core中,已经不像旧版本的 ASP.NET WebApi 那样,通过ControllerFactory来创建Controller,多亏于ASP.NET Core一脉相承的IoC框架 Microsoft.Extensions.DependencyInjection,ASP.NET Core中的内部实现变得更优雅。其中POCO控制器的核心原理就在IApplicationFeatureProvider<ControllerFeature>这个接口的实现ControllerFeatureProvider。

通过aspnet/Mvc项目的Github源码仓库中查询得知,Mvc里把Controller、ViewComponent、TagHelper、Views等组件定义为特性(Feature),如ControllerFeature,特性里就存放了应用中被识别为相组件的类型的集合,如如ControllerFeature中就存放了所有Controller类型。IApplicationFeatureProvider<ControllerFeature>这个接口是用来给MVC框架提供控制器类型识别的接口,当把这个接口的实现注册到服务配置中,就能为其中识别的类型提供控制器功能。

ControllerFeatureProvider是这个接口的默认实现,其中有一个方法IsController(TypeInfo typeInfo)的功能就是判断某类型是否为控制器的。而接口方法PopulateFeature(IEnumerable<ApplicationPart> parts,ControllerFeature feature)则为把传入的 “Mvc应用部分(ApplicationPart,大概是指Mvc的作用程序集)”中的类型都一一判断,如果是控制器,那么就加入控制器特性对象中。

ControllerFeatureProvider

ControllerFeatureProvider 是 ASP.NET Core MVC 框架中的一个类,它实现了 IApplicationFeatureProvider<ControllerFeature> 接口。这个类的主要作用是提供控制器类型的识别功能。

在 ASP.NET Core MVC 中,控制器是用来处理 HTTP 请求的类。传统的控制器类需要继承自 Controller 基类,但 ASP.NET Core 引入了一个新特性,即 POCO(Plain Old CLR Object)控制器。POCO 控制器允许你创建没有继承自 Controller 基类的类,但仍然可以将其识别为控制器,并赋予其处理 HTTP 请求的能力。

ControllerFeatureProvider 就是负责识别这些 POCO 控制器的类。它实现了 IApplicationFeatureProvider<ControllerFeature> 接口的 PopulateFeature 方法,该方法会在 MVC 框架构建应用模型时被调用。在这个方法中,ControllerFeatureProvider 会扫描应用程序中的类型,并根据一定的规则判断哪些类型应该被识别为控制器。

默认情况下,ControllerFeatureProvider 会将所有带有 "Controller" 后缀的类,或者使用了 [Controller] 特性的类识别为控制器。但你也可以通过自定义 ControllerFeatureProvider 的子类来提供自己的识别规则,以满足特定的需求。

https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Controllers/ControllerFeatureProvider.cs

实现自定义判断规则

通过上面的剖析,我们就知道要实现自定义的控制器判断规则,只需要重写ControllerFeature类或者重新实现IApplicationFeatureProvider接口,但是由于PopulateFeature不是虚方法或抽象方法,所以不能被重写,那么只能重新写一个类来实现IApplicationFeatureProvider接口了:

 public class MyDynamicControllerFeatureProvider : ControllerFeatureProvider
{
protected override bool IsController(TypeInfo typeInfo)
{
var typeInfo = type.GetTypeInfo(); if (!typeof(IDynamicWebApi).IsAssignableFrom(type) ||
!typeInfo.IsPublic || typeInfo.IsAbstract || typeInfo.IsGenericType)
{
return false;
} var attr = ReflectionHelper.GetSingleAttributeOrDefaultByFullSearch<DynamicWebApiAttribute>(typeInfo); if (attr == null)
{
return false;
} if (ReflectionHelper.GetSingleAttributeOrDefaultByFullSearch<NonDynamicWebApiAttribute>(typeInfo) != null)
{
return false;
} return true;
}
}

IApplicationModelConvention

IApplicationModelConvention 是ASP.NET Core中的一个接口,它允许开发者在应用模型构建过程中应用自定义约定。ASP.NET Core 的应用模型是描述如何构建 HTTP 请求处理管道的一组组件和服务。

通过实现 IApplicationModelConvention 接口,开发者可以注册中间件、修改路由、添加模型绑定器、配置控制器和服务等。这些约定在应用的启动过程中被应用,通常在 Startup.ConfigureServices 方法中通过调用 AddApplicationPartApplyApplicationPartManager 方法来注册。

https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelconvention?view=aspnetcore-8.0

下面是一个简单的 IApplicationModelConvention 实现示例,该示例演示了如何为所有控制器添加一个自定义操作筛选器:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels; public class CustomConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
// 为每个控制器添加自定义操作筛选器
controller.Filters.Add(new CustomActionFilter());
}
}
} public class CustomActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// 在操作执行前执行的代码
} public void OnActionExecuted(ActionExecutedContext context)
{
// 在操作执行后执行的代码
}
}

然后,在 Startup.ConfigureServices 方法中注册这个约定:

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(); services.AddApplicationPart(typeof(Startup).Assembly)
.ApplyApplicationPartManager(manager =>
{
manager.Conventions.Add(new CustomConvention());
});
}

在这个例子中,CustomConvention 被添加到了 ApplicationModel 的约定集合中。每当 ASP.NET Core 构建应用模型时,Apply 方法就会被调用,并且所有的控制器都会被添加 CustomActionFilter 筛选器。

Panda.DynamicWebApi中的实现

ConfigureApiExplorer()

首先,是对ApiExplorer进行配置。通过ApiExplorer,我们可以控制Controller级别和Action级别的Web API的可见性。一般情况下的用法是在Controller或者Action上添加ApiExplorerSettings标记,而在这里,我们只需要给ControllerModel和ActionModel的ApiExplorer属性赋值即可。

 private void ConfigureApiExplorer(ControllerModel controller)
{
if (controller.ApiExplorer.GroupName.IsNullOrEmpty())
{
controller.ApiExplorer.GroupName = controller.ControllerName;
} if (controller.ApiExplorer.IsVisible == null)
{
controller.ApiExplorer.IsVisible = true;
} foreach (var action in controller.Actions)
{
if (!CheckNoMapMethod(action))
ConfigureApiExplorer(action);
}
} private void ConfigureApiExplorer(ActionModel action)
{
if (action.ApiExplorer.IsVisible == null)
{
action.ApiExplorer.IsVisible = true;
}
}

ConfigureSelector()

接下来,是对路由进行配置。这部分的核心其实就是根据AreaName、ControllerName、ActionName来生成路由信息,我们会为没有配置过特性路由的Action生成默认的路由,这其实就是MVC里约定大于配置的一种体现啦。在这里会涉及到对ControllerName和ActionName的优化调整,主要体现在两个方面,其一是对类似XXXService、XXXController等这样的后缀进行去除,使其构造出的Api路由更加短小精简;其二是对ActionName里的Get/Save/Update等动词进行替换,使其构造出的Api路由更加符合RESTful风格。

     private void ConfigureSelector(ControllerModel controller, DynamicWebApiAttribute controllerAttr)
{ if (controller.Selectors.Any(selector => selector.AttributeRouteModel != null))
{
return;
} var areaName = string.Empty; if (controllerAttr != null)
{
areaName = controllerAttr.Module;
} foreach (var action in controller.Actions)
{
if (!CheckNoMapMethod(action))
ConfigureSelector(areaName, controller.ControllerName, action);
}
} private void ConfigureSelector(string areaName, string controllerName, ActionModel action)
{ var nonAttr = ReflectionHelper.GetSingleAttributeOrDefault<NonDynamicWebApiAttribute>(action.ActionMethod); if (nonAttr != null)
{
return;
} if (action.Selectors.IsNullOrEmpty() || action.Selectors.Any(a => a.ActionConstraints.IsNullOrEmpty()))
{
if (!CheckNoMapMethod(action))
AddAppServiceSelector(areaName, controllerName, action);
}
else
{
NormalizeSelectorRoutes(areaName, controllerName, action);
}
} private void AddAppServiceSelector(string areaName, string controllerName, ActionModel action)
{ var verb = GetHttpVerb(action); action.ActionName = GetRestFulActionName(action.ActionName); var appServiceSelectorModel = action.Selectors[0]; if (appServiceSelectorModel.AttributeRouteModel == null)
{
appServiceSelectorModel.AttributeRouteModel = CreateActionRouteModel(areaName, controllerName, action);
} if (!appServiceSelectorModel.ActionConstraints.Any())
{
appServiceSelectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { verb }));
switch (verb)
{
case "GET":
appServiceSelectorModel.EndpointMetadata.Add(new HttpGetAttribute());
break;
case "POST":
appServiceSelectorModel.EndpointMetadata.Add(new HttpPostAttribute());
break;
case "PUT":
appServiceSelectorModel.EndpointMetadata.Add(new HttpPutAttribute());
break;
case "DELETE":
appServiceSelectorModel.EndpointMetadata.Add(new HttpDeleteAttribute());
break;
default:
throw new Exception($"Unsupported http verb: {verb}.");
}
} }

ConfigureParameters()

接下来参数绑定相对简单,因为简单类型MVC自己就能完成绑定,所以,我们只需要关注复杂类型的绑定即可,最常见的一种绑定方式是FromBody:

        private void ConfigureParameters(ControllerModel controller)
{
foreach (var action in controller.Actions)
{
if (!CheckNoMapMethod(action))
foreach (var para in action.Parameters)
{
if (para.BindingInfo != null)
{
continue;
} if (!TypeHelper.IsPrimitiveExtendedIncludingNullable(para.ParameterInfo.ParameterType))
{
if (CanUseFormBodyBinding(action, para))
{
para.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() });
}
}
}
}
}

Asp .Net Core 系列:Asp .Net Core 集成 Panda.DynamicWebApi的更多相关文章

  1. [ASP.NET MVC2 系列] ASP.Net MVC教程之《在15分钟内用ASP.Net MVC创建一个电影数据库应用程序》

    [ASP.NET MVC2 系列]      [ASP.NET MVC2 系列] ASP.Net MVC教程之<在15分钟内用ASP.Net MVC创建一个电影数据库应用程序>       ...

  2. 跨平台应用集成(在ASP.NET Core MVC 应用程序中集成 Microsoft Graph)

    作者:陈希章 发表于 2017年6月25日 谈一谈.NET 的跨平台 终于要写到这一篇了.跨平台的支持可以说是 Office 365 平台在设计伊始就考虑的目标.我在前面的文章已经提到过了,Micro ...

  3. asp.net core 系列 18 web服务器实现

    一. ASP.NET Core Module 在介绍ASP.NET Core Web实现之前,先来了解下ASP.NET Core Module.该模块是插入 IIS 管道的本机 IIS 模块(本机是指 ...

  4. asp.net core 系列 16 Web主机 IWebHostBuilder

    一.概述 在asp.net core中,Host主机负责应用程序启动和生存期管理.host主机包括Web 主机(IWebHostBuilder)和通用主机(IHostBuilder).Web 主机是适 ...

  5. Asp.net Core 系列之--5.认证、授权与自定义权限的实现

    ChuanGoing 2019-11-24 asp.net core系列已经来到了第五篇,通过之前的基础介绍,我们了解了事件订阅/发布的eventbus整个流程,初探dapper ORM实现,并且简单 ...

  6. 【目录】asp.net core系列篇

    随笔分类 - asp.net core系列篇 asp.net core系列 68 Filter管道过滤器 摘要: 一.概述 本篇详细了解一下asp.net core filters,filter叫&q ...

  7. Taurus.MVC 微服务框架 入门开发教程:项目集成:2、客户端:ASP.NET Core(C#)项目集成:应用中心。

    系列目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 本系列第一篇:Taurus.MVC V3.0.3 微服务开源框架发布:让.NET 架构在大并发的演进过程更简单 ...

  8. 基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-1

    来个目录吧: 第一章 第二章 第三章 暂时就这么多.后面路线更新吧 本系列文章为翻译加上我个人的使用心得理解,希望帮助热爱学习的程序员. 珍重声明:本系列文章会跟原文有点出入,去掉了罗里吧嗦的文字. ...

  9. ASP.NET CORE系列【一】搭建ASP.NET CORE项目

    为什么要使用 ASP.NET Core? NET Core 刚发布的时候根据介绍就有点心里痒痒,微软的尿性都懂的,新东西bug太多,现在2.0也发布很久了,决定研究一下. ASP.NET Core官方 ...

  10. 1.1专题介绍「深入浅出ASP.NET Core系列」

    大家好,我是IT人张飞洪,专注于.NET平台十年有余. 工作之余喜欢阅读和写作,学习的内容包括数据结构/算法.网络技术.Linux系统原理.数据库技术原理,设计模式.前沿架构.微服务.容器技术等等…… ...

随机推荐

  1. 每天学五分钟 Liunx 0111 | 服务篇:进程权限

    程序存储在硬盘中,需要执行的时候被加载到内存里,内存中的程序以进程的方式运行,进程会根据程序的内容去做读写文件,执行指令等操作. 文件/指令等都有自己的执行权限,符合权限的才能被执行.相应的,进程也需 ...

  2. 每天学五分钟 Liunx 101 | 存储篇:LVM

    LVM LVM(Logical Volume Manager),逻辑卷管理器.一种高级文件系统管理方式,它可以动态扩展文件系统.   LVM 的示意图如下所示:

  3. spring--JDK动态代理和CGLIB代理的区别

    JDK 动态代理和 CGLIB 代理是 Java 中常用的两种动态代理实现方式,它们各有特点和适用场景: JDK 动态代理: JDK 动态代理是基于接口的代理方式,它使用 Java 反射机制来创建代理 ...

  4. python3使用diagrams生成架构图

    技术背景 对于一个架构师或者任何一个软件工程师而言,绘制架构图都是一个比较值得学习的技能.这就像我们学习的时候整理的一些Xmind那种思维逻辑图一样,不仅可以帮我们看到组件之间的联系和层级,还能够展示 ...

  5. [转帖]关于虚拟化中cpu的指令集SSE 4.2的不支持

    背景: 局域网中有两台服务器proxmox进行了虚拟化,跑一些测试应用.今天正好想要安装一下clickhouse跑一下.安装前准备: 测试服务器是否支持sse 4.2指令集-如下 [root@slav ...

  6. 【转帖】Linux中如何取消ln链接?(linuxln取消)

    https://www.dbs724.com/163754.html Linux系统使用ln命令可以快速创建链接,ln链接是指把文件和目录链接起来,当改变源时可以快速地改变整个目录下的文件和目录.有时 ...

  7. [转帖]JVM-工具-jcmd

    http://events.jianshu.io/p/011f0e3a39ff 一.jcmd 用法 1.1 基本知识 jcmd 是在 JDK1.7 以后,新增了一个命令行工具. jcmd 是一个多功能 ...

  8. buildkit 官网 service 资料

    [Unit]   Description=BuildKit   Requires=buildkit.socket   After=buildkit.socket   Documentation=htt ...

  9. 一次w3wp出现crash的简单解决方案

    1. 前几天同事求助, 说一台服务器iis出现多次崩溃的现象,重启iis就可以了.  具体原因不明. 之前遇到过类似的问题  感觉最彻底的解决方案是 抓dump然后使用windbg 进行分析. 但是自 ...

  10. React中css的module

    处理css全局作用 现在有这样一个场景: A页面和B页面都有一个相同的类名 我们在A页面中有引入css. B页面没有css 在我们切换A和B页面的时候. A页面的css也作用在了B页面. 我们只希望A ...