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.