Files
ThingsGateway/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs
2025-05-14 18:52:19 +08:00

1000 lines
45 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;
using System.Reflection;
using System.Text.RegularExpressions;
using ThingsGateway.Extensions;
using ThingsGateway.UnifyResult;
namespace ThingsGateway.DynamicApiController;
/// <summary>
/// 动态接口控制器应用模型转换器
/// </summary>
internal sealed class DynamicApiControllerApplicationModelConvention : IApplicationModelConvention
{
/// <summary>
/// 动态接口控制器配置实例
/// </summary>
private readonly DynamicApiControllerSettingsOptions _dynamicApiControllerSettings;
/// <summary>
/// 带版本的名称正则表达式
/// </summary>
private readonly Regex _nameVersionRegex;
/// <summary>
/// 服务集合
/// </summary>
private readonly IServiceCollection _services;
/// <summary>
/// 模板正则表达式
/// </summary>
private const string commonTemplatePattern = @"\{(?<p>.+?)\}";
/// <summary>
/// 动态 WebAPI 构建器
/// </summary>
private readonly DynamicApiControllerBuilder _dynamicApiControllerBuilder;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="services">服务集合</param>
public DynamicApiControllerApplicationModelConvention(IServiceCollection services)
{
_services = services;
_dynamicApiControllerSettings = App.GetConfig<DynamicApiControllerSettingsOptions>("DynamicApiControllerSettings", true);
LoadVerbToHttpMethodsConfigure();
_nameVersionRegex = new Regex(@"V(?<version>[0-9_]+$)");
_dynamicApiControllerBuilder = services.FirstOrDefault(u => u.ServiceType == typeof(DynamicApiControllerBuilder))?.ImplementationInstance as DynamicApiControllerBuilder;
}
/// <summary>
/// 配置应用模型信息
/// </summary>
/// <param name="application">引用模型</param>
public void Apply(ApplicationModel application)
{
var controllers = application.Controllers.Where(u =>
{
return Penetrates.IsApiController(u.ControllerType)
&& (_dynamicApiControllerBuilder?.ControllerFilter == null || _dynamicApiControllerBuilder.ControllerFilter.Invoke(u));
});
foreach (var controller in controllers)
{
var controllerType = controller.ControllerType;
// 解析 [ApiDescriptionSettings] 特性
var controllerApiDescriptionSettings = controllerType.IsDefined(typeof(ApiDescriptionSettingsAttribute), true) ? controllerType.GetCustomAttribute<ApiDescriptionSettingsAttribute>(true) : default;
// 判断是否处理 Mvc控制器
if (typeof(ControllerBase).IsAssignableFrom(controllerType))
{
if (!_dynamicApiControllerSettings.SupportedMvcController.Value || controller.ApiExplorer?.IsVisible == false)
{
// 存储排序给 Swagger 使用
Penetrates.ControllerOrderCollection.TryAdd(controller.ControllerName, (controllerApiDescriptionSettings?.Tag ?? controller.ControllerName, controllerApiDescriptionSettings?.Order ?? 0, controller.ControllerType));
// 控制器默认处理规范化结果
if (UnifyContext.EnabledUnifyHandler)
{
foreach (var action in controller.Actions)
{
// 配置动作方法规范化特性
ConfigureActionUnifyResultAttribute(action);
}
}
continue;
}
}
ConfigureController(controller, controllerApiDescriptionSettings);
}
}
/// <summary>
/// 配置控制器
/// </summary>
/// <param name="controller">控制器模型</param>
/// <param name="controllerApiDescriptionSettings">接口描述配置</param>
private void ConfigureController(ControllerModel controller, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
// 配置区域
ConfigureControllerArea(controller, controllerApiDescriptionSettings);
// 配置控制器名称
var isLowercaseRoute = ConfigureControllerName(controller, controllerApiDescriptionSettings);
// 配置控制器路由特性
ConfigureControllerRouteAttribute(controller, controllerApiDescriptionSettings, isLowercaseRoute);
// 存储排序给 Swagger 使用
Penetrates.ControllerOrderCollection.TryAdd(controller.ControllerName, (controllerApiDescriptionSettings?.Tag ?? controller.ControllerName, controllerApiDescriptionSettings?.Order ?? 0, controller.ControllerType));
var actions = controller.Actions;
// 查找所有重复的方法签名
var repeats = actions.GroupBy(u => new { u.ActionMethod.ReflectedType.Name, Signature = u.ActionMethod.ToString() })
.Where(u => u.Count() > 1)
.SelectMany(u => u.Where(u => u.ActionMethod.ReflectedType.Name != u.ActionMethod.DeclaringType.Name));
// 2021年04月01日 https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference
// 判断是否贴有 [ApiController] 特性
var hasApiControllerAttribute = controller.Attributes.Any(u => u.GetType() == typeof(ApiControllerAttribute));
foreach (var action in actions)
{
// 跳过相同方法签名
if (repeats.Contains(action))
{
action.ApiExplorer.IsVisible = false;
continue;
}
;
var actionMethod = action.ActionMethod;
var actionApiDescriptionSettings = actionMethod.IsDefined(typeof(ApiDescriptionSettingsAttribute), true) ? actionMethod.GetCustomAttribute<ApiDescriptionSettingsAttribute>(true) : default;
// 检查当前方法是否是继承而来
if (controller.ControllerType.IsSubclassOf(actionMethod.DeclaringType) && actionApiDescriptionSettings?.DisableInherite == true)
{
action.ApiExplorer.IsVisible = false;
continue;
}
ConfigureAction(action, actionApiDescriptionSettings, controllerApiDescriptionSettings, hasApiControllerAttribute);
// 添加 Action 自定义配置
_dynamicApiControllerBuilder?.ActionConfigure?.Invoke(action);
}
}
/// <summary>
/// 配置控制器区域
/// </summary>
/// <param name="controller"></param>
/// <param name="controllerApiDescriptionSettings"></param>
private void ConfigureControllerArea(ControllerModel controller, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
// 如果配置了区域,则跳过
if (controller.RouteValues.ContainsKey("area")) return;
// 如果没有配置区域,则跳过
var area = controllerApiDescriptionSettings?.Area ?? _dynamicApiControllerSettings.DefaultArea;
if (string.IsNullOrWhiteSpace(area)) return;
controller.RouteValues["area"] = area;
}
/// <summary>
/// 配置控制器名称
/// </summary>
/// <param name="controller">控制器模型</param>
/// <param name="controllerApiDescriptionSettings">接口描述配置</param>
/// <returns></returns>
private bool ConfigureControllerName(ControllerModel controller, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
var (Name, IsLowercaseRoute, _, _) = ConfigureControllerAndActionName(controllerApiDescriptionSettings, controller.ControllerType.Name, _dynamicApiControllerSettings.AbandonControllerAffixes, _ => _);
controller.ControllerName = Name;
return IsLowercaseRoute;
}
/// <summary>
/// 强制处理了 ForceWithDefaultPrefix 的控制器
/// </summary>
/// <remarks>避免路由无限追加</remarks>
private ConcurrentBag<Type> ForceWithDefaultPrefixRouteControllerTypes { get; } = new ConcurrentBag<Type>();
/// <summary>
/// 配置控制器路由特性
/// </summary>
/// <param name="controller"></param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <param name="isLowercaseRoute"></param>
private void ConfigureControllerRouteAttribute(ControllerModel controller, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings, bool isLowercaseRoute)
{
// 解决 Gitee 该 Issuehttps://gitee.com/dotnetchina/Furion/issues/I59B74
if (CheckIsForceWithDefaultRoute(controllerApiDescriptionSettings)
&& !string.IsNullOrWhiteSpace(_dynamicApiControllerSettings.DefaultRoutePrefix)
&& controller.Selectors[0] != null
&& controller.Selectors[0].AttributeRouteModel != null
&& !ForceWithDefaultPrefixRouteControllerTypes.Contains(controller.ControllerType))
{
// 读取模块
var module = controllerApiDescriptionSettings?.Module ?? _dynamicApiControllerSettings.DefaultModule;
var template = $"{_dynamicApiControllerSettings.DefaultRoutePrefix}/{module}";
controller.Selectors[0].AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(new AttributeRouteModel(new RouteAttribute(isLowercaseRoute ? template?.ToLower() : template))
, controller.Selectors[0].AttributeRouteModel);
ForceWithDefaultPrefixRouteControllerTypes.Add(controller.ControllerType);
}
}
/// <summary>
/// 配置动作方法
/// </summary>
/// <param name="action">控制器模型</param>
/// <param name="apiDescriptionSettings">接口描述配置</param>
/// <param name="controllerApiDescriptionSettings">控制器接口描述配置</param>
/// <param name="hasApiControllerAttribute">是否贴有 ApiController 特性</param>
private void ConfigureAction(ActionModel action, ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings, bool hasApiControllerAttribute)
{
// 配置动作方法接口可见性
ConfigureActionApiExplorer(action);
// 配置动作方法名称
var (isLowercaseRoute, isKeepName, isLowerCamelCase) = ConfigureActionName(action, apiDescriptionSettings, controllerApiDescriptionSettings);
// 配置动作方法请求谓词特性
ConfigureActionHttpMethodAttribute(action);
// 配置引用类型参数
ConfigureClassTypeParameter(action);
// 配置动作方法路由特性
ConfigureActionRouteAttribute(action, apiDescriptionSettings, controllerApiDescriptionSettings, isLowercaseRoute, isKeepName, isLowerCamelCase, hasApiControllerAttribute);
// 配置动作方法规范化特性
if (UnifyContext.EnabledUnifyHandler) ConfigureActionUnifyResultAttribute(action);
}
/// <summary>
/// 配置动作方法接口可见性
/// </summary>
/// <param name="action">动作方法模型</param>
private static void ConfigureActionApiExplorer(ActionModel action)
{
if (!action.ApiExplorer.IsVisible.HasValue) action.ApiExplorer.IsVisible = true;
}
/// <summary>
/// 配置动作方法名称
/// </summary>
/// <param name="action">动作方法模型</param>
/// <param name="apiDescriptionSettings">接口描述配置</param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <returns></returns>
private (bool IsLowercaseRoute, bool IsKeepName, bool IsLowerCamelCase) ConfigureActionName(ActionModel action, ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
// 判断是否贴有 [ActionName]
string actionName = null;
// 判断是否贴有 [ActionName] 且 Name 不为 null
var actionNameAttribute = action.ActionMethod.IsDefined(typeof(ActionNameAttribute), true)
? action.ActionMethod.GetCustomAttribute<ActionNameAttribute>(true)
: null;
if (actionNameAttribute?.Name != null)
{
actionName = actionNameAttribute.Name;
}
var (Name, IsLowercaseRoute, IsKeepName, IsLowerCamelCase) = ConfigureControllerAndActionName(apiDescriptionSettings, action.ActionMethod.Name
, _dynamicApiControllerSettings.AbandonActionAffixes
, (tempName) =>
{
// 处理动作方法名称谓词
if (!CheckIsKeepVerb(apiDescriptionSettings, controllerApiDescriptionSettings))
{
var words = tempName.SplitCamelCase();
var verbKey = words.First().ToLower();
// 处理类似 getlist,getall 多个单词
if (words.Length > 1 && Penetrates.VerbToHttpMethods.ContainsKey((words[0] + words[1]).ToLower()))
{
tempName = tempName[(words[0] + words[1]).Length..];
}
else if (Penetrates.VerbToHttpMethods.ContainsKey(verbKey)) tempName = tempName[verbKey.Length..];
}
return tempName;
}, controllerApiDescriptionSettings, actionName);
action.ActionName = Name;
return (IsLowercaseRoute, IsKeepName, IsLowerCamelCase);
}
/// <summary>
/// 配置动作方法请求谓词特性
/// </summary>
/// <param name="action">动作方法模型</param>
private void ConfigureActionHttpMethodAttribute(ActionModel action)
{
var selectorModel = action.Selectors[0];
// 跳过已配置请求谓词特性的配置
if (selectorModel.ActionConstraints.Any(u => u is HttpMethodActionConstraint)) return;
// 解析请求谓词
var words = action.ActionMethod.Name.SplitCamelCase();
var verbKey = words.First().ToLower();
// 处理类似 getlist,getall 多个单词
if (words.Length > 1 && Penetrates.VerbToHttpMethods.ContainsKey((words[0] + words[1]).ToLower()))
{
verbKey = (words[0] + words[1]).ToLower();
}
var succeed = Penetrates.VerbToHttpMethods.TryGetValue(verbKey, out var verbValue);
var verb = succeed ? verbValue : _dynamicApiControllerSettings.DefaultHttpMethod.ToUpper();
// 添加请求约束
selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { verb }));
// 添加请求谓词特性
HttpMethodAttribute httpMethodAttribute = verb switch
{
"GET" => new HttpGetAttribute(),
"POST" => new HttpPostAttribute(),
"PUT" => new HttpPutAttribute(),
"DELETE" => new HttpDeleteAttribute(),
"PATCH" => new HttpPatchAttribute(),
"HEAD" => new HttpHeadAttribute(),
_ => throw new NotSupportedException($"{verb}")
};
selectorModel.EndpointMetadata.Add(httpMethodAttribute);
}
/// <summary>
/// 处理类类型参数(添加[FromBody] 特性)
/// </summary>
/// <param name="action"></param>
private void ConfigureClassTypeParameter(ActionModel action)
{
// 没有参数无需处理
if (action.Parameters.Count == 0) return;
// 如果动作方法请求谓词只有GET和HEAD则将类转查询参数
if (_dynamicApiControllerSettings.ModelToQuery.Value)
{
var httpMethods = action.Selectors
.SelectMany(u => u.ActionConstraints.Where(u => u is HttpMethodActionConstraint)
.SelectMany(u => (u as HttpMethodActionConstraint).HttpMethods));
if (httpMethods.All(u => u.Equals("GET") || u.Equals("HEAD"))) return;
}
var parameters = action.Parameters;
foreach (var parameterModel in parameters)
{
// 如果参数已有绑定特性,则跳过
if (parameterModel.BindingInfo != null) continue;
var parameterType = parameterModel.ParameterType;
// 如果是基元类型,则跳过
if (parameterType.IsRichPrimitive()) continue;
// 如果是文件类型,则跳过
if (typeof(IFormFile).IsAssignableFrom(parameterType) || typeof(IFormFileCollection).IsAssignableFrom(parameterType)) continue;
// 处理 .NET7 接口问题,同时支持 .NET5/6 无需贴 [FromServices] 操作
if (parameterType.IsInterface
&& !parameterModel.Attributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()))
&& _services.Any(s => s.ServiceType.Name == parameterType.Name))
{
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromServicesAttribute() });
continue;
}
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromBodyAttribute() });
}
}
/// <summary>
/// 配置动作方法路由特性
/// </summary>
/// <param name="action">动作方法模型</param>
/// <param name="apiDescriptionSettings">接口描述配置</param>
/// <param name="controllerApiDescriptionSettings">控制器接口描述配置</param>
/// <param name="isLowercaseRoute"></param>
/// <param name="isKeepName"></param>
/// <param name="isLowerCamelCase"></param>
/// <param name="hasApiControllerAttribute"></param>
private void ConfigureActionRouteAttribute(ActionModel action, ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings, bool isLowercaseRoute, bool isKeepName, bool isLowerCamelCase, bool hasApiControllerAttribute)
{
foreach (var selectorModel in action.Selectors)
{
// 读取模块
var module = apiDescriptionSettings?.Module;
// 跳过已配置路由特性的配置
if (selectorModel.AttributeRouteModel != null)
{
// 1. 如果控制器自定义了 [Route] 特性,则跳过
if (action.ActionMethod.DeclaringType.IsDefined(typeof(RouteAttribute), true)
|| action.Controller.ControllerType.IsDefined(typeof(RouteAttribute), true))
{
if (string.IsNullOrWhiteSpace(selectorModel.AttributeRouteModel.Template)
&& !string.IsNullOrWhiteSpace(selectorModel.AttributeRouteModel.Name))
{
selectorModel.AttributeRouteModel.Template = selectorModel.AttributeRouteModel.Name;
}
var newTemplate = $"{(selectorModel.AttributeRouteModel.Template?.StartsWith('/') == true ? "/" : null)}{(string.IsNullOrWhiteSpace(module) ? null : $"{module}/")}{selectorModel.AttributeRouteModel.Template}";
// 处理可能存在多斜杠问题
newTemplate = Regex.Replace(newTemplate, @"\/{2,}", "/");
selectorModel.AttributeRouteModel.Template = isLowercaseRoute ? ConvertToLowerCaseExceptBrackets(newTemplate) : newTemplate;
continue;
}
// 2. 如果方法自定义路由模板且以 `/` 开头,则跳过
if (!string.IsNullOrWhiteSpace(selectorModel.AttributeRouteModel.Template) && selectorModel.AttributeRouteModel.Template.StartsWith('/')) continue;
}
string template;
string controllerRouteTemplate = null;
// 如果动作方法名称为空、参数值为空,且无需保留谓词,则只生成控制器路由模板
if (action.ActionName.Length == 0 && !isKeepName && action.Parameters.Count == 0)
{
template = GenerateControllerRouteTemplate(action.Controller, controllerApiDescriptionSettings);
if (!string.IsNullOrWhiteSpace(selectorModel.AttributeRouteModel?.Template))
{
template = $"{template}/{selectorModel.AttributeRouteModel?.Template}";
}
}
else
{
// 生成参数路由模板
var parameterRouteTemplate = GenerateParameterRouteTemplates(action, isLowercaseRoute, isLowerCamelCase, hasApiControllerAttribute);
// 生成控制器模板
controllerRouteTemplate = GenerateControllerRouteTemplate(action.Controller, controllerApiDescriptionSettings, parameterRouteTemplate);
// 拼接动作方法路由模板
var ActionStartTemplate = parameterRouteTemplate != null ? (parameterRouteTemplate.ActionStartTemplates.Count == 0 ? null : string.Join("/", parameterRouteTemplate.ActionStartTemplates)) : null;
var ActionEndTemplate = parameterRouteTemplate != null ? (parameterRouteTemplate.ActionEndTemplates.Count == 0 ? null : string.Join("/", parameterRouteTemplate.ActionEndTemplates)) : null;
// 判断是否定义了控制器路由,如果定义,则不拼接控制器路由
var actionRouteTemplate = string.IsNullOrWhiteSpace(action.ActionName)
|| (action.Controller.Selectors[0].AttributeRouteModel?.Template?.Contains("[action]") ?? false) ? null : (selectorModel?.AttributeRouteModel?.Template ?? selectorModel?.AttributeRouteModel?.Name ?? "[action]");
if (actionRouteTemplate == null && !string.IsNullOrWhiteSpace(selectorModel.AttributeRouteModel?.Template))
{
actionRouteTemplate = $"{actionRouteTemplate}/{selectorModel.AttributeRouteModel?.Template}";
}
template = string.IsNullOrWhiteSpace(controllerRouteTemplate)
? $"{(string.IsNullOrWhiteSpace(module) ? "/" : $"/{module}/")}{ActionStartTemplate}/{actionRouteTemplate}/{ActionEndTemplate}"
: $"{controllerRouteTemplate}/{(string.IsNullOrWhiteSpace(module) ? null : $"/{module}/")}{ActionStartTemplate}/{actionRouteTemplate}/{ActionEndTemplate}";
}
AttributeRouteModel actionAttributeRouteModel = null;
if (!string.IsNullOrWhiteSpace(template))
{
// 处理多个斜杆问题
template = isLowercaseRoute ? template.ToLower() : isLowerCamelCase ? template.ToLowerCamelCase() : template;
template = HandleRouteTemplateRepeat(template);
template = Regex.Replace(template, @"\/{2,}", "/");
// 生成路由
actionAttributeRouteModel = string.IsNullOrWhiteSpace(template) ? null : new AttributeRouteModel(new RouteAttribute(template));
}
// 拼接路由
selectorModel.AttributeRouteModel = string.IsNullOrWhiteSpace(controllerRouteTemplate)
? (actionAttributeRouteModel == null ? null : AttributeRouteModel.CombineAttributeRouteModel(action.Controller.Selectors[0].AttributeRouteModel, actionAttributeRouteModel))
: actionAttributeRouteModel;
}
}
/// <summary>
/// 生成控制器路由模板
/// </summary>
/// <param name="controller"></param>
/// <param name="apiDescriptionSettings"></param>
/// <param name="parameterRouteTemplate">参数路由模板</param>
/// <returns></returns>
private string GenerateControllerRouteTemplate(ControllerModel controller, ApiDescriptionSettingsAttribute apiDescriptionSettings, ParameterRouteTemplate parameterRouteTemplate = default)
{
var selectorModel = controller.Selectors[0];
// 跳过已配置路由特性的配置
if (selectorModel.AttributeRouteModel != null) return default;
// 读取模块
var module = apiDescriptionSettings?.Module ?? _dynamicApiControllerSettings.DefaultModule;
// 路由默认前缀
var routePrefix = _dynamicApiControllerSettings.DefaultRoutePrefix;
// 生成路由模板
// 如果参数路由模板为空或不包含任何控制器参数模板,则返回正常的模板
if (parameterRouteTemplate == null || (parameterRouteTemplate.ControllerStartTemplates.Count == 0 && parameterRouteTemplate.ControllerEndTemplates.Count == 0))
return $"{(string.IsNullOrWhiteSpace(routePrefix) ? null : $"{routePrefix}/")}{(string.IsNullOrWhiteSpace(module) ? null : $"/{module}/")}[controller]";
// 拼接控制器路由模板
var controllerStartTemplate = parameterRouteTemplate.ControllerStartTemplates.Count == 0 ? null : string.Join("/", parameterRouteTemplate.ControllerStartTemplates);
var controllerEndTemplate = parameterRouteTemplate.ControllerEndTemplates.Count == 0 ? null : string.Join("/", parameterRouteTemplate.ControllerEndTemplates);
var template = $"{(string.IsNullOrWhiteSpace(routePrefix) ? null : $"{routePrefix}/")}{(string.IsNullOrWhiteSpace(module) ? null : $"/{module}/")}{controllerStartTemplate}/[controller]/{controllerEndTemplate}";
return template;
}
/// <summary>
/// 生成参数路由模板(非引用类型)
/// </summary>
/// <param name="action">动作方法模型</param>
/// <param name="isLowercaseRoute"></param>
/// <param name="isLowerCamelCase"></param>
/// <param name="hasApiControllerAttribute"></param>
private ParameterRouteTemplate GenerateParameterRouteTemplates(ActionModel action, bool isLowercaseRoute, bool isLowerCamelCase, bool hasApiControllerAttribute)
{
// 如果没有参数,则跳过
if (action.Parameters.Count == 0) return default;
var parameterRouteTemplate = new ParameterRouteTemplate();
var parameters = action.Parameters
.Where(u => !(u.BindingInfo is { BindingSource.DisplayName: "Special" } || u.Attributes.Any(c => c.GetType() == typeof(BindNeverAttribute))));
// 判断是否贴有 [QueryParameters] 特性
var isQueryParametersAction = action.Attributes.Any(u => u is QueryParametersAttribute);
// 遍历所有参数
foreach (var parameterModel in parameters)
{
var parameterType = parameterModel.ParameterType;
var parameterAttributes = parameterModel.Attributes;
// 处理小写参数路由匹配问题
if (isLowercaseRoute) parameterModel.ParameterName = parameterModel.ParameterName.ToLower();
// 处理小驼峰命名
if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase();
// 判断是否贴有任何 [FromXXX] 特性了
var hasFromAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()));
// 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性
if (isQueryParametersAction && !hasFromAttribute)
{
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() });
continue;
}
// 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过
// 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过
if (!parameterAttributes.Any(u => u is FromRouteAttribute)
&& (!parameterType.IsRichPrimitive() || hasFromAttribute)) continue;
// 处理基元数组数组类型,还有全局配置参数问题
if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray)
{
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() });
continue;
}
// 处理 [ApiController] 特性情况
// https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference
if (!hasFromAttribute && hasApiControllerAttribute) continue;
// 处理默认基元参数绑定方式,若是 query[FromQuery])则跳过
if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query")
{
continue;
}
// 判断是否可以为null
var canBeNull = parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(Nullable<>);
// 判断是否贴有路由约束特性
string constraint = default;
if (parameterAttributes.FirstOrDefault(u => u is RouteConstraintAttribute) is RouteConstraintAttribute routeConstraint && !string.IsNullOrWhiteSpace(routeConstraint.Constraint))
{
constraint = !routeConstraint.Constraint.StartsWith(':')
? $":{routeConstraint.Constraint}" : routeConstraint.Constraint;
}
var template = $"{{{(constraint == ":*" ? "*" : default)}{parameterModel.ParameterName}{(canBeNull ? "?" : string.Empty)}{(constraint == ":*" ? default : constraint)}}}";
// 如果没有贴路由位置特性,则默认添加到动作方法后面
if (parameterAttributes.FirstOrDefault(u => u is ApiSeatAttribute) is not ApiSeatAttribute apiSeat)
{
parameterRouteTemplate.ActionEndTemplates.Add(template);
continue;
}
// 生成路由参数位置
switch (apiSeat.Seat)
{
// 控制器名之前
case ApiSeats.ControllerStart:
parameterRouteTemplate.ControllerStartTemplates.Add(template);
break;
// 控制器名之后
case ApiSeats.ControllerEnd:
parameterRouteTemplate.ControllerEndTemplates.Add(template);
break;
// 动作方法名之前
case ApiSeats.ActionStart:
parameterRouteTemplate.ActionStartTemplates.Add(template);
break;
// 动作方法名之后
case ApiSeats.ActionEnd:
parameterRouteTemplate.ActionEndTemplates.Add(template);
break;
default: break;
}
}
return parameterRouteTemplate;
}
/// <summary>
/// 配置控制器和动作方法名称
/// </summary>
/// <param name="apiDescriptionSettings"></param>
/// <param name="orignalName"></param>
/// <param name="affixes"></param>
/// <param name="configure"></param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <param name="actionName">针对 [ActionName] 特性和 [HttpMethod] 特性处理</param>
/// <returns></returns>
private (string Name, bool IsLowercaseRoute, bool IsKeepName, bool IsLowerCamelCase) ConfigureControllerAndActionName(ApiDescriptionSettingsAttribute apiDescriptionSettings
, string orignalName
, string[] affixes
, Func<string, string> configure
, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings = default
, string actionName = default)
{
// 获取版本号
var apiVersion = apiDescriptionSettings?.Version;
var isKeepName = false;
// 判断是否有自定义名称
var tempName = actionName ?? apiDescriptionSettings?.Name;
if (string.IsNullOrWhiteSpace(tempName))
{
// 处理版本号
var (name, version) = ResolveNameVersion(orignalName);
tempName = name;
apiVersion ??= version;
// 清除指定(前)后缀,只处理后缀,解决 ServiceService 的情况
tempName = tempName.ClearStringAffixes(1, affixes: affixes);
isKeepName = CheckIsKeepName(controllerApiDescriptionSettings == null ? null : apiDescriptionSettings, controllerApiDescriptionSettings ?? apiDescriptionSettings);
// 判断是否保留原有名称
if (!isKeepName)
{
// 自定义配置
tempName = configure.Invoke(tempName);
// 处理骆驼命名
if (CheckIsSplitCamelCase(controllerApiDescriptionSettings == null ? null : apiDescriptionSettings, controllerApiDescriptionSettings ?? apiDescriptionSettings))
{
tempName = string.Join(_dynamicApiControllerSettings.CamelCaseSeparator, tempName.SplitCamelCase());
}
}
}
// 拼接名称和版本号
var versionString = string.IsNullOrWhiteSpace(apiVersion) ? null : $"{_dynamicApiControllerSettings.VersionSeparator}{apiVersion}/";
var isLowercaseRoute = CheckIsLowercaseRoute(controllerApiDescriptionSettings == null ? null : apiDescriptionSettings, controllerApiDescriptionSettings ?? apiDescriptionSettings);
var isLowerCamelCase = CheckIsLowerCamelCase(controllerApiDescriptionSettings == null ? null : apiDescriptionSettings, controllerApiDescriptionSettings ?? apiDescriptionSettings);
tempName = isLowerCamelCase ? tempName.ToLowerCamelCase() : tempName;
// 处理版本号前后问题
var newName = _dynamicApiControllerSettings.VersionInFront == true
? $"{versionString}{tempName}"
: $"{tempName}{versionString}";
newName = newName.TrimEnd('/');
return (isLowercaseRoute ? newName.ToLower() : newName
, isLowercaseRoute, isKeepName, isLowerCamelCase);
}
/// <summary>
/// 检查是否设置了 KeepName参数
/// </summary>
/// <param name="apiDescriptionSettings"></param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <returns></returns>
private bool CheckIsKeepName(ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
bool isKeepName;
// 判断 Action 是否配置了 KeepName 属性
if (apiDescriptionSettings?.KeepName != null)
{
var canParse = bool.TryParse(apiDescriptionSettings.KeepName.ToString(), out var value);
isKeepName = canParse && value;
}
// 判断 Controller 是否配置了 KeepName 属性
else if (controllerApiDescriptionSettings?.KeepName != null)
{
var canParse = bool.TryParse(controllerApiDescriptionSettings.KeepName.ToString(), out var value);
isKeepName = canParse && value;
}
// 取全局配置
else isKeepName = _dynamicApiControllerSettings?.KeepName == true;
return isKeepName;
}
/// <summary>
/// 检查是否设置了 KeepVerb 参数
/// </summary>
/// <param name="apiDescriptionSettings"></param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <returns></returns>
private bool CheckIsKeepVerb(ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
bool isKeepVerb;
// 判断 Action 是否配置了 KeepVerb 属性
if (apiDescriptionSettings?.KeepVerb != null)
{
var canParse = bool.TryParse(apiDescriptionSettings.KeepVerb.ToString(), out var value);
isKeepVerb = canParse && value;
}
// 判断 Controller 是否配置了 KeepVerb 属性
else if (controllerApiDescriptionSettings?.KeepVerb != null)
{
var canParse = bool.TryParse(controllerApiDescriptionSettings.KeepVerb.ToString(), out var value);
isKeepVerb = canParse && value;
}
// 取全局配置
else isKeepVerb = _dynamicApiControllerSettings?.KeepVerb == true;
return isKeepVerb;
}
/// <summary>
/// 检查是否设置了 ForceWithRoutePrefix 参数
/// </summary>
/// <param name="controllerApiDescriptionSettings"></param>
/// <returns></returns>
private bool CheckIsForceWithDefaultRoute(ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
bool isForceWithRoutePrefix;
// 判断 Controller 是否配置了 ForceWithRoutePrefix 属性
if (controllerApiDescriptionSettings?.ForceWithRoutePrefix != null)
{
var canParse = bool.TryParse(controllerApiDescriptionSettings.ForceWithRoutePrefix.ToString(), out var value);
isForceWithRoutePrefix = canParse && value;
}
// 取全局配置
else isForceWithRoutePrefix = _dynamicApiControllerSettings?.ForceWithRoutePrefix == true;
return isForceWithRoutePrefix;
}
/// <summary>
/// 检查是否设置了 AsLowerCamelCase 参数
/// </summary>
/// <param name="apiDescriptionSettings"></param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <returns></returns>
private bool CheckIsLowerCamelCase(ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
bool isLowerCamelCase;
// 判断 Action 是否配置了 AsLowerCamelCase 属性
if (apiDescriptionSettings?.AsLowerCamelCase != null)
{
var canParse = bool.TryParse(apiDescriptionSettings.AsLowerCamelCase.ToString(), out var value);
isLowerCamelCase = canParse && value;
}
// 判断 Controller 是否配置了 AsLowerCamelCase 属性
else if (controllerApiDescriptionSettings?.AsLowerCamelCase != null)
{
var canParse = bool.TryParse(controllerApiDescriptionSettings.AsLowerCamelCase.ToString(), out var value);
isLowerCamelCase = canParse && value;
}
// 取全局配置
else isLowerCamelCase = _dynamicApiControllerSettings?.AsLowerCamelCase == true;
return isLowerCamelCase;
}
/// <summary>
/// 判断切割命名参数是否配置
/// </summary>
/// <param name="apiDescriptionSettings"></param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <returns></returns>
private static bool CheckIsSplitCamelCase(ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
bool isSplitCamelCase;
// 判断 Action 是否配置了 SplitCamelCase 属性
if (apiDescriptionSettings?.SplitCamelCase != null)
{
var canParse = bool.TryParse(apiDescriptionSettings.SplitCamelCase.ToString(), out var value);
isSplitCamelCase = !canParse || value;
}
// 判断 Controller 是否配置了 SplitCamelCase 属性
else if (controllerApiDescriptionSettings?.SplitCamelCase != null)
{
var canParse = bool.TryParse(controllerApiDescriptionSettings.SplitCamelCase.ToString(), out var value);
isSplitCamelCase = !canParse || value;
}
// 取全局配置
else isSplitCamelCase = true;
return isSplitCamelCase;
}
/// <summary>
/// 检查是否启用小写路由
/// </summary>
/// <param name="apiDescriptionSettings"></param>
/// <param name="controllerApiDescriptionSettings"></param>
/// <returns></returns>
private bool CheckIsLowercaseRoute(ApiDescriptionSettingsAttribute apiDescriptionSettings, ApiDescriptionSettingsAttribute controllerApiDescriptionSettings)
{
bool isLowercaseRoute;
// 判断 Action 是否配置了 LowercaseRoute 属性
if (apiDescriptionSettings?.LowercaseRoute != null)
{
var canParse = bool.TryParse(apiDescriptionSettings.LowercaseRoute.ToString(), out var value);
isLowercaseRoute = !canParse || value;
}
// 判断 Controller 是否配置了 LowercaseRoute 属性
else if (controllerApiDescriptionSettings?.LowercaseRoute != null)
{
var canParse = bool.TryParse(controllerApiDescriptionSettings.LowercaseRoute.ToString(), out var value);
isLowercaseRoute = !canParse || value;
}
// 取全局配置
else isLowercaseRoute = (_dynamicApiControllerSettings?.LowercaseRoute) != false;
return isLowercaseRoute;
}
/// <summary>
/// 配置规范化结果类型
/// </summary>
/// <param name="action"></param>
private static void ConfigureActionUnifyResultAttribute(ActionModel action)
{
// 判断是否手动添加了标注或跳过规范化处理
if (UnifyContext.CheckSucceededNonUnify(action.ActionMethod, out _, false)) return;
// 获取真实类型
var returnType = action.ActionMethod.GetRealReturnType();
if (returnType == typeof(void)) return;
// 添加规范化结果特性
action.Filters.Add(new UnifyResultAttribute(returnType, StatusCodes.Status200OK, action.ActionMethod));
}
/// <summary>
/// 解析名称中的版本号
/// </summary>
/// <param name="name">名称</param>
/// <returns>名称和版本号</returns>
private (string name, string version) ResolveNameVersion(string name)
{
if (!_nameVersionRegex.IsMatch(name)) return (name, default);
var version = _nameVersionRegex.Match(name).Groups["version"].Value.Replace("_", ".");
return (_nameVersionRegex.Replace(name, ""), version);
}
/// <summary>
/// 获取方法名映射 [HttpMethod] 规则
/// </summary>
/// <returns></returns>
private void LoadVerbToHttpMethodsConfigure()
{
var defaultVerbToHttpMethods = Penetrates.VerbToHttpMethods;
// 获取配置的复写映射规则
var verbToHttpMethods = _dynamicApiControllerSettings.VerbToHttpMethods;
if (verbToHttpMethods is not null)
{
// 获取所有参数大于1的配置
var settingsVerbToHttpMethods = verbToHttpMethods
.Where(u => u.Length > 1)
.ToDictionary(u => u[0].ToString().ToLower(), u => u[1]?.ToString());
defaultVerbToHttpMethods.AddOrUpdate(settingsVerbToHttpMethods);
}
}
/// <summary>
/// 处理路由模板重复参数
/// </summary>
/// <param name="template"></param>
/// <returns></returns>
private static string HandleRouteTemplateRepeat(string template)
{
var isStartDiagonal = template.StartsWith('/');
var paths = template.Split('/', StringSplitOptions.RemoveEmptyEntries);
var routeParts = new List<string>();
// 参数模板
var paramTemplates = new List<string>();
foreach (var part in paths)
{
// 不包含 {} 模板的直接添加
if (!Regex.IsMatch(part, commonTemplatePattern))
{
routeParts.Add(part);
continue;
}
else
{
var templates = Regex.Matches(part, commonTemplatePattern).Select(t => t.Value);
foreach (var temp in templates)
{
// 处理带路由约束的路由参数模板 https://gitee.com/zuohuaijun/Admin.NET/issues/I736XJ
var t = !temp.Contains('?', StringComparison.CurrentCulture)
? (!temp.Contains(':', StringComparison.CurrentCulture)
? temp
: temp[..temp.IndexOf(':')] + "}")
: temp[..temp.IndexOf('?')] + "}";
if (!paramTemplates.Contains(t, StringComparer.OrdinalIgnoreCase))
{
routeParts.Add(part);
paramTemplates.Add(t);
continue;
}
}
}
}
var tmp = string.Join('/', routeParts);
return isStartDiagonal ? "/" + tmp : tmp;
}
/// <summary>
/// 排除自定义参数模板并进行路由小写
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private static string ConvertToLowerCaseExceptBrackets(string input)
{
if (string.IsNullOrWhiteSpace(input)) return input;
// 将整个字符串转为小写
var lowerCaseInput = input.ToLower();
// 匹配花括号内的内容
var regex = new Regex(@"\{.*?\}");
// 找到花括号内容并替换回原始大写形式
var matches = regex.Matches(input);
foreach (Match match in matches)
{
var startIndex = match.Index;
var length = match.Length;
var originalPart = input.Substring(startIndex, length);
lowerCaseInput = lowerCaseInput.Remove(startIndex, length).Insert(startIndex, originalPart);
}
return lowerCaseInput;
}
}