using System.Collections.Immutable;
using System.Text;
using Dpz.Core.SourceGenerator.Models;

namespace Dpz.Core.SourceGenerator;

/// <summary>
/// 根据缓存元数据生成缓存装饰器源码。
/// </summary>
/// <remarks>
/// 生成的缓存键格式为 <c>{namespace}.{className}:{method}:key1=value1&amp;key2=value2</c>。
/// </remarks>
internal static class CacheDecoratorSourceGenerator
{
    internal static string Generate(ServiceRegistration registration)
    {
        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 sealed class ");
        source.Append(registration.DecoratorName);
        source.Append("(");
        source.Append(registration.ImplementationType);
        source.Append(" inner, global::ZiggyCreatures.Caching.Fusion.IFusionCache fusionCache) : ");
        source.AppendLine(registration.ServiceType);
        source.AppendLine("{");

        foreach (var property in registration.InterfaceProperties)
        {
            AppendDecoratorProperty(source, property);
        }

        foreach (var @event in registration.InterfaceEvents)
        {
            AppendDecoratorEvent(source, @event);
        }

        foreach (var method in registration.InterfaceMethods)
        {
            AppendDecoratorMethod(source, registration, method);
        }

        source.AppendLine("}");
        return source.ToString();
    }

    private static void AppendDecoratorMethod(
        StringBuilder source,
        ServiceRegistration registration,
        InterfaceMethod method
    )
    {
        var cachedMethod = FindCachedMethod(registration, method);
        var invalidatedMethod = FindInvalidatedMethod(registration, method);

        source.Append("    public ");
        if (cachedMethod.Name is not null || invalidatedMethod.Name is not null)
        {
            source.Append("async ");
        }

        source.Append(method.ReturnType);
        source.Append(" ");
        source.Append(method.Name);
        source.Append(method.TypeParameterList);
        source.Append("(");
        AppendParameterList(source, method.Parameters);
        source.Append(")");
        source.AppendLine(method.ConstraintClauses);
        source.AppendLine("    {");

        if (cachedMethod.Name is not null)
        {
            AppendCachedMethodBody(source, registration, cachedMethod);
        }
        else if (invalidatedMethod.Name is not null)
        {
            AppendInvalidatedMethodBody(source, registration, invalidatedMethod);
        }
        else
        {
            source.Append("        return inner.");
            source.Append(method.Name);
            source.Append("(");
            AppendArgumentList(source, method.Parameters);
            source.AppendLine(");");
        }

        source.AppendLine("    }");
        source.AppendLine();
    }

    private static CachedMethod FindCachedMethod(
        ServiceRegistration registration,
        InterfaceMethod method
    )
    {
        return registration.CachedMethods.FirstOrDefault(cached =>
            cached.Name == method.Name
            && cached.Parameters.Length == method.Parameters.Length
            && cached
                .Parameters.Zip(method.Parameters, (left, right) => left.TypeName == right.TypeName)
                .All(static match => match)
        );
    }

    private static InvalidatedMethod FindInvalidatedMethod(
        ServiceRegistration registration,
        InterfaceMethod method
    )
    {
        return registration.InvalidatedMethods.FirstOrDefault(invalidated =>
            invalidated.Name == method.Name
            && invalidated.Parameters.Length == method.Parameters.Length
            && invalidated
                .Parameters.Zip(method.Parameters, (left, right) => left.TypeName == right.TypeName)
                .All(static match => match)
        );
    }

    private static void AppendCachedMethodBody(
        StringBuilder source,
        ServiceRegistration registration,
        CachedMethod cachedMethod
    )
    {
        var prefix = cachedMethod.Options.Prefix ?? GetCachePrefix(registration.ImplementationType);
        var cancellationToken = GetCancellationTokenArgument(cachedMethod.Parameters);

        source.Append("        var cacheKey = ");
        AppendGeneratedCacheKeyCall(source, registration, cachedMethod);
        source.AppendLine(";");
        // IPagedList<T> is an interface and does not round-trip through FusionCache serializers
        // reliably, so generated decorators cache the serializable PagedListWarp<T> DTO instead.
        if (TryGetPagedListValueType(cachedMethod.ValueType, out var pagedListItemType))
        {
            source.AppendLine("        var cached = await fusionCache.GetOrSetAsync<");
            source.Append("            global::Dpz.Core.Web.Pager.PagedListWarp<");
            source.Append(pagedListItemType);
            source.AppendLine(">>(");
            source.AppendLine("            cacheKey,");
            source.AppendLine("            async (context, factoryCancellationToken) =>");
            source.AppendLine("            {");
            AppendGeneratedCacheTags(source, registration, prefix, cachedMethod);
            source.Append("                var pagedList = await inner.");
            source.Append(cachedMethod.Name);
            source.Append("(");
            AppendArgumentList(source, cachedMethod.Parameters);
            source.AppendLine(");");
            source.AppendLine("                return pagedList.ToPagedListWarp();");
            source.AppendLine("            },");
            AppendCacheEntryOptions(source, cachedMethod);
            source.AppendLine(",");
            source.Append("            token: ");
            source.Append(cancellationToken);
            source.AppendLine();
            source.AppendLine("        );");
            source.AppendLine("        var result = cached.ToPagedList();");
            AppendPostProcessCall(source, cachedMethod, "result");
            source.AppendLine("        return result;");
            return;
        }

        source.AppendLine("        var result = await fusionCache.GetOrSetAsync<");
        source.Append("            ");
        source.Append(cachedMethod.ValueType);
        source.AppendLine(">(");
        source.AppendLine("            cacheKey,");
        source.AppendLine("            async (context, factoryCancellationToken) =>");
        source.AppendLine("            {");
        AppendGeneratedCacheTags(source, registration, prefix, cachedMethod);
        source.Append("                return await inner.");
        source.Append(cachedMethod.Name);
        source.Append("(");
        AppendArgumentList(source, cachedMethod.Parameters);
        source.AppendLine(");");
        source.AppendLine("            },");
        AppendCacheEntryOptions(source, cachedMethod);
        source.AppendLine(",");
        source.Append("            token: ");
        source.Append(cancellationToken);
        source.AppendLine();
        source.AppendLine("        );");
        AppendPostProcessCall(source, cachedMethod, "result");
        source.AppendLine("        return result;");
    }

    private static void AppendCacheEntryOptions(StringBuilder source, CachedMethod cachedMethod)
    {
        source.Append(
            "            options => options.SetDuration(global::System.TimeSpan.FromSeconds("
        );
        source.Append(cachedMethod.Options.ExpirationSeconds);
        source.Append("))");
        if (cachedMethod.Options.HasExplicitExpirationSeconds)
        {
            source.Append(".SetFailSafe(false)");
        }
    }

    private static void AppendInvalidatedMethodBody(
        StringBuilder source,
        ServiceRegistration registration,
        InvalidatedMethod invalidatedMethod
    )
    {
        var cancellationToken = GetCancellationTokenArgument(invalidatedMethod.Parameters);
        if (IsGenericAwaitable(invalidatedMethod.ReturnType))
        {
            source.Append("        var result = await inner.");
            source.Append(invalidatedMethod.Name);
            source.Append("(");
            AppendArgumentList(source, invalidatedMethod.Parameters);
            source.AppendLine(");");
            AppendRemoveMethodTags(source, registration, invalidatedMethod, cancellationToken);
            source.AppendLine("        return result;");
            return;
        }

        source.Append("        await inner.");
        source.Append(invalidatedMethod.Name);
        source.Append("(");
        AppendArgumentList(source, invalidatedMethod.Parameters);
        source.AppendLine(");");
        AppendRemoveMethodTags(source, registration, invalidatedMethod, cancellationToken);
    }

    private static bool IsGenericAwaitable(string returnType)
    {
        return returnType.StartsWith(
                "global::System.Threading.Tasks.Task<",
                StringComparison.Ordinal
            )
            || returnType.StartsWith(
                "global::System.Threading.Tasks.ValueTask<",
                StringComparison.Ordinal
            );
    }

    private static void AppendRemoveMethodTags(
        StringBuilder source,
        ServiceRegistration registration,
        InvalidatedMethod invalidatedMethod,
        string cancellationToken
    )
    {
        source.Append("        await fusionCache.RemoveByTagAsync(new[] { ");
        for (var i = 0; i < invalidatedMethod.Methods.Length; i++)
        {
            if (i > 0)
            {
                source.Append(", ");
            }

            source.Append(registration.CacheMetadataName);
            source.Append(".");
            source.Append(invalidatedMethod.Methods[i]);
            source.Append("MethodTag");
        }

        source.Append(" }, token: ");
        source.Append(cancellationToken);
        source.AppendLine(");");
    }

    private static void AppendDecoratorProperty(StringBuilder source, InterfaceProperty property)
    {
        source.Append("    public ");
        source.Append(property.TypeName);
        source.Append(" ");
        source.Append(property.Name);
        source.AppendLine();
        source.AppendLine("    {");

        if (property.HasGetter)
        {
            source.Append("        get => inner.");
            source.Append(property.Name);
            source.AppendLine(";");
        }

        if (property.HasSetter)
        {
            source.Append("        set => inner.");
            source.Append(property.Name);
            source.AppendLine(" = value;");
        }

        source.AppendLine("    }");
        source.AppendLine();
    }

    private static void AppendDecoratorEvent(StringBuilder source, InterfaceEvent @event)
    {
        source.Append("    public event ");
        source.Append(@event.TypeName);
        source.Append(" ");
        source.Append(@event.Name);
        source.AppendLine();
        source.AppendLine("    {");
        source.Append("        add => inner.");
        source.Append(@event.Name);
        source.AppendLine(" += value;");
        source.Append("        remove => inner.");
        source.Append(@event.Name);
        source.AppendLine(" -= value;");
        source.AppendLine("    }");
        source.AppendLine();
    }

    private static void AppendParameterList(
        StringBuilder source,
        ImmutableArray<CachedParameter> parameters
    )
    {
        for (var i = 0; i < parameters.Length; i++)
        {
            if (i > 0)
            {
                source.Append(", ");
            }

            if (parameters[i].IsParams)
            {
                source.Append("params ");
            }

            AppendRefKind(source, parameters[i].RefKind);
            source.Append(parameters[i].TypeName);
            source.Append(" ");
            source.Append(parameters[i].Name);
            source.Append(parameters[i].DefaultValueLiteral);
        }
    }

    private static void AppendArgumentList(
        StringBuilder source,
        ImmutableArray<CachedParameter> parameters
    )
    {
        for (var i = 0; i < parameters.Length; i++)
        {
            if (i > 0)
            {
                source.Append(", ");
            }

            AppendRefKind(source, parameters[i].RefKind);
            source.Append(parameters[i].Name);
        }
    }

    private static void AppendRefKind(StringBuilder source, Microsoft.CodeAnalysis.RefKind refKind)
    {
        switch (refKind)
        {
            case Microsoft.CodeAnalysis.RefKind.Ref:
                source.Append("ref ");
                break;
            case Microsoft.CodeAnalysis.RefKind.Out:
                source.Append("out ");
                break;
            case Microsoft.CodeAnalysis.RefKind.In:
                source.Append("in ");
                break;
        }
    }

    private static string GetCancellationTokenArgument(ImmutableArray<CachedParameter> parameters)
    {
        foreach (var parameter in parameters)
        {
            if (parameter.TypeName == "global::System.Threading.CancellationToken")
            {
                return parameter.Name;
            }
        }

        return "default";
    }

    private static void AppendGeneratedCacheKeyCall(
        StringBuilder source,
        ServiceRegistration registration,
        CachedMethod method
    )
    {
        source.Append(registration.CacheMetadataName);
        source.Append(".Build");
        source.Append(method.Name);
        source.Append("CacheKey(");
        AppendCacheKeyArgumentList(source, method);
        source.Append(")");
    }

    private static bool TryGetPagedListValueType(string valueType, out string itemType)
    {
        const string pagedListPrefix = "global::Dpz.Core.Web.Pager.IPagedList<";
        itemType = string.Empty;
        if (!valueType.StartsWith(pagedListPrefix, StringComparison.Ordinal))
        {
            return false;
        }

        itemType = valueType.Substring(
            pagedListPrefix.Length,
            valueType.Length - pagedListPrefix.Length - 1
        );
        return true;
    }

    private static void AppendCacheKeyArgumentList(StringBuilder source, CachedMethod method)
    {
        if (!string.IsNullOrWhiteSpace(method.Options.CacheKey))
        {
            return;
        }

        var parameters = method
            .Parameters.Where(parameter =>
                parameter.TypeName != "global::System.Threading.CancellationToken"
            )
            .ToList();

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

            source.Append(parameters[i].Name);
        }
    }

    private static void AppendGeneratedCacheTags(
        StringBuilder source,
        ServiceRegistration registration,
        string prefix,
        CachedMethod cachedMethod
    )
    {
        source.Append("                context.Tags = new[] { ");
        source.Append(ToCSharpStringLiteral(prefix));
        source.Append(", ");
        source.Append(registration.CacheMetadataName);
        source.Append(".");
        source.Append(cachedMethod.Name);
        source.Append("MethodTag");
        foreach (var tag in cachedMethod.Options.AdditionalTags)
        {
            source.Append(", ");
            source.Append(ToCSharpStringLiteral(tag));
        }
        source.AppendLine(" };");
    }

    private static void AppendPostProcessCall(
        StringBuilder source,
        CachedMethod cachedMethod,
        string valueExpression
    )
    {
        if (string.IsNullOrWhiteSpace(cachedMethod.Options.PostProcess))
        {
            return;
        }

        source.Append("        await inner.");
        source.Append(cachedMethod.Options.PostProcess);
        source.Append("(");
        source.Append(valueExpression);
        source.AppendLine(");");
    }

    private static string ToCSharpStringLiteral(string value)
    {
        return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
    }

    private static string GetCachePrefix(string fullyQualifiedTypeName)
    {
        return fullyQualifiedTypeName.Replace("global::", string.Empty);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 C# 源代码生成器类,用于自动生成缓存装饰器(Cache Decorator)代码。它是编译时代码生成的一部分,基于元数据自动创建缓存代理类。

核心功能

1. 主要目的

为服务接口自动生成一个装饰器类,该装饰器:

  • 拦截接口方法调用
  • 在方法调用前后添加缓存逻辑
  • 使用 FusionCache 进行缓存管理

2. 生成的代码结构

// 生成的装饰器类示例
internal sealed class ServiceDecorator(Service inner, IFusionCache fusionCache) : IService
{
    // 属性代理
    public string Property { get => inner.Property; set => inner.Property = value; }
    
    // 事件代理
    public event EventHandler Event { add => inner.Event += value; remove => inner.Event -= value; }
    
    // 缓存方法
    public async Task<Data> GetData(int id, CancellationToken token) { /* 缓存逻辑 */ }
}

关键方法说明

Generate(ServiceRegistration)

主入口方法,生成完整的装饰器类代码:

  • 生成类声明(带构造函数注入)
  • 生成属性代理
  • 生成事件代理
  • 生成方法代理(含缓存逻辑)

AppendDecoratorMethod()

生成方法的核心逻辑:

  1. 普通方法:直接转发到内部实现
  2. 缓存方法(带 [Cached] 特性):添加缓存读写逻辑
  3. 失效方法(带 [Invalidate] 特性):添加缓存清除逻辑

AppendCachedMethodBody()

生成缓存方法体:

var cacheKey = BuildCacheKey(param1, param2);
var result = await fusionCache.GetOrSetAsync<T>(
    cacheKey,
    async (context, token) => {
        context.Tags = ["prefix", "methodTag"];
        return await inner.Method(param1, param2);
    },
    options => options.SetDuration(TimeSpan.FromSeconds(60))
);
return result;

AppendInvalidatedMethodBody()

生成缓存失效方法体:

var result = await inner.Method(params);
await fusionCache.RemoveByTagAsync(new[] { "tag1", "tag2" }, token);
return result;

特殊处理

分页列表(IPagedList)

  • 检测 IPagedList<T> 类型
  • 转换为可序列化的 PagedListWarp<T> 进行缓存
  • 从缓存读取后再转换回 IPagedList<T>

CancellationToken 处理

  • 自动检测参数中的 CancellationToken
  • 如果没有则使用 default

缓存键生成

格式:{namespace}.{className}:{method}:key1=value1&key2=value2

辅助功能

方法作用
FindCachedMethod根据方法签名匹配元数据中的缓存配置
AppendDecoratorProperty生成属性的 getter/setter 代理
AppendDecoratorEvent生成事件的 add/remove 代理
AppendParameterList生成带修饰符的参数列表(ref/out/in/params)
ToCSharpStringLiteral转义字符串为 C# 字符串字面量

使用场景

这个代码生成器通常在以下场景使用:

  1. 自动缓存服务方法:无需手写缓存逻辑
  2. 缓存失效管理:自动清理相关缓存标签
  3. 编译时生成:零运行时反射开销
  4. 类型安全:完全保留原始接口契约

典型的使用模式是通过 Roslyn Source Generator 在编译时调用此类生成代码。

评论加载中...