mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-27 05:37:10 +08:00
1000 lines
45 KiB
C#
1000 lines
45 KiB
C#
// ------------------------------------------------------------------------
|
||
// 版权信息
|
||
// 版权归百小僧及百签科技(广东)有限公司所有。
|
||
// 所有权利保留。
|
||
// 官方网站: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 该 Issue:https://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;
|
||
}
|
||
} |