XFEExtension.NetCore.ServerInteractive
[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig
XFEstudio/XFEExtension.NetCore.ServerInteractive
Add Name property to Request/Response attributes, support AllowMultiple, unify dictionaries, add duplicate path diagnostic
- RequestAttribute and ResponseAttribute: AllowMultiple=true, optional Name property - ClientRequestGenerator: handles multiple attributes per method, generates name entries in dictionaries, generates RequestRouteMap, duplicate path diagnostic XFE0011 - IStandardRequestService/StandardRequestServiceBase: add RequestRouteMap dictionary - XFEClientRequester: remove StandardMultiRequestServiceListDictionary, use RequestRouteMap for URL resolution - XFEClientRequesterBuilder: auto-register all routes with parameterless AddRequest<T>(), remove AddRequest<T>(string) and AddRequest<T>(List<string>) Agent-Logs-Url: https://github.com/XFEstudio/XFEExtension.NetCore.ServerInteractive/sessions/a1e53d1d-33f2-4970-bb42-5a5c1de44993 Co-authored-by: XFEstudio <132526994+XFEstudio@users.noreply.github.com>
1d0c5b6
代码差异
@@ -12,4 +12,5 @@ XFE0006 | XFEServerInteractive | Error | EntryPointGenerator, [Documentation](ht
XFE0007 | XFEServerInteractive | Error | ClientRequestGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0007)
XFE0008 | XFEServerInteractive | Error | ClientRequestGenerator, [Documentation](https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0008)
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)
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)
@@ -53,14 +53,24 @@ public class ClientRequestGenerator : IIncrementalGenerator
helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0010",
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor DuplicatePathRule = new(
id: "XFE0011",
title: "Request/Response路径重复注册",
messageFormat: "路径或名称'{0}'在类'{1}'的[{2}]方法中重复注册",
category: "XFEServerInteractive",
defaultSeverity: DiagnosticSeverity.Error,
helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0011",
isEnabledByDefault: true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 找到所有标记了RequestAttribute或ResponseAttribute的方法
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.IsDefault && m.Length > 0)
.SelectMany(static (m, _) => m);
// 按类分组
var compilationAndMethods = context.CompilationProvider.Combine(methodDeclarations.Collect());
@@ -72,77 +82,120 @@ public class ClientRequestGenerator : IIncrementalGenerator
private static bool IsCandidateMethod(SyntaxNode node) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 };
private static ClientRequestMethodCandidate? GetMethodForGeneration(GeneratorSyntaxContext context)
private static ImmutableArray<ClientRequestMethodCandidate> GetMethodsForGeneration(GeneratorSyntaxContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration);
if (methodSymbol is null)
return null;
return default;
// 检查是否有RequestAttribute或ResponseAttribute
var requestAttribute = methodSymbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.Name == "RequestAttribute");
var responseAttribute = methodSymbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.Name == "ResponseAttribute");
var attributes = methodSymbol.GetAttributes();
var requestAttributes = attributes.Where(a => a.AttributeClass?.Name == "RequestAttribute").ToList();
var responseAttributes = attributes.Where(a => a.AttributeClass?.Name == "ResponseAttribute").ToList();
if (requestAttribute is null && responseAttribute is null)
return null;
if (requestAttributes.Count == 0 && responseAttributes.Count == 0)
return default;
var isRequest = requestAttribute is not null;
var attribute = isRequest ? requestAttribute! : responseAttribute!;
var attributeName = isRequest ? "Request" : "Response";
// 获取Path参数
var path = attribute.ConstructorArguments.FirstOrDefault().Value?.ToString();
if (string.IsNullOrEmpty(path))
return null;
// 检查返回类型:必须为object
// 共有信息
var returnType = methodSymbol.ReturnType;
var isObjectReturn = returnType.SpecialType == SpecialType.System_Object;
// 检查包含类型是否为partial
var classDeclaration = methodDeclaration.Parent as ClassDeclarationSyntax;
var isContainingTypePartial = classDeclaration?.Modifiers.Any(SyntaxKind.PartialKeyword) ?? false;
// 获取泛型类型参数和约束
var typeParameters = classDeclaration?.TypeParameterList?.ToString().Trim() ?? "";
var typeConstraints = classDeclaration?.ConstraintClauses.ToString().Trim() ?? "";
var containingType = methodSymbol.ContainingType;
// 获取位置信息
var methodLocation = LocationInfo.From(methodDeclaration.GetLocation());
var classLocation = classDeclaration is not null
? LocationInfo.From(classDeclaration.Identifier.GetLocation())
: methodLocation;
// 收集源文件的using指令
var compilationUnit = methodDeclaration.SyntaxTree.GetRoot() as CompilationUnitSyntax;
var usingDirectives = compilationUnit?.Usings
.Select(u => u.ToString().Trim())
.ToArray() ?? System.Array.Empty<string>();
return new ClientRequestMethodCandidate(
containingType.ContainingNamespace.ToDisplayString(),
containingType.Name,
methodSymbol.Name,
path!,
isRequest,
isContainingTypePartial,
methodSymbol.Parameters.Length,
isObjectReturn,
returnType.ToDisplayString(),
methodLocation,
classLocation,
typeParameters,
typeConstraints,
attributeName,
usingDirectives);
var results = new List<ClientRequestMethodCandidate>();
// 为每个Request属性创建候选
foreach (var attr in requestAttributes)
{
var path = attr.ConstructorArguments.FirstOrDefault().Value?.ToString();
if (string.IsNullOrEmpty(path))
continue;
var name = (string?)null;
if (!attr.NamedArguments.IsDefaultOrEmpty)
{
var nameArg = attr.NamedArguments.FirstOrDefault(kvp => kvp.Key == "Name");
if (nameArg.Key is not null)
name = nameArg.Value.Value?.ToString();
}
results.Add(new ClientRequestMethodCandidate(
containingType.ContainingNamespace.ToDisplayString(),
containingType.Name,
methodSymbol.Name,
path!,
name,
isRequest: true,
isContainingTypePartial,
methodSymbol.Parameters.Length,
isObjectReturn,
returnType.ToDisplayString(),
methodLocation,
classLocation,
typeParameters,
typeConstraints,
"Request",
usingDirectives));
}
// 为每个Response属性创建候选
foreach (var attr in responseAttributes)
{
var path = attr.ConstructorArguments.FirstOrDefault().Value?.ToString();
if (string.IsNullOrEmpty(path))
continue;
var name = (string?)null;
if (!attr.NamedArguments.IsDefaultOrEmpty)
{
var nameArg = attr.NamedArguments.FirstOrDefault(kvp => kvp.Key == "Name");
if (nameArg.Key is not null)
name = nameArg.Value.Value?.ToString();
}
results.Add(new ClientRequestMethodCandidate(
containingType.ContainingNamespace.ToDisplayString(),
containingType.Name,
methodSymbol.Name,
path!,
name,
isRequest: false,
isContainingTypePartial,
methodSymbol.Parameters.Length,
isObjectReturn,
returnType.ToDisplayString(),
methodLocation,
classLocation,
typeParameters,
typeConstraints,
"Response",
usingDirectives));
}
if (results.Count == 0)
return default;
return results.ToImmutableArray();
}
private static void Execute(Compilation compilation, ImmutableArray<ClientRequestMethodCandidate?> methods, SourceProductionContext context)
private static void Execute(Compilation compilation, ImmutableArray<ClientRequestMethodCandidate> methods, SourceProductionContext context)
{
if (methods.IsDefaultOrEmpty)
return;
@@ -152,8 +205,6 @@ public class ClientRequestGenerator : IIncrementalGenerator
// 校验并报告诊断信息
foreach (var method in methods)
{
if (method is null) continue;
var isValid = true;
// 校验:包含类型必须为partial
@@ -219,7 +270,16 @@ public class ClientRequestGenerator : IIncrementalGenerator
var requestMethods = methodInfos.Where(m => m.IsRequest).ToList();
var responseMethods = methodInfos.Where(m => !m.IsRequest).ToList();
// 收集所有路径(去重)
// 检查重复路径/名称
var hasDuplicateError = false;
hasDuplicateError |= CheckDuplicateKeys(context, requestMethods, className, "Request");
hasDuplicateError |= CheckDuplicateKeys(context, responseMethods, className, "Response");
if (hasDuplicateError)
continue;
// 收集所有路径(去重)— 仅包含实际路由路径
var allPaths = methodInfos.Select(m => m.Path).Distinct().ToList();
// 收集所有using指令(合并去重)
@@ -275,10 +335,14 @@ namespace {namespaceName}
get => _generatedRequestPoints ??= new Dictionary<string, Func<object>>()
{{");
// 添加请求方法
// 添加请求方法(路径 → 方法 + 名称 → 方法)
foreach (var method in requestMethods)
{
sourceBuilder.AppendLine($" {{ \"{EscapeStringLiteral(method.Path)}\", {method.MethodName} }},");
if (!string.IsNullOrEmpty(method.Name))
{
sourceBuilder.AppendLine($" {{ \"{EscapeStringLiteral(method.Name!)}\", {method.MethodName} }},");
}
}
sourceBuilder.AppendLine($@" }};
@@ -291,10 +355,39 @@ namespace {namespaceName}
get => _generatedResponsePoints ??= new Dictionary<string, Func<object>>()
{{");
// 添加响应方法
// 添加响应方法(路径 → 方法 + 名称 → 方法)
foreach (var method in responseMethods)
{
sourceBuilder.AppendLine($" {{ \"{EscapeStringLiteral(method.Path)}\", {method.MethodName} }},");
if (!string.IsNullOrEmpty(method.Name))
{
sourceBuilder.AppendLine($" {{ \"{EscapeStringLiteral(method.Name!)}\", {method.MethodName} }},");
}
}
sourceBuilder.AppendLine($@" }};
}}
private Dictionary<string, string>? _generatedRequestRouteMap;
/// <inheritdoc/>
public override Dictionary<string, string> RequestRouteMap
{{
get => _generatedRequestRouteMap ??= new Dictionary<string, string>()
{{");
// 构建路由映射(所有键 → 实际路径)
var allMappings = new HashSet<string>();
foreach (var method in methodInfos)
{
var pathKey = method.Path;
if (allMappings.Add(pathKey))
{
sourceBuilder.AppendLine($" {{ \"{EscapeStringLiteral(pathKey)}\", \"{EscapeStringLiteral(pathKey)}\" }},");
}
if (!string.IsNullOrEmpty(method.Name) && allMappings.Add(method.Name!))
{
sourceBuilder.AppendLine($" {{ \"{EscapeStringLiteral(method.Name!)}\", \"{EscapeStringLiteral(pathKey)}\" }},");
}
}
sourceBuilder.AppendLine(@" };
@@ -306,6 +399,42 @@ namespace {namespaceName}
}
}
/// <summary>
/// 检查同类型(Request或Response)候选中是否存在重复的路径或名称键
/// </summary>
private static bool CheckDuplicateKeys(SourceProductionContext context, List<ClientRequestMethodCandidate> candidates, string className, string attributeName)
{
var seenKeys = new HashSet<string>();
var hasDuplicate = false;
foreach (var candidate in candidates)
{
if (!seenKeys.Add(candidate.Path))
{
context.ReportDiagnostic(Diagnostic.Create(
DuplicatePathRule,
candidate.MethodLocation.ToLocation(),
candidate.Path,
className,
attributeName));
hasDuplicate = true;
}
if (!string.IsNullOrEmpty(candidate.Name) && !seenKeys.Add(candidate.Name!))
{
context.ReportDiagnostic(Diagnostic.Create(
DuplicatePathRule,
candidate.MethodLocation.ToLocation(),
candidate.Name!,
className,
attributeName));
hasDuplicate = true;
}
}
return hasDuplicate;
}
/// <summary>
/// 转义字符串字面量中的特殊字符
/// </summary>
@@ -7,12 +7,13 @@ namespace XFEExtension.NetCore.ServerInteractive.SourceGenerator.Models;
/// <summary>
/// 客户端请求方法候选信息,包含验证所需的所有数据
/// </summary>
public class ClientRequestMethodCandidate(string namespaceName, string className, string methodName, string path, bool isRequest, bool isContainingTypePartial, int parameterCount, bool hasValidReturnType, string returnTypeName, LocationInfo methodLocation, LocationInfo classLocation, string typeParameters, string typeConstraints, string attributeName, string[] usingDirectives)
public class ClientRequestMethodCandidate(string namespaceName, string className, string methodName, string path, string? name, bool isRequest, bool isContainingTypePartial, int parameterCount, bool hasValidReturnType, string returnTypeName, LocationInfo methodLocation, LocationInfo classLocation, string typeParameters, string typeConstraints, string attributeName, string[] usingDirectives)
{
public string Namespace { get; } = namespaceName;
public string ClassName { get; } = className;
public string MethodName { get; } = methodName;
public string Path { get; } = path;
public string? Name { get; } = name;
public bool IsRequest { get; } = isRequest;
public bool IsContainingTypePartial { get; } = isContainingTypePartial;
public int ParameterCount { get; } = parameterCount;
@@ -3,7 +3,7 @@ namespace XFEExtension.NetCore.ServerInteractive.Attributes;
/// <summary>
/// 请求特性,用于标记IStandardRequestService中构造请求体的方法
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class RequestAttribute : Attribute
{
/// <summary>
@@ -11,6 +11,11 @@ public class RequestAttribute : Attribute
/// </summary>
public string Path { get; }
/// <summary>
/// 请求名称(可选),使用请求器时可通过名称代替路径进行请求
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 创建请求特性
/// </summary>
@@ -3,7 +3,7 @@ namespace XFEExtension.NetCore.ServerInteractive.Attributes;
/// <summary>
/// 响应特性,用于标记IStandardRequestService中解析响应的方法
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class ResponseAttribute : Attribute
{
/// <summary>
@@ -11,6 +11,11 @@ public class ResponseAttribute : Attribute
/// </summary>
public string Path { get; }
/// <summary>
/// 响应名称(可选),使用请求器时可通过名称代替路径进行请求
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 创建响应特性
/// </summary>
@@ -33,4 +33,7 @@ public abstract partial class StandardRequestServiceBase : IStandardRequestServi
/// <inheritdoc/>
public virtual Dictionary<string, Func<object>> ResponsePoints { get; } = new();
/// <inheritdoc/>
public virtual Dictionary<string, string> RequestRouteMap { get; } = new();
}
@@ -37,4 +37,8 @@ public interface IStandardRequestService : IRequestServiceBase
/// 响应方法字典,Key为请求路径,Value为对应的解析响应方法
/// </summary>
Dictionary<string, Func<object>> ResponsePoints { get; }
/// <summary>
/// 请求路由映射,Key为请求路径或名称,Value为实际路由路径(用于URL构建)
/// </summary>
Dictionary<string, string> RequestRouteMap { get; }
}
@@ -75,7 +75,7 @@ public static class XFEClientRequesterBuilderExtensions
/// </summary>
/// <typeparam name="T">登录返回用户接口类型</typeparam>
/// <returns></returns>
public XFEClientRequesterBuilder AddLoginRequest<T>() where T : IUserFaceInfo => xFEClientRequesterBuilder.AddRequest<LoginRequestService<T>>("user/login").AddRequest<ReloginRequestService<T>>("user/relogin");
public XFEClientRequesterBuilder AddLoginRequest<T>() where T : IUserFaceInfo => xFEClientRequesterBuilder.AddRequest<LoginRequestService<T>>().AddRequest<ReloginRequestService<T>>();
/// <summary>
/// 使用XFE标准服务器服务请求
@@ -20,7 +20,6 @@ public abstract class XFEClientRequester : IRequesterBase
private readonly JsonSerializerOptions _jsonSerializerOptions = new();
internal Dictionary<string, Func<IRequestService>> RequestServiceDictionary = [];
internal Dictionary<string, Func<IStandardRequestService>> StandardRequestServiceDictionary = [];
internal Dictionary<List<string>, Func<IStandardRequestService>> StandardMultiRequestServiceListDictionary = [];
internal Dictionary<string, StandardClientInstanceRequest> StandardClientInstanceRequestDictionary = [];
/// <inheritdoc/>
public string RequestAddress { get; set; } = string.Empty;
@@ -71,12 +70,14 @@ public abstract class XFEClientRequester : IRequesterBase
{
var xFEService = xFEServiceFactory();
xFEService.XFEClientRequester = this;
xFEService.Route = serviceName;
xFEService.DeviceInfo = DeviceInfo;
xFEService.Parameters = parameters;
// 通过RequestRouteMap解析实际路由路径
var actualRoute = xFEService.RequestRouteMap.TryGetValue(serviceName, out var route) ? route : serviceName;
xFEService.Route = actualRoute;
if (!xFEService.RequestPoints.TryGetValue(serviceName, out var requestHandler))
throw new XFERequesterException($"未找到路由'{serviceName}'对应的[Request]方法");
var (response, code) = await InteractiveHelper.GetServerResponse(RequestAddress + $"/{serviceName}", requestHandler(), _jsonSerializerOptions);
var (response, code) = await InteractiveHelper.GetServerResponse(RequestAddress + $"/{actualRoute}", requestHandler(), _jsonSerializerOptions);
result.StatusCode = code;
if (code == HttpStatusCode.OK)
{
@@ -113,34 +114,6 @@ public abstract class XFEClientRequester : IRequesterBase
result.Message = response;
}
}
foreach (var instance in from key in StandardMultiRequestServiceListDictionary.Keys where key.Contains(serviceName) select StandardMultiRequestServiceListDictionary[key] into factory select factory())
{
instance.XFEClientRequester = this;
instance.Route = serviceName;
instance.DeviceInfo = DeviceInfo;
instance.Parameters = parameters;
if (!instance.RequestPoints.TryGetValue(serviceName, out var requestHandler))
throw new XFERequesterException($"未找到路由'{serviceName}'对应的[Request]方法");
var (response, code) = await InteractiveHelper.GetServerResponse(RequestAddress + $"/{serviceName}", requestHandler(), _jsonSerializerOptions);
result.StatusCode = code;
if (code == HttpStatusCode.OK)
{
instance.Response = response;
instance.UnescapedResponse = Regex.Unescape(response);
if (instance.ResponsePoints.TryGetValue(serviceName, out var responseHandler))
{
var requestResult = responseHandler();
result.Result = requestResult;
}
MessageReceived?.Invoke(this, new ServerInteractiveEventArgsImpl("Success", code));
result.Message = "Success";
}
else
{
MessageReceived?.Invoke(this, new ServerInteractiveEventArgsImpl(response, code));
result.Message = response;
}
}
return result;
}
catch (Exception ex)
@@ -15,7 +15,6 @@ public abstract class XFEClientRequesterBuilder : XFEBuilderBase<XFEClientReques
private readonly XFEClientRequester _xFEClientRequester = new XFEClientRequesterImpl();
private readonly Dictionary<string, Func<IRequestService>> _requestServiceDictionary = [];
private readonly Dictionary<string, Func<IStandardRequestService>> _standardRequestServiceDictionary = [];
private readonly Dictionary<List<string>, Func<IStandardRequestService>> _standardMultiRequestServiceListDictionary = [];
private readonly Dictionary<string, StandardClientInstanceRequest> _standardClientInstanceRequestDictionary = [];
/// <summary>
/// 创建构建器
@@ -46,36 +45,34 @@ public abstract class XFEClientRequesterBuilder : XFEBuilderBase<XFEClientReques
}
/// <summary>
/// 添加标准请求器
/// 注册标准请求服务(从RequestPoints/ResponsePoints/RequestRouteMap自动获取路由路径和名称)
/// </summary>
/// <typeparam name="T">请求服务泛型</typeparam>
/// <param name="serviceName">请求服务名称</param>
/// <returns></returns>
public XFEClientRequesterBuilder AddRequest<T>(string serviceName) where T : IStandardRequestService, new()
public XFEClientRequesterBuilder AddRequest<T>() where T : IStandardRequestService, new()
{
_standardRequestServiceDictionary.Add(serviceName, () =>
{
var inst = new T();
ApplyParameter(inst);
return inst;
});
return this;
}
var probeService = new T();
ApplyParameter(probeService);
/// <summary>
/// 添加标准请求器
/// </summary>
/// <typeparam name="T">请求服务泛型</typeparam>
/// <param name="serviceName">请求服务名称</param>
/// <returns></returns>
public XFEClientRequesterBuilder AddRequest<T>(List<string> serviceName) where T : IStandardRequestService, new()
{
_standardMultiRequestServiceListDictionary.Add(serviceName, () =>
var routeKeys = probeService.RequestPoints.Keys
.Concat(probeService.ResponsePoints.Keys)
.Concat(probeService.RequestRouteMap.Keys)
.Distinct()
.ToList();
if (routeKeys.Count == 0)
throw new InvalidOperationException($"类型 {typeof(T).Name} 的 RequestPoints/ResponsePoints 为空");
// 为每个路径/名称注册服务工厂
foreach (var key in routeKeys)
{
var inst = new T();
ApplyParameter(inst);
return inst;
});
_standardRequestServiceDictionary.Add(key, () =>
{
var inst = new T();
ApplyParameter(inst);
return inst;
});
}
return this;
}
@@ -115,7 +112,6 @@ public abstract class XFEClientRequesterBuilder : XFEBuilderBase<XFEClientReques
_xFEClientRequester.DeviceInfo = options.DeviceInfo;
_xFEClientRequester.RequestServiceDictionary = _requestServiceDictionary;
_xFEClientRequester.StandardRequestServiceDictionary = _standardRequestServiceDictionary;
_xFEClientRequester.StandardMultiRequestServiceListDictionary = _standardMultiRequestServiceListDictionary;
_xFEClientRequester.StandardClientInstanceRequestDictionary = _standardClientInstanceRequestDictionary;
return _xFEClientRequester;
}