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(", ");
        var segments = parameters
            .SelectMany(parameter =>
                parameter.CacheKeySegments.IsDefaultOrEmpty
                    ?
                    [
                        new CachedKeySegment(
                            parameter.Name,
                            "FormatCacheKeyValue(" + parameter.Name + ")"
                        ),
                    ]
                    : parameter.CacheKeySegments
            )
            .ToList();

        for (var i = 0; i < segments.Count; i++)
        {
            if (i > 0)
            {
                source.Append(", \"&\", ");
            }

            var segment = segments[i];
            source.Append(SymbolDisplay.ToCSharpStringLiteral(segment.Name + "="));
            source.Append(", ");
            source.Append(segment.ValueExpression);
        }
        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 FormatCacheKeyCollection<T>(global::System.Collections.Generic.IEnumerable<T> values, global::System.Func<T, string> formatter)"
        );
        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 = formatter(value);");
        source.AppendLine("            if (!string.IsNullOrEmpty(segment))");
        source.AppendLine("            {");
        source.AppendLine("                segments.Add(EscapeCacheKeySegment(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分析的结果,请根据实际情况进行判断。

代码解释

这是一个 C# 源代码生成器,专门用于为服务类生成缓存相关的元数据代码。它是 Roslyn 源代码生成器的一部分,在编译时自动生成辅助代码。

主要功能

1. Generate 方法 - 核心生成逻辑

internal static string Generate(ServiceRegistration registration)
  • 为给定的服务注册信息生成一个静态类
  • 生成的类包含:
    • 默认缓存前缀常量
    • 每个缓存方法的前缀和方法标签常量
    • 动态获取前缀/方法标签的方法
    • 缓存键生成方法
    • 辅助工具方法

2. AppendPrefixMethods - 生成前缀相关方法

生成三个方法:

  • GetPrefix(string methodName) - 根据方法名获取缓存前缀
  • GetMethodTag(string methodName) - 获取方法标签(前缀:方法名)
  • GetPrefixes() - 获取所有不重复的前缀数组

3. AppendCacheKeyMethod - 生成缓存键构建方法

为每个缓存方法生成如 BuildXxxCacheKey(...) 的方法:

public static string BuildGetUserCacheKey(int userId)
{
    return "prefix:GetUser:" + userId;
}

4. AppendCacheKeyExpression - 生成缓存键表达式

根据不同情况生成缓存键:

  • 自定义缓存键:使用 CacheKey 属性指定的键
  • 无参数方法:仅使用 "前缀:方法名"
  • 有参数方法:拼接所有参数为 "前缀:方法名:参数1=值1&参数2=值2"

5. AppendCacheKeyHelpers - 生成辅助方法

生成多个辅助方法用于格式化缓存键值:

  • FormatCacheKeyValue<T> - 泛型格式化
  • FormatCacheKeyCollection - 处理集合类型
  • FormatCacheKeySingleValue - 处理单个值(DateTime、IFormattable等)
  • EscapeCacheKeySegment - 转义特殊字符(%、:、&、=、,)

生成代码示例

假设有一个服务类 UserService,带有缓存方法 GetUser(int userId),生成的代码类似:

// <auto-generated />
#nullable enable

namespace MyApp.Services;

internal static class UserServiceCacheMetadata
{
    public const string DefaultPrefix = "MyApp.Services.UserService";
    public const string GetUserPrefix = "MyApp.Services.UserService";
    public const string GetUserMethodTag = "MyApp.Services.UserService:GetUser";

    public static string GetPrefix(string methodName) { ... }
    public static string GetMethodTag(string methodName) { ... }
    public static string[] GetPrefixes() { ... }
    
    public static string BuildGetUserCacheKey(int userId)
    {
        return string.Concat("MyApp.Services.UserService:GetUser:", "userId=", FormatCacheKeyValue(userId));
    }
    
    private static string FormatCacheKeyValue<T>(T value) { ... }
    private static string EscapeCacheKeySegment(string value) { ... }
}

设计亮点

  1. 编译时生成:避免运行时反射,提高性能
  2. 类型安全:生成强类型方法
  3. 智能处理:自动处理集合、日期时间等复杂类型
  4. URL安全:转义特殊字符确保缓存键有效
  5. 可扩展:支持自定义前缀和缓存键
评论加载中...