简介

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. 全流程机器视觉工程开发(二)PaddleDetection:拉框,然后开始训练模型

    前言 我现在在准备做一个全流程的机器视觉的工程,之前做了很多理论相关的工作.大概理解了机器视觉的原理,然后大概了解了一下,我发现现在的库其实已经很发展了,完全不需要用到非常多的理论,只需要知道开发过程 ...

  2. C#多曲线数据分析

    数据如下 统计效果图如下 程序初始化 private void Form1_Load(object sender, EventArgs e) { using (SqlConnection con = ...

  3. C#通过泛型实现对子窗体的不同操作

    private void button1_Click(object sender, EventArgs e) { FormOperate<object>();//调用FormOperate ...

  4. 43 干货系列从零用Rust编写负载均衡及代理,内网穿透方案完整部署

    wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代 ...

  5. Jquery - 获取所有子节点 ( 并删除 )

    1,获取所有子节点   $(".parent").find('.child') 2,获取所有子节点,通过上层 div 的类名 , 获取上层 div 节点 $(".pare ...

  6. sql server 数据恢复

    1) 备份当前数据库的事务日志:BACKUP LOG [数据库名] TO disk= N'备份文件名' WITH NORECOVERY 2) 恢复一个误删除之前的完全备份:RESTORE DATABA ...

  7. SQLServer解决deadlock问题的一个场景

    SQLServer解决deadlock问题的一个场景 背景 公司产品出现过很多次dead lock 跟研发讨论了很久, 都没有具体的解决思路 但是这边知道了一个SQLServer数据库上面计划100% ...

  8. [转帖]Kingbase实现Oracle userenv函数功能

    目录 1. 问题 2. 文档概述 3. Oracle userenv()函数功能调研 3.1. 函数名称/函数原型 3.2. 函数功能 3.3. 参数介绍 3.3.1. Parameter 3.4. ...

  9. [转帖]GC日志分析工具——GCViewer案例

    原创 石页粑粑 来自zxsk的码农 2020-09-28 06:18 一.GCViewer介绍 业界较为流行分析GC日志的两个工具--GCViewer.GCEasy.GCEasy部分功能还是要收费的, ...

  10. sed 删除包含某字符的一行 给包含某字符的一行添加 逗号的简单方法

    今天处理环境折腾死了 方法: #给包含 configdata 的一行 添加 逗号结尾 find . -name "*.json" |xargs sed -i '/configdat ...