XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.ServerInteractive

[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEExtension.NetCore.ServerInteractive

引入 [NoLog] 特性以支持入口点日志抑制

为 XFE Server Core 框架新增 [NoLog] 方法特性,允许开发者在高频或无需常规日志的入口点方法上标记 [NoLog],关闭自动输出的请求、校验、路由、耗时等日志,仅保留异常和业务日志。实现包括 NoLogAttribute 特性类、增量生成器支持、核心类路由集合传递与注册、日志输出点适配 SuppressLog 标记、示例服务用法展示,并同步更新文档和版本说明,标明 3.3.0 版本新增。

b2d8e05
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

13 个文件 +193 -67
Modified README.md +16 -0
@@ -185,6 +185,16 @@ XFEServerCoreBuilder.CreateBuilder()
185 185 .Build(options => { options.BindIP("http://localhost:3300/"); });
186 186 ```
187 187
188 Add `[NoLog]` to a high-frequency entry point when its routine request logs are not needed:
189
190 ```csharp
191 [EntryPoint("health")]
192 [NoLog]
193 public async Task Health() => await Close("OK");
194 ```
195
196 This suppresses the framework's request-received, validation, route, and elapsed-time messages. Exception logs are still emitted, and business logs explicitly written inside the method through `Console.Write*` or another logger are unaffected.
197
188 198 **Commonly accessible properties in service methods:**
189 199
190 200 | Property | Type | Description |
@@ -712,6 +722,7 @@ Automatically generates `SyncEntryPoints` and `AsyncEntryPoints` dictionaries fo
712 722 public partial class ApiService : ServerCoreStandardServiceBase
713 723 {
714 724 [EntryPoint("user/profile")]
725 [NoLog]
715 726 public async Task GetProfile()
716 727 {
717 728 // ...
@@ -747,6 +758,11 @@ public override Dictionary<string, Func<Task>> AsyncEntryPoints => new()
747 758 { "user/profile", GetProfile },
748 759 { "resource/*/details", ResourceDetails },
749 760 };
761
762 public override HashSet<string> NoLogEntryPoints => new()
763 {
764 "user/profile",
765 };
750 766 ```
751 767
752 768 ---
Modified README.zh-CN.md +16 -0
@@ -185,6 +185,16 @@ XFEServerCoreBuilder.CreateBuilder()
185 185 .Build(options => { options.BindIP("http://localhost:3300/"); });
186 186 ```
187 187
188 如果某个高频入口点不需要输出常规请求日志,可在方法上添加 `[NoLog]`:
189
190 ```csharp
191 [EntryPoint("health")]
192 [NoLog]
193 public async Task Health() => await Close("OK");
194 ```
195
196 该特性会关闭框架产生的“接收到请求、校验结果、请求路由、耗时”日志。异常日志仍会输出;方法内部主动调用 `Console.Write*` 或其他日志组件产生的业务日志不受影响。
197
188 198 **可在服务方法中访问的常用属性:**
189 199
190 200 | 属性 | 类型 | 说明 |
@@ -712,6 +722,7 @@ public class Order : IIdModel
712 722 public partial class ApiService : ServerCoreStandardServiceBase
713 723 {
714 724 [EntryPoint("user/profile")]
725 [NoLog]
715 726 public async Task GetProfile()
716 727 {
717 728 // ...
@@ -747,6 +758,11 @@ public override Dictionary<string, Func<Task>> AsyncEntryPoints => new()
747 758 { "user/profile", GetProfile },
748 759 { "resource/*/details", ResourceDetails },
749 760 };
761
762 public override HashSet<string> NoLogEntryPoints => new()
763 {
764 "user/profile",
765 };
750 766 ```
751 767
752 768 ---
Modified XFEExtension.NetCore.ServerInteractive.SourceGenerator/EntryPointGenerator.cs +16 -1
@@ -107,6 +107,10 @@ public class EntryPointGenerator : IIncrementalGenerator
107 107 if (entryPointAttributes.Count == 0)
108 108 return default;
109 109
110 // [NoLog] 作用于方法,因此同一方法声明的所有入口点都继承该配置。
111 var noLog = methodSymbol.GetAttributes()
112 .Any(a => a.AttributeClass?.ToDisplayString() == "XFEExtension.NetCore.ServerInteractive.Attributes.NoLogAttribute");
113
110 114 // 检查返回类型:根据返回类型(而非async关键字)判断同步/异步
111 115 // 注意:Task和Task<T>都是有效的异步返回类型(Task<T>可通过协变赋值给Func<Task>)
112 116 var returnType = methodSymbol.ReturnType;
@@ -132,7 +136,7 @@ public class EntryPointGenerator : IIncrementalGenerator
132 136 ? LocationInfo.From(classDeclaration.Identifier.GetLocation())
133 137 : methodLocation;
134 138
135 var results = (from attr in entryPointAttributes select attr.ConstructorArguments.FirstOrDefault().Value?.ToString() into rawPath select string.IsNullOrEmpty(rawPath) ? "*" : rawPath!.Trim('/') into path select new MethodCandidate(containingType.ContainingNamespace.ToDisplayString(), containingType.Name, methodSymbol.Name, path, isAsync, isContainingTypePartial, methodSymbol.Parameters.Length, hasValidReturnType, returnType.ToDisplayString(), methodLocation, classLocation, typeParameters, typeConstraints)).ToList();
139 var results = (from attr in entryPointAttributes select attr.ConstructorArguments.FirstOrDefault().Value?.ToString() into rawPath select string.IsNullOrEmpty(rawPath) ? "*" : rawPath!.Trim('/') into path select new MethodCandidate(containingType.ContainingNamespace.ToDisplayString(), containingType.Name, methodSymbol.Name, path, isAsync, noLog, isContainingTypePartial, methodSymbol.Parameters.Length, hasValidReturnType, returnType.ToDisplayString(), methodLocation, classLocation, typeParameters, typeConstraints)).ToList();
136 140
137 141 return results.Count == 0 ? default : [..results];
138 142 }
@@ -278,6 +282,17 @@ namespace {namespaceName}
278 282 }
279 283 sourceBuilder.AppendLine($@" }};
280 284
285 /// <summary>
286 /// 禁用框架请求日志的入口点集合。
287 /// </summary>
288 public override HashSet<string> NoLogEntryPoints {{ get; }} = new()
289 {{");
290 foreach (var method in methodInfos.Where(m => m.NoLog))
291 {
292 sourceBuilder.AppendLine($" \"{EscapeStringLiteral(method.Path)}\",");
293 }
294 sourceBuilder.AppendLine($@" }};
295
281 296 private Dictionary<string, Action>? _generatedSyncEntryPoints;
282 297 /// <inheritdoc/>
283 298 public override Dictionary<string, Action> SyncEntryPoints
Modified XFEExtension.NetCore.ServerInteractive.SourceGenerator/Models/MethodCandidate.cs +3 -2
@@ -3,13 +3,14 @@
3 3 /// <summary>
4 4 /// 方法候选信息,包含验证所需的所有数据
5 5 /// </summary>
6 public class MethodCandidate(string namespaceName, string className, string methodName, string path, bool isAsync, bool isContainingTypePartial, int parameterCount, bool hasValidReturnType, string returnTypeName, LocationInfo methodLocation, LocationInfo classLocation, string typeParameters, string typeConstraints)
6 public class MethodCandidate(string namespaceName, string className, string methodName, string path, bool isAsync, bool noLog, bool isContainingTypePartial, int parameterCount, bool hasValidReturnType, string returnTypeName, LocationInfo methodLocation, LocationInfo classLocation, string typeParameters, string typeConstraints)
7 7 {
8 8 public string Namespace { get; } = namespaceName;
9 9 public string ClassName { get; } = className;
10 10 public string MethodName { get; } = methodName;
11 11 public string Path { get; } = path;
12 12 public bool IsAsync { get; } = isAsync;
13 public bool NoLog { get; } = noLog;
13 14 public bool IsContainingTypePartial { get; } = isContainingTypePartial;
14 15 public int ParameterCount { get; } = parameterCount;
15 16 public bool HasValidReturnType { get; } = hasValidReturnType;
@@ -18,4 +19,4 @@ public class MethodCandidate(string namespaceName, string className, string meth
18 19 public LocationInfo ClassLocation { get; } = classLocation;
19 20 public string TypeParameters { get; } = typeParameters;
20 21 public string TypeConstraints { get; } = typeConstraints;
21 }
22 }
Modified XFEExtension.NetCore.ServerInteractive.TServer/Services/StatusCoreService.cs +1 -0
@@ -9,6 +9,7 @@ namespace XFEExtension.NetCore.ServerInteractive.TServer.Services;
9 9 public partial class StatusCoreService : ServerCoreStandardServiceBase
10 10 {
11 11 [EntryPoint("status")]
12 [NoLog]
12 13 public async Task StatusEntryPoint()
13 14 {
14 15 var version = typeof(StatusCoreService).Assembly.GetName().Version?.ToString() ?? "unknown";
Added XFEExtension.NetCore.ServerInteractive/Attributes/NoLogAttribute.cs +11 -0
@@ -0,0 +1,11 @@
1 namespace XFEExtension.NetCore.ServerInteractive.Attributes;
2
3 /// <summary>
4 /// 禁用入口点方法的框架请求日志。
5 /// </summary>
6 /// <remarks>
7 /// 此特性会关闭框架输出的请求接收、校验、路由和耗时日志,但不会吞掉异常日志,
8 /// 也不会拦截入口点方法内部主动写入的日志。
9 /// </remarks>
10 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
11 public sealed class NoLogAttribute : Attribute;
Modified XFEExtension.NetCore.ServerInteractive/Implements/CoreService/ServerCoreStandardServiceBase.cs +5 -0
@@ -12,6 +12,11 @@ public abstract class ServerCoreStandardServiceBase : XFEServerCoreServiceBase,
12 12 /// </summary>
13 13 public virtual List<string> EntryPointList { get; } = [];
14 14
15 /// <summary>
16 /// 禁用框架请求日志的入口点集合(由增量生成器根据 <c>[NoLog]</c> 自动填充)
17 /// </summary>
18 public virtual HashSet<string> NoLogEntryPoints { get; } = [];
19
15 20 /// <inheritdoc/>
16 21 public virtual Dictionary<string, Action> SyncEntryPoints { get; } = new();
17 22
Modified XFEExtension.NetCore.ServerInteractive/Models/ServerModels/ServerCoreReturnArgs.cs +4 -0
@@ -23,6 +23,10 @@ public class ServerCoreReturnArgs : Exception
23 23 /// </summary>
24 24 public string ClientIP { get; set; } = string.Empty;
25 25 /// <summary>
26 /// 是否禁用当前入口点的框架请求日志
27 /// </summary>
28 public bool SuppressLog { get; internal set; }
29 /// <summary>
26 30 /// 是否已经处理完成
27 31 /// </summary>
28 32 public bool Handled { get; set; }
Modified XFEExtension.NetCore.ServerInteractive/Utilities/Server/Services/CoreService/EntryPointVerifyService.cs +8 -4
@@ -12,10 +12,12 @@ public class EntryPointVerifyService : ServerCoreVerifyServiceBase
12 12 /// <inheritdoc/>
13 13 public override bool VerifyRequest()
14 14 {
15 Console.Write($"[INFO]({XFEServerCore.ServerCoreName})【{ClientIP}】接收到请求");
15 if (!ReturnArgs.SuppressLog)
16 Console.Write($"[INFO]({XFEServerCore.ServerCoreName})【{ClientIP}】接收到请求");
16 17 if (ServerBaseProfile.BannedIPAddressList.Contains(ClientIP))
17 18 {
18 Console.WriteLine("-校验失败");
19 if (!ReturnArgs.SuppressLog)
20 Console.WriteLine("-校验失败");
19 21 throw Error("您的IP已被封禁", HttpStatusCode.Forbidden);
20 22 }
21 23
@@ -23,7 +25,8 @@ public class EntryPointVerifyService : ServerCoreVerifyServiceBase
23 25 var isAllowed = (XFEServerCore.AcceptGet && method == "GET") || (XFEServerCore.AcceptPost && method == "POST");
24 26 if (!isAllowed)
25 27 {
26 Console.WriteLine("-校验失败");
28 if (!ReturnArgs.SuppressLog)
29 Console.WriteLine("-校验失败");
27 30 var allowedMethods = new List<string>();
28 31 if (XFEServerCore.AcceptGet) allowedMethods.Add("GET");
29 32 if (XFEServerCore.AcceptPost) allowedMethods.Add("POST");
@@ -31,7 +34,8 @@ public class EntryPointVerifyService : ServerCoreVerifyServiceBase
31 34 throw Error($"不接受的请求方法 {method},当前允许的方法:{allowedStr}", HttpStatusCode.MethodNotAllowed);
32 35 }
33 36
34 Console.WriteLine("-校验通过");
37 if (!ReturnArgs.SuppressLog)
38 Console.WriteLine("-校验通过");
35 39 return true;
36 40 }
37 41 }
Modified XFEExtension.NetCore.ServerInteractive/Utilities/Server/Services/CoreService/ServerCoreExceptionProcessService.cs +4 -1
@@ -36,7 +36,10 @@ public class ServerCoreExceptionProcessService : ServerCoreOriginalServiceBase
36 36 errorInfo = returnArgs.ReturnMessage;
37 37 if (e.ReturnArgs.IsStandardError)
38 38 {
39 Console.Write($"\t【{errorInfo}】");
39 if (e.ReturnArgs.SuppressLog)
40 Console.WriteLine($"[WARN]({sender.ServerCoreName})【{errorInfo}】");
41 else
42 Console.Write($"\t【{errorInfo}】");
40 43 }
41 44 else
42 45 {
Modified XFEExtension.NetCore.ServerInteractive/Utilities/Server/XFEServerCore.cs +96 -58
@@ -65,6 +65,10 @@ public abstract class XFEServerCore : ServerCoreServiceBase
65 65 /// </summary>
66 66 internal List<(string Pattern, Func<IServerCoreStandardService> Factory)> WildcardCoreServiceList = [];
67 67 /// <summary>
68 /// 禁用框架请求日志的入口点路径或通配符模式集合
69 /// </summary>
70 internal HashSet<string> NoLogEntryPointSet = [];
71 /// <summary>
68 72 /// 网络通讯服务器
69 73 /// </summary>
70 74 public CyberCommServer CyberCommServer { get; internal set; } = new();
@@ -94,6 +98,59 @@ public abstract class XFEServerCore : ServerCoreServiceBase
94 98 var clientIP = e.ClientIP;
95 99 try { clientIP = GetIPFunction(e); } catch (Exception ex) { Console.WriteLine($"[WARN]获取IP地址失败:{ex.Message}"); }
96 100 r.ClientIP = clientIP;
101
102 // 在校验日志产生前解析并匹配入口点,以便 [NoLog] 能关闭完整的框架请求日志。
103 string route;
104 Func<IServerCoreStandardService>? serviceFactory;
105 string? matchedPattern;
106 try
107 {
108 var url = e.Request.Url ?? e.RequestUrl;
109 if (url is null)
110 {
111 ServerCoreError?.Invoke(this, new()
112 {
113 StatusCode = HttpStatusCode.BadRequest,
114 ReturnArgs = r,
115 ServerException = new ProcessStandardRequestException("请求URL为空")
116 });
117 return;
118 }
119 if (!TryGetRoute(url, out route))
120 {
121 ServerCoreError?.Invoke(this, new()
122 {
123 StatusCode = HttpStatusCode.BadRequest,
124 ReturnArgs = r,
125 ServerException = new ProcessStandardRequestException($"请求路径不匹配主入口点: {MainEntryPoint}")
126 });
127 return;
128 }
129 if (route.IsNullOrEmpty())
130 {
131 ServerCoreError?.Invoke(this, new()
132 {
133 StatusCode = HttpStatusCode.BadRequest,
134 ReturnArgs = r,
135 ServerException = new ProcessStandardRequestException("请求路由为空")
136 });
137 return;
138 }
139
140 TryResolveService(route, out serviceFactory, out matchedPattern);
141 r.SuppressLog = matchedPattern is not null && NoLogEntryPointSet.Contains(matchedPattern);
142 }
143 catch (Exception ex)
144 {
145 ServerCoreError?.Invoke(this, new()
146 {
147 StatusCode = HttpStatusCode.InternalServerError,
148 ReturnArgs = r,
149 ServerException = new ProcessStandardRequestException("解析请求路由时发生异常", ex)
150 });
151 return;
152 }
153
97 154 try
98 155 {
99 156 foreach (var serverCoreVerifyService in ServerCoreVerifyServiceList.Select(serverCoreVerifyFactory => serverCoreVerifyFactory()))
@@ -142,63 +199,11 @@ public abstract class XFEServerCore : ServerCoreServiceBase
142 199
143 200 try
144 201 {
145 // 从URL中提取路由:www.xxx.com/[mainEntryPoint?][/subEntryPoint?]*
146 var url = e.Request.Url ?? e.RequestUrl;
147 if (url is null)
148 {
149 ServerCoreError?.Invoke(this, new()
150 {
151 StatusCode = HttpStatusCode.BadRequest,
152 ReturnArgs = r,
153 ServerException = new ProcessStandardRequestException("请求URL为空")
154 });
155 return;
156 }
157 if (!TryGetRoute(url, out var route))
158 {
159 ServerCoreError?.Invoke(this, new()
160 {
161 StatusCode = HttpStatusCode.BadRequest,
162 ReturnArgs = r,
163 ServerException = new ProcessStandardRequestException($"请求路径不匹配主入口点: {MainEntryPoint}")
164 });
165 return;
166 }
167
168 if (route.IsNullOrEmpty())
169 {
170 ServerCoreError?.Invoke(this, new()
171 {
172 StatusCode = HttpStatusCode.BadRequest,
173 ReturnArgs = r,
174 ServerException = new ProcessStandardRequestException("请求路由为空")
175 });
176 return;
177 }
178
179 Console.Write($"({ServerCoreName})【{clientIP}】请求路由-{route}:");
180 var stopWatch = Stopwatch.StartNew();
181
182 // 在字典中查找对应的服务(先精确匹配,再通配符匹配)
183 string? matchedPattern = null;
184
185 if (StandardCoreServiceDictionary.TryGetValue(route, out var serviceFactory))
202 Stopwatch? stopWatch = null;
203 if (!r.SuppressLog)
186 204 {
187 matchedPattern = route;
188 }
189 else
190 {
191 var bestPriority = int.MinValue;
192 foreach (var (pattern, factory) in WildcardCoreServiceList)
193 {
194 if (!RouteMatchHelper.MatchWildcardRoute(pattern, route)) continue;
195 // 尝试通配符匹配:在所有命中的候选中选择最具体的模式(字面量段越多越优先),避免结果依赖注册顺序
196 var currentPriority = RouteMatchHelper.GetWildcardPatternPriority(pattern);
197 if (currentPriority <= bestPriority) continue;
198 bestPriority = currentPriority;
199 matchedPattern = pattern;
200 serviceFactory = factory;
201 }
205 Console.Write($"({ServerCoreName})【{clientIP}】请求路由-{route}:");
206 stopWatch = Stopwatch.StartNew();
202 207 }
203 208
204 209 if (serviceFactory is not null && matchedPattern is not null)
@@ -238,8 +243,11 @@ public abstract class XFEServerCore : ServerCoreServiceBase
238 243 ServerException = new XFEServerCoreRequestInnerException($"请求异常-{route}", ex)
239 244 });
240 245 }
241 stopWatch.Stop();
242 Console.WriteLine($"\t[耗时 {InteractiveHelper.GetStopWatchTime(stopWatch)}]");
246 if (stopWatch is not null)
247 {
248 stopWatch.Stop();
249 Console.WriteLine($"\t[耗时 {InteractiveHelper.GetStopWatchTime(stopWatch)}]");
250 }
243 251 return;
244 252 }
245 253 r.Handled = true;
@@ -325,6 +333,36 @@ public abstract class XFEServerCore : ServerCoreServiceBase
325 333 return true;
326 334 }
327 335
336 /// <summary>
337 /// 按精确路由优先、最具体通配符次之的规则查找服务。
338 /// </summary>
339 private bool TryResolveService(string route, out Func<IServerCoreStandardService>? serviceFactory, out string? matchedPattern)
340 {
341 if (StandardCoreServiceDictionary.TryGetValue(route, out serviceFactory))
342 {
343 matchedPattern = route;
344 return true;
345 }
346
347 matchedPattern = null;
348 serviceFactory = null;
349 var bestPriority = int.MinValue;
350 foreach (var (pattern, factory) in WildcardCoreServiceList)
351 {
352 if (!RouteMatchHelper.MatchWildcardRoute(pattern, route)) continue;
353
354 // 在所有命中的候选中选择最具体的模式(字面量段越多越优先),避免结果依赖注册顺序。
355 var currentPriority = RouteMatchHelper.GetWildcardPatternPriority(pattern);
356 if (currentPriority <= bestPriority) continue;
357
358 bestPriority = currentPriority;
359 matchedPattern = pattern;
360 serviceFactory = factory;
361 }
362
363 return serviceFactory is not null;
364 }
365
328 366 internal void NotifyServerStarted(object? sender, EventArgs e)
329 367 {
330 368 foreach (var service in ServerCoreServiceList)
Modified XFEExtension.NetCore.ServerInteractive/Utilities/Server/XFEServerCoreBuilder.cs +10 -0
@@ -1,5 +1,6 @@
1 1 using XFEExtension.NetCore.AutoImplement;
2 2 using XFEExtension.NetCore.ServerInteractive.Implements;
3 using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;
3 4 using XFEExtension.NetCore.ServerInteractive.Interfaces.CoreService;
4 5 using XFEExtension.NetCore.ServerInteractive.Options;
5 6 using XFEExtension.NetCore.ServerInteractive.Utilities.Helpers;
@@ -19,6 +20,7 @@ public abstract class XFEServerCoreBuilder : XFEBuilderBase<XFEServerCoreBuilder
19 20 private readonly List<Func<IServerCoreVerifyService>> _serverCoreVerifyServiceList = [];
20 21 private readonly Dictionary<string, Func<IServerCoreStandardService>> _serverStandardCoreServiceDictionary = [];
21 22 private readonly List<(string Pattern, Func<IServerCoreStandardService> Factory)> _serverWildcardCoreServiceList = [];
23 private readonly HashSet<string> _noLogEntryPointSet = [];
22 24
23 25 /// <summary>
24 26 /// 创建XFE服务器核心构建器
@@ -115,9 +117,16 @@ public abstract class XFEServerCoreBuilder : XFEBuilderBase<XFEServerCoreBuilder
115 117 if (entryPointList is null || entryPointList.Count == 0)
116 118 throw new InvalidOperationException($"类型 {typeof(T).Name} 的 EntryPointList 为空");
117 119
120 var noLogEntryPoints = probeService is ServerCoreStandardServiceBase standardService
121 ? standardService.NoLogEntryPoints
122 : [];
123
118 124 // 为每个入口点注册服务工厂(区分精确路由和通配符路由)
119 125 foreach (var route in entryPointList)
120 126 {
127 if (noLogEntryPoints.Contains(route))
128 _noLogEntryPointSet.Add(route);
129
121 130 Func<IServerCoreStandardService> factory = () =>
122 131 {
123 132 var inst = new T();
@@ -152,6 +161,7 @@ public abstract class XFEServerCoreBuilder : XFEBuilderBase<XFEServerCoreBuilder
152 161 _xFEServerCore.ServerCoreVerifyServiceList = _serverCoreVerifyServiceList;
153 162 _xFEServerCore.StandardCoreServiceDictionary = _serverStandardCoreServiceDictionary;
154 163 _xFEServerCore.WildcardCoreServiceList = _serverWildcardCoreServiceList;
164 _xFEServerCore.NoLogEntryPointSet = _noLogEntryPointSet;
155 165
156 166 if (xFEServerCoreOptions is not null)
157 167 {
Modified XFEExtension.NetCore.ServerInteractive/XFEExtension.NetCore.ServerInteractive.csproj +3 -1