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 方法

生成缓存键的具体表达式,支持三种模式:

  1. 自定义缓存键:使用预定义的 CacheKey 选项
  2. 无参数方法:直接使用 Prefix:MethodName
  3. 带参数方法:组合参数值,格式为 Prefix:MethodName:param1=value1&param2=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. 集合排序:确保 {1,2,3}{3,2,1} 生成相同的缓存键
  3. 文化无关格式化:使用 InvariantCulture 确保跨环境一致性
  4. 类型安全:使用 SymbolDisplay.ToCSharpStringLiteral 生成安全的 C# 字符串字面量
  5. 可扩展性:支持自定义前缀和缓存键模板

使用场景

这个生成器通常用于缓存装饰器模式或**AOP(面向切面编程)**场景,自动为服务方法生成标准化的缓存键管理代码,减少手动编写和维护成本。

评论加载中...