To create a code generator that automatically caches any function with a cache attribute that takes a duration, you can use a source generator in .NET. Source generators allow you to generate additional source code at compile time.


Step 1: Define the Cache Attribute

First, define the cache attribute that will be used to mark methods for caching:

using System;

[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class CacheAttribute : Attribute
{
    public CacheAttribute(int durationInSeconds)
    {
        DurationInSeconds = durationInSeconds;
    }

    public int DurationInSeconds { get; }
}

Step 2: Create the Source Generator

Next, create the source generator that will generate the caching logic for methods marked with the CacheAttribute.

  1. Create a new class library project for the source generator.

  2. Add the necessary NuGet packages: Microsoft.CodeAnalysis.CSharp, Microsoft.CodeAnalysis.Analyzers, and Microsoft.CodeAnalysis.CSharp.Workspaces.

  3. Implement the source generator:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;

[Generator]
public class CacheSourceGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Register a syntax receiver that will be created for each generation pass
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // Retrieve the populated receiver
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
            return;

        // Get the cache attribute symbol
        var cacheAttributeSymbol = context.Compilation.GetTypeByMetadataName("CacheAttribute");

        foreach (var method in receiver.CandidateMethods)
        {
            var model = context.Compilation.GetSemanticModel(method.SyntaxTree);
            var methodSymbol = model.GetDeclaredSymbol(method) as IMethodSymbol;

            if (methodSymbol == null)
                continue;

            // Check if the method has the CacheAttribute
            var cacheAttribute = methodSymbol.GetAttributes().FirstOrDefault(ad => ad.AttributeClass.Equals(cacheAttributeSymbol, SymbolEqualityComparer.Default));
            if (cacheAttribute == null)
                continue;

            // Get the duration from the attribute
            var duration = (int)cacheAttribute.ConstructorArguments[0].Value;

            // Generate the caching logic
            var source = GenerateCachingLogic(methodSymbol, duration);
            context.AddSource($"{methodSymbol.Name}_Cache.cs", SourceText.From(source, Encoding.UTF8));
        }
    }

    private string GenerateCachingLogic(IMethodSymbol methodSymbol, int duration)
    {
        var namespaceName = methodSymbol.ContainingNamespace.ToDisplayString();
        var className = methodSymbol.ContainingType.Name;
        var methodName = methodSymbol.Name;
        var returnType = methodSymbol.ReturnType.ToDisplayString();
        var parameters = string.Join(", ", methodSymbol.Parameters.Select(p => $"{p.Type.ToDisplayString()} {p.Name}"));
        var arguments = string.Join(", ", methodSymbol.Parameters.Select(p => p.Name));

        return $@"
using System;
using System.Threading.Tasks;
using ZiggyCreatures.Caching.Fusion;

namespace {namespaceName}
{{
    public partial class {className}
    {{
        private readonly IFusionCache _cache;

        public {className}(IFusionCache cache)
        {{
            _cache = cache;
        }}

        public async Task<{returnType}> {methodName}_WithCache({parameters})
        {{
            var cacheKey = $""{methodName}_{{string.Join('_', new object[] {{ {arguments} }})}}"";
            return await _cache.GetOrSetAsync(
                cacheKey,
                async () => await {methodName}({arguments}),
                TimeSpan.FromSeconds({duration})
            );
        }}
    }}
}}
";
    }

    private class SyntaxReceiver : ISyntaxReceiver
    {
        public List<MethodDeclarationSyntax> CandidateMethods { get; } = new List<MethodDeclarationSyntax>();

        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
            // Find all method declarations with the CacheAttribute
            if (syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax &&
                methodDeclarationSyntax.AttributeLists.Count > 0)
            {
                CandidateMethods.Add(methodDeclarationSyntax);
            }
        }
    }
}

Step 3: Use the Source Generator

  1. Add the source generator project as a reference to your main project.
  2. Mark methods with the CacheAttribute to enable caching.

Example Usage:

public class BookService
{
    [Cache(60)]
    public async Task<Book> GetBookAsync(int bookId)
    {
        // Method implementation
    }
}

The source generator will automatically generate a method with caching logic for each method marked with the CacheAttribute.


Summary

This approach uses a source generator to automatically generate caching logic for methods marked with a CacheAttribute. The generated code uses FusionCache to handle caching, and the duration is specified in the attribute. This ensures that caching is applied consistently and automatically to the specified methods.

Related Posts