Automating FusionCache with a Source Generator: A Step-by-Step Guide

Using a source generator to automate code for calling the GetOrSetAsync function from FusionCache is an elegant way to handle caching. Let’s walk through how you can use Roslyn, the .NET Compiler Platform, to create a generator tailored to your needs.


Step 1: Create the Custom Attribute

Start by defining a FusionCacheAttribute that will decorate the methods you want to cache:

[AttributeUsage(AttributeTargets.Method)]
public class FusionCacheAttribute : Attribute
{
    public int Expiration { get; set; } = 300;  // Default expiration time in seconds
    public string CacheKeyPrefix { get; set; } = string.Empty;
}

Step 2: Create the Source Generator

Use the Roslyn API to create a generator that looks for methods with the FusionCacheAttribute and generates caching code.

First, add the Roslyn NuGet package:

dotnet add package Microsoft.CodeAnalysis.CSharp

Then, implement your source generator:

[Generator]
public class FusionCacheSourceGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

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

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

            var returnType = methodSymbol.ReturnType.ToString();
            var methodName = methodSymbol.Name;
            var className = methodSymbol.ContainingType.Name;
            var namespaceName = methodSymbol.ContainingNamespace.ToString();
            var cacheKeyPrefix = methodSymbol.GetAttributes()
                .First(a => a.AttributeClass.Name == "FusionCacheAttribute")
                .NamedArguments.FirstOrDefault(arg => arg.Key == "CacheKeyPrefix").Value.Value?.ToString() ?? string.Empty;
            var expiration = methodSymbol.GetAttributes()
                .First(a => a.AttributeClass.Name == "FusionCacheAttribute")
                .NamedArguments.FirstOrDefault(arg => arg.Key == "Expiration").Value.Value?.ToString() ?? "300";

            var sourceBuilder = new StringBuilder($@"
namespace {namespaceName}
{{
    public partial class {className}
    {{
        public async Task<{returnType}> {methodName}_WithCacheAsync()
        {{
            string cacheKey = ""{cacheKeyPrefix}_{methodName}"";
            return await _cacheClient.GetOrSetAsync<{returnType}>(cacheKey, async () => await {methodName}(), TimeSpan.FromSeconds({expiration}));
        }}
    }}
}}
");
            context.AddSource($"{className}_{methodName}_WithCache.g.cs", sourceBuilder.ToString());
        }
    }

    private class SyntaxReceiver : ISyntaxReceiver
    {
        public List<MethodDeclarationSyntax> CandidateMethods { get; } = new();
        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
            if (syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax &&
                methodDeclarationSyntax.AttributeLists.Count > 0)
            {
                var attributes = methodDeclarationSyntax.AttributeLists
                    .SelectMany(attrList => attrList.Attributes)
                    .Select(attr => attr.Name.ToString());
                if (attributes.Contains("FusionCache"))
                {
                    CandidateMethods.Add(methodDeclarationSyntax);
                }
            }
        }
    }
}

Step 3: Use the Generated Code

Apply FusionCacheAttribute to your methods:

public partial class MyService
{
    [FusionCache(CacheKeyPrefix = "myMethod", Expiration = 600)]
    public async Task<MyResult> MyMethodAsync()
    {
        // Your method logic
    }
}

When the source generator runs, you’ll get an auto-generated method with caching:

var result = await myService.MyMethodAsync_WithCacheAsync();

Conclusion

With source generators, automating caching logic becomes seamless and efficient. Not only does this approach reduce boilerplate code, but it also makes your caching infrastructure consistent and easier to manage.

Related Posts