文章目录

    • 简介
    • Asp .Net Core 集成 Panda.DynamicWebApi
    • 配置
    • 原理
      • 什么是POCO Controller?
      • POCO控制器原理
      • ControllerFeatureProvider
      • 实现自定义判断规则
      • IApplicationModelConvention
      • Panda.DynamicWebApi中的实现
        • ConfigureApiExplorer()
        • ConfigureSelector()
        • ConfigureParameters()

简介

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 Apples=new Dictionary(){[1]="Big Apple",[2]="Small Apple"};/// /// Get An Apple./// /// /// [HttpGet("{id:int}")]public string Get(int id){if (Apples.ContainsKey(id)){return Apples[id];}else{return "No Apple!";}}/// /// Get All Apple./// /// public IEnumerable Get(){return Apples.Values;}public void Update(UpdateAppleDto dto){if (Apples.ContainsKey(dto.Id)){Apples[dto.Id] =dto.Name;}}/// /// Delete Apple/// /// Apple Id[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这个接口的实现ControllerFeatureProvider。

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

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

ControllerFeatureProvider

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

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

ControllerFeatureProvider 就是负责识别这些 POCO 控制器的类。它实现了 IApplicationFeatureProvider 接口的 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(typeInfo);if (attr == null){return false;}if (ReflectionHelper.GetSingleAttributeOrDefaultByFullSearch(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(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() });}}}}}