mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 05:08:58 +08:00
Compare commits
15 Commits
0e32dab4a2
...
ddaabd8104
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddaabd8104 | ||
|
|
8d8b94f608 | ||
|
|
76fcdb8233 | ||
|
|
5fd71f3620 | ||
|
|
4557dde631 | ||
|
|
0ad2ed1761 | ||
|
|
590f2cb516 | ||
|
|
f5f2c251e5 | ||
|
|
7da985947c | ||
|
|
104ac25dc3 | ||
|
|
1395b84439 | ||
|
|
e1c1eb1123 | ||
|
|
9109eecea9 | ||
|
|
121df440c3 | ||
|
|
ed269d4a34 |
@ -154,6 +154,29 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
string HandlerInterfaceLogName,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> ServiceTypeArguments);
|
||||
|
||||
/// <summary>
|
||||
/// 描述单个程序集级 reflection fallback 特性实例的发射内容。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 某些运行时合同允许生成器把可直接引用的 fallback handlers 与必须按名称恢复的 handlers
|
||||
/// 拆成多个特性实例,以进一步减少运行时字符串查找成本。
|
||||
/// </remarks>
|
||||
private readonly record struct ReflectionFallbackAttributeEmissionSpec(
|
||||
bool EmitDirectTypeReferences,
|
||||
ImmutableArray<string> Values);
|
||||
|
||||
/// <summary>
|
||||
/// 描述本轮生成应如何发射程序集级 reflection fallback 元数据。
|
||||
/// </summary>
|
||||
private readonly record struct ReflectionFallbackEmissionSpec(
|
||||
ImmutableArray<ReflectionFallbackAttributeEmissionSpec> Attributes)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前是否需要发射任何 fallback 元数据。
|
||||
/// </summary>
|
||||
public bool HasFallbackHandlers => !Attributes.IsDefaultOrEmpty;
|
||||
}
|
||||
|
||||
private readonly record struct ImplementationRegistrationSpec(
|
||||
string ImplementationTypeDisplayName,
|
||||
string ImplementationLogName,
|
||||
@ -161,6 +184,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations,
|
||||
ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations,
|
||||
string? ReflectionTypeMetadataName,
|
||||
string? ReflectionFallbackHandlerTypeDisplayName,
|
||||
string? ReflectionFallbackHandlerTypeMetadataName);
|
||||
|
||||
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
|
||||
@ -172,6 +196,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
ImmutableArray<ReflectedImplementationRegistrationSpec> reflectedImplementationRegistrations,
|
||||
ImmutableArray<PreciseReflectedRegistrationSpec> preciseReflectedRegistrations,
|
||||
string? reflectionTypeMetadataName,
|
||||
string? reflectionFallbackHandlerTypeDisplayName,
|
||||
string? reflectionFallbackHandlerTypeMetadataName)
|
||||
{
|
||||
ImplementationTypeDisplayName = implementationTypeDisplayName;
|
||||
@ -180,6 +205,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
ReflectedImplementationRegistrations = reflectedImplementationRegistrations;
|
||||
PreciseReflectedRegistrations = preciseReflectedRegistrations;
|
||||
ReflectionTypeMetadataName = reflectionTypeMetadataName;
|
||||
ReflectionFallbackHandlerTypeDisplayName = reflectionFallbackHandlerTypeDisplayName;
|
||||
ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName;
|
||||
}
|
||||
|
||||
@ -195,6 +221,8 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
|
||||
public string? ReflectionTypeMetadataName { get; }
|
||||
|
||||
public string? ReflectionFallbackHandlerTypeDisplayName { get; }
|
||||
|
||||
public string? ReflectionFallbackHandlerTypeMetadataName { get; }
|
||||
|
||||
public bool Equals(HandlerCandidateAnalysis other)
|
||||
@ -204,6 +232,10 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
!string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) ||
|
||||
!string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName,
|
||||
StringComparison.Ordinal) ||
|
||||
!string.Equals(
|
||||
ReflectionFallbackHandlerTypeDisplayName,
|
||||
other.ReflectionFallbackHandlerTypeDisplayName,
|
||||
StringComparison.Ordinal) ||
|
||||
!string.Equals(
|
||||
ReflectionFallbackHandlerTypeMetadataName,
|
||||
other.ReflectionFallbackHandlerTypeMetadataName,
|
||||
@ -254,6 +286,10 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
(ReflectionTypeMetadataName is null
|
||||
? 0
|
||||
: StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName));
|
||||
hashCode = (hashCode * 397) ^
|
||||
(ReflectionFallbackHandlerTypeDisplayName is null
|
||||
? 0
|
||||
: StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeDisplayName));
|
||||
hashCode = (hashCode * 397) ^
|
||||
(ReflectionFallbackHandlerTypeMetadataName is null
|
||||
? 0
|
||||
@ -280,5 +316,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
|
||||
private readonly record struct GenerationEnvironment(
|
||||
bool GenerationEnabled,
|
||||
bool SupportsReflectionFallbackAttribute);
|
||||
bool SupportsNamedReflectionFallbackTypes,
|
||||
bool SupportsDirectReflectionFallbackTypes,
|
||||
bool SupportsMultipleReflectionFallbackAttributes);
|
||||
}
|
||||
|
||||
@ -8,31 +8,29 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
/// <summary>
|
||||
/// 生成程序集级 CQRS handler 注册器源码。
|
||||
/// </summary>
|
||||
/// <param name="generationEnvironment">
|
||||
/// 当前轮次的生成环境,用于决定 runtime 是否提供 <c>CqrsReflectionFallbackAttribute</c> 契约,以及是否需要在输出中发射对应的程序集级元数据。
|
||||
/// </param>
|
||||
/// <param name="registrations">
|
||||
/// 已整理并排序的 handler 注册描述。方法会据此生成 <c>CqrsHandlerRegistry.g.cs</c>,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。
|
||||
/// </param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">
|
||||
/// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。
|
||||
/// 调用方必须先确保:若该集合非空,则 <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
|
||||
/// <param name="reflectionFallbackEmission">
|
||||
/// 当前轮次选定的程序集级 reflection fallback 元数据发射策略。
|
||||
/// 调用方必须先确保:若该策略包含 fallback handlers,则当前 runtime 已声明支持对应的 fallback attribute 契约;
|
||||
/// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。
|
||||
/// </param>
|
||||
/// <returns>完整的注册器源代码文本。</returns>
|
||||
/// <remarks>
|
||||
/// 当 <paramref name="fallbackHandlerTypeMetadataNames" /> 为空时,输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
|
||||
/// 当其非空且 runtime 合同可用时,输出还会附带程序集级 <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
|
||||
/// 当 <paramref name="reflectionFallbackEmission" /> 不包含任何 fallback handlers 时,
|
||||
/// 输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
|
||||
/// 当其包含 fallback handlers 且 runtime 合同可用时,输出还会附带一个或多个程序集级
|
||||
/// <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
|
||||
/// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
|
||||
/// </remarks>
|
||||
private static string GenerateSource(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
|
||||
{
|
||||
var sourceShape = CreateGeneratedRegistrySourceShape(registrations);
|
||||
var builder = new StringBuilder();
|
||||
AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames);
|
||||
AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission);
|
||||
AppendGeneratedRegistryType(builder, registrations, sourceShape);
|
||||
return builder.ToString();
|
||||
}
|
||||
@ -66,19 +64,17 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="generationEnvironment">当前轮次的生成环境。</param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">需要程序集级 reflection fallback 的 handler 元数据名称。</param>
|
||||
/// <param name="reflectionFallbackEmission">需要写入程序集级 reflection fallback 特性的元数据策略。</param>
|
||||
private static void AppendGeneratedSourcePreamble(
|
||||
StringBuilder builder,
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
|
||||
{
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
builder.AppendLine("#nullable enable");
|
||||
builder.AppendLine();
|
||||
if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0)
|
||||
if (reflectionFallbackEmission.HasFallbackHandlers)
|
||||
{
|
||||
AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames);
|
||||
AppendReflectionFallbackAttributes(builder, reflectionFallbackEmission);
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
@ -95,25 +91,53 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
/// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">需要写入特性的 handler 元数据名称。</param>
|
||||
/// <param name="reflectionFallbackEmission">需要写入特性的 fallback 元数据策略。</param>
|
||||
private static void AppendReflectionFallbackAttributes(
|
||||
StringBuilder builder,
|
||||
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
|
||||
{
|
||||
for (var index = 0; index < reflectionFallbackEmission.Attributes.Length; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
AppendReflectionFallbackAttribute(builder, reflectionFallbackEmission.Attributes[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射单个程序集级 reflection fallback 元数据特性实例。
|
||||
/// </summary>
|
||||
private static void AppendReflectionFallbackAttribute(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
ReflectionFallbackAttributeEmissionSpec attributeEmission)
|
||||
{
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsReflectionFallbackAttribute(");
|
||||
for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++)
|
||||
|
||||
for (var index = 0; index < attributeEmission.Values.Length; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(", ");
|
||||
|
||||
builder.Append('"');
|
||||
builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index]));
|
||||
builder.Append('"');
|
||||
if (attributeEmission.EmitDirectTypeReferences)
|
||||
{
|
||||
builder.Append("typeof(");
|
||||
builder.Append(attributeEmission.Values[index]);
|
||||
builder.Append(')');
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append('"');
|
||||
builder.Append(EscapeStringLiteral(attributeEmission.Values[index]));
|
||||
builder.Append('"');
|
||||
}
|
||||
}
|
||||
|
||||
builder.AppendLine(")]");
|
||||
builder.Append(")]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -56,6 +56,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
|
||||
private static GenerationEnvironment CreateGenerationEnvironment(Compilation compilation)
|
||||
{
|
||||
var reflectionFallbackAttributeType =
|
||||
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName);
|
||||
var generationEnabled = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
|
||||
@ -64,10 +66,26 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
CqrsHandlerRegistryAttributeMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
|
||||
var supportsReflectionFallbackAttribute =
|
||||
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null;
|
||||
var stringType = compilation.GetSpecialType(SpecialType.System_String);
|
||||
var typeType = compilation.GetTypeByMetadataName("System.Type");
|
||||
var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
|
||||
HasParamsArrayConstructor(
|
||||
reflectionFallbackAttributeType,
|
||||
stringType);
|
||||
var supportsDirectReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
|
||||
typeType is not null &&
|
||||
HasParamsArrayConstructor(
|
||||
reflectionFallbackAttributeType,
|
||||
typeType);
|
||||
var supportsMultipleReflectionFallbackAttributes = reflectionFallbackAttributeType is not null &&
|
||||
SupportsMultipleAttributeInstances(
|
||||
reflectionFallbackAttributeType);
|
||||
|
||||
return new GenerationEnvironment(generationEnabled, supportsReflectionFallbackAttribute);
|
||||
return new GenerationEnvironment(
|
||||
generationEnabled,
|
||||
supportsNamedReflectionFallbackTypes,
|
||||
supportsDirectReflectionFallbackTypes,
|
||||
supportsMultipleReflectionFallbackAttributes);
|
||||
}
|
||||
|
||||
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||
@ -130,6 +148,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
ImmutableArray.CreateBuilder<ReflectedImplementationRegistrationSpec>(handlerInterfaces.Length);
|
||||
var preciseReflectedRegistrations =
|
||||
ImmutableArray.CreateBuilder<PreciseReflectedRegistrationSpec>(handlerInterfaces.Length);
|
||||
string? reflectionFallbackHandlerTypeDisplayName = null;
|
||||
string? reflectionFallbackHandlerTypeMetadataName = null;
|
||||
foreach (var handlerInterface in handlerInterfaces)
|
||||
{
|
||||
@ -151,6 +170,9 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
// If a future Roslyn type shape still slips through this net, keep the generator conservative:
|
||||
// preserve the static registrations we do understand, and let the runtime recover the remaining
|
||||
// interfaces via the existing assembly-level targeted reflection fallback contract.
|
||||
if (canReferenceImplementation)
|
||||
reflectionFallbackHandlerTypeDisplayName ??= implementationTypeDisplayName;
|
||||
|
||||
reflectionFallbackHandlerTypeMetadataName ??= GetReflectionTypeMetadataName(type);
|
||||
}
|
||||
|
||||
@ -161,6 +183,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
reflectedImplementationRegistrations.ToImmutable(),
|
||||
preciseReflectedRegistrations.ToImmutable(),
|
||||
canReferenceImplementation ? null : GetReflectionTypeMetadataName(type),
|
||||
reflectionFallbackHandlerTypeDisplayName,
|
||||
reflectionFallbackHandlerTypeMetadataName);
|
||||
}
|
||||
|
||||
@ -259,57 +282,71 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
if (registrations.Count == 0)
|
||||
return;
|
||||
|
||||
var fallbackHandlerTypeMetadataNames = registrations
|
||||
.Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName)
|
||||
.Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Cast<string>()
|
||||
.ToArray();
|
||||
var reflectionFallbackEmission = CreateReflectionFallbackEmissionSpec(generationEnvironment, registrations);
|
||||
|
||||
if (!CanEmitGeneratedRegistry(
|
||||
generationEnvironment.SupportsReflectionFallbackAttribute,
|
||||
fallbackHandlerTypeMetadataNames.Length))
|
||||
generationEnvironment,
|
||||
reflectionFallbackEmission))
|
||||
{
|
||||
ReportMissingReflectionFallbackContractDiagnostic(
|
||||
context,
|
||||
fallbackHandlerTypeMetadataNames);
|
||||
registrations);
|
||||
return;
|
||||
}
|
||||
|
||||
context.AddSource(
|
||||
HintName,
|
||||
GenerateSource(generationEnvironment, registrations, fallbackHandlerTypeMetadataNames));
|
||||
GenerateSource(registrations, reflectionFallbackEmission));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前轮次是否允许输出生成注册器。
|
||||
/// </summary>
|
||||
/// <param name="supportsReflectionFallbackAttribute">
|
||||
/// runtime 合同中是否存在 <c>CqrsReflectionFallbackAttribute</c>,以承载生成器无法静态精确表达的 handler 回退元数据。
|
||||
/// </param>
|
||||
/// <param name="fallbackHandlerTypeCount">
|
||||
/// 当前轮次需要依赖程序集级 reflection fallback 元数据恢复的 handler 数量。
|
||||
/// </param>
|
||||
/// <param name="generationEnvironment">当前轮次可用的 fallback 合同能力。</param>
|
||||
/// <param name="reflectionFallbackEmission">当前轮次选定的 fallback 元数据发射策略。</param>
|
||||
/// <returns>
|
||||
/// 当没有 handler 依赖 fallback,或 runtime 已提供承载该元数据的特性契约时返回 <see langword="true" />;
|
||||
/// 当没有 handler 依赖 fallback,或 runtime 已提供本轮策略所需的元数据承载重载时返回 <see langword="true" />;
|
||||
/// 否则返回 <see langword="false" />,调用方必须放弃生成以避免输出会静默漏注册的半成品注册器。
|
||||
/// </returns>
|
||||
private static bool CanEmitGeneratedRegistry(
|
||||
bool supportsReflectionFallbackAttribute,
|
||||
int fallbackHandlerTypeCount)
|
||||
GenerationEnvironment generationEnvironment,
|
||||
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
|
||||
{
|
||||
return fallbackHandlerTypeCount == 0 || supportsReflectionFallbackAttribute;
|
||||
if (!reflectionFallbackEmission.HasFallbackHandlers)
|
||||
return true;
|
||||
|
||||
foreach (var attributeEmission in reflectionFallbackEmission.Attributes)
|
||||
{
|
||||
if (attributeEmission.EmitDirectTypeReferences)
|
||||
{
|
||||
if (!generationEnvironment.SupportsDirectReflectionFallbackTypes)
|
||||
return false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!generationEnvironment.SupportsNamedReflectionFallbackTypes)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 报告当前轮次因缺少 fallback 元数据承载契约而无法安全生成注册器的诊断。
|
||||
/// </summary>
|
||||
/// <param name="context">源生产上下文。</param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">需要通过程序集级 reflection fallback 元数据恢复的 handler 元数据名称。</param>
|
||||
/// <param name="registrations">当前轮次汇总后的 handler 注册描述。</param>
|
||||
private static void ReportMissingReflectionFallbackContractDiagnostic(
|
||||
SourceProductionContext context,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations)
|
||||
{
|
||||
var fallbackHandlerTypeMetadataNames = registrations
|
||||
.Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName)
|
||||
.Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Cast<string>()
|
||||
.ToArray();
|
||||
var handlerList = string.Join(
|
||||
", ",
|
||||
fallbackHandlerTypeMetadataNames.OrderBy(static name => name, StringComparer.Ordinal));
|
||||
@ -346,6 +383,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
candidate.ReflectedImplementationRegistrations,
|
||||
candidate.PreciseReflectedRegistrations,
|
||||
candidate.ReflectionTypeMetadataName,
|
||||
candidate.ReflectionFallbackHandlerTypeDisplayName,
|
||||
candidate.ReflectionFallbackHandlerTypeMetadataName));
|
||||
}
|
||||
|
||||
@ -354,6 +392,224 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return registrations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选择本轮生成应采用的 fallback 元数据发射策略。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当所有 fallback handlers 都能被生成代码直接引用,且 runtime 暴露了 <c>params Type[]</c> 重载时,
|
||||
/// 优先输出单个直接 <see cref="Type" /> 元数据特性;当 runtime 同时支持多个特性实例时,
|
||||
/// mixed 场景会拆分成“直接 <see cref="Type" /> + 字符串类型名”两类特性;其余场景统一回退到字符串元数据。
|
||||
/// </remarks>
|
||||
private static ReflectionFallbackEmissionSpec CreateReflectionFallbackEmissionSpec(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations)
|
||||
{
|
||||
var fallbackCandidates = CollectFallbackCandidates(registrations);
|
||||
if (fallbackCandidates.Count == 0)
|
||||
return new ReflectionFallbackEmissionSpec(ImmutableArray<ReflectionFallbackAttributeEmissionSpec>.Empty);
|
||||
|
||||
var fallbackHandlerTypeMetadataNames = GetSortedFallbackMetadataNames(fallbackCandidates);
|
||||
var fallbackHandlerTypeDisplayNames = GetSortedDirectFallbackDisplayNames(fallbackCandidates);
|
||||
|
||||
if (TryCreateDirectFallbackEmission(
|
||||
generationEnvironment,
|
||||
fallbackHandlerTypeDisplayNames,
|
||||
fallbackHandlerTypeMetadataNames,
|
||||
out var directFallbackEmission))
|
||||
{
|
||||
return directFallbackEmission;
|
||||
}
|
||||
|
||||
if (TryCreateMixedFallbackEmission(
|
||||
generationEnvironment,
|
||||
fallbackCandidates,
|
||||
fallbackHandlerTypeDisplayNames,
|
||||
out var mixedFallbackEmission))
|
||||
{
|
||||
return mixedFallbackEmission;
|
||||
}
|
||||
|
||||
return CreateNamedFallbackEmission(fallbackHandlerTypeMetadataNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集本轮所有 fallback handlers 的稳定元数据名和可选直接引用显示名。
|
||||
/// </summary>
|
||||
private static Dictionary<string, string?> CollectFallbackCandidates(
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations)
|
||||
{
|
||||
var fallbackCandidates = new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(registration.ReflectionFallbackHandlerTypeMetadataName))
|
||||
continue;
|
||||
|
||||
fallbackCandidates[registration.ReflectionFallbackHandlerTypeMetadataName!] =
|
||||
registration.ReflectionFallbackHandlerTypeDisplayName;
|
||||
}
|
||||
|
||||
return fallbackCandidates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取按稳定顺序排列的 fallback handler 元数据名称集合。
|
||||
/// </summary>
|
||||
private static ImmutableArray<string> GetSortedFallbackMetadataNames(
|
||||
IReadOnlyDictionary<string, string?> fallbackCandidates)
|
||||
{
|
||||
return fallbackCandidates.Keys
|
||||
.OrderBy(static metadataName => metadataName, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取按稳定顺序排列的可直接引用 fallback handler 显示名集合。
|
||||
/// </summary>
|
||||
private static ImmutableArray<string> GetSortedDirectFallbackDisplayNames(
|
||||
IReadOnlyDictionary<string, string?> fallbackCandidates)
|
||||
{
|
||||
return fallbackCandidates.Values
|
||||
.Where(static typeDisplayName => !string.IsNullOrWhiteSpace(typeDisplayName))
|
||||
.Cast<string>()
|
||||
.OrderBy(static typeDisplayName => typeDisplayName, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当全部 fallback handlers 都可直接引用时,尝试创建直接 <see cref="Type" /> 元数据发射策略。
|
||||
/// </summary>
|
||||
private static bool TryCreateDirectFallbackEmission(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
ImmutableArray<string> fallbackHandlerTypeDisplayNames,
|
||||
ImmutableArray<string> fallbackHandlerTypeMetadataNames,
|
||||
out ReflectionFallbackEmissionSpec emission)
|
||||
{
|
||||
if (generationEnvironment.SupportsDirectReflectionFallbackTypes &&
|
||||
fallbackHandlerTypeDisplayNames.Length == fallbackHandlerTypeMetadataNames.Length)
|
||||
{
|
||||
emission = new ReflectionFallbackEmissionSpec(
|
||||
[
|
||||
new ReflectionFallbackAttributeEmissionSpec(
|
||||
EmitDirectTypeReferences: true,
|
||||
fallbackHandlerTypeDisplayNames)
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
emission = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 runtime 允许多个 fallback 特性实例时,尝试为 mixed 场景拆分直接 <see cref="Type" /> 与字符串元数据。
|
||||
/// </summary>
|
||||
private static bool TryCreateMixedFallbackEmission(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyDictionary<string, string?> fallbackCandidates,
|
||||
ImmutableArray<string> fallbackHandlerTypeDisplayNames,
|
||||
out ReflectionFallbackEmissionSpec emission)
|
||||
{
|
||||
if (!generationEnvironment.SupportsDirectReflectionFallbackTypes ||
|
||||
!generationEnvironment.SupportsNamedReflectionFallbackTypes ||
|
||||
!generationEnvironment.SupportsMultipleReflectionFallbackAttributes ||
|
||||
fallbackHandlerTypeDisplayNames.Length == 0)
|
||||
{
|
||||
emission = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var namedOnlyFallbackMetadataNames = fallbackCandidates
|
||||
.Where(static pair => string.IsNullOrWhiteSpace(pair.Value))
|
||||
.Select(static pair => pair.Key)
|
||||
.OrderBy(static metadataName => metadataName, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
if (namedOnlyFallbackMetadataNames.Length == 0)
|
||||
{
|
||||
emission = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
emission = new ReflectionFallbackEmissionSpec(
|
||||
[
|
||||
new ReflectionFallbackAttributeEmissionSpec(
|
||||
EmitDirectTypeReferences: true,
|
||||
fallbackHandlerTypeDisplayNames),
|
||||
new ReflectionFallbackAttributeEmissionSpec(
|
||||
EmitDirectTypeReferences: false,
|
||||
namedOnlyFallbackMetadataNames)
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建统一的字符串 fallback 元数据发射策略。
|
||||
/// </summary>
|
||||
private static ReflectionFallbackEmissionSpec CreateNamedFallbackEmission(
|
||||
ImmutableArray<string> fallbackHandlerTypeMetadataNames)
|
||||
{
|
||||
return new ReflectionFallbackEmissionSpec(
|
||||
[
|
||||
new ReflectionFallbackAttributeEmissionSpec(
|
||||
EmitDirectTypeReferences: false,
|
||||
fallbackHandlerTypeMetadataNames)
|
||||
]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断目标特性是否暴露了指定元素类型的 <c>params T[]</c> 构造函数。
|
||||
/// </summary>
|
||||
private static bool HasParamsArrayConstructor(INamedTypeSymbol attributeType, ITypeSymbol elementType)
|
||||
{
|
||||
foreach (var constructor in attributeType.InstanceConstructors)
|
||||
{
|
||||
if (constructor.Parameters.Length != 1)
|
||||
continue;
|
||||
|
||||
var parameter = constructor.Parameters[0];
|
||||
if (!parameter.IsParams)
|
||||
continue;
|
||||
|
||||
if (parameter.Type is IArrayTypeSymbol { Rank: 1 } arrayType &&
|
||||
SymbolEqualityComparer.Default.Equals(arrayType.ElementType, elementType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断目标特性的 <see cref="AttributeUsageAttribute" /> 是否允许在同一程序集上声明多个实例。
|
||||
/// </summary>
|
||||
private static bool SupportsMultipleAttributeInstances(INamedTypeSymbol attributeType)
|
||||
{
|
||||
foreach (var attribute in attributeType.GetAttributes())
|
||||
{
|
||||
if (!string.Equals(
|
||||
attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
"global::System.AttributeUsageAttribute",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var namedArgument in attribute.NamedArguments)
|
||||
{
|
||||
if (string.Equals(namedArgument.Key, "AllowMultiple", StringComparison.Ordinal) &&
|
||||
namedArgument.Value.Value is bool allowMultiple)
|
||||
{
|
||||
return allowMultiple;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsConcreteHandlerType(INamedTypeSymbol type)
|
||||
{
|
||||
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
- `Cqrs/CqrsHandlerRegistryGenerator.cs`
|
||||
|
||||
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。
|
||||
当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;如果 runtime 允许同一程序集声明多个 fallback 特性实例,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少运行时按类型名回查程序集的成本。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
|
||||
@ -341,6 +341,64 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当程序集同时声明直接 <see cref="Type" /> fallback 与字符串名称 fallback 时,
|
||||
/// 运行时会优先复用直接类型,并只对名称 fallback 做定向 <c>GetType(...)</c> 查找。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Use_Mixed_Fallback_Metadata_With_Targeted_Type_Lookups_Only_For_Named_Entries()
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
.SetupGet(static assembly => assembly.FullName)
|
||||
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.Assembly.FullName);
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
|
||||
.Returns(
|
||||
[
|
||||
new CqrsReflectionFallbackAttribute(
|
||||
ReflectionFallbackNotificationContainer.DirectFallbackHandlerType),
|
||||
new CqrsReflectionFallbackAttribute(
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
|
||||
]);
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetType(
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
|
||||
false,
|
||||
false))
|
||||
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);
|
||||
|
||||
var container = new MicrosoftDiContainer();
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
|
||||
var registrations = container.GetServicesUnsafe
|
||||
.Where(static descriptor =>
|
||||
descriptor.ServiceType == typeof(INotificationHandler<GeneratedRegistryNotification>) &&
|
||||
descriptor.ImplementationType is not null)
|
||||
.Select(static descriptor => descriptor.ImplementationType!)
|
||||
.ToList();
|
||||
|
||||
Assert.That(
|
||||
registrations,
|
||||
Is.EqualTo(
|
||||
[
|
||||
typeof(GeneratedRegistryNotificationHandler),
|
||||
ReflectionFallbackNotificationContainer.DirectFallbackHandlerType,
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
|
||||
]));
|
||||
|
||||
generatedAssembly.Verify(
|
||||
static assembly => assembly.GetType(
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
|
||||
false,
|
||||
false),
|
||||
Times.Once);
|
||||
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一程序集对象重复接入多个容器时,会复用已解析的 registry / fallback 元数据,
|
||||
/// 而不是重复读取程序集级 attribute 或重复执行 type-name lookup。
|
||||
|
||||
@ -10,11 +10,34 @@ namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
/// </summary>
|
||||
internal sealed class ReflectionFallbackNotificationContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取可被直接引用、适合通过 <see cref="Type" /> 元数据补扫的处理器类型。
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// 可被生成注册器直接引用的 fallback 处理器类型,用于验证 runtime 会优先消费 <see cref="Type" /> 元数据。
|
||||
/// </returns>
|
||||
public static Type DirectFallbackHandlerType => typeof(DirectFallbackGeneratedRegistryNotificationHandler);
|
||||
|
||||
/// <summary>
|
||||
/// 获取仅能通过反射补扫接入的私有嵌套处理器类型。
|
||||
/// </summary>
|
||||
public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler);
|
||||
|
||||
private sealed class DirectFallbackGeneratedRegistryNotificationHandler
|
||||
: INotificationHandler<GeneratedRegistryNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理测试通知。
|
||||
/// </summary>
|
||||
/// <param name="notification">通知实例。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>已完成任务。</returns>
|
||||
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler
|
||||
: INotificationHandler<GeneratedRegistryNotification>
|
||||
{
|
||||
|
||||
@ -52,7 +52,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
var request = new TestValidatedRequest { Value = -1 }; // 无效值
|
||||
|
||||
Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||
await _context!.SendRequestAsync(request));
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -62,7 +62,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
TestRetryBehavior.AttemptCount = 0;
|
||||
var request = new TestRetryRequest { ShouldFailTimes = 0 }; // 不失败
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Success"));
|
||||
Assert.That(TestRetryBehavior.AttemptCount, Is.EqualTo(1));
|
||||
@ -82,7 +82,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
tasks.Add(_context!.SendRequestAsync(request).AsTask());
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
stopwatch.Stop();
|
||||
|
||||
// 验证所有请求都成功处理
|
||||
@ -102,7 +102,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
for (int i = 0; i < requestCount; i++)
|
||||
{
|
||||
var request = new TestMemoryRequest { Data = new string('x', 1000) };
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
// 定期强制GC来测试内存泄漏
|
||||
if (i % 100 == 0)
|
||||
@ -126,7 +126,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
TestTransientErrorHandler.ErrorCount = 0;
|
||||
var request = new TestTransientErrorRequest { MaxErrors = 0 }; // 不出错
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Success"));
|
||||
Assert.That(TestTransientErrorHandler.ErrorCount, Is.EqualTo(0));
|
||||
@ -140,7 +140,8 @@ public class MediatorAdvancedFeaturesTests
|
||||
{
|
||||
try
|
||||
{
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true });
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true })
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -151,7 +152,8 @@ public class MediatorAdvancedFeaturesTests
|
||||
// 验证断路器已打开,后续请求应该快速失败
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false }));
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false })
|
||||
.ConfigureAwait(false));
|
||||
stopwatch.Stop();
|
||||
|
||||
// 验证快速失败(应该在很短时间内完成)
|
||||
@ -172,7 +174,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
// 执行saga
|
||||
foreach (var request in requests)
|
||||
{
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 验证所有步骤都成功执行
|
||||
@ -192,10 +194,10 @@ public class MediatorAdvancedFeaturesTests
|
||||
};
|
||||
|
||||
// 执行saga,第二步会失败
|
||||
await _context!.SendRequestAsync(requests[0]);
|
||||
await _context!.SendRequestAsync(requests[0]).ConfigureAwait(false);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context.SendRequestAsync(requests[1]));
|
||||
await _context.SendRequestAsync(requests[1]).ConfigureAwait(false));
|
||||
|
||||
// 验证回滚机制被触发
|
||||
Assert.That(sagaData.CompletedSteps, Is.EqualTo(new[] { 1 })); // 只有第一步完成
|
||||
@ -206,7 +208,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
[Test]
|
||||
public async Task Request_Chaining_With_Dependencies_Should_Work_Correctly()
|
||||
{
|
||||
var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest());
|
||||
var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest()).ConfigureAwait(false);
|
||||
|
||||
Assert.That(chainResult, Is.EqualTo("Chain completed: Step1 -> Step2 -> Step3"));
|
||||
}
|
||||
@ -218,7 +220,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
var request = new TestExternalServiceRequest { TimeoutMs = 1000 };
|
||||
|
||||
Assert.ThrowsAsync<TaskCanceledException>(async () =>
|
||||
await _context!.SendRequestAsync(request, cts.Token));
|
||||
await _context!.SendRequestAsync(request, cts.Token).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -227,13 +229,15 @@ public class MediatorAdvancedFeaturesTests
|
||||
var testData = new List<string>();
|
||||
var request = new TestDatabaseRequest { Data = "test data", Storage = testData };
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Data saved successfully"));
|
||||
Assert.That(testData, Contains.Item("test data"));
|
||||
}
|
||||
}
|
||||
|
||||
// 这些高级特性测试需要把一组仅供当前文件使用的辅助类型共置,避免拆成多个噪声文件。
|
||||
#pragma warning disable MA0048
|
||||
#region Advanced Test Classes
|
||||
|
||||
public sealed class TestRetryRequestHandler : IRequestHandler<TestRetryRequest, string>
|
||||
@ -329,7 +333,7 @@ public sealed class TestChainStartRequestHandler : IRequestHandler<TestChainStar
|
||||
public async ValueTask<string> Handle(TestChainStartRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟链式调用
|
||||
await Task.Delay(10, cancellationToken);
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
|
||||
return "Chain completed: Step1 -> Step2 -> Step3";
|
||||
}
|
||||
}
|
||||
@ -338,7 +342,7 @@ public sealed class TestExternalServiceRequestHandler : IRequestHandler<TestExte
|
||||
{
|
||||
public async ValueTask<string> Handle(TestExternalServiceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(request.TimeoutMs, cancellationToken);
|
||||
await Task.Delay(request.TimeoutMs, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return "External service response";
|
||||
}
|
||||
@ -378,7 +382,7 @@ public sealed class TestValidatedRequestHandler : IRequestHandler<TestValidatedR
|
||||
// 验证输入
|
||||
if (request.Value < 0)
|
||||
{
|
||||
throw new ArgumentException("Value must be non-negative", nameof(request.Value));
|
||||
throw new ArgumentException("Value must be non-negative", nameof(request));
|
||||
}
|
||||
|
||||
return new ValueTask<string>($"Value: {request.Value}");
|
||||
@ -406,7 +410,7 @@ public sealed class TestPerformanceRequestHandler : IRequestHandler<TestPerforma
|
||||
{
|
||||
public async ValueTask<int> Handle(TestPerformanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(request.ProcessingTimeMs, cancellationToken);
|
||||
await Task.Delay(request.ProcessingTimeMs, cancellationToken).ConfigureAwait(false);
|
||||
return request.Id;
|
||||
}
|
||||
}
|
||||
@ -503,3 +507,4 @@ public sealed record TestDatabaseRequest : IRequest<string>
|
||||
}
|
||||
|
||||
#endregion
|
||||
#pragma warning restore MA0048
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Architectures;
|
||||
@ -62,7 +63,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestContextAwareHandler.LastContext = _context; // 直接设置
|
||||
var request = new TestContextAwareRequest();
|
||||
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(TestContextAwareHandler.LastContext, Is.Not.Null);
|
||||
Assert.That(TestContextAwareHandler.LastContext, Is.SameAs(_context));
|
||||
@ -74,7 +75,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestServiceRetrievalHandler.LastRetrievedService = null;
|
||||
var request = new TestServiceRetrievalRequest();
|
||||
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.Not.Null);
|
||||
Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.InstanceOf<TestService>());
|
||||
@ -86,7 +87,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestNestedRequestHandler2.ExecutionCount = 0;
|
||||
var request = new TestNestedRequest { Depth = 1 }; // 简化为深度1
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Nested execution completed at depth 1"));
|
||||
Assert.That(TestNestedRequestHandler2.ExecutionCount, Is.EqualTo(1));
|
||||
@ -99,7 +100,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestLifecycleHandler.DisposalCount = 0;
|
||||
|
||||
var request = new TestLifecycleRequest();
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
// 验证生命周期管理
|
||||
Assert.That(TestLifecycleHandler.InitializationCount, Is.EqualTo(1));
|
||||
@ -116,14 +117,14 @@ public class MediatorArchitectureIntegrationTests
|
||||
.Select(async i =>
|
||||
{
|
||||
var request = new TestScopedServiceRequest { RequestId = i };
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
lock (results)
|
||||
{
|
||||
results.Add(result);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证每个请求都得到了独立的scope实例
|
||||
Assert.That(results.Distinct().Count(), Is.EqualTo(10));
|
||||
@ -135,7 +136,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
var request = new TestErrorPropagationRequest();
|
||||
|
||||
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context!.SendRequestAsync(request));
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false));
|
||||
|
||||
Assert.That(ex!.Message, Is.EqualTo("Test error from handler"));
|
||||
Assert.That(ex.Data["RequestId"], Is.Not.Null);
|
||||
@ -148,7 +149,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
var request = new TestExceptionRequest();
|
||||
|
||||
Assert.ThrowsAsync<DivideByZeroException>(async () =>
|
||||
await _context!.SendRequestAsync(request));
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false));
|
||||
|
||||
// 验证异常被捕获和记录
|
||||
Assert.That(TestExceptionHandler.LastException, Is.Not.Null);
|
||||
@ -164,7 +165,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var request = new TestPerformanceRequest2 { Id = i };
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(i));
|
||||
}
|
||||
|
||||
@ -188,7 +189,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var request = new TestUncachedRequest { Id = i };
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
stopwatch.Stop();
|
||||
uncachedTimes.Add(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
@ -198,7 +199,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var request = new TestCachedRequest { Id = i };
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
stopwatch.Stop();
|
||||
cachedTimes.Add(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
@ -224,12 +225,12 @@ public class MediatorArchitectureIntegrationTests
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
var request = new TestConcurrentRequest { RequestId = requestId, OrderTracker = executionOrder };
|
||||
return await _context!.SendRequestAsync(request);
|
||||
return await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
});
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证所有请求都成功完成
|
||||
Assert.That(results.Length, Is.EqualTo(concurrentRequests));
|
||||
@ -253,10 +254,10 @@ public class MediatorArchitectureIntegrationTests
|
||||
SharedState = sharedState,
|
||||
Increment = 1
|
||||
};
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证最终状态正确(20个并发操作,每个+1)
|
||||
Assert.That(sharedState.Counter, Is.EqualTo(concurrentOperations));
|
||||
@ -269,7 +270,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestIntegrationHandler.LastSystemCall = null;
|
||||
var request = new TestIntegrationRequest();
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Integration successful"));
|
||||
Assert.That(TestIntegrationHandler.LastSystemCall, Is.EqualTo("System executed"));
|
||||
@ -285,7 +286,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
|
||||
// 使用Mediator
|
||||
var mediatorRequest = new TestMediatorRequest { Value = 42 };
|
||||
var result = await _context.SendRequestAsync(mediatorRequest);
|
||||
var result = await _context.SendRequestAsync(mediatorRequest).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
|
||||
// 验证两者可以共存
|
||||
@ -296,8 +297,8 @@ public class MediatorArchitectureIntegrationTests
|
||||
[Test]
|
||||
public async Task ContextAware_Handler_Should_Use_A_Fresh_Instance_Per_Request()
|
||||
{
|
||||
var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest());
|
||||
var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest());
|
||||
var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false);
|
||||
var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@ -306,312 +307,312 @@ public class MediatorArchitectureIntegrationTests
|
||||
Assert.That(TestPerDispatchContextAwareHandler.Contexts, Has.All.SameAs(_context));
|
||||
});
|
||||
}
|
||||
}
|
||||
#region Integration Test Classes
|
||||
|
||||
#region Integration Test Classes
|
||||
|
||||
public sealed class TestContextAwareRequestHandler : IRequestHandler<TestContextAwareRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestContextAwareRequestHandler : IRequestHandler<TestContextAwareRequest, string>
|
||||
{
|
||||
// 保持测试中设置的上下文,不要重置为null
|
||||
return new ValueTask<string>("Context accessed");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestServiceRetrievalRequestHandler : IRequestHandler<TestServiceRetrievalRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestServiceRetrievalHandler.LastRetrievedService = new TestService();
|
||||
return new ValueTask<string>("Service retrieved");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestNestedRequestHandler : IRequestHandler<TestNestedRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestNestedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestNestedRequestHandler2.ExecutionCount++;
|
||||
|
||||
if (request.Depth >= 1) // 简化条件
|
||||
public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 保持测试中设置的上下文,不要重置为null
|
||||
return new ValueTask<string>("Context accessed");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestServiceRetrievalRequestHandler : IRequestHandler<TestServiceRetrievalRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestServiceRetrievalHandler.LastRetrievedService = new TestService();
|
||||
return new ValueTask<string>("Service retrieved");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestNestedRequestHandler : IRequestHandler<TestNestedRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestNestedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestNestedRequestHandler2.ExecutionCount++;
|
||||
// 模拟嵌套调用
|
||||
return new ValueTask<string>($"Nested execution completed at depth {request.Depth}");
|
||||
}
|
||||
|
||||
return new ValueTask<string>($"Nested execution completed at depth {request.Depth}");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestLifecycleRequestHandler : IRequestHandler<TestLifecycleRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestLifecycleRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestLifecycleRequestHandler : IRequestHandler<TestLifecycleRequest, string>
|
||||
{
|
||||
TestLifecycleHandler.InitializationCount++;
|
||||
// 模拟一些工作
|
||||
TestLifecycleHandler.DisposalCount++;
|
||||
return new ValueTask<string>("Lifecycle managed");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestScopedServiceRequestHandler : IRequestHandler<TestScopedServiceRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestScopedServiceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟返回请求ID
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestErrorPropagationRequestHandler : IRequestHandler<TestErrorPropagationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var ex = new InvalidOperationException("Test error from handler");
|
||||
ex.Data["RequestId"] = Guid.NewGuid();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestExceptionRequestHandler : IRequestHandler<TestExceptionRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestExceptionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestExceptionHandler.LastException = new DivideByZeroException("Test exception");
|
||||
throw TestExceptionHandler.LastException;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestPerformanceRequest2Handler : IRequestHandler<TestPerformanceRequest2, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(request.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestUncachedRequestHandler : IRequestHandler<TestUncachedRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestUncachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟一些处理时间
|
||||
Task.Delay(5, cancellationToken).Wait(cancellationToken);
|
||||
return new ValueTask<int>(request.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestCachedRequestHandler : IRequestHandler<TestCachedRequest, int>
|
||||
{
|
||||
private static readonly Dictionary<int, int> _cache = new();
|
||||
|
||||
public ValueTask<int> Handle(TestCachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cache.TryGetValue(request.Id, out var cachedValue))
|
||||
public ValueTask<string> Handle(TestLifecycleRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(cachedValue);
|
||||
TestLifecycleHandler.InitializationCount++;
|
||||
// 模拟一些工作
|
||||
TestLifecycleHandler.DisposalCount++;
|
||||
return new ValueTask<string>("Lifecycle managed");
|
||||
}
|
||||
|
||||
// 模拟处理时间
|
||||
Task.Delay(10, cancellationToken).Wait(cancellationToken);
|
||||
var newValue = request.Id;
|
||||
_cache[request.Id] = newValue;
|
||||
return new ValueTask<int>(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestConcurrentRequestHandler : IRequestHandler<TestConcurrentRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestConcurrentRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestScopedServiceRequestHandler : IRequestHandler<TestScopedServiceRequest, int>
|
||||
{
|
||||
lock (request.OrderTracker)
|
||||
public ValueTask<int> Handle(TestScopedServiceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.OrderTracker.Add(request.RequestId);
|
||||
// 模拟返回请求ID
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestStateModificationRequestHandler : IRequestHandler<TestStateModificationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestStateModificationRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestErrorPropagationRequestHandler : IRequestHandler<TestErrorPropagationRequest, string>
|
||||
{
|
||||
request.SharedState.Counter += request.Increment;
|
||||
return new ValueTask<string>("State modified");
|
||||
public ValueTask<string> Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var ex = new InvalidOperationException("Test error from handler");
|
||||
ex.Data["RequestId"] = Guid.NewGuid();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestIntegrationRequestHandler : IRequestHandler<TestIntegrationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestIntegrationRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestExceptionRequestHandler : IRequestHandler<TestExceptionRequest, string>
|
||||
{
|
||||
TestIntegrationHandler.LastSystemCall = "System executed";
|
||||
return new ValueTask<string>("Integration successful");
|
||||
public ValueTask<string> Handle(TestExceptionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestExceptionHandler.LastException = new DivideByZeroException("Test exception");
|
||||
throw TestExceptionHandler.LastException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestMediatorRequestHandler : IRequestHandler<TestMediatorRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestMediatorRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestPerformanceRequest2Handler : IRequestHandler<TestPerformanceRequest2, int>
|
||||
{
|
||||
return new ValueTask<int>(request.Value);
|
||||
public ValueTask<int> Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(request.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。
|
||||
/// </summary>
|
||||
public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase,
|
||||
IRequestHandler<TestPerDispatchContextAwareRequest, int>
|
||||
{
|
||||
private static int _nextInstanceId;
|
||||
private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId);
|
||||
|
||||
public static List<IArchitectureContext?> Contexts { get; } = [];
|
||||
public static List<int> SeenInstanceIds { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前实例编号与收到的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="request">请求实例。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>当前处理器实例编号。</returns>
|
||||
public ValueTask<int> Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestUncachedRequestHandler : IRequestHandler<TestUncachedRequest, int>
|
||||
{
|
||||
Contexts.Add(Context);
|
||||
SeenInstanceIds.Add(_instanceId);
|
||||
return ValueTask.FromResult(_instanceId);
|
||||
public async ValueTask<int> Handle(TestUncachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟一些处理时间
|
||||
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
|
||||
return request.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestCachedRequestHandler : IRequestHandler<TestCachedRequest, int>
|
||||
{
|
||||
private static readonly ConcurrentDictionary<int, int> _cache = new();
|
||||
|
||||
public async ValueTask<int> Handle(TestCachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cache.TryGetValue(request.Id, out var cachedValue))
|
||||
{
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
// 模拟处理时间
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
|
||||
return _cache.GetOrAdd(request.Id, static id => id);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestConcurrentRequestHandler : IRequestHandler<TestConcurrentRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestConcurrentRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (request.OrderTracker)
|
||||
{
|
||||
request.OrderTracker.Add(request.RequestId);
|
||||
}
|
||||
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestStateModificationRequestHandler : IRequestHandler<TestStateModificationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestStateModificationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.SharedState.IncrementBy(request.Increment);
|
||||
return new ValueTask<string>("State modified");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestIntegrationRequestHandler : IRequestHandler<TestIntegrationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestIntegrationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestIntegrationHandler.LastSystemCall = "System executed";
|
||||
return new ValueTask<string>("Integration successful");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestMediatorRequestHandler : IRequestHandler<TestMediatorRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestMediatorRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(request.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置跨测试共享的实例跟踪状态。
|
||||
/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase,
|
||||
IRequestHandler<TestPerDispatchContextAwareRequest, int>
|
||||
{
|
||||
Contexts.Clear();
|
||||
SeenInstanceIds.Clear();
|
||||
_nextInstanceId = 0;
|
||||
}
|
||||
}
|
||||
private static int _nextInstanceId;
|
||||
private static readonly List<IArchitectureContext?> TrackedContexts = [];
|
||||
private static readonly List<int> TrackedInstanceIds = [];
|
||||
private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId);
|
||||
|
||||
public sealed record TestContextAwareRequest : IRequest<string>;
|
||||
public static IReadOnlyList<IArchitectureContext?> Contexts => TrackedContexts;
|
||||
public static IReadOnlyList<int> SeenInstanceIds => TrackedInstanceIds;
|
||||
|
||||
public static class TestContextAwareHandler
|
||||
{
|
||||
public static IArchitectureContext? LastContext { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 记录当前实例编号与收到的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="request">请求实例。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>当前处理器实例编号。</returns>
|
||||
public ValueTask<int> Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TrackedContexts.Add(Context);
|
||||
TrackedInstanceIds.Add(_instanceId);
|
||||
return ValueTask.FromResult(_instanceId);
|
||||
}
|
||||
|
||||
public sealed record TestServiceRetrievalRequest : IRequest<string>;
|
||||
|
||||
public static class TestServiceRetrievalHandler
|
||||
{
|
||||
public static object? LastRetrievedService { get; set; }
|
||||
}
|
||||
|
||||
public class TestService
|
||||
{
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
public sealed record TestNestedRequest : IRequest<string>
|
||||
{
|
||||
public int Depth { get; init; }
|
||||
}
|
||||
|
||||
public static class TestNestedRequestHandler2
|
||||
{
|
||||
public static int ExecutionCount { get; set; }
|
||||
}
|
||||
|
||||
// 生命周期相关类
|
||||
public sealed record TestLifecycleRequest : IRequest<string>;
|
||||
|
||||
public static class TestLifecycleHandler
|
||||
{
|
||||
public static int InitializationCount { get; set; }
|
||||
public static int DisposalCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestScopedServiceRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
}
|
||||
|
||||
// 错误处理相关类
|
||||
public sealed record TestErrorPropagationRequest : IRequest<string>;
|
||||
|
||||
public static class TestExceptionHandler
|
||||
{
|
||||
public static Exception? LastException { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestExceptionRequest : IRequest<string>;
|
||||
|
||||
// 性能测试相关类
|
||||
public sealed record TestPerformanceRequest2 : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestUncachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestCachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
// 并发测试相关类
|
||||
public class SharedState
|
||||
{
|
||||
public int Counter { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestConcurrentRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
public List<int> OrderTracker { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed record TestStateModificationRequest : IRequest<string>
|
||||
{
|
||||
public SharedState SharedState { get; init; } = null!;
|
||||
public int Increment { get; init; }
|
||||
}
|
||||
|
||||
// 集成测试相关类
|
||||
public static class TestIntegrationHandler
|
||||
{
|
||||
public static string? LastSystemCall { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestIntegrationRequest : IRequest<string>;
|
||||
|
||||
public sealed record TestMediatorRequest : IRequest<int>
|
||||
{
|
||||
public int Value { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。
|
||||
/// </summary>
|
||||
public sealed record TestPerDispatchContextAwareRequest : IRequest<int>;
|
||||
|
||||
// 传统命令用于混合测试
|
||||
public class TestTraditionalCommand : ICommand
|
||||
{
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
public void Execute() => Executed = true;
|
||||
|
||||
public void SetContext(IArchitectureContext context)
|
||||
{
|
||||
/// <summary>
|
||||
/// 重置跨测试共享的实例跟踪状态。
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
TrackedContexts.Clear();
|
||||
TrackedInstanceIds.Clear();
|
||||
_nextInstanceId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public IArchitectureContext GetContext() => null!;
|
||||
}
|
||||
public sealed record TestContextAwareRequest : IRequest<string>;
|
||||
|
||||
#endregion
|
||||
public static class TestContextAwareHandler
|
||||
{
|
||||
public static IArchitectureContext? LastContext { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestServiceRetrievalRequest : IRequest<string>;
|
||||
|
||||
public static class TestServiceRetrievalHandler
|
||||
{
|
||||
public static object? LastRetrievedService { get; set; }
|
||||
}
|
||||
|
||||
public class TestService
|
||||
{
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
public sealed record TestNestedRequest : IRequest<string>
|
||||
{
|
||||
public int Depth { get; init; }
|
||||
}
|
||||
|
||||
public static class TestNestedRequestHandler2
|
||||
{
|
||||
public static int ExecutionCount { get; set; }
|
||||
}
|
||||
|
||||
// 生命周期相关类
|
||||
public sealed record TestLifecycleRequest : IRequest<string>;
|
||||
|
||||
public static class TestLifecycleHandler
|
||||
{
|
||||
public static int InitializationCount { get; set; }
|
||||
public static int DisposalCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestScopedServiceRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
}
|
||||
|
||||
// 错误处理相关类
|
||||
public sealed record TestErrorPropagationRequest : IRequest<string>;
|
||||
|
||||
public static class TestExceptionHandler
|
||||
{
|
||||
public static Exception? LastException { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestExceptionRequest : IRequest<string>;
|
||||
|
||||
// 性能测试相关类
|
||||
public sealed record TestPerformanceRequest2 : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestUncachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestCachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
// 并发测试相关类
|
||||
public class SharedState
|
||||
{
|
||||
private int _counter;
|
||||
|
||||
public int Counter => _counter;
|
||||
|
||||
public void IncrementBy(int increment)
|
||||
{
|
||||
Interlocked.Add(ref _counter, increment);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TestConcurrentRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
public ICollection<int> OrderTracker { get; init; } = new List<int>();
|
||||
}
|
||||
|
||||
public sealed record TestStateModificationRequest : IRequest<string>
|
||||
{
|
||||
public SharedState SharedState { get; init; } = null!;
|
||||
public int Increment { get; init; }
|
||||
}
|
||||
|
||||
// 集成测试相关类
|
||||
public static class TestIntegrationHandler
|
||||
{
|
||||
public static string? LastSystemCall { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestIntegrationRequest : IRequest<string>;
|
||||
|
||||
public sealed record TestMediatorRequest : IRequest<int>
|
||||
{
|
||||
public int Value { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。
|
||||
/// </summary>
|
||||
public sealed record TestPerDispatchContextAwareRequest : IRequest<int>;
|
||||
|
||||
// 传统命令用于混合测试
|
||||
public class TestTraditionalCommand : ICommand
|
||||
{
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
public void Execute() => Executed = true;
|
||||
|
||||
public void SetContext(IArchitectureContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public IArchitectureContext GetContext() => null!;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ public class MediatorComprehensiveTests
|
||||
public async Task SendRequestAsync_Should_ReturnResult_When_Request_IsValid()
|
||||
{
|
||||
var testRequest = new TestRequest { Value = 42 };
|
||||
var result = await _context!.SendRequestAsync(testRequest);
|
||||
var result = await _context!.SendRequestAsync(testRequest).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
}
|
||||
@ -98,7 +98,7 @@ public class MediatorComprehensiveTests
|
||||
public void SendRequestAsync_Should_ThrowArgumentNullException_When_Request_IsNull()
|
||||
{
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () =>
|
||||
await _context!.SendRequestAsync<int>(null!));
|
||||
await _context!.SendRequestAsync<int>(null!).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -122,8 +122,8 @@ public class MediatorComprehensiveTests
|
||||
TestNotificationHandler.LastReceivedMessage = null;
|
||||
var notification = new TestNotification { Message = "test" };
|
||||
|
||||
await _context!.PublishAsync(notification);
|
||||
await Task.Delay(100);
|
||||
await _context!.PublishAsync(notification).ConfigureAwait(false);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
|
||||
Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("test"));
|
||||
}
|
||||
@ -138,7 +138,7 @@ public class MediatorComprehensiveTests
|
||||
var stream = _context!.CreateStream(testStreamRequest);
|
||||
|
||||
var results = new List<int>();
|
||||
await foreach (var item in stream)
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
@ -153,7 +153,7 @@ public class MediatorComprehensiveTests
|
||||
public async Task SendAsync_CommandWithoutResult_Should_Execute_When_Command_IsValid()
|
||||
{
|
||||
var testCommand = new TestCommand { ShouldExecute = true };
|
||||
await _context!.SendAsync(testCommand);
|
||||
await _context!.SendAsync(testCommand).ConfigureAwait(false);
|
||||
|
||||
Assert.That(testCommand.Executed, Is.True);
|
||||
}
|
||||
@ -165,7 +165,7 @@ public class MediatorComprehensiveTests
|
||||
public async Task SendAsync_CommandWithResult_Should_ReturnResult_When_Command_IsValid()
|
||||
{
|
||||
var testCommand = new TestCommandWithResult { ResultValue = 42 };
|
||||
var result = await _context!.SendAsync(testCommand);
|
||||
var result = await _context!.SendAsync(testCommand).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
}
|
||||
@ -198,7 +198,7 @@ public class MediatorComprehensiveTests
|
||||
var testRequest = new TestRequest { Value = 42 };
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await contextWithoutHandlers.SendRequestAsync(testRequest));
|
||||
await contextWithoutHandlers.SendRequestAsync(testRequest).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -213,8 +213,8 @@ public class MediatorComprehensiveTests
|
||||
TestNotificationHandler3.LastReceivedMessage = null;
|
||||
|
||||
var notification = new TestNotification { Message = "multi-handler test" };
|
||||
await _context!.PublishAsync(notification);
|
||||
await Task.Delay(100);
|
||||
await _context!.PublishAsync(notification).ConfigureAwait(false);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
|
||||
// 验证所有处理器都被调用
|
||||
Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("multi-handler test"));
|
||||
@ -233,7 +233,7 @@ public class MediatorComprehensiveTests
|
||||
|
||||
// 应该在50ms后被取消
|
||||
Assert.ThrowsAsync<TaskCanceledException>(async () =>
|
||||
await _context!.SendRequestAsync(longRequest, cts.Token));
|
||||
await _context!.SendRequestAsync(longRequest, cts.Token).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -251,7 +251,7 @@ public class MediatorComprehensiveTests
|
||||
// 流应该在100ms后被取消(TaskCanceledException 继承自 OperationCanceledException)
|
||||
Assert.CatchAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
await foreach (var item in stream)
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
@ -277,7 +277,7 @@ public class MediatorComprehensiveTests
|
||||
tasks.Add(_context!.SendRequestAsync(request).AsTask());
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证所有结果都正确返回
|
||||
Assert.That(results.Length, Is.EqualTo(requestCount));
|
||||
@ -293,7 +293,7 @@ public class MediatorComprehensiveTests
|
||||
var faultyRequest = new TestFaultyRequest();
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context!.SendRequestAsync(faultyRequest));
|
||||
await _context!.SendRequestAsync(faultyRequest).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -306,8 +306,8 @@ public class MediatorComprehensiveTests
|
||||
var command1 = new TestModifyDataCommand { Data = sharedData, Value = 10 };
|
||||
var command2 = new TestModifyDataCommand { Data = sharedData, Value = 20 };
|
||||
|
||||
await _context!.SendAsync(command1);
|
||||
await _context.SendAsync(command2);
|
||||
await _context!.SendAsync(command1).ConfigureAwait(false);
|
||||
await _context.SendAsync(command2).ConfigureAwait(false);
|
||||
|
||||
// 验证数据被正确修改
|
||||
Assert.That(sharedData.Value, Is.EqualTo(30)); // 10 + 20
|
||||
@ -331,10 +331,10 @@ public class MediatorComprehensiveTests
|
||||
|
||||
foreach (var notification in notifications)
|
||||
{
|
||||
await _context!.PublishAsync(notification);
|
||||
await _context!.PublishAsync(notification).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(200); // 等待所有处理完成
|
||||
await Task.Delay(200).ConfigureAwait(false); // 等待所有处理完成
|
||||
|
||||
// 验证接收顺序与发送顺序一致
|
||||
Assert.That(receivedOrder.Count, Is.EqualTo(3));
|
||||
@ -358,7 +358,7 @@ public class MediatorComprehensiveTests
|
||||
var stream = _context!.CreateStream(filterRequest);
|
||||
var results = new List<int>();
|
||||
|
||||
await foreach (var item in stream)
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
@ -377,7 +377,7 @@ public class MediatorComprehensiveTests
|
||||
var invalidCommand = new TestValidatedCommand { Name = "" }; // 无效:空字符串
|
||||
|
||||
Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||
await _context!.SendAsync(invalidCommand));
|
||||
await _context!.SendAsync(invalidCommand).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -392,7 +392,7 @@ public class MediatorComprehensiveTests
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var request = new TestRequest { Value = i };
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(i));
|
||||
}
|
||||
|
||||
@ -417,15 +417,13 @@ public class MediatorComprehensiveTests
|
||||
|
||||
// 使用自有 CQRS 方式
|
||||
var mediatorCommand = new TestCommandWithResult { ResultValue = 999 };
|
||||
var result = await _context.SendAsync(mediatorCommand);
|
||||
var result = await _context.SendAsync(mediatorCommand).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(999));
|
||||
|
||||
// 验证两者可以同时工作
|
||||
Assert.That(legacyCommand.Executed, Is.True);
|
||||
Assert.That(result, Is.EqualTo(999));
|
||||
}
|
||||
}
|
||||
|
||||
#region Advanced Test Classes for CQRS Features
|
||||
|
||||
public sealed record TestLongRunningRequest : IRequest<string>
|
||||
@ -437,7 +435,7 @@ public sealed class TestLongRunningRequestHandler : IRequestHandler<TestLongRunn
|
||||
{
|
||||
public async ValueTask<string> Handle(TestLongRunningRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(request.DelayMs, cancellationToken);
|
||||
await Task.Delay(request.DelayMs, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return "Completed";
|
||||
}
|
||||
@ -458,7 +456,7 @@ public sealed class TestLongStreamRequestHandler : IStreamRequestHandler<TestLon
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return i;
|
||||
await Task.Delay(10, cancellationToken); // 模拟处理延迟
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false); // 模拟处理延迟
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -496,7 +494,7 @@ public sealed class TestModifyDataCommandHandler : IRequestHandler<TestModifyDat
|
||||
public sealed record TestCachingQuery : IRequest<string>
|
||||
{
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public Dictionary<string, string> Cache { get; init; } = new();
|
||||
public IDictionary<string, string> Cache { get; init; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class TestCachingQueryHandler : IRequestHandler<TestCachingQuery, string>
|
||||
@ -522,7 +520,7 @@ public sealed record TestOrderedNotification : INotification
|
||||
|
||||
public sealed class TestOrderedNotificationHandler : INotificationHandler<TestOrderedNotification>
|
||||
{
|
||||
public static List<string> ReceivedMessages { get; set; } = new();
|
||||
public static ICollection<string> ReceivedMessages { get; set; } = new List<string>();
|
||||
|
||||
public ValueTask Handle(TestOrderedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
@ -590,7 +588,7 @@ public sealed class TestValidatedCommandHandler : IRequestHandler<TestValidatedC
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ArgumentException($"Name cannot be empty {nameof(request.Name)}");
|
||||
throw new ArgumentException("Name cannot be empty.", nameof(request));
|
||||
}
|
||||
|
||||
return ValueTask.FromResult(Unit.Value);
|
||||
@ -719,3 +717,5 @@ public sealed class TestStreamRequestHandler : IStreamRequestHandler<TestStreamR
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@ -7,8 +7,10 @@ namespace GFramework.Cqrs;
|
||||
/// 该特性通常由源码生成器自动添加到消费端程序集。
|
||||
/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描,
|
||||
/// 以覆盖那些生成代码无法直接引用的 handler 类型。
|
||||
/// 允许同一程序集声明多个该特性实例,以便生成器把“可直接引用的 fallback handlers”
|
||||
/// 和“仍需按名称恢复的 fallback handlers”拆成独立元数据块,进一步减少运行时字符串查找成本。
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
147
GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs
Normal file
147
GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using GFramework.Game.Config;
|
||||
|
||||
namespace GFramework.Game.Tests.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 验证内部 schema 运行时模型会在构造阶段拒绝无效状态,
|
||||
/// 避免调用方把不一致的约束对象继续传入加载器和校验器。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class YamlConfigModelContractTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证枚举允许值模型会拒绝空白比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AllowedValue_Should_Reject_Whitespace_Comparable_Value()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigAllowedValue(" ", "visible"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证枚举允许值模型会保留空对象等合法结构产生的空比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AllowedValue_Should_Accept_Empty_Comparable_Value()
|
||||
{
|
||||
var allowedValue = new YamlConfigAllowedValue(string.Empty, "{}");
|
||||
|
||||
Assert.That(allowedValue.ComparableValue, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证常量约束模型会拒绝空白比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConstantValue_Should_Reject_Whitespace_Comparable_Value()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigConstantValue(" ", "\"visible\""));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证常量约束模型会保留空对象等合法结构产生的空比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConstantValue_Should_Accept_Empty_Comparable_Value()
|
||||
{
|
||||
var constantValue = new YamlConfigConstantValue(string.Empty, "{}");
|
||||
|
||||
Assert.That(constantValue.ComparableValue, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 contains 约束模型会在构造阶段拦截负值和反向区间。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ArrayContainsConstraints_Should_Reject_Invalid_Bounds()
|
||||
{
|
||||
var itemNode = CreateStringNode();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayContainsConstraints(itemNode, -1, null));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayContainsConstraints(itemNode, null, -1));
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigArrayContainsConstraints(itemNode, 3, 2));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组约束模型会在构造阶段拦截负值和反向区间。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ArrayConstraints_Should_Reject_Invalid_Bounds()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayConstraints(-1, null, false, null));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayConstraints(null, -1, false, null));
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigArrayConstraints(4, 3, false, null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证对象约束模型会在构造阶段拦截负值和反向区间。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ObjectConstraints_Should_Reject_Invalid_Bounds()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
new YamlConfigObjectConstraints(-1, null, null, null, null, null));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
new YamlConfigObjectConstraints(null, -1, null, null, null, null));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new YamlConfigObjectConstraints(5, 4, null, null, null, null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串约束模型要求正则原文与预编译正则成对出现。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void StringConstraints_Should_Require_Pattern_And_Regex_To_Be_Paired()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new YamlConfigStringConstraints(null, null, "value", null, null));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new YamlConfigStringConstraints(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new Regex("value", RegexOptions.None, TimeSpan.FromSeconds(1)),
|
||||
null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 schema 模型会复制引用表集合,避免外部可变集合继续污染内部状态。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Schema_Should_Copy_Referenced_Table_Names()
|
||||
{
|
||||
var referencedTableNames = new List<string> { "item" };
|
||||
var schema = new YamlConfigSchema("monster.schema.json", CreateStringNode(), referencedTableNames);
|
||||
|
||||
referencedTableNames.Add("weapon");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(schema.ReferencedTableNames, Is.EqualTo(new[] { "item" }));
|
||||
Assert.That(schema.ReferencedTableNames, Is.Not.SameAs(referencedTableNames));
|
||||
});
|
||||
}
|
||||
|
||||
private static YamlConfigSchemaNode CreateStringNode()
|
||||
{
|
||||
return YamlConfigSchemaNode.CreateScalar(
|
||||
YamlConfigSchemaPropertyType.String,
|
||||
referenceTableName: null,
|
||||
allowedValues: null,
|
||||
constraints: null,
|
||||
schemaPathHint: "tests.schema.json");
|
||||
}
|
||||
}
|
||||
39
GFramework.Game/Config/YamlConfigAllowedValue.cs
Normal file
39
GFramework.Game/Config/YamlConfigAllowedValue.cs
Normal file
@ -0,0 +1,39 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个节点上声明的单个 <c>enum</c> 候选值。
|
||||
/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigAllowedValue
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个枚举候选值模型。
|
||||
/// </summary>
|
||||
/// <param name="comparableValue">用于与 YAML 节点比较的稳定键。</param>
|
||||
/// <param name="displayValue">用于诊断输出的原始 JSON 文本。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="comparableValue"/> 或 <paramref name="displayValue"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="comparableValue"/> 虽然非空但仅包含空白字符,或 <paramref name="displayValue"/> 为空或仅包含空白字符时抛出。</exception>
|
||||
public YamlConfigAllowedValue(string comparableValue, string displayValue)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(comparableValue);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(displayValue);
|
||||
if (comparableValue.Length > 0 &&
|
||||
string.IsNullOrWhiteSpace(comparableValue))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue));
|
||||
}
|
||||
|
||||
ComparableValue = comparableValue;
|
||||
DisplayValue = displayValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于运行时比较的稳定键。
|
||||
/// </summary>
|
||||
public string ComparableValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于诊断输出的原始 JSON 文本。
|
||||
/// </summary>
|
||||
public string DisplayValue { get; }
|
||||
}
|
||||
66
GFramework.Game/Config/YamlConfigArrayConstraints.cs
Normal file
66
GFramework.Game/Config/YamlConfigArrayConstraints.cs
Normal file
@ -0,0 +1,66 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个数组节点上声明的元素数量、去重与 contains 匹配计数约束。
|
||||
/// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigArrayConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化数组约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minItems">最小元素数量约束。</param>
|
||||
/// <param name="maxItems">最大元素数量约束。</param>
|
||||
/// <param name="uniqueItems">是否要求数组元素唯一。</param>
|
||||
/// <param name="containsConstraints">数组 contains 约束;未声明时为空。</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minItems"/> 或 <paramref name="maxItems"/> 为负数时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="minItems"/> 大于 <paramref name="maxItems"/> 时抛出。</exception>
|
||||
public YamlConfigArrayConstraints(
|
||||
int? minItems,
|
||||
int? maxItems,
|
||||
bool uniqueItems,
|
||||
YamlConfigArrayContainsConstraints? containsConstraints)
|
||||
{
|
||||
if (minItems is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minItems), minItems, "minItems 不能为负数。");
|
||||
}
|
||||
|
||||
if (maxItems is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxItems), maxItems, "maxItems 不能为负数。");
|
||||
}
|
||||
|
||||
if (minItems.HasValue &&
|
||||
maxItems.HasValue &&
|
||||
minItems.Value > maxItems.Value)
|
||||
{
|
||||
throw new ArgumentException("minItems 不能大于 maxItems。", nameof(minItems));
|
||||
}
|
||||
|
||||
MinItems = minItems;
|
||||
MaxItems = maxItems;
|
||||
UniqueItems = uniqueItems;
|
||||
ContainsConstraints = containsConstraints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小元素数量约束。
|
||||
/// </summary>
|
||||
public int? MinItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大元素数量约束。
|
||||
/// </summary>
|
||||
public int? MaxItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否要求数组元素唯一。
|
||||
/// </summary>
|
||||
public bool UniqueItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组 contains 约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigArrayContainsConstraints? ContainsConstraints { get; }
|
||||
}
|
||||
60
GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs
Normal file
60
GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs
Normal file
@ -0,0 +1,60 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示数组节点声明的 <c>contains</c> 匹配约束。
|
||||
/// 该模型把 contains 子 schema 与匹配数量边界聚合在一起,避免数组节点再额外散落多组相关成员。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigArrayContainsConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化数组 contains 约束模型。
|
||||
/// </summary>
|
||||
/// <param name="containsNode">contains 子 schema。</param>
|
||||
/// <param name="minContains">最小匹配数量;为 <see langword="null" /> 时按 JSON Schema 语义默认 1。</param>
|
||||
/// <param name="maxContains">最大匹配数量。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="containsNode"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minContains"/> 或 <paramref name="maxContains"/> 为负数时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="minContains"/> 大于 <paramref name="maxContains"/> 时抛出。</exception>
|
||||
public YamlConfigArrayContainsConstraints(
|
||||
YamlConfigSchemaNode containsNode,
|
||||
int? minContains,
|
||||
int? maxContains)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(containsNode);
|
||||
if (minContains is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minContains), minContains, "minContains 不能为负数。");
|
||||
}
|
||||
|
||||
if (maxContains is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxContains), maxContains, "maxContains 不能为负数。");
|
||||
}
|
||||
|
||||
if (minContains.HasValue &&
|
||||
maxContains.HasValue &&
|
||||
minContains.Value > maxContains.Value)
|
||||
{
|
||||
throw new ArgumentException("minContains 不能大于 maxContains。", nameof(minContains));
|
||||
}
|
||||
|
||||
ContainsNode = containsNode;
|
||||
MinContains = minContains;
|
||||
MaxContains = maxContains;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 contains 子 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode ContainsNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小匹配数量;未显式声明时返回空,由调用方按默认值 1 解释。
|
||||
/// </summary>
|
||||
public int? MinContains { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大匹配数量。
|
||||
/// </summary>
|
||||
public int? MaxContains { get; }
|
||||
}
|
||||
42
GFramework.Game/Config/YamlConfigConditionalSchemas.cs
Normal file
42
GFramework.Game/Config/YamlConfigConditionalSchemas.cs
Normal file
@ -0,0 +1,42 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个对象节点上声明的 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
|
||||
/// 三个分支都共享父对象已声明字段集合,不会把分支 schema 扩展成新的生成类型形状。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigConditionalSchemas
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化条件分支约束模型。
|
||||
/// </summary>
|
||||
/// <param name="ifSchema">条件判断 schema。</param>
|
||||
/// <param name="thenSchema">条件命中时需要满足的 schema。</param>
|
||||
/// <param name="elseSchema">条件未命中时需要满足的 schema。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="ifSchema"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
public YamlConfigConditionalSchemas(
|
||||
YamlConfigSchemaNode ifSchema,
|
||||
YamlConfigSchemaNode? thenSchema,
|
||||
YamlConfigSchemaNode? elseSchema)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ifSchema);
|
||||
|
||||
IfSchema = ifSchema;
|
||||
ThenSchema = thenSchema;
|
||||
ElseSchema = elseSchema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取条件判断 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode IfSchema { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取条件命中时需要满足的 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? ThenSchema { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取条件未命中时需要满足的 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? ElseSchema { get; }
|
||||
}
|
||||
39
GFramework.Game/Config/YamlConfigConstantValue.cs
Normal file
39
GFramework.Game/Config/YamlConfigConstantValue.cs
Normal file
@ -0,0 +1,39 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个节点上声明的 <c>const</c> 约束。
|
||||
/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigConstantValue
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化常量约束模型。
|
||||
/// </summary>
|
||||
/// <param name="comparableValue">用于与 YAML 节点比较的稳定键。</param>
|
||||
/// <param name="displayValue">用于诊断输出的原始常量文本。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="comparableValue"/> 或 <paramref name="displayValue"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="comparableValue"/> 虽然非空但仅包含空白字符,或 <paramref name="displayValue"/> 为空或仅包含空白字符时抛出。</exception>
|
||||
public YamlConfigConstantValue(string comparableValue, string displayValue)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(comparableValue);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(displayValue);
|
||||
if (comparableValue.Length > 0 &&
|
||||
string.IsNullOrWhiteSpace(comparableValue))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue));
|
||||
}
|
||||
|
||||
ComparableValue = comparableValue;
|
||||
DisplayValue = displayValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于运行时比较的稳定键。
|
||||
/// </summary>
|
||||
public string ComparableValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于诊断输出的原始 JSON 常量文本。
|
||||
/// </summary>
|
||||
public string DisplayValue { get; }
|
||||
}
|
||||
55
GFramework.Game/Config/YamlConfigNumericConstraints.cs
Normal file
55
GFramework.Game/Config/YamlConfigNumericConstraints.cs
Normal file
@ -0,0 +1,55 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示标量节点上声明的数值范围与步进约束。
|
||||
/// 该类型只覆盖整数 / 浮点共享的关键字,避免字符串字段继续暴露不相关的成员。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigNumericConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化数值约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minimum">最小值约束。</param>
|
||||
/// <param name="maximum">最大值约束。</param>
|
||||
/// <param name="exclusiveMinimum">开区间最小值约束。</param>
|
||||
/// <param name="exclusiveMaximum">开区间最大值约束。</param>
|
||||
/// <param name="multipleOf">数值步进约束。</param>
|
||||
public YamlConfigNumericConstraints(
|
||||
double? minimum,
|
||||
double? maximum,
|
||||
double? exclusiveMinimum,
|
||||
double? exclusiveMaximum,
|
||||
double? multipleOf)
|
||||
{
|
||||
Minimum = minimum;
|
||||
Maximum = maximum;
|
||||
ExclusiveMinimum = exclusiveMinimum;
|
||||
ExclusiveMaximum = exclusiveMaximum;
|
||||
MultipleOf = multipleOf;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小值约束。
|
||||
/// </summary>
|
||||
public double? Minimum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大值约束。
|
||||
/// </summary>
|
||||
public double? Maximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取开区间最小值约束。
|
||||
/// </summary>
|
||||
public double? ExclusiveMinimum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取开区间最大值约束。
|
||||
/// </summary>
|
||||
public double? ExclusiveMaximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数值步进约束。
|
||||
/// </summary>
|
||||
public double? MultipleOf { get; }
|
||||
}
|
||||
86
GFramework.Game/Config/YamlConfigObjectConstraints.cs
Normal file
86
GFramework.Game/Config/YamlConfigObjectConstraints.cs
Normal file
@ -0,0 +1,86 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个对象节点上声明的属性数量约束、字段依赖约束、条件子 schema 与组合约束。
|
||||
/// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigObjectConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化对象约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minProperties">最小属性数量约束。</param>
|
||||
/// <param name="maxProperties">最大属性数量约束。</param>
|
||||
/// <param name="dependentRequired">对象内字段依赖约束。</param>
|
||||
/// <param name="dependentSchemas">对象内条件 schema 约束。</param>
|
||||
/// <param name="allOfSchemas">对象内组合 schema 约束。</param>
|
||||
/// <param name="conditionalSchemas">对象内条件分支约束。</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minProperties"/> 或 <paramref name="maxProperties"/> 为负数时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="minProperties"/> 大于 <paramref name="maxProperties"/> 时抛出。</exception>
|
||||
public YamlConfigObjectConstraints(
|
||||
int? minProperties,
|
||||
int? maxProperties,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>>? dependentRequired,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode>? dependentSchemas,
|
||||
IReadOnlyList<YamlConfigSchemaNode>? allOfSchemas,
|
||||
YamlConfigConditionalSchemas? conditionalSchemas)
|
||||
{
|
||||
if (minProperties is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minProperties), minProperties, "minProperties 不能为负数。");
|
||||
}
|
||||
|
||||
if (maxProperties is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxProperties), maxProperties, "maxProperties 不能为负数。");
|
||||
}
|
||||
|
||||
if (minProperties.HasValue &&
|
||||
maxProperties.HasValue &&
|
||||
minProperties.Value > maxProperties.Value)
|
||||
{
|
||||
throw new ArgumentException("minProperties 不能大于 maxProperties。", nameof(minProperties));
|
||||
}
|
||||
|
||||
MinProperties = minProperties;
|
||||
MaxProperties = maxProperties;
|
||||
DependentRequired = dependentRequired;
|
||||
DependentSchemas = dependentSchemas;
|
||||
AllOfSchemas = allOfSchemas;
|
||||
ConditionalSchemas = conditionalSchemas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小属性数量约束。
|
||||
/// </summary>
|
||||
public int? MinProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大属性数量约束。
|
||||
/// </summary>
|
||||
public int? MaxProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内字段依赖约束。
|
||||
/// 键表示“触发字段”,值表示“触发字段出现后还必须存在的同级字段集合”。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>>? DependentRequired { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内条件 schema 约束。
|
||||
/// 键表示“触发字段”,值表示“触发字段出现后当前对象还必须满足的额外 schema 子树”。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? DependentSchemas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内 <c>allOf</c> 组合约束。
|
||||
/// 每个条目都表示“当前对象还必须额外满足的 focused constraint block”。
|
||||
/// </summary>
|
||||
public IReadOnlyList<YamlConfigSchemaNode>? AllOfSchemas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
|
||||
/// 该模型会先用 <c>if</c> 试匹配当前对象,再只对命中的分支叠加 focused constraint block。
|
||||
/// </summary>
|
||||
public YamlConfigConditionalSchemas? ConditionalSchemas { get; }
|
||||
}
|
||||
74
GFramework.Game/Config/YamlConfigReferenceUsage.cs
Normal file
74
GFramework.Game/Config/YamlConfigReferenceUsage.cs
Normal file
@ -0,0 +1,74 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示单个 YAML 文件中提取出的跨表引用。
|
||||
/// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigReferenceUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个跨表引用使用记录。
|
||||
/// </summary>
|
||||
/// <param name="yamlPath">源 YAML 文件路径。</param>
|
||||
/// <param name="schemaPath">定义该引用的 schema 文件路径。</param>
|
||||
/// <param name="propertyPath">声明引用的字段路径。</param>
|
||||
/// <param name="rawValue">YAML 中的原始标量值。</param>
|
||||
/// <param name="referencedTableName">目标配置表名称。</param>
|
||||
/// <param name="valueType">引用值的 schema 标量类型。</param>
|
||||
public YamlConfigReferenceUsage(
|
||||
string yamlPath,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string rawValue,
|
||||
string referencedTableName,
|
||||
YamlConfigSchemaPropertyType valueType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(yamlPath);
|
||||
ArgumentNullException.ThrowIfNull(schemaPath);
|
||||
ArgumentNullException.ThrowIfNull(propertyPath);
|
||||
ArgumentNullException.ThrowIfNull(rawValue);
|
||||
ArgumentNullException.ThrowIfNull(referencedTableName);
|
||||
|
||||
YamlPath = yamlPath;
|
||||
SchemaPath = schemaPath;
|
||||
PropertyPath = propertyPath;
|
||||
RawValue = rawValue;
|
||||
ReferencedTableName = referencedTableName;
|
||||
ValueType = valueType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取源 YAML 文件路径。
|
||||
/// </summary>
|
||||
public string YamlPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取定义该引用的 schema 文件路径。
|
||||
/// </summary>
|
||||
public string SchemaPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取声明引用的字段路径。
|
||||
/// </summary>
|
||||
public string PropertyPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取 YAML 中的原始标量值。
|
||||
/// </summary>
|
||||
public string RawValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标配置表名称。
|
||||
/// </summary>
|
||||
public string ReferencedTableName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取引用值的 schema 标量类型。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaPropertyType ValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取便于诊断显示的字段路径。
|
||||
/// </summary>
|
||||
public string DisplayPath => PropertyPath;
|
||||
}
|
||||
31
GFramework.Game/Config/YamlConfigScalarConstraints.cs
Normal file
31
GFramework.Game/Config/YamlConfigScalarConstraints.cs
Normal file
@ -0,0 +1,31 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 聚合一个标量节点上声明的数值约束与字符串约束。
|
||||
/// 该包装层保留“标量字段有约束”的统一入口,同时把不同语义的约束分成更小的专用模型。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigScalarConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化标量约束模型。
|
||||
/// </summary>
|
||||
/// <param name="numericConstraints">数值约束分组。</param>
|
||||
/// <param name="stringConstraints">字符串约束分组。</param>
|
||||
public YamlConfigScalarConstraints(
|
||||
YamlConfigNumericConstraints? numericConstraints,
|
||||
YamlConfigStringConstraints? stringConstraints)
|
||||
{
|
||||
NumericConstraints = numericConstraints;
|
||||
StringConstraints = stringConstraints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数值约束分组。
|
||||
/// </summary>
|
||||
public YamlConfigNumericConstraints? NumericConstraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串约束分组。
|
||||
/// </summary>
|
||||
public YamlConfigStringConstraints? StringConstraints { get; }
|
||||
}
|
||||
45
GFramework.Game/Config/YamlConfigSchema.cs
Normal file
45
GFramework.Game/Config/YamlConfigSchema.cs
Normal file
@ -0,0 +1,45 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示已解析并可用于运行时校验的 JSON Schema。
|
||||
/// 该模型保留根节点与引用依赖集合,避免运行时引入完整 schema 引擎。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个可用于运行时校验的 schema 模型。
|
||||
/// </summary>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="rootNode">根节点模型。</param>
|
||||
/// <param name="referencedTableNames">Schema 声明的目标引用表名称集合。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="schemaPath"/>、<paramref name="rootNode"/> 或 <paramref name="referencedTableNames"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
public YamlConfigSchema(
|
||||
string schemaPath,
|
||||
YamlConfigSchemaNode rootNode,
|
||||
IReadOnlyCollection<string> referencedTableNames)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schemaPath);
|
||||
ArgumentNullException.ThrowIfNull(rootNode);
|
||||
ArgumentNullException.ThrowIfNull(referencedTableNames);
|
||||
|
||||
SchemaPath = schemaPath;
|
||||
RootNode = rootNode;
|
||||
ReferencedTableNames = [.. referencedTableNames];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 文件路径。
|
||||
/// </summary>
|
||||
public string SchemaPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取根节点模型。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode RootNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 声明的目标引用表名称集合。
|
||||
/// 该信息用于热重载时推导受影响的依赖表闭包。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> ReferencedTableNames { get; }
|
||||
}
|
||||
321
GFramework.Game/Config/YamlConfigSchemaNode.cs
Normal file
321
GFramework.Game/Config/YamlConfigSchemaNode.cs
Normal file
@ -0,0 +1,321 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示单个 schema 节点的最小运行时描述。
|
||||
/// 同一个模型同时覆盖对象、数组和标量,便于递归校验逻辑只依赖一种树结构。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigSchemaNode
|
||||
{
|
||||
private readonly NodeChildren _children;
|
||||
private readonly NodeValidation _validation;
|
||||
|
||||
private YamlConfigSchemaNode(
|
||||
YamlConfigSchemaPropertyType nodeType,
|
||||
NodeChildren children,
|
||||
NodeValidation validation,
|
||||
string schemaPathHint)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(children);
|
||||
ArgumentNullException.ThrowIfNull(validation);
|
||||
ArgumentNullException.ThrowIfNull(schemaPathHint);
|
||||
|
||||
_children = children;
|
||||
_validation = validation;
|
||||
NodeType = nodeType;
|
||||
Properties = children.Properties;
|
||||
RequiredProperties = children.RequiredProperties;
|
||||
ItemNode = children.ItemNode;
|
||||
ReferenceTableName = validation.ReferenceTableName;
|
||||
AllowedValues = validation.AllowedValues;
|
||||
Constraints = validation.Constraints;
|
||||
ArrayConstraints = validation.ArrayConstraints;
|
||||
ObjectConstraints = validation.ObjectConstraints;
|
||||
ConstantValue = validation.ConstantValue;
|
||||
NegatedSchemaNode = validation.NegatedSchemaNode;
|
||||
SchemaPathHint = schemaPathHint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点类型。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaPropertyType NodeType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象属性集合;非对象节点时返回空。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象必填属性集合;非对象节点时返回空。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string>? RequiredProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组元素节点;非数组节点时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? ItemNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标引用表名称;未声明跨表引用时返回空。
|
||||
/// </summary>
|
||||
public string? ReferenceTableName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点允许值集合;未声明 <c>enum</c> 时返回空。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取标量范围与长度约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigScalarConstraints? Constraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象属性数量约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigObjectConstraints? ObjectConstraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组元素数量约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigArrayConstraints? ArrayConstraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点常量约束;未声明 <c>const</c> 时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigConstantValue? ConstantValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点声明的 <c>not</c> 子 schema;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? NegatedSchemaNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于诊断显示的 schema 路径提示。
|
||||
/// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。
|
||||
/// </summary>
|
||||
public string SchemaPathHint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建对象节点描述。
|
||||
/// </summary>
|
||||
/// <param name="properties">对象属性集合。</param>
|
||||
/// <param name="requiredProperties">对象必填属性集合。</param>
|
||||
/// <param name="objectConstraints">对象属性数量约束。</param>
|
||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||
/// <returns>对象节点模型。</returns>
|
||||
public static YamlConfigSchemaNode CreateObject(
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode>? properties,
|
||||
IReadOnlyCollection<string>? requiredProperties,
|
||||
YamlConfigObjectConstraints? objectConstraints,
|
||||
string schemaPathHint)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
YamlConfigSchemaPropertyType.Object,
|
||||
new NodeChildren(properties, requiredProperties, itemNode: null),
|
||||
new NodeValidation(
|
||||
referenceTableName: null,
|
||||
allowedValues: null,
|
||||
constraints: null,
|
||||
arrayConstraints: null,
|
||||
objectConstraints,
|
||||
constantValue: null,
|
||||
negatedSchemaNode: null),
|
||||
schemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建数组节点描述。
|
||||
/// </summary>
|
||||
/// <param name="itemNode">数组元素节点。</param>
|
||||
/// <param name="allowedValues">数组节点允许值集合。</param>
|
||||
/// <param name="arrayConstraints">数组元素数量约束。</param>
|
||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||
/// <returns>数组节点模型。</returns>
|
||||
public static YamlConfigSchemaNode CreateArray(
|
||||
YamlConfigSchemaNode itemNode,
|
||||
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||
YamlConfigArrayConstraints? arrayConstraints,
|
||||
string schemaPathHint)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
YamlConfigSchemaPropertyType.Array,
|
||||
new NodeChildren(properties: null, requiredProperties: null, itemNode),
|
||||
new NodeValidation(
|
||||
referenceTableName: null,
|
||||
allowedValues,
|
||||
constraints: null,
|
||||
arrayConstraints,
|
||||
objectConstraints: null,
|
||||
constantValue: null,
|
||||
negatedSchemaNode: null),
|
||||
schemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建标量节点描述。
|
||||
/// </summary>
|
||||
/// <param name="nodeType">标量节点类型。</param>
|
||||
/// <param name="referenceTableName">目标引用表名称。</param>
|
||||
/// <param name="allowedValues">标量允许值集合。</param>
|
||||
/// <param name="constraints">标量范围与长度约束。</param>
|
||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||
/// <returns>标量节点模型。</returns>
|
||||
public static YamlConfigSchemaNode CreateScalar(
|
||||
YamlConfigSchemaPropertyType nodeType,
|
||||
string? referenceTableName,
|
||||
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||
YamlConfigScalarConstraints? constraints,
|
||||
string schemaPathHint)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
nodeType,
|
||||
NodeChildren.None,
|
||||
new NodeValidation(
|
||||
referenceTableName,
|
||||
allowedValues,
|
||||
constraints,
|
||||
arrayConstraints: null,
|
||||
objectConstraints: null,
|
||||
constantValue: null,
|
||||
negatedSchemaNode: null),
|
||||
schemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换引用表名称的新节点。
|
||||
/// 该方法用于把数组级别的 ref-table 语义挂接到元素节点上。
|
||||
/// </summary>
|
||||
/// <param name="referenceTableName">新的目标引用表名称。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithReferenceTable(string referenceTableName)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithReferenceTable(referenceTableName),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换 <c>enum</c> 允许值集合的新节点。
|
||||
/// </summary>
|
||||
/// <param name="allowedValues">新的允许值集合。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithAllowedValues(allowedValues),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换常量约束的新节点。
|
||||
/// </summary>
|
||||
/// <param name="constantValue">新的常量约束。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithConstantValue(YamlConfigConstantValue? constantValue)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithConstantValue(constantValue),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换 <c>not</c> 子 schema 的新节点。
|
||||
/// </summary>
|
||||
/// <param name="negatedSchemaNode">新的 negated schema。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithNegatedSchemaNode(negatedSchemaNode),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
private sealed class NodeChildren
|
||||
{
|
||||
public NodeChildren(
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode>? properties,
|
||||
IReadOnlyCollection<string>? requiredProperties,
|
||||
YamlConfigSchemaNode? itemNode)
|
||||
{
|
||||
Properties = properties;
|
||||
RequiredProperties = requiredProperties;
|
||||
ItemNode = itemNode;
|
||||
}
|
||||
|
||||
public static NodeChildren None { get; } = new(properties: null, requiredProperties: null, itemNode: null);
|
||||
|
||||
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? Properties { get; }
|
||||
|
||||
public IReadOnlyCollection<string>? RequiredProperties { get; }
|
||||
|
||||
public YamlConfigSchemaNode? ItemNode { get; }
|
||||
}
|
||||
|
||||
private sealed class NodeValidation
|
||||
{
|
||||
public NodeValidation(
|
||||
string? referenceTableName,
|
||||
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||
YamlConfigScalarConstraints? constraints,
|
||||
YamlConfigArrayConstraints? arrayConstraints,
|
||||
YamlConfigObjectConstraints? objectConstraints,
|
||||
YamlConfigConstantValue? constantValue,
|
||||
YamlConfigSchemaNode? negatedSchemaNode)
|
||||
{
|
||||
ReferenceTableName = referenceTableName;
|
||||
AllowedValues = allowedValues;
|
||||
Constraints = constraints;
|
||||
ArrayConstraints = arrayConstraints;
|
||||
ObjectConstraints = objectConstraints;
|
||||
ConstantValue = constantValue;
|
||||
NegatedSchemaNode = negatedSchemaNode;
|
||||
}
|
||||
|
||||
public string? ReferenceTableName { get; }
|
||||
|
||||
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
|
||||
|
||||
public YamlConfigScalarConstraints? Constraints { get; }
|
||||
|
||||
public YamlConfigArrayConstraints? ArrayConstraints { get; }
|
||||
|
||||
public YamlConfigObjectConstraints? ObjectConstraints { get; }
|
||||
|
||||
public YamlConfigConstantValue? ConstantValue { get; }
|
||||
|
||||
public YamlConfigSchemaNode? NegatedSchemaNode { get; }
|
||||
|
||||
public NodeValidation WithReferenceTable(string referenceTableName)
|
||||
{
|
||||
return new NodeValidation(referenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, ConstantValue, NegatedSchemaNode);
|
||||
}
|
||||
|
||||
public NodeValidation WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
|
||||
{
|
||||
return new NodeValidation(ReferenceTableName, allowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, ConstantValue, NegatedSchemaNode);
|
||||
}
|
||||
|
||||
public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue)
|
||||
{
|
||||
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, constantValue, NegatedSchemaNode);
|
||||
}
|
||||
|
||||
public NodeValidation WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode)
|
||||
{
|
||||
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, ConstantValue, negatedSchemaNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
GFramework.Game/Config/YamlConfigSchemaPropertyType.cs
Normal file
40
GFramework.Game/Config/YamlConfigSchemaPropertyType.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Enums;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示当前运行时 schema 校验器支持的属性类型。
|
||||
/// </summary>
|
||||
[GenerateEnumExtensions]
|
||||
internal enum YamlConfigSchemaPropertyType
|
||||
{
|
||||
/// <summary>
|
||||
/// 对象类型。
|
||||
/// </summary>
|
||||
Object,
|
||||
|
||||
/// <summary>
|
||||
/// 整数类型。
|
||||
/// </summary>
|
||||
Integer,
|
||||
|
||||
/// <summary>
|
||||
/// 数值类型。
|
||||
/// </summary>
|
||||
Number,
|
||||
|
||||
/// <summary>
|
||||
/// 布尔类型。
|
||||
/// </summary>
|
||||
Boolean,
|
||||
|
||||
/// <summary>
|
||||
/// 字符串类型。
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// 数组类型。
|
||||
/// </summary>
|
||||
Array
|
||||
}
|
||||
@ -104,67 +104,12 @@ internal static partial class YamlConfigSchemaValidator
|
||||
var dependentRequired = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal);
|
||||
foreach (var dependency in dependentRequiredElement.EnumerateObject())
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargets = new List<string>();
|
||||
var seenDependencyTargets = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var dependencyTarget in dependency.Value.EnumerateArray())
|
||||
{
|
||||
if (dependencyTarget.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargetName = dependencyTarget.GetString();
|
||||
if (string.IsNullOrWhiteSpace(dependencyTargetName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (!properties.ContainsKey(dependencyTargetName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (seenDependencyTargets.Add(dependencyTargetName))
|
||||
{
|
||||
dependencyTargets.Add(dependencyTargetName);
|
||||
}
|
||||
}
|
||||
|
||||
var dependencyTargets = ParseDependentRequiredConstraint(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
dependency,
|
||||
properties);
|
||||
if (dependencyTargets.Count > 0)
|
||||
{
|
||||
dependentRequired[dependency.Name] = dependencyTargets;
|
||||
@ -176,6 +121,116 @@ internal static partial class YamlConfigSchemaValidator
|
||||
: dependentRequired;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单个 <c>dependentRequired</c> 触发字段的依赖目标列表。
|
||||
/// 触发字段和目标字段必须都来自父对象已声明属性;重复目标会被去重以保持运行时约束稳定。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="dependency">当前触发字段声明。</param>
|
||||
/// <param name="properties">当前对象已声明的属性集合。</param>
|
||||
/// <returns>去重后的依赖目标列表。</returns>
|
||||
private static IReadOnlyList<string> ParseDependentRequiredConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonProperty dependency,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargets = new List<string>();
|
||||
var seenDependencyTargets = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var dependencyTarget in dependency.Value.EnumerateArray())
|
||||
{
|
||||
var dependencyTargetName = ParseDependentRequiredTargetName(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
dependency.Name,
|
||||
dependencyTarget,
|
||||
properties);
|
||||
if (seenDependencyTargets.Add(dependencyTargetName))
|
||||
{
|
||||
dependencyTargets.Add(dependencyTargetName);
|
||||
}
|
||||
}
|
||||
|
||||
return dependencyTargets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取并校验 <c>dependentRequired</c> 的单个目标字段名。
|
||||
/// 目标必须是非空字符串并已在同一个对象 schema 中声明,避免依赖关系指向不可满足字段。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="dependencyName">当前触发字段名称。</param>
|
||||
/// <param name="dependencyTarget">当前依赖目标节点。</param>
|
||||
/// <param name="properties">当前对象已声明的属性集合。</param>
|
||||
/// <returns>已校验的依赖目标字段名。</returns>
|
||||
private static string ParseDependentRequiredTargetName(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string dependencyName,
|
||||
JsonElement dependencyTarget,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (dependencyTarget.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargetName = dependencyTarget.GetString();
|
||||
if (string.IsNullOrWhiteSpace(dependencyTargetName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (properties.ContainsKey(dependencyTargetName))
|
||||
{
|
||||
return dependencyTargetName;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析对象节点声明的 <c>dependentSchemas</c> 条件 schema。
|
||||
/// 当前实现把它作为“当触发字段出现时,当前对象还必须额外满足一段内联 schema”来解释,
|
||||
@ -212,43 +267,12 @@ internal static partial class YamlConfigSchemaValidator
|
||||
var dependentSchemas = new Dictionary<string, YamlConfigSchemaNode>(StringComparer.Ordinal);
|
||||
foreach (var dependency in dependentSchemasElement.EnumerateObject())
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}");
|
||||
var dependencySchemaNode = ParseNode(
|
||||
dependentSchemas[dependency.Name] = ParseDependentSchemaConstraint(
|
||||
tableName,
|
||||
schemaPath,
|
||||
dependencySchemaPath,
|
||||
dependency.Value);
|
||||
if (dependencySchemaNode.NodeType != YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(dependencySchemaPath));
|
||||
}
|
||||
|
||||
dependentSchemas[dependency.Name] = dependencySchemaNode;
|
||||
propertyPath,
|
||||
dependency,
|
||||
properties);
|
||||
}
|
||||
|
||||
return dependentSchemas.Count == 0
|
||||
@ -256,6 +280,62 @@ internal static partial class YamlConfigSchemaValidator
|
||||
: dependentSchemas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单个 <c>dependentSchemas</c> 触发字段关联的 object-typed schema。
|
||||
/// 触发字段必须属于当前对象,关联 schema 继续通过通用节点解析流程获得完整约束模型。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="dependency">当前触发字段声明。</param>
|
||||
/// <param name="properties">当前对象已声明的属性集合。</param>
|
||||
/// <returns>解析后的 object-typed 条件 schema。</returns>
|
||||
private static YamlConfigSchemaNode ParseDependentSchemaConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonProperty dependency,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}");
|
||||
var dependencySchemaNode = ParseNode(
|
||||
tableName,
|
||||
schemaPath,
|
||||
dependencySchemaPath,
|
||||
dependency.Value);
|
||||
if (dependencySchemaNode.NodeType == YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
return dependencySchemaNode;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(dependencySchemaPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析对象节点声明的 <c>allOf</c> 组合约束。
|
||||
/// 当前实现仅接受 object-typed 内联 schema,并把每个条目当成 focused constraint block
|
||||
@ -293,40 +373,13 @@ internal static partial class YamlConfigSchemaValidator
|
||||
var allOfIndex = 0;
|
||||
foreach (var allOfSchemaElement in allOfElement.EnumerateArray())
|
||||
{
|
||||
if (allOfSchemaElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]");
|
||||
ValidateInlineObjectSchemaTargetsAgainstParentObject(
|
||||
var allOfSchemaNode = ParseAllOfSchemaConstraint(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
allOfSchemaPath,
|
||||
$"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'",
|
||||
allOfSchemaElement,
|
||||
allOfIndex,
|
||||
properties);
|
||||
var allOfSchemaNode = ParseNode(
|
||||
tableName,
|
||||
schemaPath,
|
||||
allOfSchemaPath,
|
||||
allOfSchemaElement);
|
||||
if (allOfSchemaNode.NodeType != YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf' for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(allOfSchemaPath));
|
||||
}
|
||||
|
||||
allOfSchemas.Add(allOfSchemaNode);
|
||||
allOfIndex++;
|
||||
}
|
||||
@ -336,6 +389,64 @@ internal static partial class YamlConfigSchemaValidator
|
||||
: allOfSchemas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 <c>allOf</c> 中的单个 object-focused schema 条目。
|
||||
/// 每个条目只允许约束父对象已声明的字段,并且必须保持 object-typed 语义。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="allOfSchemaElement">当前 allOf 条目。</param>
|
||||
/// <param name="allOfIndex">当前 allOf 条目的零基索引。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
/// <returns>解析后的 object-typed schema。</returns>
|
||||
private static YamlConfigSchemaNode ParseAllOfSchemaConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement allOfSchemaElement,
|
||||
int allOfIndex,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (allOfSchemaElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var allOfEntryLabel = $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'";
|
||||
var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]");
|
||||
ValidateInlineObjectSchemaTargetsAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
allOfSchemaPath,
|
||||
allOfEntryLabel,
|
||||
allOfSchemaElement,
|
||||
properties);
|
||||
|
||||
var allOfSchemaNode = ParseNode(
|
||||
tableName,
|
||||
schemaPath,
|
||||
allOfSchemaPath,
|
||||
allOfSchemaElement);
|
||||
if (allOfSchemaNode.NodeType == YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
return allOfSchemaNode;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{allOfEntryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(allOfSchemaPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析对象节点声明的 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
|
||||
/// 当前共享子集要求三段内联 schema 都保持 object-typed focused block 语义,
|
||||
@ -362,25 +473,7 @@ internal static partial class YamlConfigSchemaValidator
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasIf)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (!hasThen && !hasElse)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
ValidateConditionalSchemaKeywordPresence(tableName, schemaPath, propertyPath, hasIf, hasThen, hasElse);
|
||||
|
||||
var ifSchemaPath = BuildNestedSchemaPath(propertyPath, "if");
|
||||
var ifSchemaNode = ParseConditionalObjectSchema(
|
||||
@ -391,31 +484,100 @@ internal static partial class YamlConfigSchemaValidator
|
||||
"if",
|
||||
ifElement,
|
||||
properties);
|
||||
|
||||
var thenSchemaNode = hasThen
|
||||
? ParseConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
BuildNestedSchemaPath(propertyPath, "then"),
|
||||
"then",
|
||||
thenElement,
|
||||
properties)
|
||||
: null;
|
||||
var elseSchemaNode = hasElse
|
||||
? ParseConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
BuildNestedSchemaPath(propertyPath, "else"),
|
||||
"else",
|
||||
elseElement,
|
||||
properties)
|
||||
: null;
|
||||
var thenSchemaNode = ParseOptionalConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
"then",
|
||||
hasThen,
|
||||
thenElement,
|
||||
properties);
|
||||
var elseSchemaNode = ParseOptionalConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
"else",
|
||||
hasElse,
|
||||
elseElement,
|
||||
properties);
|
||||
|
||||
return new YamlConfigConditionalSchemas(ifSchemaNode, thenSchemaNode, elseSchemaNode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 条件关键字的组合关系。
|
||||
/// <c>then</c> 与 <c>else</c> 只能跟随 <c>if</c>,而单独的 <c>if</c> 没有可执行分支。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="hasIf">是否声明 if。</param>
|
||||
/// <param name="hasThen">是否声明 then。</param>
|
||||
/// <param name="hasElse">是否声明 else。</param>
|
||||
private static void ValidateConditionalSchemaKeywordPresence(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
bool hasIf,
|
||||
bool hasThen,
|
||||
bool hasElse)
|
||||
{
|
||||
if (!hasIf)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (hasThen || hasElse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析可选的 <c>then</c> 或 <c>else</c> 条件分支。
|
||||
/// 未声明的分支保留为空,声明的分支必须通过 object-focused schema 校验。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="keywordName">条件关键字名称。</param>
|
||||
/// <param name="hasKeyword">是否声明该条件关键字。</param>
|
||||
/// <param name="keywordElement">条件关键字对应的 schema 节点。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
/// <returns>解析后的条件分支;未声明时返回空。</returns>
|
||||
private static YamlConfigSchemaNode? ParseOptionalConditionalObjectSchema(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string keywordName,
|
||||
bool hasKeyword,
|
||||
JsonElement keywordElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
return hasKeyword
|
||||
? ParseConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
BuildNestedSchemaPath(propertyPath, keywordName),
|
||||
keywordName,
|
||||
keywordElement,
|
||||
properties)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单个条件分支的 object-focused 内联 schema。
|
||||
/// </summary>
|
||||
@ -494,34 +656,96 @@ internal static partial class YamlConfigSchemaValidator
|
||||
JsonElement inlineSchemaElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement))
|
||||
ValidateInlineObjectSchemaPropertiesAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
inlineSchemaPath,
|
||||
entryLabel,
|
||||
inlineSchemaElement,
|
||||
properties);
|
||||
|
||||
ValidateInlineRequiredPropertiesAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
inlineSchemaPath,
|
||||
entryLabel,
|
||||
inlineSchemaElement,
|
||||
properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 内联 schema 的 <c>properties</c> 只引用父对象字段。
|
||||
/// focused block 不负责声明新字段,所以任何父对象未声明字段都会在 schema 加载时被拒绝。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
|
||||
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
|
||||
/// <param name="inlineSchemaElement">当前内联 schema。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
private static void ValidateInlineObjectSchemaPropertiesAgainstParentObject(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string inlineSchemaPath,
|
||||
string entryLabel,
|
||||
JsonElement inlineSchemaElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement))
|
||||
{
|
||||
if (inlinePropertiesElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
foreach (var property in inlinePropertiesElement.EnumerateObject())
|
||||
{
|
||||
if (properties.ContainsKey(property.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inlinePropertiesElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
foreach (var property in inlinePropertiesElement.EnumerateObject())
|
||||
{
|
||||
if (properties.ContainsKey(property.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 内联 schema 的 <c>required</c> 只引用父对象字段。
|
||||
/// 该校验在加载期暴露不可满足的条件块,而不是等到运行时才发现无效字段名。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
|
||||
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
|
||||
/// <param name="inlineSchemaElement">当前内联 schema。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
private static void ValidateInlineRequiredPropertiesAgainstParentObject(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string inlineSchemaPath,
|
||||
string entryLabel,
|
||||
JsonElement inlineSchemaElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!inlineSchemaElement.TryGetProperty("required", out var inlineRequiredElement))
|
||||
{
|
||||
return;
|
||||
@ -539,39 +763,69 @@ internal static partial class YamlConfigSchemaValidator
|
||||
|
||||
foreach (var requiredProperty in inlineRequiredElement.EnumerateArray())
|
||||
{
|
||||
if (requiredProperty.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
var requiredPropertyName = requiredProperty.GetString();
|
||||
if (string.IsNullOrWhiteSpace(requiredPropertyName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
if (properties.ContainsKey(requiredPropertyName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ValidateInlineRequiredPropertyAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
inlineSchemaPath,
|
||||
entryLabel,
|
||||
requiredProperty,
|
||||
properties);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 内联 schema 的单个 <c>required</c> 字段名。
|
||||
/// 字段名必须是非空字符串并且属于父对象声明范围,保持条件块与父对象形状一致。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
|
||||
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
|
||||
/// <param name="requiredProperty">当前 required 条目。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
private static void ValidateInlineRequiredPropertyAgainstParentObject(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string inlineSchemaPath,
|
||||
string entryLabel,
|
||||
JsonElement requiredProperty,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (requiredProperty.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.",
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
var requiredPropertyName = requiredProperty.GetString();
|
||||
if (string.IsNullOrWhiteSpace(requiredPropertyName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
if (properties.ContainsKey(requiredPropertyName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
64
GFramework.Game/Config/YamlConfigStringConstraints.cs
Normal file
64
GFramework.Game/Config/YamlConfigStringConstraints.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示标量节点上声明的字符串长度、模式与 format 约束。
|
||||
/// 该模型将正则原文、预编译正则和共享 format 枚举绑定保存,
|
||||
/// 保证诊断内容与运行时匹配逻辑保持一致。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigStringConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化字符串约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minLength">最小长度约束。</param>
|
||||
/// <param name="maxLength">最大长度约束。</param>
|
||||
/// <param name="pattern">正则模式约束原文。</param>
|
||||
/// <param name="patternRegex">已编译的正则表达式。</param>
|
||||
/// <param name="formatConstraint">字符串 format 约束。</param>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="pattern"/> 与 <paramref name="patternRegex"/> 未成对出现时抛出。</exception>
|
||||
public YamlConfigStringConstraints(
|
||||
int? minLength,
|
||||
int? maxLength,
|
||||
string? pattern,
|
||||
Regex? patternRegex,
|
||||
YamlConfigStringFormatConstraint? formatConstraint)
|
||||
{
|
||||
if ((pattern is null) != (patternRegex is null))
|
||||
{
|
||||
throw new ArgumentException("pattern 与 patternRegex 必须同时为空或同时提供。", nameof(pattern));
|
||||
}
|
||||
|
||||
MinLength = minLength;
|
||||
MaxLength = maxLength;
|
||||
Pattern = pattern;
|
||||
PatternRegex = patternRegex;
|
||||
FormatConstraint = formatConstraint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小长度约束。
|
||||
/// </summary>
|
||||
public int? MinLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大长度约束。
|
||||
/// </summary>
|
||||
public int? MaxLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取正则模式约束原文。
|
||||
/// </summary>
|
||||
public string? Pattern { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取已编译的正则表达式。
|
||||
/// </summary>
|
||||
public Regex? PatternRegex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串 format 约束。
|
||||
/// </summary>
|
||||
public YamlConfigStringFormatConstraint? FormatConstraint { get; }
|
||||
}
|
||||
35
GFramework.Game/Config/YamlConfigStringFormatConstraint.cs
Normal file
35
GFramework.Game/Config/YamlConfigStringFormatConstraint.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个已归一化的字符串 format 约束。
|
||||
/// 该模型同时保留 schema 原文与共享枚举,方便诊断信息稳定展示,又避免运行时校验反复解析字符串。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigStringFormatConstraint
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化字符串 format 约束模型。
|
||||
/// </summary>
|
||||
/// <param name="schemaName">schema 中声明的 format 名称。</param>
|
||||
/// <param name="kind">归一化后的共享 format 枚举。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="schemaName"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="schemaName"/> 为空或仅包含空白字符时抛出。</exception>
|
||||
public YamlConfigStringFormatConstraint(
|
||||
string schemaName,
|
||||
YamlConfigStringFormatKind kind)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(schemaName);
|
||||
|
||||
SchemaName = schemaName;
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 中声明的 format 名称。
|
||||
/// </summary>
|
||||
public string SchemaName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取归一化后的共享 format 枚举。
|
||||
/// </summary>
|
||||
public YamlConfigStringFormatKind Kind { get; }
|
||||
}
|
||||
45
GFramework.Game/Config/YamlConfigStringFormatKind.cs
Normal file
45
GFramework.Game/Config/YamlConfigStringFormatKind.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Enums;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。
|
||||
/// </summary>
|
||||
[GenerateEnumExtensions]
|
||||
internal enum YamlConfigStringFormatKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示 <c>yyyy-MM-dd</c> 形式的日期。
|
||||
/// </summary>
|
||||
Date,
|
||||
|
||||
/// <summary>
|
||||
/// 表示带显式时区偏移的 RFC 3339 日期时间。
|
||||
/// </summary>
|
||||
DateTime,
|
||||
|
||||
/// <summary>
|
||||
/// 表示 day-time duration 形式的持续时间。
|
||||
/// </summary>
|
||||
Duration,
|
||||
|
||||
/// <summary>
|
||||
/// 表示基础电子邮件地址格式。
|
||||
/// </summary>
|
||||
Email,
|
||||
|
||||
/// <summary>
|
||||
/// 表示带显式时区偏移的 RFC 3339 时间。
|
||||
/// </summary>
|
||||
Time,
|
||||
|
||||
/// <summary>
|
||||
/// 表示绝对 URI。
|
||||
/// </summary>
|
||||
Uri,
|
||||
|
||||
/// <summary>
|
||||
/// 表示连字符分隔的 UUID 文本。
|
||||
/// </summary>
|
||||
Uuid
|
||||
}
|
||||
@ -9,6 +9,7 @@
|
||||
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Core.SourceGenerators.Abstractions\GFramework.Core.SourceGenerators.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
@ -1363,6 +1363,172 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
}
|
||||
""";
|
||||
|
||||
private const string AssemblyLevelDirectFallbackMetadataSource = """
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public interface IServiceCollection { }
|
||||
|
||||
public static class ServiceCollectionServiceExtensions
|
||||
{
|
||||
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Debug(string msg);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs
|
||||
{
|
||||
public interface IRequest<TResponse> { }
|
||||
public interface INotification { }
|
||||
public interface IStreamRequest<TResponse> { }
|
||||
|
||||
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs
|
||||
{
|
||||
public interface ICqrsHandlerRegistry
|
||||
{
|
||||
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsHandlerRegistryAttribute : Attribute
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
|
||||
|
||||
public CqrsReflectionFallbackAttribute(params Type[] fallbackHandlerTypes) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
public sealed class Container
|
||||
{
|
||||
private unsafe struct AlphaResponse
|
||||
{
|
||||
}
|
||||
|
||||
private unsafe struct BetaResponse
|
||||
{
|
||||
}
|
||||
|
||||
private unsafe sealed record AlphaRequest() : IRequest<delegate* unmanaged<AlphaResponse>>;
|
||||
|
||||
private unsafe sealed record BetaRequest() : IRequest<delegate* unmanaged<BetaResponse>>;
|
||||
|
||||
public unsafe sealed class BetaHandler : IRequestHandler<BetaRequest, delegate* unmanaged<BetaResponse>>
|
||||
{
|
||||
}
|
||||
|
||||
public unsafe sealed class AlphaHandler : IRequestHandler<AlphaRequest, delegate* unmanaged<AlphaResponse>>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string AssemblyLevelMixedFallbackMetadataSource = """
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public interface IServiceCollection { }
|
||||
|
||||
public static class ServiceCollectionServiceExtensions
|
||||
{
|
||||
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Debug(string msg);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs
|
||||
{
|
||||
public interface IRequest<TResponse> { }
|
||||
public interface INotification { }
|
||||
public interface IStreamRequest<TResponse> { }
|
||||
|
||||
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs
|
||||
{
|
||||
public interface ICqrsHandlerRegistry
|
||||
{
|
||||
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsHandlerRegistryAttribute : Attribute
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
|
||||
|
||||
public CqrsReflectionFallbackAttribute(params Type[] fallbackHandlerTypes) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
public sealed class Container
|
||||
{
|
||||
private unsafe struct AlphaResponse
|
||||
{
|
||||
}
|
||||
|
||||
private unsafe struct BetaResponse
|
||||
{
|
||||
}
|
||||
|
||||
private unsafe sealed record AlphaRequest() : IRequest<delegate* unmanaged<AlphaResponse>>;
|
||||
|
||||
private unsafe sealed record BetaRequest() : IRequest<delegate* unmanaged<BetaResponse>>;
|
||||
|
||||
public unsafe sealed class AlphaHandler : IRequestHandler<AlphaRequest, delegate* unmanaged<AlphaResponse>>
|
||||
{
|
||||
}
|
||||
|
||||
private unsafe sealed class BetaHandler : IRequestHandler<BetaRequest, delegate* unmanaged<BetaResponse>>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
|
||||
/// </summary>
|
||||
@ -1690,6 +1856,116 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当所有 fallback handlers 本身都可直接引用,且 runtime 同时支持字符串与 <see cref="Type" /> 元数据承载时,
|
||||
/// 生成器会优先发射直接 <c>typeof(...)</c> 的 fallback 特性,减少运行时按名称回查程序集类型。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void
|
||||
Emits_Direct_Type_Fallback_Metadata_When_All_Fallback_Handlers_Are_Referenceable_And_Runtime_Type_Contract_Is_Available()
|
||||
{
|
||||
var execution = ExecuteGenerator(
|
||||
AssemblyLevelDirectFallbackMetadataSource,
|
||||
allowUnsafe: true);
|
||||
var inputCompilationErrors = execution.InputCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatorErrors = execution.GeneratorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedSource = execution.GeneratedSources[0].content;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
|
||||
Assert.That(generatedCompilationErrors, Is.Empty);
|
||||
Assert.That(generatorErrors, Is.Empty);
|
||||
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler), typeof(global::TestApp.Container.BetaHandler))]"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Not.Contain(
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+AlphaHandler\", \"TestApp.Container+BetaHandler\")]"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute()"));
|
||||
Assert.That(
|
||||
CountOccurrences(
|
||||
generatedSource,
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute"),
|
||||
Is.EqualTo(1));
|
||||
Assert.That(
|
||||
CountOccurrences(
|
||||
generatedSource,
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\""),
|
||||
Is.Zero);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 runtime 允许多个 fallback 特性实例,且本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers 时,
|
||||
/// 生成器会拆分出直接 <see cref="Type" /> 与字符串两类元数据,避免 mixed 场景整体退回字符串 fallback。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void
|
||||
Emits_Mixed_Direct_Type_And_String_Fallback_Metadata_When_Runtime_Allows_Multiple_Fallback_Attributes()
|
||||
{
|
||||
var execution = ExecuteGenerator(
|
||||
AssemblyLevelMixedFallbackMetadataSource,
|
||||
allowUnsafe: true);
|
||||
var inputCompilationErrors = execution.InputCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatorErrors = execution.GeneratorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedSource = execution.GeneratedSources[0].content;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
|
||||
Assert.That(generatedCompilationErrors, Is.Empty);
|
||||
Assert.That(generatorErrors, Is.Empty);
|
||||
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler))]"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+BetaHandler\")]"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute()"));
|
||||
Assert.That(
|
||||
CountOccurrences(
|
||||
generatedSource,
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute"),
|
||||
Is.EqualTo(2));
|
||||
Assert.That(
|
||||
CountOccurrences(
|
||||
generatedSource,
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\""),
|
||||
Is.EqualTo(1));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler))]" +
|
||||
Environment.NewLine +
|
||||
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+BetaHandler\")]" +
|
||||
Environment.NewLine +
|
||||
"[assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
|
||||
/// </summary>
|
||||
@ -1741,6 +2017,31 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
return execution.GeneratedSources[0].content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。
|
||||
/// </summary>
|
||||
/// <param name="text">待统计的完整生成源码。</param>
|
||||
/// <param name="value">需要计数的固定片段。</param>
|
||||
/// <returns><paramref name="value" /> 在 <paramref name="text" /> 中出现的次数。</returns>
|
||||
private static int CountOccurrences(string text, string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
throw new ArgumentException("The search value must not be null or empty.", nameof(value));
|
||||
|
||||
var count = 0;
|
||||
var startIndex = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var nextIndex = text.IndexOf(value, startIndex, global::System.StringComparison.Ordinal);
|
||||
if (nextIndex < 0)
|
||||
return count;
|
||||
|
||||
count++;
|
||||
startIndex = nextIndex + value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行 CQRS handler registry generator,并返回生成输出及相关诊断。
|
||||
/// </summary>
|
||||
|
||||
@ -12,11 +12,6 @@ help the current worktree land on the right recovery documents without scanning
|
||||
|
||||
## Active Topics
|
||||
|
||||
- `analyzer-warning-reduction`
|
||||
- Purpose: track the analyzer warning reduction branch, including the current recovery point, remaining warning
|
||||
hotspots, and the next safe warning-reduction slice.
|
||||
- Tracking: `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md`
|
||||
- Trace: `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md`
|
||||
- `ai-plan-governance`
|
||||
- Purpose: govern the `ai-plan/` directory model, startup index, and archive policy.
|
||||
- Tracking: `ai-plan/public/ai-plan-governance/todos/ai-plan-governance-tracking.md`
|
||||
@ -50,9 +45,6 @@ help the current worktree land on the right recovery documents without scanning
|
||||
|
||||
## Worktree To Active Topic Map
|
||||
|
||||
- Branch: `fix/analyzer-warning-reduction-batch`
|
||||
- Worktree hint: `GFramework-analyzer`
|
||||
- Priority 1: `analyzer-warning-reduction`
|
||||
- Branch: `feat/ai-first-config`
|
||||
- Worktree hint: `GFramework-Ai-First-Config`
|
||||
- Priority 1: `ai-first-config-system`
|
||||
@ -75,6 +67,9 @@ help the current worktree land on the right recovery documents without scanning
|
||||
- Priority 1: `documentation-full-coverage-governance`
|
||||
## Archived Topics
|
||||
|
||||
- `analyzer-warning-reduction`
|
||||
- Archive root: `ai-plan/public/archive/analyzer-warning-reduction/`
|
||||
- Note: 长期 warning-reduction 分支已收尾;PR #301 的最终 review follow-up 已本地闭环,后续仅作为历史恢复材料保留。
|
||||
- `cqrs-cache-docs-hardening`
|
||||
- Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/`
|
||||
- Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review.
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
# Analyzer Warning Reduction 跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支,并保持 active recovery 文档只保留当前真值。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-092`
|
||||
- 当前阶段:`Phase 92`
|
||||
- 当前焦点:
|
||||
- `2026-04-28` 复核 `PR #300` 最新 open threads:代码类线程已与当前工作树对齐,仅剩 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` 的文件计数与验证口径漂移仍然成立
|
||||
- 已将 tracking 文档修正为与 `6cc87a9...HEAD` 的实际变更规模一致,并与 trace 中记录的 `dotnet build`、定向 `dotnet test`、`git diff --check` 验证口径保持一致
|
||||
- `dotnet format --verify-no-changes` 的 `GFramework.Core.Tests` 既有 `FINALNEWLINE`、`CHARSET`、`WHITESPACE` 基线仍保持独立,不与当前 `ai-plan` 同步修复混提
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前 `origin/main` 基线提交为 `6cc87a9`(`2026-04-27T20:28:50+08:00`)。
|
||||
- 当前直接验证结果:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
|
||||
- 最新结果:成功;`67` 通过、`0` 失败
|
||||
- 当前批次摘要:
|
||||
- 当前分支相对 `6cc87a9...HEAD` 包含 `18` 个已修改文件与 `38` 个新增文件(合计 `56` 个变更文件),分别位于 `GFramework.Core.Tests`、`GFramework.Cqrs.Tests`、`GFramework.Core`、`.agents/skills/gframework-pr-review/` 与 `ai-plan/public/analyzer-warning-reduction`
|
||||
- 本轮没有触碰 `Mediator/*`、`YamlConfigSchemaValidator*` 或 `GFramework.Core.Tests` 的整项目格式基线波次
|
||||
|
||||
## 当前风险
|
||||
|
||||
- GitHub PR 上的 open threads 可能仍显示为未关闭,因为当前只同步了 `ai-plan` 文档,尚未推送新的 head 供审查机器人重新折叠线程。
|
||||
- 缓解措施:推送本次 `ai-plan` 同步提交后重新执行 `$gframework-pr-review`,以最新 head 再核对 thread 状态。
|
||||
- `dotnet format GFramework.Core.Tests/GFramework.Core.Tests.csproj --verify-no-changes` 当前会命中项目内大量历史格式诊断。
|
||||
- 缓解措施:本轮只记录为现存基线,不把 `PR #300` 的 review follow-up 扩展成整项目格式清理。
|
||||
- `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,但与本轮 review 修复无交集。
|
||||
- 缓解措施:继续保持为独立高耦合波次。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
- 当前轮次归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 历史跟踪归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- 历史 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 权威验证结果统一维护在“当前活跃事实”。
|
||||
- `GFramework.Core.Tests` 的当前受影响项目 Release 构建已清零,并通过对应定向测试回归。
|
||||
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
|
||||
- warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。
|
||||
|
||||
## 下一步建议
|
||||
|
||||
1. 提交本轮 `PR #300` nitpick follow-up、技能规则更新与 `ai-plan` 同步。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认 `ai-plan` 相关 thread 是否随最新 head 自动收口。
|
||||
3. 若要清理 `dotnet format` 基线,另开 `GFramework.Core.Tests` 格式治理切片,不与当前 PR review 修复混提。
|
||||
@ -1,67 +0,0 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-28 — RP-092
|
||||
|
||||
### 阶段:复核 `PR #300` 的 open threads,并只修正当前分支仍然成立的 `ai-plan` 漂移
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求恢复当前 `$gframework-pr-review` 任务,继续以 PR head 上的开放线程为准做 triage
|
||||
- 主线程实施:
|
||||
- 重新读取 `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 的 latest head open threads
|
||||
- 逐条对照本地文件后确认:`TestArchitectureContextBehaviorTests`、`TestArchitectureWithRegistry`、`TestResourceLoader`、`PartialGeneratedNotificationHandlerRegistry` 相关 CodeRabbit 线程在当前工作树上都已匹配修复,仅线程状态尚未随新 head 折叠
|
||||
- 继续核对 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,确认当前实现 `RegistryInitializationHookBase.OnPhase` 已在缺少注册表时保持 no-op,定向回归测试通过
|
||||
- 修正 `analyzer-warning-reduction-tracking.md` 中仍然成立的两处漂移:
|
||||
- 将文件计数更新为相对 `6cc87a9...HEAD` 的实际规模:`18` 个已修改文件、`38` 个新增文件、合计 `56` 个变更文件
|
||||
- 将验证口径统一为 trace 已记录的 `dotnet build`、定向 `dotnet test`、`git diff --check`
|
||||
- 验证里程碑:
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
|
||||
- 结果:成功;`10` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 2026-04-28 — RP-091
|
||||
|
||||
### 阶段:收口 `PR #300` 的共享测试基础设施 nitpick,并升级 PR-review triage 规则
|
||||
|
||||
- 触发背景:
|
||||
- 用户追问 `TestArchitectureContext` / `TestArchitectureContextV3` 的共享基础设施 nitpick 是否已经处理完成
|
||||
- 同时要求把“本地验证后仍然成立的 nitpick 不能默认降级为可选项”写入 `AGENTS.md` 或 `$gframework-pr-review`
|
||||
- 主线程实施:
|
||||
- 新增 `TestArchitectureContextBase`,把容器解析、共享 `EventBus` 行为,以及 legacy / CQRS 失败契约统一收敛到一处
|
||||
- 将 `TestArchitectureContext` 与 `TestArchitectureContextV3` 收窄为薄包装类型,只保留各自的命名入口与 `Id` 差异
|
||||
- 更新 `.agents/skills/gframework-pr-review/SKILL.md`,明确要求:latest-head `Nitpick comment` 一旦本地验证仍成立且指向真实漂移/回归风险,就必须作为 actionable review input 处理,而不是默认视作可选
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
|
||||
- 结果:成功;`67` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 活跃风险
|
||||
|
||||
- GitHub PR 上的 open threads 在本地提交前仍可能显示为未关闭。
|
||||
- 缓解措施:以当前工作树和定向验证作为真值,推送后再让 PR 线程重新比对最新 head。
|
||||
- `GFramework.Core.Tests` 项目当前存在独立于本轮改动的 `dotnet format` 基线。
|
||||
- 缓解措施:保持为后续单独格式治理切片,不在当前 PR review follow-up 中扩写。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 提交本轮 `ai-plan` 同步修复,使 PR head 能重新折叠文档相关线程。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否已经下降。
|
||||
|
||||
## 历史归档指针
|
||||
|
||||
- 最新 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- 历史 todo 归档:
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 早期归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
@ -0,0 +1,72 @@
|
||||
# Analyzer Warning Reduction 跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支,并保持 active recovery 文档只保留当前真值。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-096`
|
||||
- 当前阶段:`Completed`
|
||||
- 当前焦点:
|
||||
- `2026-04-29` 已完成 `PR #301` latest-head review threads 的最终本地复核,并修复仍然成立的空对象 `const` 比较键回归
|
||||
- 当前 topic 已达到归档条件:长期 warning-reduction 分支的实现、PR review follow-up 与最小验证均已完成
|
||||
- 当前目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,后续仅保留历史恢复价值
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前 `origin/main` 基线提交为 `0e32dab`(`2026-04-28T17:15:47+08:00`)。
|
||||
- 当前直接验证结果:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
|
||||
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"`
|
||||
- 最新结果:成功;`10` 通过、`0` 失败
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"`
|
||||
- 最新结果:成功;`25` 通过、`0` 失败
|
||||
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs`
|
||||
- 最新结果:成功;当前修复范围内无格式漂移
|
||||
- `git diff --check`
|
||||
- 最新结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 当前批次摘要:
|
||||
- 当前最终收尾切片直接修改 `3` 个已有文件,不再扩写 warning-batch 的多文件清理范围
|
||||
- 这次收尾把 `YamlConfigAllowedValue` / `YamlConfigConstantValue` 的 `comparableValue` 契约收窄为“允许空字符串,但拒绝非空纯空白”,恢复空对象 `const` / `enum` 的合法比较键语义
|
||||
- PR review triage 结论:
|
||||
- 接受并完成:并发共享状态、阻塞等待、无效约束状态、缺失 `<exception>` 文档、空对象比较键回归
|
||||
- 归档前剩余 open threads 只包含两类:尚未推送折叠的 stale 线程,以及已明确延后 / 驳回的建议(`DisplayPath` 与枚举特性泛化)
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 当前 GitHub PR 在本地提交并推送前仍可能显示旧的 open threads。
|
||||
- 缓解措施:以本文件中的本地验证结果为 archive 真值;若未来需要复查 PR 页面,应从 archive 恢复而不是重新激活 topic。
|
||||
- 本轮仅对 `GFramework.Game` 收尾回归做了受影响模块验证,没有重新建立新的仓库根 clean build 基线。
|
||||
- 缓解措施:后续若有新的 warning-reduction 任务,应创建新 topic,并重新执行仓库根 `dotnet clean` + `dotnet build` 采样。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
- 当前轮次归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 历史跟踪归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- 历史 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 权威验证结果统一维护在“当前活跃事实”。
|
||||
- `GFramework.Game` 当前 Release 构建已清零,并通过空对象 `const` 回归与模型契约定向测试。
|
||||
- `GFramework.Cqrs.Tests` 当前 PR-review follow-up 定向测试通过,说明并发/缓存测试辅助实现的行为修正没有破坏现有集成断言。
|
||||
- `dotnet format --verify-no-changes` 已确认当前收尾改动未引入新的格式化偏差。
|
||||
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
|
||||
- 本 topic 已进入 archive;若未来重启 warning reduction,应以新 topic 和新的仓库级 clean build 基线继续。
|
||||
|
||||
## 下一步建议
|
||||
|
||||
1. 保持当前 archive 状态,不要再把该 topic 作为默认 boot 入口。
|
||||
2. 若未来需要继续 warning reduction,创建新的 active topic,并重新建立仓库根 clean build 真值。
|
||||
@ -0,0 +1,209 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-29 — RP-096
|
||||
|
||||
### 阶段:完成 `PR #301` 最终收尾并归档长期 warning-reduction 主题
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求先用 `$gframework-pr-review` 解决当前 PR review 的剩余问题,然后把整个长期分支主题归档
|
||||
- 本轮 triage 结论:
|
||||
- `MediatorArchitectureIntegrationTests` 并发更新、`YamlConfigConditionalSchemas` / `YamlConfigStringFormatConstraint` 的 `<exception>` 文档,以及两个枚举的 `[GenerateEnumExtensions]` 在当前工作树上均已存在,对应 open threads 判定为 stale
|
||||
- `YamlConfigReferenceUsage.DisplayPath` 删除建议继续判定为不成立,因为 loader 诊断、引用索引和测试断言仍把它作为稳定语义标签使用
|
||||
- `LoadAsync_Should_Accept_Empty_Object_Schema_Const` 失败仍然成立:上轮把 `YamlConfigAllowedValue` / `YamlConfigConstantValue` 的 `comparableValue` 收紧成 `ThrowIfNullOrWhiteSpace(...)` 后,误伤了空对象常量的合法空比较键
|
||||
- 主线程实施:
|
||||
- 将 `YamlConfigAllowedValue` 与 `YamlConfigConstantValue` 的比较键契约调整为:
|
||||
- 允许 `string.Empty`
|
||||
- 继续拒绝非空纯空白字符串
|
||||
- 保留 `displayValue` 的非空白要求
|
||||
- 扩充 `YamlConfigModelContractTests`,新增空比较键的正向覆盖,同时保留纯空白比较键的回归保护
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"`
|
||||
- 结果:成功;`10` 通过、`0` 失败
|
||||
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs`
|
||||
- 结果:成功
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 归档结论:
|
||||
- `analyzer-warning-reduction` 当前 topic 已满足归档条件:长期 warning-reduction 主线已收尾,PR #301 的本地 follow-up 闭环完成
|
||||
- 整个 topic 目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,不再作为 active 默认入口
|
||||
|
||||
## 2026-04-29 — RP-095
|
||||
|
||||
### 阶段:复核 `PR #301` latest-head review threads,并只修复当前工作树上仍然成立的问题
|
||||
|
||||
- 触发背景:
|
||||
- 用户显式要求执行 `$gframework-pr-review`,需要把 GitHub PR review 信号与本地代码现状重新核对,而不是沿用旧的 warning-batch 假设
|
||||
- 本轮 triage 结论:
|
||||
- 接受并修复:
|
||||
- `MediatorArchitectureIntegrationTests` 中 `Task.Delay().Wait()` 阻塞、静态 `Dictionary` 竞态、`SharedState.Counter +=` 非原子更新、以及 `TestNestedRequestHandler` 冗余分支
|
||||
- `GFramework.Game/Config` 中仍然成立的模型契约缺口:空白比较键、数组 / 对象边界非法状态、`Pattern` / `PatternRegex` 不一致、`ReferencedTableNames` 未做 defensive copy、以及缺失的 `<exception>` XML 文档
|
||||
- `MediatorAdvancedFeaturesTests` 中 `MA0048` 抑制缺少原因注释
|
||||
- `YamlConfigSchemaNode.NodeValidation.None` 未被引用,按 review 建议删除死代码
|
||||
- 明确不接受或延后:
|
||||
- `YamlConfigReferenceUsage.DisplayPath`:当前在 loader 诊断与测试断言中承担独立语义标签,不作为“纯冗余 alias”删除
|
||||
- `YamlConfigSchemaPropertyType` / `YamlConfigStringFormatKind` 补 `[GenerateEnumExtensions]`:仓库产品代码没有现成约定或使用面,判断为泛化误报
|
||||
- 主线程实施:
|
||||
- 将 CQRS 集成测试辅助处理器改为真正异步,并用 `ConcurrentDictionary` / `Interlocked` 收口并发共享状态
|
||||
- 为 `YamlConfigAllowedValue`、`YamlConfigConstantValue`、`YamlConfigArrayContainsConstraints`、`YamlConfigArrayConstraints`、`YamlConfigObjectConstraints`、`YamlConfigStringConstraints`、`YamlConfigSchema`、`YamlConfigConditionalSchemas`、`YamlConfigStringFormatConstraint` 补运行时契约或 `<exception>` 注释
|
||||
- 新增 `YamlConfigModelContractTests`,锁定上述模型拒绝无效状态的行为
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~YamlConfigModelContractTests"`
|
||||
- 第一次结果:成功;`10` 通过、`0` 失败,但新增测试触发 `MA0009`
|
||||
- 第二次结果:成功;`10` 通过、`0` 失败;为测试中的 `Regex` 补 timeout 后 warning 清零
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"`
|
||||
- 结果:成功;`25` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 下一步:
|
||||
- 提交当前 PR-review follow-up 与 `ai-plan` 同步
|
||||
- 推送后重新执行 `$gframework-pr-review`,确认 remaining open threads 是否已缩减到延后 / 误报项
|
||||
|
||||
## 2026-04-29 — RP-094
|
||||
|
||||
### 阶段:收尾 `YamlConfigSchemaValidator` 剩余 `MA0051` 并将仓库根 clean build 归零
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求先拿构建 warning,再在 warning 很多时分批指派 subagent;本轮按 `$gframework-batch-boot 50` 继续执行
|
||||
- 基线与停机判断:
|
||||
- 当前 `origin/main` 仍为 `0e32dab`(`2026-04-28T17:15:47+08:00`)
|
||||
- 本轮标准仓库根 `dotnet clean` + `dotnet build` 直接成功;warning 总数为 `15`,但全部集中在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs`
|
||||
- 由于 `15` 条 warning 实际只对应同一文件内 `5` 个独立 `MA0051` 方法,不满足“warning 非常多且可安全分派多个独立写边界”的条件,因此不再新增 worker
|
||||
- 主线程实施:
|
||||
- 将 `ParseNode` 拆成 `ResolveNodeTypeName`、`ValidateObjectOnlyKeywords`、`CreateParsedNodeForType`
|
||||
- 将 `ValidateObjectNode` 拆成对象类型确认、属性遍历与 required 校验 helper
|
||||
- 将 `ValidateObjectConstraints` 拆成 property count、`dependentRequired`、`dependentSchemas`、`allOf`、条件分支五个 helper
|
||||
- 将 `ValidateScalarNode` 与 `ValidateNumericScalarConstraints` 分别拆成标量类型确认、引用回写、数值上下界和 `multipleOf` helper
|
||||
- 追加 `ValidateConditionalSchemaBranch` 收口 if/then/else 分支;随后修正该 helper 引入的 `MA0006`
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
|
||||
- 第一次结果:成功;`3` warnings、`0` errors(均为新 helper 中 `branchName == "then"` 引入的 `MA0006`)
|
||||
- 第二次结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
|
||||
- 结果:成功;`80` 通过、`0` 失败
|
||||
- `dotnet clean`
|
||||
- 结果:成功
|
||||
- `dotnet build`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 当前指标:
|
||||
- 仓库根 clean build warning:`15` -> `0`
|
||||
- 当前分支相对 `origin/main...HEAD` 仍为 `22` 个变更文件,低于 `$gframework-batch-boot 50` 的文件阈值
|
||||
- 当前停止原因:warning hotspot 已耗尽,不再有可重复切片
|
||||
- 下一步:
|
||||
- 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 真值更新
|
||||
|
||||
## 2026-04-29 — RP-093
|
||||
|
||||
### 阶段:按 `$gframework-batch-boot 50` 从 clean build warning 基线分批清理
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求先拿构建 warning,再分批指派 subagent 加快处理;停止条件解析为分支相对 `origin/main` 接近 `50` 个变更文件
|
||||
- 基线与环境:
|
||||
- 当前 `origin/main` 为 `0e32dab`(`2026-04-28T17:15:47+08:00`)
|
||||
- 标准 `dotnet clean` 在当前 WSL 环境仍被 Windows NuGet fallback package folder 阻塞;按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后,使用 `-p:RestoreFallbackFolders=` 完成 clean / build
|
||||
- clean 后 warning 基线:`236` warnings、`0` errors
|
||||
- 已接受的 worker 范围:
|
||||
- `ed269d4`:`GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016`
|
||||
- `121df44`:`GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015`
|
||||
- `9109eec`:`GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015`
|
||||
- 主线程实施:
|
||||
- 在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 为固定格式正则与 schema `pattern` 正则补充 timeout,避免运行时正则输入继续触发 `MA0009`
|
||||
- 将三处字符串等值比较改为 ordinal `string.Equals`,清理 `MA0006`
|
||||
- 接受 `1395b84` 的 `YamlConfigSchemaValidator.ObjectKeywords.cs` 方法拆分,清理该文件 `MA0051`
|
||||
- 收口被中止 worker 留下的 schema model 拆文件变更,将 `YamlConfigSchemaValidator.cs` 末尾类型移动到同名文件,清理 `MA0048`
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"`
|
||||
- 结果:成功;`45` 通过、`0` 失败
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
|
||||
- 结果:成功;`80` 通过、`0` 失败
|
||||
- `dotnet clean -p:RestoreFallbackFolders= -v:quiet`
|
||||
- 结果:成功
|
||||
- `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false`
|
||||
- 中间结果:成功;`75` warnings、`0` errors
|
||||
- `dotnet clean -p:RestoreFallbackFolders= -v:quiet`
|
||||
- 结果:成功
|
||||
- `dotnet build -p:RestoreFallbackFolders= -clp:Summary -v:minimal -m:1 -nodeReuse:false`
|
||||
- 结果:成功;`15 Warning(s)`、`0 Error(s)`
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 当前指标:
|
||||
- warning 总数:`236` -> `15`
|
||||
- 剩余 warning 分布:`GFramework.Game/Config/YamlConfigSchemaValidator.cs` 的 `MA0051` `15` 条(5 个方法跨 3 个 TFM)
|
||||
- 本轮提交后预计分支 diff:`22` 个文件,低于 `50` 个文件阈值
|
||||
- 下一步:
|
||||
- 按用户要求本轮到此结束;下一轮只处理 `YamlConfigSchemaValidator.cs` 剩余 `MA0051` 方法拆分
|
||||
|
||||
## 2026-04-28 — RP-092
|
||||
|
||||
### 阶段:复核 `PR #300` 的 open threads,并只修正当前分支仍然成立的 `ai-plan` 漂移
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求恢复当前 `$gframework-pr-review` 任务,继续以 PR head 上的开放线程为准做 triage
|
||||
- 主线程实施:
|
||||
- 重新读取 `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 的 latest head open threads
|
||||
- 逐条对照本地文件后确认:`TestArchitectureContextBehaviorTests`、`TestArchitectureWithRegistry`、`TestResourceLoader`、`PartialGeneratedNotificationHandlerRegistry` 相关 CodeRabbit 线程在当前工作树上都已匹配修复,仅线程状态尚未随新 head 折叠
|
||||
- 继续核对 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,确认当前实现 `RegistryInitializationHookBase.OnPhase` 已在缺少注册表时保持 no-op,定向回归测试通过
|
||||
- 修正 `analyzer-warning-reduction-tracking.md` 中仍然成立的两处漂移:
|
||||
- 将文件计数更新为相对 `6cc87a9...HEAD` 的实际规模:`18` 个已修改文件、`38` 个新增文件、合计 `56` 个变更文件
|
||||
- 将验证口径统一为 trace 已记录的 `dotnet build`、定向 `dotnet test`、`git diff --check`
|
||||
- 验证里程碑:
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
|
||||
- 结果:成功;`10` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 2026-04-28 — RP-091
|
||||
|
||||
### 阶段:收口 `PR #300` 的共享测试基础设施 nitpick,并升级 PR-review triage 规则
|
||||
|
||||
- 触发背景:
|
||||
- 用户追问 `TestArchitectureContext` / `TestArchitectureContextV3` 的共享基础设施 nitpick 是否已经处理完成
|
||||
- 同时要求把“本地验证后仍然成立的 nitpick 不能默认降级为可选项”写入 `AGENTS.md` 或 `$gframework-pr-review`
|
||||
- 主线程实施:
|
||||
- 新增 `TestArchitectureContextBase`,把容器解析、共享 `EventBus` 行为,以及 legacy / CQRS 失败契约统一收敛到一处
|
||||
- 将 `TestArchitectureContext` 与 `TestArchitectureContextV3` 收窄为薄包装类型,只保留各自的命名入口与 `Id` 差异
|
||||
- 更新 `.agents/skills/gframework-pr-review/SKILL.md`,明确要求:latest-head `Nitpick comment` 一旦本地验证仍成立且指向真实漂移/回归风险,就必须作为 actionable review input 处理,而不是默认视作可选
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
|
||||
- 结果:成功;`67` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 活跃风险
|
||||
|
||||
- GitHub PR 上的 open threads 在本地提交前仍可能显示为未关闭。
|
||||
- 缓解措施:以当前工作树和定向验证作为真值,推送后再让 PR 线程重新比对最新 head。
|
||||
- `GFramework.Core.Tests` 项目当前存在独立于本轮改动的 `dotnet format` 基线。
|
||||
- 缓解措施:保持为后续单独格式治理切片,不在当前 PR review follow-up 中扩写。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 提交本轮 `ai-plan` 同步修复,使 PR head 能重新折叠文档相关线程。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否已经下降。
|
||||
|
||||
## 历史归档指针
|
||||
|
||||
- 最新 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- 历史 todo 归档:
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 早期归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
@ -7,10 +7,14 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-050`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-052`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前焦点:
|
||||
- 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口
|
||||
- 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射
|
||||
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
|
||||
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
|
||||
- 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时,mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers
|
||||
- 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
|
||||
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
|
||||
- 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码
|
||||
@ -51,6 +55,22 @@ CQRS 迁移与收敛。
|
||||
- `CqrsHandlerRegistrar` 现会在单次 reflection 注册流程开始时构建已注册 handler 映射索引
|
||||
- 同一批注册中后续 duplicate handler mapping 不再重复线性扫描 `IServiceCollection`
|
||||
- `GFramework.Cqrs.Tests` 已补充“程序集返回重复 handler 类型时仍只注册一份映射”的回归
|
||||
- `2026-04-29` 已完成一轮 generator fallback 元数据收敛:
|
||||
- `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否同时支持 `params string[]` 与 `params Type[]` 两类 `CqrsReflectionFallbackAttribute` 构造函数
|
||||
- 当本轮 fallback handlers 全部可被生成代码直接引用时,生成器会优先发射 `typeof(...)` 形式的程序集级 fallback 元数据,减少运行时 `Assembly.GetType(...)` 回查
|
||||
- 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续统一发射字符串元数据,避免 mixed 场景只恢复部分 handlers
|
||||
- `GFramework.SourceGenerators.Tests` 已补充 runtime 同时暴露两类构造函数时优先选择直接 `Type` 元数据的回归
|
||||
- `2026-04-29` 已完成一轮 mixed fallback 元数据拆分:
|
||||
- `CqrsReflectionFallbackAttribute` 现显式允许 `AllowMultiple = true`
|
||||
- `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否允许多个 fallback 特性实例
|
||||
- 当本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 同时支持 `Type[]`、`string[]` 和多实例特性时,生成器会拆分输出两段 fallback 元数据
|
||||
- `GFramework.Cqrs.Tests` 已补充 mixed fallback metadata 回归,锁定 registrar 只对字符串条目执行定向 `Assembly.GetType(...)`
|
||||
- `GFramework.SourceGenerators.Tests` 已补充 mixed fallback emission 回归,锁定 generator 会输出两个程序集级 fallback 特性实例而不是整体退回字符串
|
||||
- `2026-04-29` 已重新执行 `$gframework-pr-review`:
|
||||
- 当前分支对应 `PR #302`,状态为 `OPEN`
|
||||
- latest reviewed commit 当前剩余 `3` 条 open AI review threads:`2` 条 Greptile、`1` 条 CodeRabbit
|
||||
- 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断
|
||||
- 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档
|
||||
- 当前主线优先级:
|
||||
- generator 覆盖面继续扩大
|
||||
- dispatch/invoker 反射占比继续下降
|
||||
@ -64,7 +84,6 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
- 模块拆分计划:`ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md`
|
||||
- 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md)
|
||||
- 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md)
|
||||
|
||||
@ -84,9 +103,39 @@ CQRS 迁移与收敛。
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
|
||||
- 结果:通过
|
||||
- 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
|
||||
- 结果:通过
|
||||
- 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
|
||||
- 结果:通过
|
||||
- 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
|
||||
- 结果:通过
|
||||
- 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json`
|
||||
- 结果:通过
|
||||
- 备注:确认当前分支对应 `PR #302`;latest head review 仍有 `3` 条 open AI threads,其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
|
||||
- 结果:通过
|
||||
- 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
|
||||
- 结果:通过
|
||||
- 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口或 dispatch / invoker 反射收敛点继续推进
|
||||
1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口,继续减少仍必须依赖字符串 fallback 元数据的 handler 类型形态
|
||||
2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述
|
||||
3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤
|
||||
|
||||
@ -1,7 +1,84 @@
|
||||
# CQRS 重写迁移追踪
|
||||
|
||||
## 2026-04-29
|
||||
|
||||
### 阶段:mixed fallback 元数据拆分(CQRS-REWRITE-RP-052)
|
||||
|
||||
- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮把上一批的“全部可直接引用 fallback handlers 走 `Type[]`”继续推进到 mixed 场景
|
||||
- 先复核现状后确认:
|
||||
- `CqrsHandlerRegistrar` 已天然支持读取多个 `CqrsReflectionFallbackAttribute` 实例
|
||||
- 上一批真正阻止 mixed 场景继续收敛的点,是 runtime attribute 本身尚未开放多实例,以及 generator 只能二选一发射单个 fallback 特性
|
||||
- 已在 `GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs` 中将特性约束改为 `AllowMultiple = true`,并补充注释说明多个实例的用途
|
||||
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中扩展 fallback 合同探测:
|
||||
- 探测 runtime 是否支持 `params string[]`
|
||||
- 探测 runtime 是否支持 `params Type[]`
|
||||
- 探测 runtime 是否允许多个 `CqrsReflectionFallbackAttribute` 实例
|
||||
- 已在 `CqrsHandlerRegistryGenerator.Models.cs` 与 `CqrsHandlerRegistryGenerator.SourceEmission.cs` 中重构 fallback 发射模型:
|
||||
- fallback 元数据现在可表示为一个或多个程序集级特性实例
|
||||
- 当 fallback handlers 全部可直接引用时,继续优先输出单个 `Type[]` 特性
|
||||
- 当 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 支持多实例时,拆分输出一条 `Type[]` 特性和一条字符串特性
|
||||
- 若 runtime 不支持多实例或缺少相应构造函数,仍整体回退到字符串元数据,避免 mixed 场景漏注册
|
||||
- 已补充 runtime 与 generator 双侧回归:
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 新增 mixed fallback metadata 用例,锁定 registrar 只对字符串条目调用一次 `Assembly.GetType(...)`
|
||||
- `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增 mixed fallback emission 用例,锁定 generator 会输出两个程序集级 fallback 特性实例
|
||||
- 同步更新:
|
||||
- `GFramework.Cqrs.SourceGenerators/README.md`
|
||||
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 说明 mixed 场景现在会拆分 `Type` 元数据与字符串元数据
|
||||
- 定向验证已通过:
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
|
||||
- `13/13` passed
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
|
||||
- `18/18` passed
|
||||
- 随后按 `$gframework-pr-review` 重新拉取当前分支 PR 审查数据:
|
||||
- 当前 worktree `feat/cqrs-optimization` 已对应 `PR #302`
|
||||
- latest head commit 仍有 `3` 条 open AI review threads:Greptile 指向 generator preamble 的死参数与多实例 fallback 特性空行,CodeRabbit 指向 mixed/direct fallback 测试断言过宽
|
||||
- MegaLinter 仍只暴露 `dotnet-format` 的 `Restore operation failed`,未给出本地仍成立的格式文件线索,因此按环境噪音处理
|
||||
- 本轮已继续收口 `RP-052` 的 follow-up:
|
||||
- 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 中移除已不再参与判断的 `generationEnvironment` 透传参数
|
||||
- 调整多实例 fallback 特性发射时的换行策略,避免最后一个 fallback 特性与 `CqrsHandlerRegistryAttribute` 之间保留多余空行
|
||||
- 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补强 direct/mixed fallback 发射断言,锁定特性实例个数、拒绝空 marker,并确保 mixed 场景的程序集级 preamble 排版稳定
|
||||
- 在 `GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs` 中为 `DirectFallbackHandlerType` 补齐 `<returns>` XML 文档
|
||||
- 本轮 review follow-up 验证已通过:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
|
||||
- `0 warning / 0 error`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
|
||||
- `18/18` passed
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
|
||||
- `13/13` passed
|
||||
|
||||
## 2026-04-20
|
||||
|
||||
### 阶段:direct fallback 元数据优先级收敛(CQRS-REWRITE-RP-051)
|
||||
|
||||
- 重新按 `gframework-batch-boot 50` 恢复 `Phase 8` 后,先复核当前 worktree 的恢复入口、`origin/main` 基线与分支规模:
|
||||
- worktree 仍映射到 `cqrs-rewrite`
|
||||
- 基线按批处理约定固定为 `origin/main`
|
||||
- 本轮开始前分支累计 diff 为 `0 files / 0 lines`
|
||||
- 结合当前代码热点与历史归档后,选择本轮批次目标为“继续收敛 generator fallback 元数据,进一步减少 runtime 按字符串类型名回查 handler 的场景”
|
||||
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中新增 runtime fallback 合同探测:
|
||||
- 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params string[]`
|
||||
- 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params Type[]`
|
||||
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs` 与
|
||||
`CqrsHandlerRegistryGenerator.SourceEmission.cs` 中收敛 fallback 发射策略:
|
||||
- 当本轮所有 fallback handlers 都可被生成代码直接引用,且 runtime 支持 `params Type[]` 时,生成器现优先发射 `typeof(...)` 形式的程序集级 fallback 元数据
|
||||
- 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续整体回退到字符串元数据,避免 mixed 场景下部分 handler 走 `Type[]`、其余 handler 丢失恢复入口
|
||||
- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 补充回归:
|
||||
- 锁定 runtime 同时暴露字符串与 `Type` 两类 fallback 构造函数时,生成器优先选择直接 `Type` 元数据
|
||||
- 保留现有字符串 fallback 合同测试,确保旧 contract 兼容路径不回退
|
||||
- 同步更新:
|
||||
- `GFramework.Cqrs.SourceGenerators/README.md`
|
||||
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 说明“可直接引用的 fallback handlers 会优先走 `typeof(...)` 元数据,减少运行时字符串回查”
|
||||
- 定向验证已通过:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
|
||||
- `17/17` passed
|
||||
- 额外修正:
|
||||
- active tracking 中原先引用的 `ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md` 在当前 worktree 已不存在;本轮已移除该失效路径,后续以 active tracking / trace 作为默认恢复入口
|
||||
|
||||
### 阶段:pointer / function pointer 泛型合同拒绝(CQRS-REWRITE-RP-050)
|
||||
|
||||
- 重新执行 `$gframework-pr-review` 后,确认当前分支对应 `PR #261`,状态仍为 `OPEN`
|
||||
@ -72,6 +149,6 @@
|
||||
|
||||
### 当前下一步
|
||||
|
||||
1. 回到 `Phase 8` 主线,优先选一个明确的 dispatch / invoker 反射缩减点继续推进
|
||||
1. 回到 `Phase 8` 主线,优先再找一个 generator 覆盖缺口,继续减少仍需程序集级字符串 fallback 元数据的 handler 场景
|
||||
2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述
|
||||
3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号
|
||||
|
||||
@ -32,6 +32,7 @@ runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整
|
||||
- 程序集级 `CqrsReflectionFallbackAttribute`
|
||||
|
||||
这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。
|
||||
如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
@ -108,6 +109,9 @@ RegisterCqrsHandlersFromAssemblies(
|
||||
- 能直接引用的 handler,生成直接注册语句
|
||||
- 实现类型不能直接引用、但服务接口还能精确表达时,生成反射实现类型查找
|
||||
- 服务接口本身也需要运行时解析时,生成精确 type lookup
|
||||
- 当 fallback handlers 全部可直接引用且 runtime 暴露 `params Type[]` 合同时,优先发射直接 `Type` 元数据
|
||||
- 当 mixed 场景同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 允许多个 fallback 特性实例时,拆分发射 `Type` 元数据和字符串元数据
|
||||
- 其余场景统一回退到字符串元数据,避免 mixed 场景漏注册
|
||||
- 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果
|
||||
|
||||
如果当前编译环境缺少这个 fallback 合同,而某些 handler 又必须依赖它,生成器会报:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user