简介
Panda.DynamicWebApi
是一个动态生成WebApi的组件,生成的API符合Restful风格,受启发于ABP。它可以根据符合条件的类来生成WebApi,由MVC框架直接调用逻辑,无性能问题,完美兼容Swagger来构建API说明文档,与手动编写Controller相比并无区别。
应用场景:DDD架构中的应用逻辑层,可使用本组件来直接生成WebApi,而无需再用Controller来调用。
Asp .*** Core 集成 Panda.DynamicWebApi
(1)新建一个 ASP.*** 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((do***ame, description) => true);
var baseDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
var xmlFile = System.AppDomain.CurrentDomain.FriendlyName + ".xml";
var xmlPath = Path.***bine(baseDirectory, xmlFile);
options.IncludeXml***ments(xmlPath);
});
配置
所有的配置均在对象 DynamicWebApiOptions
中,说明如下:
属性名 | 是否必须 | 说明 |
---|---|---|
DefaultHttpVerb | 否 | 默认值:POST。默认HTTP动词 |
DefaultAreaName | 否 | 默认值:空。Area 路由名称 |
DefaultApiPrefix | 否 | 默认值:api。API路由前缀 |
RemoveControllerPostfixes | 否 | 默认值:AppService/ApplicationService。类名需要移除的后缀 |
RemoveActionPostfixes | 否 | 默认值:Async。方法名需要移除的后缀 |
FormBodyBindingIgnoredTypes | 否 | 默认值:IFormFile。不通过MVC绑定到参数列表的类型。 |
原理
什么是POCO Controller?
POCO Controller是 ASP.*** Core 中的一个特性,虽然在2015年刚发布的时候就有这个特性了,可是大多数开发者都只是按原有的方式去写,而没有用到这个特性。其实,如果利用这个特性进行稍微封装后,用在SOA架构中Service层的场景中是极其便利的。这篇文章主要就是说我最近在学习使用开源AOP库AspectCore写WebApi动态代理客户端的时候,实现为普通类无添加WebApi服务的过程。
POCO控制器就是ASP.*** 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.*** Core中,已经不像旧版本的 ASP.*** WebApi 那样,通过ControllerFactory来创建Controller,多亏于ASP.*** Core一脉相承的IoC框架 Microsoft.Extensions.DependencyInjection,ASP.*** Core中的内部实现变得更优雅。其中POCO控制器的核心原理就在IApplicationFeatureProvider<ControllerFeature>这个接口的实现ControllerFeatureProvider。
通过asp***/Mvc项目的Github源码仓库中查询得知,Mvc里把Controller、View***ponent、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.*** Core MVC 框架中的一个类,它实现了 IApplicationFeatureProvider<ControllerFeature>
接口。这个类的主要作用是提供控制器类型的识别功能。
在 ASP.*** Core MVC 中,控制器是用来处理 HTTP 请求的类。传统的控制器类需要继承自 Controller
基类,但 ASP.*** Core 引入了一个新特性,即 POCO(Plain Old CLR Object)控制器。POCO 控制器允许你创建没有继承自 Controller
基类的类,但仍然可以将其识别为控制器,并赋予其处理 HTTP 请求的能力。
ControllerFeatureProvider
就是负责识别这些 POCO 控制器的类。它实现了 IApplicationFeatureProvider<ControllerFeature>
接口的 PopulateFeature
方法,该方法会在 MVC 框架构建应用模型时被调用。在这个方法中,ControllerFeatureProvider
会扫描应用程序中的类型,并根据一定的规则判断哪些类型应该被识别为控制器。
默认情况下,ControllerFeatureProvider
会将所有带有 “Controller” 后缀的类,或者使用了 [Controller]
特性的类识别为控制器。但你也可以通过自定义 ControllerFeatureProvider
的子类来提供自己的识别规则,以满足特定的需求。
https://github.***/dot***/asp***core/blob/main/src/Mvc/Mvc.Core/src/Controllers/ControllerFeatureProvider.cs
实现自定义判断规则
通过上面的剖析,我们就知道要实现自定义的控制器判断规则,只需要重写ControllerFeature类或者重新实现IApplicationFeatureProvider接口,但是由于PopulateFeature不是虚方法或抽象方法,所以不能被重写,那么只能重新写一个类来实现IApplicationFeatureProvider接口了:
public class MyDynami***ontrollerFeatureProvider : 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.*** Core中的一个接口,它允许开发者在应用模型构建过程中应用自定义约定。ASP.*** Core 的应用模型是描述如何构建 HTTP 请求处理管道的一组组件和服务。
通过实现 IApplicationModelConvention
接口,开发者可以注册中间件、修改路由、添加模型绑定器、配置控制器和服务等。这些约定在应用的启动过程中被应用,通常在 Startup.ConfigureServices
方法中通过调用 AddApplicationPart
和 ApplyApplicationPartManager
方法来注册。
https://learn.microsoft.***/zh-***/dot***/api/microsoft.asp***core.mvc.applicationmodels.iapplicationmodelconvention?view=asp***core-8.0
下面是一个简单的 IApplicationModelConvention
实现示例,该示例演示了如何为所有控制器添加一个自定义操作筛选器:
using Microsoft.Asp***Core.Mvc;
using Microsoft.Asp***Core.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.*** 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() });
}
}
}
}
}