using System.Text;
using Dpz.Core.SourceGenerator.Models;
namespace Dpz.Core.SourceGenerator;
/// <summary>
/// 为服务实现生成缓存前缀、方法标签和缓存键元数据。
/// </summary>
internal static class CacheMetadataSourceGenerator
{
internal static string Generate(ServiceRegistration registration)
{
var defaultPrefix = GetCachePrefix(registration.ImplementationType);
var source = new StringBuilder();
source.AppendLine("// <auto-generated />");
source.AppendLine("#nullable enable");
source.AppendLine();
source.Append("namespace ");
source.Append(registration.ImplementationNamespace);
source.AppendLine(";");
source.AppendLine();
source.Append("internal static class ");
source.AppendLine(registration.CacheMetadataName);
source.AppendLine("{");
source.Append(" public const string DefaultPrefix = ");
source.Append(SymbolDisplay.ToCSharpStringLiteral(defaultPrefix));
source.AppendLine(";");
source.AppendLine();
foreach (var method in GetDistinctCachedMethods(registration))
{
var prefix = method.Options.Prefix ?? defaultPrefix;
source.Append(" public const string ");
source.Append(method.Name);
source.Append("Prefix = ");
source.Append(SymbolDisplay.ToCSharpStringLiteral(prefix));
source.AppendLine(";");
source.Append(" public const string ");
source.Append(method.Name);
source.Append("MethodTag = ");
source.Append(SymbolDisplay.ToCSharpStringLiteral(prefix + ":" + method.Name));
source.AppendLine(";");
source.AppendLine();
}
AppendPrefixMethods(source, registration, defaultPrefix);
foreach (var method in registration.CachedMethods)
{
AppendCacheKeyMethod(source, method, method.Options.Prefix ?? defaultPrefix);
}
if (registration.CachedMethods.Length > 0)
{
AppendCacheKeyHelpers(source);
}
source.AppendLine("}");
return source.ToString();
}
private static void AppendPrefixMethods(
StringBuilder source,
ServiceRegistration registration,
string defaultPrefix
)
{
var distinctMethods = GetDistinctCachedMethods(registration).ToList();
var customPrefixes = distinctMethods
.Where(method =>
!string.IsNullOrWhiteSpace(method.Options.Prefix)
&& method.Options.Prefix != defaultPrefix
)
.ToList();
source.AppendLine(" public static string GetPrefix(string methodName)");
source.AppendLine(" {");
if (customPrefixes.Count == 0)
{
source.AppendLine(" return DefaultPrefix;");
}
else
{
source.AppendLine(" return methodName switch");
source.AppendLine(" {");
foreach (var method in customPrefixes)
{
source.Append(" ");
source.Append(SymbolDisplay.ToCSharpStringLiteral(method.Name));
source.Append(" => ");
source.Append(method.Name);
source.AppendLine("Prefix,");
}
source.AppendLine(" _ => DefaultPrefix,");
source.AppendLine(" };");
}
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" public static string GetMethodTag(string methodName)");
source.AppendLine(" {");
if (distinctMethods.Count == 0)
{
source.AppendLine(" return DefaultPrefix + \":\" + methodName;");
}
else
{
source.AppendLine(" return methodName switch");
source.AppendLine(" {");
foreach (var method in distinctMethods)
{
source.Append(" ");
source.Append(SymbolDisplay.ToCSharpStringLiteral(method.Name));
source.Append(" => ");
source.Append(method.Name);
source.AppendLine("MethodTag,");
}
source.AppendLine(" _ => GetPrefix(methodName) + \":\" + methodName,");
source.AppendLine(" };");
}
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" public static string[] GetPrefixes()");
source.AppendLine(" {");
source.Append(" return new[] { ");
AppendDistinctPrefixes(source, registration, defaultPrefix);
source.AppendLine(" };");
source.AppendLine(" }");
source.AppendLine();
}
private static void AppendCacheKeyMethod(
StringBuilder source,
CachedMethod method,
string prefix
)
{
source.Append(" public static string Build");
source.Append(method.Name);
source.Append("CacheKey(");
AppendCacheKeyParameterList(source, method);
source.AppendLine(")");
source.AppendLine(" {");
source.Append(" return ");
AppendCacheKeyExpression(source, method, prefix);
source.AppendLine(";");
source.AppendLine(" }");
source.AppendLine();
}
private static void AppendCacheKeyExpression(
StringBuilder source,
CachedMethod method,
string prefix
)
{
var baseKey = prefix + ":" + method.Name;
if (!string.IsNullOrWhiteSpace(method.Options.CacheKey))
{
source.Append(SymbolDisplay.ToCSharpStringLiteral(baseKey + ":"));
source.Append(" + EscapeCacheKeySegment(");
source.Append(SymbolDisplay.ToCSharpStringLiteral(method.Options.CacheKey!));
source.Append(")");
return;
}
var parameters = GetCacheKeyParameters(method).ToList();
if (parameters.Count == 0)
{
source.Append(SymbolDisplay.ToCSharpStringLiteral(baseKey));
return;
}
source.Append("string.Concat(");
source.Append(SymbolDisplay.ToCSharpStringLiteral(baseKey + ":"));
source.Append(", ");
for (var i = 0; i < parameters.Count; i++)
{
if (i > 0)
{
source.Append(", \"&\", ");
}
var parameter = parameters[i];
source.Append(SymbolDisplay.ToCSharpStringLiteral(parameter.Name + "="));
source.Append(", FormatCacheKeyValue(");
source.Append(parameter.Name);
source.Append(")");
}
source.Append(")");
}
private static void AppendCacheKeyParameterList(StringBuilder source, CachedMethod method)
{
var keyParameters = GetCacheKeyParameters(method).ToList();
for (var i = 0; i < keyParameters.Count; i++)
{
if (i > 0)
{
source.Append(", ");
}
source.Append(keyParameters[i].TypeName);
source.Append(" ");
source.Append(keyParameters[i].Name);
}
}
private static IEnumerable<CachedParameter> GetCacheKeyParameters(CachedMethod method)
{
if (!string.IsNullOrWhiteSpace(method.Options.CacheKey))
{
return [];
}
return method.Parameters.Where(parameter =>
parameter.TypeName != "global::System.Threading.CancellationToken"
);
}
private static void AppendCacheKeyHelpers(StringBuilder source)
{
source.AppendLine(" private static string FormatCacheKeyValue<T>(T value)");
source.AppendLine(" {");
source.AppendLine(" if (value is null)");
source.AppendLine(" {");
source.AppendLine(" return string.Empty;");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" if (value is string text)");
source.AppendLine(" {");
source.AppendLine(" return EscapeCacheKeySegment(text);");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" if (value is global::System.Collections.IEnumerable values)");
source.AppendLine(" {");
source.AppendLine(" return FormatCacheKeyCollection(values);");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" return FormatCacheKeySingleValue(value);");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(
" private static string FormatCacheKeyCollection(global::System.Collections.IEnumerable values)"
);
source.AppendLine(" {");
source.AppendLine(
" var segments = new global::System.Collections.Generic.List<string>();"
);
source.AppendLine(" foreach (var value in values)");
source.AppendLine(" {");
source.AppendLine(" var segment = FormatCacheKeySingleValue(value);");
source.AppendLine(" if (!string.IsNullOrEmpty(segment))");
source.AppendLine(" {");
source.AppendLine(" segments.Add(segment);");
source.AppendLine(" }");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" segments.Sort(global::System.StringComparer.Ordinal);");
source.AppendLine(" return string.Join(\",\", segments);");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" private static string FormatCacheKeySingleValue(object? value)");
source.AppendLine(" {");
source.AppendLine(" if (value is null)");
source.AppendLine(" {");
source.AppendLine(" return string.Empty;");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" return value switch");
source.AppendLine(" {");
source.AppendLine(
" global::System.DateTime dateTime => EscapeCacheKeySegment(dateTime.ToString(\"O\", global::System.Globalization.CultureInfo.InvariantCulture)),"
);
source.AppendLine(
" global::System.DateTimeOffset dateTimeOffset => EscapeCacheKeySegment(dateTimeOffset.ToString(\"O\", global::System.Globalization.CultureInfo.InvariantCulture)),"
);
source.AppendLine(
" global::System.IFormattable formattable => EscapeCacheKeySegment(formattable.ToString(null, global::System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty),"
);
source.AppendLine(
" _ => EscapeCacheKeySegment(value.ToString() ?? string.Empty),"
);
source.AppendLine(" };");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" private static string EscapeCacheKeySegment(string value)");
source.AppendLine(" {");
source.AppendLine(" return value");
source.AppendLine(" .Replace(\"%\", \"%25\")");
source.AppendLine(" .Replace(\":\", \"%3A\")");
source.AppendLine(" .Replace(\"&\", \"%26\")");
source.AppendLine(" .Replace(\"=\", \"%3D\")");
source.AppendLine(" .Replace(\",\", \"%2C\");");
source.AppendLine(" }");
source.AppendLine();
}
private static void AppendDistinctPrefixes(
StringBuilder source,
ServiceRegistration registration,
string defaultPrefix
)
{
var prefixes = new[] { defaultPrefix }
.Concat(
registration
.CachedMethods.Select(method => method.Options.Prefix)
.Where(static prefix => !string.IsNullOrWhiteSpace(prefix))
.Select(static prefix => prefix!)
)
.Distinct(StringComparer.Ordinal)
.ToList();
for (var i = 0; i < prefixes.Count; i++)
{
if (i > 0)
{
source.Append(", ");
}
source.Append(SymbolDisplay.ToCSharpStringLiteral(prefixes[i]));
}
}
private static IEnumerable<CachedMethod> GetDistinctCachedMethods(
ServiceRegistration registration
)
{
return registration
.CachedMethods.GroupBy(method => method.Name)
.Select(static group => group.First());
}
private static string GetCachePrefix(string fullyQualifiedTypeName)
{
return fullyQualifiedTypeName.Replace("global::", string.Empty);
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码说明文档
概述
这是一个用于生成缓存元数据代码的源代码生成器(Source Generator)。它为服务实现类自动生成缓存相关的常量、方法和辅助工具,主要用于构建缓存键(Cache Key)、缓存前缀(Prefix)和方法标签(Method Tag)。
核心功能
1. Generate 方法(主入口)
根据服务注册信息生成完整的静态类代码,包含:
- 默认缓存前缀常量
- 每个缓存方法的前缀和方法标签常量
- 前缀获取方法
- 缓存键构建方法
- 辅助工具方法
生成的代码示例:
namespace MyNamespace;
internal static class MyCacheMetadata
{
public const string DefaultPrefix = "MyService";
public const string GetUserPrefix = "MyService";
public const string GetUserMethodTag = "MyService:GetUser";
// ... 其他生成的代码
}
2. AppendPrefixMethods 方法
生成三个关键方法:
a) GetPrefix(string methodName)
根据方法名返回对应的缓存前缀,支持自定义前缀。
b) GetMethodTag(string methodName)
生成方法标签,格式为 Prefix:MethodName。
c) GetPrefixes()
返回所有不重复的缓存前缀数组。
3. AppendCacheKeyMethod 方法
为每个缓存方法生成专用的缓存键构建方法。
生成示例:
public static string BuildGetUserCacheKey(int userId, string name)
{
return string.Concat("MyService:GetUser:", "userId=", FormatCacheKeyValue(userId),
"&", "name=", FormatCacheKeyValue(name));
}
4. AppendCacheKeyExpression 方法
生成缓存键的具体表达式,支持三种模式:
- 自定义缓存键:使用预定义的
CacheKey选项 - 无参数方法:直接使用
Prefix:MethodName - 带参数方法:组合参数值,格式为
Prefix:MethodName:param1=value1¶m2=value2
5. AppendCacheKeyHelpers 方法
生成辅助工具方法:
a) FormatCacheKeyValue<T>(T value)
格式化缓存键值,处理:
- null 值 → 空字符串
- 字符串 → 转义处理
- 集合 → 调用集合格式化
- 其他类型 → 调用单值格式化
b) FormatCacheKeyCollection(IEnumerable values)
处理集合类型参数:
- 遍历集合元素
- 格式化每个元素
- 排序后用逗号连接(确保相同集合生成相同缓存键)
c) FormatCacheKeySingleValue(object? value)
格式化单个值,特殊处理:
DateTime/DateTimeOffset→ ISO 8601 格式("O")IFormattable→ 使用不变文化格式化- 其他 → 调用
ToString()
d) EscapeCacheKeySegment(string value)
转义缓存键中的特殊字符:
% → %25
: → %3A
& → %26
= → %3D
, → %2C
辅助方法
GetCacheKeyParameters
筛选需要参与缓存键构建的参数(排除 CancellationToken)。
GetDistinctCachedMethods
按方法名去重,避免重复生成元数据。
GetCachePrefix
从完全限定类型名移除 global:: 前缀作为默认缓存前缀。
设计亮点
- 字符串转义:防止缓存键中的特殊字符导致解析错误
- 集合排序:确保
{1,2,3}和{3,2,1}生成相同的缓存键 - 文化无关格式化:使用
InvariantCulture确保跨环境一致性 - 类型安全:使用
SymbolDisplay.ToCSharpStringLiteral生成安全的 C# 字符串字面量 - 可扩展性:支持自定义前缀和缓存键模板
使用场景
这个生成器通常用于缓存装饰器模式或**AOP(面向切面编程)**场景,自动为服务方法生成标准化的缓存键管理代码,减少手动编写和维护成本。
评论加载中...