前言:
远距组织工作早已两个月了,前段时间也称得上较为闲,每晚中午下班打个卡,加速修修当日要做的组织工作后就欢乐北窝去了。以后在用 ABP 架构(旧版本)的时候就觉得数据服务层写出来吗爽,为什么同时实现了个 IApplicationService 的空USB就可以变为 Web API,可是的是以后一直无暇去研究这几块的基本原理及其同时实现,Seiches里也找不出有关同时实现基本原理的该文(旧版本 ABP 的倒是有,但是 asp.net core 难以参照)。前段时间闲出来,就看了呵呵 abp vnext 的源代码,因此也参照了呵呵KroneMaster 如是说的 Panda.DynamicWebApi。我自己也单纯同时实现了两遍静态 Web API,不由得感慨 asp.net core 结构设计之绝妙。
abp vnext:https://abp.io
Panda.DynamicWebApi:https://github.com/pdafx/Panda.DynamicWebApi
这里先非常感谢这两个库的有关相关人员,没有他们的组织工作,责任编辑也出现没法。另外在此新闻稿,责任编辑以图探求其同时实现基本原理并同时实现两个固定式版,若耶莱贝切勿用作生产环境。
节录:
具体来说先建立他们的软件系统如下表所示:
即使静态 Web API 这一机能是与业务毫无关系的,而且为了F83E43Se,他们应该把这一机能的同时实现写到两个原则上的C#之中。左图中 Demo 工程项目是 asp.net core 3.1 版的 Web API 工程项目,用作模拟他们的固定式静态 Web API,而 SimpleDynamicWebAPI 的 .net standard 2.0 工程项目则是他们的固定式静态 Web API 工程项目。
要同时实现静态 Web API,具体来说要做的第二件事情就是要有两个准则,来认定两个类呢静态 Web API。在 abp vnext 之中,主要提供三种形式,两个是同时实现 IRemoteService USB(前述合作开发过程中一般都是同时实现 IApplicationService USB),另一种形式记号 RemoteServiceAttribute。而在 Panda.DynamicWebApi 中,则是同时实现 IDynamicWebApi USB因此记号 DynamicWebApi。即使责任编辑是要同时实现固定式版,因此只选空USB形式。在 SimpleDynamicWebAPI 工程项目中建立如下表所示空USB:
namespace SimpleDynamicWebAPI
{
public interface IApplicationService
{
}
}
接下去,他们有了 IApplicationService USB,他们也晓得同时实现了那个USB的类是要成为静态 Web API 的,但那个是他们所晓得的准则,asp.net core 架构它是不晓得的,他们需要把那个准则告诉它。
这几块 abp vnext 有点儿繁杂,他们参照 Panda.DynamicWebAPI 的同时实现:
https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiServiceExtensions.cs#L46
https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiControllerFeatureProvider.cs
下面图中
DynamicWebApiControllerFeatureProvider 的 IsController 方法很明显了。查阅 msdn:
粗俗点翻译过来就是判断两个类呢控制器。
接下去开始依样画葫芦。具体来说一点 ControllerFeatureProvider 类是属于 asp.net core 的,理论上是位于
Microsoft.AspNetCore.Mvc.Core 那个 nuget 包的,但是那个包的 3.x 版并没有发布在 nuget 上。如果他们的 SimpleDynamicWebAPI 引用 2.x 版的,而 Demo 工程项目又是 3.x 版的,则很可能会引起冲突。保险起见,他们
修改 SimpleDynamicWebAPI 为两个 asp.net core 的C#。反正那个库本来也不可能会被其它类型诸如 WPF 的工程项目引用。
修改
SimpleDynamicWebAPI.csproj 如下表所示:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1
</TargetFramework>
<OutputType>Library
</OutputType>
</PropertyGroup>
</Project>
接下去建立
ApplicationServiceControllerFeatureProvider 类,并修改代码如下表所示:
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Reflection;
namespace SimpleDynamicWebAPI
{
public class ApplicationServiceControllerFeatureProvider :
ControllerFeatureProvider
{
protected override bool IsController(TypeInfo typeInfo)
{
if (
typeof(IApplicationService).IsAssignableFrom(typeInfo))
{
if (!typeInfo.IsInterface &&
!typeInfo.IsAbstract &&
!typeInfo.IsGenericType &&
typeInfo.IsPublic)
{
return true;
}
}
return false;
}
}
}
具体来说先要判断呢同时实现了 IApplicationService USB,那个是他们一开始所定下的准则。
接下去,1、如果两个USB即使它同时实现了 IApplicationService,但它仍然不能是两个控制器,那是即使USB是难以实例化的;2、抽象类同理,也是即使难以实例化;3、泛型类也不允许,即使需要确切的类型才能实例化;4、public 代表着公开,可被外界访问,如果两个类不是 public 的,那么就不应该成为两个静态 Web API 控制器。
接下去就是要把那个
ApplicationServiceControllerFeatureProvider 加入到 asp.net core 架构中。
建立
SimpleDynamicWebApiExtensions 扩展类,修改代码如下表所示:
using Microsoft.Extensions.DependencyInjection;
using System;
namespace SimpleDynamicWebAPI
{
public static class SimpleDynamicWebApiExtensions
{
public static IMvcBuilder AddDynamicWebApi(this IMvcBuilder builder)
{
if (builder ==
null)
{
throw new ArgumentNullException(
nameof(builder));
}
builder.ConfigureApplicationPartManager(applicationPartManager =>
{
applicationPartManager.FeatureProviders.Add(
new ApplicationServiceControllerFeatureProvider());
});
return builder;
}
public static IMvcCoreBuilder AddDynamicWebApi(this IMvcCoreBuilder builder)
{
if (builder ==
null)
{
throw new ArgumentNullException(
nameof(builder));
}
builder.ConfigureApplicationPartManager(applicationPartManager =>
{
applicationPartManager.FeatureProviders.Add(
new ApplicationServiceControllerFeatureProvider());
});
return builder;
}
}
}
即使
ConfigureApplicationPartManager 扩展方法分别在 IMvcBuilder 和 IMvcCoreBuilder 上都有,所以他们也只能写两遍。当然参照 abp vnext 或 Panda.DynamicWebApi 从 services 中获取 ApplicationPartManager 对象实例也是可行的。
接下去回到 Demo 工程项目,在 AddControllers 后面加上 AddDynamicWebApi:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddDynamicWebApi();
}
现在他们早已完成第一步了,同时实现了 IApplicationService USB的类将被视作控制器处理。但仅仅这样并不足够,假设有多个类同时同时实现 IApplicationService USB,那应该如何映射呢,如果没错的话,那个时候你应该会想到是——路由。他们还需要做的组织工作就是把这些控制器与路由配置出来。
abp vnext 这块为了在配置过程中获取 services 而延迟加载导致包了一层,有点儿繁杂。这里参照 Panda.DynamicWebApi
https://github.com/pdafx/Panda.DynamicWebApi/blob/master/src/Panda.DynamicWebApi/DynamicWebApiServiceExtensions.cs#L51
注释告诉了他们这里是配置控制器的路由,非常感谢作者大大。
继续画葫芦,建立
ApplicationServiceConvention 类并同时实现
IApplicationModelConvention USB:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
namespace SimpleDynamicWebAPI
{
public class ApplicationServiceConvention :
IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
throw new NotImplementedException();
}
}
}
Apply 方法的同时实现等下再考虑,先把它注册到 asp.net core 架构,修改
SimpleDynamicWebApiExtensions 扩展类如下表所示:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace SimpleDynamicWebAPI
{
public static class SimpleDynamicWebApiExtensions
{
public static IMvcBuilder AddDynamicWebApi(this IMvcBuilder builder)
{
if (builder ==
null)
{
throw new ArgumentNullException(
nameof(builder));
}
builder.ConfigureApplicationPartManager(applicationPartManager =>
{
applicationPartManager.FeatureProviders.Add(
new ApplicationServiceControllerFeatureProvider());
});
builder.Services.Configure<MvcOptions>(options =>
{
options.Conventions.Add(
new ApplicationServiceConvention());
});
return builder;
}
public static IMvcCoreBuilder AddDynamicWebApi(this IMvcCoreBuilder builder)
{
if (builder ==
null)
{
throw new ArgumentNullException(
nameof(builder));
}
builder.ConfigureApplicationPartManager(applicationPartManager =>
{
applicationPartManager.FeatureProviders.Add(
new ApplicationServiceControllerFeatureProvider());
});
builder.Services.Configure<MvcOptions>(options =>
{
options.Conventions.Add(
new ApplicationServiceConvention());
});
return builder;
}
}
}
对服务容器中的 MvcOptions 进行配置,添加上
ApplicationServiceConvention。ok,接下去回到考虑 Apply 方法同时实现的问题了。
这里参照 abp vnext:
https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L33
左图中的 ApplyForControllers 方法的方法体关键部分很好懂,foreach 遍历了所有的控制器,如果控制器同时实现了 IRemoteService USB或者记号了 RemoteServiceAttribute,则调用 ConfigureRemoteService 进一步处理。即使他们的固定式版是只有USB,else 部分的他们就不需要了。
修改
ApplicationServiceConvention 代码如下表所示:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
namespace SimpleDynamicWebAPI
{
public class ApplicationServiceConvention :
IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (
var controller
in application.Controllers)
{
if (
typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
{
ConfigureApplicationService(controller);
}
}
}
private void ConfigureApplicationService(ControllerModel controller)
{
throw new NotImplementedException();
}
}
}
那 abp vnext 的 ConfigureRemoteService 方法中又干了什么呢?跳转到 ConfigureRemoteService 的同时实现:
做了三件事情。
1、ConfigureApiExplorer。ApiExplorer,单纯点说就是 API 是否可被发现。举个栗子,加入你写了两个 Web API,工程项目又配置了 swagger,而且你又想 swagger 不显示那个 Web API 的话,那么可以在 Action 上加上:
[ApiExplorerSettings(IgnoreApi =
true)]
具体这里就不说了,大家可以自行 google。
2、ConfigureSelector。Selector,选择器,可能不太好理解。但是第三个明显是配置参数,那么第二那个只能是配置路由了,那个方法将会是他们的关键。
3、ConfigureParameters。第二点说了,配置参数。
那么继续修改他们的
ApplicationServiceConvention 类因此同时实现他们的 ConfigureApiExplorer:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
namespace SimpleDynamicWebAPI
{
public class ApplicationServiceConvention :
IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (
var controller
in application.Controllers)
{
if (
typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
{
ConfigureApplicationService(controller);
}
}
}
private void ConfigureApplicationService(ControllerModel controller)
{
ConfigureApiExplorer(controller);
ConfigureSelector(controller);
ConfigureParameters(controller);
}
private void ConfigureApiExplorer(ControllerModel controller)
{
if (!controller.ApiExplorer.IsVisible.HasValue)
{
controller.ApiExplorer.IsVisible =
true;
}
foreach (
var action
in controller.Actions)
{
if (!action.ApiExplorer.IsVisible.HasValue)
{
action.ApiExplorer.IsVisible =
true;
}
}
}
private void ConfigureSelector(ControllerModel controller)
{
throw new NotImplementedException();
}
private void ConfigureParameters(ControllerModel controller)
{
throw new NotImplementedException();
}
}
}
ConfigureApiExplorer 这块,IsVisible 只要没有值,就无脑设为 true 好了。
接下去 ConfigureSelector 看 abp vnext 的同时实现:
https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L170
具体来说第一行 RemoveEmptySelectors 这是两个关键点。虽然他们的静态 Web API 控制器一开始并没有配置路由,但前述上 asp.net core 架构会为此生成一些空白信息。abp vnext 在这里就抹除掉了这些空白信息。而 Panda.DynamicWebApi 虽然没有这样干,但是后面的判断逻辑就相对繁杂了一些(大大别打我)。
实抄现袭他们的 RemoveEmptySelectors:
private void RemoveEmptySelectors(IList<SelectorModel> selectors)
{
for (var i = selectors.Count - 1; i >= 0; i--)
{
var selector = selectors[i];
if (selector.AttributeRouteModel == null &&
(selector.ActionConstraints == null || selector.ActionConstraints.Count <= 0) &&
(selector.EndpointMetadata == null || selector.EndpointMetadata.Count <= 0))
{
selectors.Remove(selector);
}
}
}
使用倒序删除小技巧,就不需要担心下标越界的问题了。
if 第一行明显可以看出判断路由信息是否存在,第二行判断的 Action 的约束,而约束则是指 HttpGet、HttpPost 这种约束,第三行判断了端点元数据信息,例如记号了什么 Attribute 之类的。假如这些都没有,那么这条 selector 就可以断定为空白信息了。
接下去回到 abp vnext 代码截图的 181 行:
https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L181
假如移除过空白信息后仍然有路由的话,则后续不进行处理。
接下去的 foreach 就开始处理 Action 了。先完善他们的代码,再开始处理 Action 的路由:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleDynamicWebAPI
{
public class ApplicationServiceConvention :
IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (
var controller
in application.Controllers)
{
if (
typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
{
ConfigureApplicationService(controller);
}
}
}
private void ConfigureApplicationService(ControllerModel controller)
{
ConfigureApiExplorer(controller);
ConfigureSelector(controller);
ConfigureParameters(controller);
}
private void ConfigureApiExplorer(ControllerModel controller)
{
if (!controller.ApiExplorer.IsVisible.HasValue)
{
controller.ApiExplorer.IsVisible =
true;
}
foreach (
var action
in controller.Actions)
{
if (!action.ApiExplorer.IsVisible.HasValue)
{
action.ApiExplorer.IsVisible =
true;
}
}
}
private void ConfigureSelector(ControllerModel controller)
{
RemoveEmptySelectors(controller.Selectors);
if (controller.Selectors.Any(temp => temp.AttributeRouteModel !=
null))
{
return;
}
foreach (
var action
in controller.Actions)
{
ConfigureSelector(action);
}
}
private void ConfigureSelector(ActionModel action)
{
throw new NotImplementedException();
}
private void ConfigureParameters(ControllerModel controller)
{
throw new NotImplementedException();
}
private void RemoveEmptySelectors(IList<SelectorModel> selectors)
{
for (
var i = selectors.Count -
1; i >=
0; i--)
{
var selector = selectors[i];
if (selector.AttributeRouteModel ==
null &&
(selector.ActionConstraints ==
null || selector.ActionConstraints.Count <=
0) &&
(selector.EndpointMetadata ==
null || selector.EndpointMetadata.Count <=
0))
{
selectors.Remove(selector);
}
}
}
}
}
开始处理 Action 的路由,参照 abp vnext 的 194 行到 212 行:
第一行仍然是移除空白信息。
关键在最后的判断,假如没有 selector 的话,加上就是了。但是如果早已有了呢?那就修改呗。举个栗子,假如他们同时实现 IApplicationService USB的类的两个方法记号了 HttpGet,那么那个 Action 是有约束的,但是它却是没有路由的。这几行无论是 abp vnext 还是 Panda.DynamicWebApi 都是一样的。
初步同时实现添加 selector 方法,这里我叫它
AddApplicationServiceSelector:
private void AddApplicationServiceSelector(ActionModel action)
{
var selector =
new SelectorModel();
selector.AttributeRouteModel =
new AttributeRouteModel(
new RouteAttribute(CalculateRouteTemplate(action)));
selector.ActionConstraints.Add(
new HttpMethodActionConstraint(
new[] { GetHttpMethod(action) }));
action.Selectors.Add(selector);
}
private string CalculateRouteTemplate(ActionModel action)
{
throw new NotImplementedException();
}
private string GetHttpMethod(ActionModel action)
{
throw new NotImplementedException();
}
接下去他们需要添加路由因此配置约束。
要计算路由,他们先举个栗子(嗯,第三颗栗子了)。假设他们有两个叫 BookController 的 API 控制器,有两个叫 Save 的 Action,那么它的路由一般就是:
api/books/{id}/save
也就是说,一般 API 控制器的路由如下表所示:
api/[controller]s(/{id})?(/[action])?
那么他们大概能写出如下表所示代码:
private string CalculateRouteTemplate(ActionModel action)
{
var routeTemplate =
new StringBuilder();
routeTemplate.Append(
"api");
// 控制器名称部分
var controllerName = action.Controller.ControllerName;
if (controllerName.EndsWith(
"ApplicationService"))
{
controllerName = controllerName.Substring(
0, controllerName.Length -
"ApplicationService".Length);
}
else if (controllerName.EndsWith(
"AppService"))
{
controllerName = controllerName.Substring(
0, controllerName.Length -
"AppService".Length);
}
controllerName +=
"s";
routeTemplate.Append(
$"/{controllerName}");
// id 部分
if (action.Parameters.Any(temp => temp.ParameterName ==
"id"))
{
routeTemplate.Append(
"/{id}");
}
// Action 名称部分
var actionName = action.ActionName;
if (actionName.EndsWith(
"Async"))
{
actionName = actionName.Substring(
0, actionName.Length -
"Async".Length);
}
var trimPrefixes =
new[]
{
"GetAll",
"GetList",
"Get",
"Post",
"Create",
"Add",
"Insert",
"Put",
"Update",
"Delete",
"Remove",
"Patch"
};
foreach (
var trimPrefix
in trimPrefixes)
{
if (actionName.StartsWith(trimPrefix))
{
actionName = actionName.Substring(trimPrefix.Length);
break;
}
}
if (!
string.IsNullOrEmpty(actionName))
{
routeTemplate.Append(
$"/{actionName}");
}
return routeTemplate.ToString();
}
以 api 开头。
控制器部分,如果名字结尾是 ApplicationService 或者 AppService,那就裁掉。因此变为复数。即使这里是固定式版,直接加 s 了是。前述建议使用 Inflector 等之类的库。不然 bus 这种词直接加 s 就太奇怪了。
id 部分没啥好说的。
最后是 Action 部分,假如是 Async 结尾的,裁掉。接下去看开头呢以 Get、Post、Create 等等这些开头,是的话也裁掉,注意要先判断 GetAll 和 GetList 然后再判断 Get。即使最后裁掉后有可能是空字符串,所以还需要判断呵呵再确定是否添加到路由中。
通过 Action 部分的计算,以后他们剩下的 GetHttpMethod 方法也很好写了:
private string GetHttpMethod(ActionModel action)
{
var actionName = action.ActionName;
if (actionName.StartsWith(
"Get"))
{
return "GET";
}
if (actionName.StartsWith(
"Put") || actionName.StartsWith(
"Update"))
{
return "PUT";
}
if (actionName.StartsWith(
"Delete") || actionName.StartsWith(
"Remove"))
{
return "DELETE";
}
if (actionName.StartsWith(
"Patch"))
{
return "PATCH";
}
return "POST";
}
根据 Action 名开头返回 Http 方法就是了,如果什么都匹配不上就假定 POST。
添加 Selector 总算写完了,修改 Selector 还难么?同时实现他们自己的 NormalizeSelectorRoutes 方法:
private void NormalizeSelectorRoutes(ActionModel action)
{
foreach (
var selector
in action.Selectors)
{
if (selector.AttributeRouteModel ==
null)
{
selector.AttributeRouteModel =
new AttributeRouteModel(
new RouteAttribute(CalculateRouteTemplate(action)));
}
if (selector.ActionConstraints.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods?.FirstOrDefault() ==
null)
{
selector.ActionConstraints.Add(
new HttpMethodActionConstraint(
new[] { GetHttpMethod(action) }));
}
}
}
没有路由就给它补路由,没有约束就给它补约束。
现在他们的
ApplicationServiceConvention 的代码应该如下表所示:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SimpleDynamicWebAPI
{
public class ApplicationServiceConvention :
IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (
var controller
in application.Controllers)
{
if (
typeof(IApplicationService).IsAssignableFrom(controller.ControllerType))
{
ConfigureApplicationService(controller);
}
}
}
private void ConfigureApplicationService(ControllerModel controller)
{
ConfigureApiExplorer(controller);
ConfigureSelector(controller);
ConfigureParameters(controller);
}
private void ConfigureApiExplorer(ControllerModel controller)
{
if (!controller.ApiExplorer.IsVisible.HasValue)
{
controller.ApiExplorer.IsVisible =
true;
}
foreach (
var action
in controller.Actions)
{
if (!action.ApiExplorer.IsVisible.HasValue)
{
action.ApiExplorer.IsVisible =
true;
}
}
}
private void ConfigureSelector(ControllerModel controller)
{
RemoveEmptySelectors(controller.Selectors);
if (controller.Selectors.Any(temp => temp.AttributeRouteModel !=
null))
{
return;
}
foreach (
var action
in controller.Actions)
{
ConfigureSelector(action);
}
}
private void ConfigureSelector(ActionModel action)
{
RemoveEmptySelectors(action.Selectors);
if (action.Selectors.Count <=
0)
{
AddApplicationServiceSelector(action);
}
else
{
NormalizeSelectorRoutes(action);
}
}
private void ConfigureParameters(ControllerModel controller)
{
throw new NotImplementedException();
}
private void NormalizeSelectorRoutes(ActionModel action)
{
foreach (
var selector
in action.Selectors)
{
if (selector.AttributeRouteModel ==
null)
{
selector.AttributeRouteModel =
new AttributeRouteModel(
new RouteAttribute(CalculateRouteTemplate(action)));
}
if (selector.ActionConstraints.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods?.FirstOrDefault() ==
null)
{
selector.ActionConstraints.Add(
new HttpMethodActionConstraint(
new[] { GetHttpMethod(action) }));
}
}
}
private void AddApplicationServiceSelector(ActionModel action)
{
var selector =
new SelectorModel();
selector.AttributeRouteModel =
new AttributeRouteModel(
new RouteAttribute(CalculateRouteTemplate(action)));
selector.ActionConstraints.Add(
new HttpMethodActionConstraint(
new[] { GetHttpMethod(action) }));
action.Selectors.Add(selector);
}
private string CalculateRouteTemplate(ActionModel action)
{
var routeTemplate =
new StringBuilder();
routeTemplate.Append(
"api");
// 控制器名称部分
var controllerName = action.Controller.ControllerName;
if (controllerName.EndsWith(
"ApplicationService"))
{
controllerName = controllerName.Substring(
0, controllerName.Length -
"ApplicationService".Length);
}
else if (controllerName.EndsWith(
"AppService"))
{
controllerName = controllerName.Substring(
0, controllerName.Length -
"AppService".Length);
}
controllerName +=
"s";
routeTemplate.Append(
$"/{controllerName}");
// id 部分
if (action.Parameters.Any(temp => temp.ParameterName ==
"id"))
{
routeTemplate.Append(
"/{id}");
}
// Action 名称部分
var actionName = action.ActionName;
if (actionName.EndsWith(
"Async"))
{
actionName = actionName.Substring(
0, actionName.Length -
"Async".Length);
}
var trimPrefixes =
new[]
{
"GetAll",
"GetList",
"Get",
"Post",
"Create",
"Add",
"Insert",
"Put",
"Update",
"Delete",
"Remove",
"Patch"
};
foreach (
var trimPrefix
in trimPrefixes)
{
if (actionName.StartsWith(trimPrefix))
{
actionName = actionName.Substring(trimPrefix.Length);
break;
}
}
if (!
string.IsNullOrEmpty(actionName))
{
routeTemplate.Append(
$"/{actionName}");
}
return routeTemplate.ToString();
}
private string GetHttpMethod(ActionModel action)
{
var actionName = action.ActionName;
if (actionName.StartsWith(
"Get"))
{
return "GET";
}
if (actionName.StartsWith(
"Put") || actionName.StartsWith(
"Update"))
{
return "PUT";
}
if (actionName.StartsWith(
"Delete") || actionName.StartsWith(
"Remove"))
{
return "DELETE";
}
if (actionName.StartsWith(
"Patch"))
{
return "PATCH";
}
return "POST";
}
private void RemoveEmptySelectors(IList<SelectorModel> selectors)
{
for (
var i = selectors.Count -
1; i >=
0; i--)
{
var selector = selectors[i];
if (selector.AttributeRouteModel ==
null &&
(selector.ActionConstraints ==
null || selector.ActionConstraints.Count <=
0) &&
(selector.EndpointMetadata ==
null || selector.EndpointMetadata.Count <=
0))
{
selectors.Remove(selector);
}
}
}
}
}
嗯,他们差不多完成了,就剩最后两个 ConfigureParameters。继续参照 abp vnext(这块 Panda.DynamicWebApi 同时实现也几乎一样了):
https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs#L67
没啥东西,主要是配置控制器的每个 Action 的每个参数的 Binding。
关键就后面两个判断,
TypeHelper.IsPrimitiveExtended 类似于判断呢基础类型。例如 int、long 这些就是基础类型,是不应该加上 FromBody 绑定的,因此 abp vnext 进一步判断像 Nullable<int>、Nullable<long>、DateTime 这些也不应该加 FromBody 绑定。那个阅读 TypeHelper 的源代码还是很好懂的。第二个判断则判断了当前 Http 约束是否能用 FormBody,例如 GET、DELETE 请求是没办法用 FromBody 的。
即使是固定式版,他们可以同时实现如下表所示:
private void ConfigureParameters(ControllerModel controller)
{
foreach (
var action
in controller.Actions)
{
foreach (
var parameter
in action.Parameters)
{
if (parameter.BindingInfo !=
null)
{
continue;
}
if (parameter.ParameterType.IsClass &&
parameter.ParameterType !=
typeof(
string) &&
parameter.ParameterType !=
typeof(IFormFile))
{
var httpMethods = action.Selectors.SelectMany(
temp => temp.ActionConstraints).OfType<HttpMethodActionConstraint>().SelectMany(
temp => temp.HttpMethods).ToList();
if (httpMethods.Contains(
"GET") ||
httpMethods.Contains(
"DELETE") ||
httpMethods.Contains(
"TRACE") ||
httpMethods.Contains(
"HEAD"))
{
continue;
}
parameter.BindingInfo = BindingInfo.GetBindingInfo(
new[] {
new FromBodyAttribute() });
}
}
}
}
当然第两个判断没有 abp vnext 和 Panda.DynamicWebApi 严谨,但 90% 情况足够用了。第二个判断则把 Http 约束通通查出来,如果有 GET、DELETE 等等这些则不能加 FromBody 约束,反之则加上。
模拟:
历经千辛万苦,他们的固定式版静态 Web API 终于完成了。接下去他们可以给 Demo 工程项目添加呵呵测试代码以及配置 swagger 来看呵呵效果。
在 Demo 工程项目中添加测试代码 PersonAppService:
using SimpleDynamicWebAPI;
using System.Collections.Generic;
using System.Linq;
namespace Demo.Application
{
public class CreateUpdatePersonInput
{
public string Name {
get;
set; }
}
public class PersonDto
{
public string Name {
get;
set; }
}
public class PersonAppService :
IApplicationService
{
public string Create(CreateUpdatePersonInput input)
{
return $"你造了个名字叫:{input.Name} 的人";
}
public string Delete(int id)
{
return $"你把 Id:{id} 的人干掉了";
}
public string Get(int id)
{
return $"你输入的 Id 是:{id}";
}
public List<PersonDto> GetAll()
{
return "服务器向你扔了一堆人"
.ToCharArray()
.Select(temp =>
new PersonDto
{
Name = temp.ToString()
})
.ToList();
}
public string Update(int id, CreateUpdatePersonInput input)
{
return $"你把 Id:{id} 的人的名字改成了 {input.Name}";
}
}
}
配置 swagger 的该文Seiches里多得是,这里就不贴代码了。
完事后跑出来。
感觉还行。
结语:
他们总算同时实现了两个非常固定式的静态 Web API,也相当于又造了两遍轮子,但在这造轮子的过程中,他们了解到了其同时实现的基本原理,假如以后发现 abp vnext 等架构的静态 Web API 满足没法他们的时候,他们也有一定能力进行修改。最后我再次新闻稿,如果没有把握的话,千万别用作生产环境。abp vnext 这种是经过大量工程项目验证的,即使有 bug,abp vnext 官方也有足够人力去修复。
最后附上 Gayhub 源代码:
https://github.com/h82258652/SimpleDynamicWebAPI
原文来源:博客园
原文地址:
https://www.cnblogs.com/h82258652/p/12454554.html