XFEExtension.NetCore.ServerInteractive
[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig
XFEstudio/XFEExtension.NetCore.ServerInteractive
feat: support multiple EntryPointAttribute per method and wildcard route matching
- Changed EntryPointAttribute to AllowMultiple=true - Updated EntryPointGenerator to collect all EntryPointAttribute instances per method - Added XFE0013 diagnostic for invalid wildcard usage (e.g., v1/a*b) - Added RouteMatchHelper for wildcard pattern matching (* matches any segment) - Updated XFEServerCore to try exact match first, then wildcard patterns - Updated XFEServerCoreBuilder to separate exact and wildcard routes - Empty path in [EntryPoint("")] is treated as "*" (match all) Agent-Logs-Url: https://github.com/XFEstudio/XFEExtension.NetCore.ServerInteractive/sessions/2a0c1e13-19b9-4694-8959-523c6d627a1a Co-authored-by: XFEstudio <132526994+XFEstudio@users.noreply.github.com>
43e8266
代码差异
@@ -14,4 +14,5 @@ XFE0008 | XFEServerInteractive | Error | ClientRequestGenerator, [Documentation]
XFE0009 | XFEServerInteractive | Error | ClientRequestGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0009)
XFE0010 | XFEServerInteractive | Error | ClientRequestGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0010)
XFE0011 | XFEServerInteractive | Error | ClientRequestGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0011)
XFE0012 | XFEServerInteractive | Error | EntryPointGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0012)
XFE0012 | XFEServerInteractive | Error | EntryPointGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0012)
XFE0013 | XFEServerInteractive | Error | EntryPointGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0013)
@@ -62,14 +62,24 @@ public class EntryPointGenerator : IIncrementalGenerator
helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0012",
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor InvalidWildcardUsageRule = new(
id: "XFE0013",
title: "EntryPoint通配符使用无效",
messageFormat: "入口点路径'{0}'中的通配符'*'必须作为完整的路径段使用(例如:v1/*/test),不能与其他字符混合(例如:v1/a*b)",
category: "XFEServerInteractive",
defaultSeverity: DiagnosticSeverity.Error,
helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0013",
isEnabledByDefault: true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 找到所有标记了EntryPointAttribute的方法
var methodDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => IsCandidateMethod(s),
transform: static (ctx, _) => GetMethodForGeneration(ctx))
.Where(static m => m is not null);
transform: static (ctx, _) => GetMethodsForGeneration(ctx))
.Where(static m => m is { IsDefault: false, Length: > 0 })
.SelectMany(static (m, _) => m);
// 按类分组
var compilationAndMethods = context.CompilationProvider.Combine(methodDeclarations.Collect());
@@ -81,25 +91,21 @@ public class EntryPointGenerator : IIncrementalGenerator
private static bool IsCandidateMethod(SyntaxNode node) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 };
private static MethodCandidate? GetMethodForGeneration(GeneratorSyntaxContext context)
private static ImmutableArray<MethodCandidate> GetMethodsForGeneration(GeneratorSyntaxContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration);
if (methodSymbol is null)
return null;
return default;
// 检查是否有EntryPointAttribute
var entryPointAttribute = methodSymbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.Name == "EntryPointAttribute");
// 获取所有EntryPointAttribute
var entryPointAttributes = methodSymbol.GetAttributes()
.Where(a => a.AttributeClass?.Name == "EntryPointAttribute")
.ToList();
if (entryPointAttribute is null)
return null;
// 获取Path参数
var path = entryPointAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString();
if (string.IsNullOrEmpty(path))
return null;
if (entryPointAttributes.Count == 0)
return default;
// 检查返回类型:根据返回类型(而非async关键字)判断同步/异步
// 注意:Task和Task<T>都是有效的异步返回类型(Task<T>可通过协变赋值给Func<Task>)
@@ -126,23 +132,35 @@ public class EntryPointGenerator : IIncrementalGenerator
? LocationInfo.From(classDeclaration.Identifier.GetLocation())
: methodLocation;
return new MethodCandidate(
containingType.ContainingNamespace.ToDisplayString(),
containingType.Name,
methodSymbol.Name,
path!,
isAsync,
isContainingTypePartial,
methodSymbol.Parameters.Length,
hasValidReturnType,
returnType.ToDisplayString(),
methodLocation,
classLocation,
typeParameters,
typeConstraints);
var results = new List<MethodCandidate>();
foreach (var attr in entryPointAttributes)
{
var path = attr.ConstructorArguments.FirstOrDefault().Value?.ToString() ?? "*";
// 空路径或 "*" 均视为全匹配通配符
if (string.IsNullOrEmpty(path))
path = "*";
results.Add(new MethodCandidate(
containingType.ContainingNamespace.ToDisplayString(),
containingType.Name,
methodSymbol.Name,
path,
isAsync,
isContainingTypePartial,
methodSymbol.Parameters.Length,
hasValidReturnType,
returnType.ToDisplayString(),
methodLocation,
classLocation,
typeParameters,
typeConstraints));
}
return results.Count == 0 ? default : ImmutableArray.CreateRange(results);
}
private static void Execute(Compilation compilation, ImmutableArray<MethodCandidate?> methods, SourceProductionContext context)
private static void Execute(Compilation compilation, ImmutableArray<MethodCandidate> methods, SourceProductionContext context)
{
if (methods.IsDefaultOrEmpty)
return;
@@ -152,8 +170,6 @@ public class EntryPointGenerator : IIncrementalGenerator
// 校验并报告诊断信息
foreach (var method in methods)
{
if (method is null) continue;
var isValid = true;
// 校验:包含类型必须为partial
@@ -197,6 +213,24 @@ public class EntryPointGenerator : IIncrementalGenerator
isValid = false;
}
// 校验:通配符 '*' 必须作为完整路径段使用
if (method.Path.Contains("*") && method.Path != "*")
{
var segments = method.Path.Split('/');
foreach (var segment in segments)
{
if (segment.Contains("*") && segment != "*")
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidWildcardUsageRule,
method.MethodLocation.ToLocation(),
method.Path));
isValid = false;
break;
}
}
}
if (isValid)
{
validMethods.Add(method);
@@ -3,7 +3,7 @@ namespace XFEExtension.NetCore.ServerInteractive.Attributes;
/// <summary>
/// 次级入口点特性,用于标记IServerCoreStandardService中的处理方法
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class EntryPointAttribute : Attribute
{
/// <summary>
@@ -0,0 +1,47 @@
namespace XFEExtension.NetCore.ServerInteractive.Utilities.Helpers;
/// <summary>
/// 路由通配符匹配辅助类
/// </summary>
internal static class RouteMatchHelper
{
/// <summary>
/// 判断路由是否包含通配符
/// </summary>
/// <param name="pattern">路由模式</param>
/// <returns>是否包含通配符</returns>
public static bool IsWildcardRoute(string pattern) => pattern.Contains('*');
/// <summary>
/// 将路由模式与实际路由进行通配符匹配
/// <para>支持的通配符:</para>
/// <para>- <c>*</c>:匹配任意路径</para>
/// <para>- <c>v1/*</c>:匹配 v1/ 后跟一个路径段</para>
/// <para>- <c>v1/*/test</c>:匹配 v1/ 后跟任意一个段再跟 /test</para>
/// </summary>
/// <param name="pattern">包含通配符的路由模式</param>
/// <param name="route">实际请求路由</param>
/// <returns>是否匹配</returns>
public static bool MatchWildcardRoute(string pattern, string route)
{
// 单独的 "*" 匹配所有路由
if (pattern == "*")
return true;
var patternSegments = pattern.Split('/');
var routeSegments = route.Split('/');
if (patternSegments.Length != routeSegments.Length)
return false;
for (var i = 0; i < patternSegments.Length; i++)
{
if (patternSegments[i] == "*")
continue;
if (patternSegments[i] != routeSegments[i])
return false;
}
return true;
}
}
@@ -52,6 +52,10 @@ public abstract class XFEServerCore : ServerCoreServiceBase
/// </summary>
internal Dictionary<string, Func<IServerCoreStandardService>> StandardCoreServiceDictionary = [];
/// <summary>
/// 通配符路由服务工厂列表(模式 → 工厂)
/// </summary>
internal List<(string Pattern, Func<IServerCoreStandardService> Factory)> WildcardCoreServiceList = [];
/// <summary>
/// 网络通讯服务器
/// </summary>
public CyberCommServer CyberCommServer { get; internal set; } = new();
@@ -165,8 +169,29 @@ public abstract class XFEServerCore : ServerCoreServiceBase
Console.Write($"({ServerCoreName})【{clientIP}】请求路由-{route}:");
var stopWatch = Stopwatch.StartNew();
// 在字典中查找对应的服务
if (StandardCoreServiceDictionary.TryGetValue(route, out var serviceFactory))
// 在字典中查找对应的服务(先精确匹配,再通配符匹配)
Func<IServerCoreStandardService>? serviceFactory = null;
string? matchedPattern = null;
if (StandardCoreServiceDictionary.TryGetValue(route, out serviceFactory))
{
matchedPattern = route;
}
else
{
// 尝试通配符匹配
foreach (var (pattern, factory) in WildcardCoreServiceList)
{
if (RouteMatchHelper.MatchWildcardRoute(pattern, route))
{
matchedPattern = pattern;
serviceFactory = factory;
break;
}
}
}
if (serviceFactory is not null && matchedPattern is not null)
{
var serviceInstance = serviceFactory();
try
@@ -178,10 +203,10 @@ public abstract class XFEServerCore : ServerCoreServiceBase
serviceInstance.ReturnArgs = r;
serviceInstance.Initialize();
// 根据路由调用对应的处理方法(同步与异步互斥,优先同步)
if (serviceInstance.SyncEntryPoints.TryGetValue(route, out var syncHandler))
// 根据匹配的模式调用对应的处理方法(同步与异步互斥,优先同步)
if (serviceInstance.SyncEntryPoints.TryGetValue(matchedPattern, out var syncHandler))
syncHandler();
else if (serviceInstance.AsyncEntryPoints.TryGetValue(route, out var asyncHandler))
else if (serviceInstance.AsyncEntryPoints.TryGetValue(matchedPattern, out var asyncHandler))
await asyncHandler();
else
{
@@ -2,6 +2,7 @@
using XFEExtension.NetCore.ServerInteractive.Implements;
using XFEExtension.NetCore.ServerInteractive.Interfaces.CoreService;
using XFEExtension.NetCore.ServerInteractive.Options;
using XFEExtension.NetCore.ServerInteractive.Utilities.Helpers;
using XFEExtension.NetCore.StringExtension;
namespace XFEExtension.NetCore.ServerInteractive.Utilities.Server;
@@ -17,6 +18,7 @@ public abstract class XFEServerCoreBuilder : XFEBuilderBase<XFEServerCoreBuilder
private readonly List<IServerCoreOriginalService> _serverCoreServiceList = [];
private readonly List<Func<IServerCoreVerifyService>> _serverCoreVerifyServiceList = [];
private readonly Dictionary<string, Func<IServerCoreStandardService>> _serverStandardCoreServiceDictionary = [];
private readonly List<(string Pattern, Func<IServerCoreStandardService> Factory)> _serverWildcardCoreServiceList = [];
/// <summary>
/// 创建XFE服务器核心构建器
@@ -102,15 +104,24 @@ public abstract class XFEServerCoreBuilder : XFEBuilderBase<XFEServerCoreBuilder
if (entryPointList is null || entryPointList.Count == 0)
throw new InvalidOperationException($"类型 {typeof(T).Name} 的 EntryPointList 为空");
// 为每个入口点注册服务工厂
// 为每个入口点注册服务工厂(区分精确路由和通配符路由)
foreach (var route in entryPointList)
{
_serverStandardCoreServiceDictionary.Add(route, () =>
Func<IServerCoreStandardService> factory = () =>
{
var inst = new T();
ApplyParameter(inst);
return inst;
});
};
if (RouteMatchHelper.IsWildcardRoute(route))
{
_serverWildcardCoreServiceList.Add((route, factory));
}
else
{
_serverStandardCoreServiceDictionary.Add(route, factory);
}
}
return this;
}
@@ -128,6 +139,7 @@ public abstract class XFEServerCoreBuilder : XFEBuilderBase<XFEServerCoreBuilder
}
_xFEServerCore.ServerCoreVerifyServiceList = _serverCoreVerifyServiceList;
_xFEServerCore.StandardCoreServiceDictionary = _serverStandardCoreServiceDictionary;
_xFEServerCore.WildcardCoreServiceList = _serverWildcardCoreServiceList;
if (xFEServerCoreOptions is not null)
{