XFEExtension.NetCore.ServerInteractive
[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig
关注
0
Fork
0
Star
0
返回提交历史
Modified
README.md
+16
-0
Modified
README.zh-CN.md
+16
-0
Modified
XFEExtension.NetCore.ServerInteractive.SourceGenerator/EntryPointGenerator.cs
+16
-1
Modified
XFEExtension.NetCore.ServerInteractive.SourceGenerator/Models/MethodCandidate.cs
+3
-2
Modified
XFEExtension.NetCore.ServerInteractive.TServer/Services/StatusCoreService.cs
+1
-0
Added
XFEExtension.NetCore.ServerInteractive/Attributes/NoLogAttribute.cs
+11
-0
Modified
XFEExtension.NetCore.ServerInteractive/Implements/CoreService/ServerCoreStandardServiceBase.cs
+5
-0
Modified
XFEExtension.NetCore.ServerInteractive/Models/ServerModels/ServerCoreReturnArgs.cs
+4
-0
Modified
XFEExtension.NetCore.ServerInteractive/Utilities/Server/Services/CoreService/EntryPointVerifyService.cs
+8
-4
Modified
XFEExtension.NetCore.ServerInteractive/Utilities/Server/Services/CoreService/ServerCoreExceptionProcessService.cs
+4
-1
Modified
XFEExtension.NetCore.ServerInteractive/Utilities/Server/XFEServerCore.cs
+96
-58
Modified
XFEExtension.NetCore.ServerInteractive/Utilities/Server/XFEServerCoreBuilder.cs
+10
-0
Modified
XFEExtension.NetCore.ServerInteractive/XFEExtension.NetCore.ServerInteractive.csproj
+3
-1
XFEstudio/XFEExtension.NetCore.ServerInteractive
引入 [NoLog] 特性以支持入口点日志抑制
为 XFE Server Core 框架新增 [NoLog] 方法特性,允许开发者在高频或无需常规日志的入口点方法上标记 [NoLog],关闭自动输出的请求、校验、路由、耗时等日志,仅保留异常和业务日志。实现包括 NoLogAttribute 特性类、增量生成器支持、核心类路由集合传递与注册、日志输出点适配 SuppressLog 标记、示例服务用法展示,并同步更新文档和版本说明,标明 3.3.0 版本新增。
b2d8e05
代码差异
13 个文件
+193
-67
@@ -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
---
@@ -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
---
@@ -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
@@ -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
}
@@ -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";
@@ -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;
@@ -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
@@ -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; }
@@ -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
}
@@ -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
{
@@ -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)
@@ -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
{