Declarative caching for .NET with attributes and source generation
Switch caching on in minutes, run it safely in production, and stay in control at runtime.
Use Cases: Database query caching • API response caching • Expensive computation caching • Redis distributed caching • Multi-layer L1/L2 caching
Problems Solved: Slow API responses • High database load • Expensive computations • Cloud infrastructure costs • Performance bottlenecks
Alternative to: IMemoryCache • Manual cache-aside pattern • LazyCache • FusionCache • EasyCaching
MethodCache gives teams the three things they crave most from caching:
- Immediate productivity – decorate a method or call the fluent API and the source generator emits zero-reflection decorators for you.
- Operational control – runtime overrides, analyzers, and metrics keep caches observable and tweakable without redeploying.
- Scale without lock-in – plug in in-memory, Redis, hybrid, and ETag layers while reusing the same configuration surfaces.
Whether you are wrapping your own services or slapping caching onto third-party SDKs, MethodCache keeps business code clean, deploys safely, and gives operations a kill switch when they need it.
- Quick Start
- Why MethodCache?
- Configuration Surfaces
- Attributes
- Fluent API - 🆕 Method Chaining API
- JSON / YAML
- Runtime Overrides
- Cache Third‑Party Libraries
- Feature Highlights
- Architecture at a Glance
- Packages
- Documentation & Samples
- Contributing
# Minimal setup
dotnet add package MethodCache.Core
# Source generator + analyzers (recommended)
dotnet add package MethodCache.SourceGenerator
public interface IUserService
{
Task<UserProfile> GetUserAsync(int userId);
}
public class UserService : IUserService
{
[Cache]
public Task<UserProfile> GetUserAsync(int userId)
=> _db.Users.FindAsync(userId).AsTask();
}
builder.Services.AddMethodCache(config =>
{
config.DefaultDuration(TimeSpan.FromMinutes(10))
.DefaultKeyGenerator<MessagePackKeyGenerator>();
});
That's it – the source generator emits decorators, ICacheManager
handles storage, and you retain clean business code.
For non-generated scenarios or third-party libraries, use the intuitive method chaining API:
// Inject ICacheManager and use fluent chaining
var user = await cache.Cache(() => userService.GetUserAsync(userId))
.WithDuration(TimeSpan.FromHours(1))
.WithTags("user")
.WithKeyGenerator<JsonKeyGenerator>()
.ExecuteAsync();
Perfect for caching external APIs, legacy code, or when you prefer explicit control over attribute-based configuration.
- ✅ 75% less code – Declarative instead of manual cache-aside pattern
- ✅ 8276x faster cache hits – Zero-reflection with source generation (~145ns)
- ✅ Built-in tag invalidation – No manual tracking needed
- ✅ Better developer experience – IntelliSense, analyzers, clear error messages
- ✅ No boilerplate – Eliminate repetitive
TryGetValue
,Set
, key generation code - ✅ Compile-time safety – Analyzers catch mistakes before runtime
- ✅ Consistent patterns – Team-wide caching standards
Capability | What it means for you |
---|---|
Compile‑time decorators | Roslyn source generator produces zero‑reflection proxies with per‑method caching logic. |
Method Chaining API | NEW! Intuitive fluent interface: cache.Cache(() => service.GetData()).WithDuration(TimeSpan.FromHours(1)).ExecuteAsync() |
Flexible configuration | Choose attributes, fluent API (versioning, custom key generators, predicates), configuration files, or runtime overrides. |
Smart key generation | FastHashKeyGenerator (performance), JsonKeyGenerator (debugging), MessagePackKeyGenerator (complex objects). |
Provider agnostic | In‑memory L1, Redis L2, hybrid orchestration, compression, distributed locks, multi‑region support. |
Safe by default | Analyzers validate usage, circuit breakers and stampede protection guard your downstreams. |
Observability ready | Metrics hooks, structured logging, health checks, diagnostics – built to operate in production. |
Third‑party caching | Layer caching onto NuGet packages or SDKs without touching their source. |
Lightweight opt‑in. Apply [Cache]
(and [CacheInvalidate]
) to interface members or virtual methods.
public interface IOrdersService
{
[Cache("orders", Duration = "00:15:00", Tags = new[] { "orders", "customers" }, Version = 2,
KeyGeneratorType = typeof(FastHashKeyGenerator))]
Task<Order> GetAsync(int id);
}
Attributes describe intent; everything can be overridden downstream.
Method Chaining API - NEW! Chain configuration methods for intuitive, readable cache operations:
// Simple usage
var user = await cache.Cache(() => userService.GetUserAsync(userId))
.WithDuration(TimeSpan.FromHours(1))
.WithTags("user", $"user:{userId}")
.ExecuteAsync();
// Advanced configuration
var orders = await cache.Cache(() => orderService.GetOrdersAsync(customerId, status))
.WithDuration(TimeSpan.FromMinutes(30))
.WithStampedeProtection()
.WithKeyGenerator<JsonKeyGenerator>()
.When(ctx => customerId > 0)
.OnHit(ctx => logger.LogInformation($"Cache hit: {ctx.Key}"))
.ExecuteAsync();
Configuration-Based Fluent API - Express richer policies with IntelliSense support:
services.AddMethodCacheFluent(fluent =>
{
fluent.DefaultPolicy(o => o
.WithDuration(TimeSpan.FromMinutes(10))
.WithTags("default"));
fluent.ForService<IOrdersService>()
.Method(s => s.GetAsync(default))
.WithGroup("orders")
.WithVersion(3)
.WithKeyGenerator<FastHashKeyGenerator>()
.When(ctx => ctx.Key.Contains("Get"))
.RequireIdempotent();
});
Environment‑specific configuration without recompiling.
{
"MethodCache": {
"Defaults": { "Duration": "00:05:00" },
"Services": {
"MyApp.Services.IOrdersService.GetAsync": {
"Duration": "00:15:00",
"Tags": ["orders", "customers"],
"Version": 3
}
}
}
}
Runtime sources carry the highest precedence – perfect for management UIs and incident response.
var configurator = app.Services.GetRequiredService<IRuntimeCacheConfigurator>();
// Apply a live override using the same fluent API you use at startup
await configurator.ApplyFluentAsync(fluent =>
{
fluent.ForService<IOrdersService>()
.Method(s => s.GetAsync(default))
.Configure(o => o
.WithDuration(TimeSpan.FromMinutes(1))
.WithTags("runtime-override"));
});
// Surface overrides to your management UI
var overrides = await configurator.GetOverridesAsync();
// Roll back specific overrides without touching attributes or JSON
await configurator.RemoveOverrideAsync(typeof(IOrdersService).FullName!, nameof(IOrdersService.GetAsync));
// Or reset the runtime layer completely
await configurator.ClearOverridesAsync();
// Need the full effective picture (after attributes/config/runtime)?
var effectiveConfig = await configurator.GetEffectiveConfigurationAsync();
IRuntimeCacheConfigurator
is registered automatically when you callAddMethodCacheWithSources(...)
, making it trivial to plug a UI or management API on top of the fluent builders you already use at startup.
Drop caching onto external interfaces (Stripe, AWS SDKs, GraphQL clients, etc.) without modifying their code.
{
"MethodCache": {
"Services": {
"WeatherApi.Client.IWeatherApiClient.GetCurrentWeatherAsync": {
"Duration": "00:05:00",
"Tags": ["weather", "external-api"]
}
}
}
}
Read the full guide: Third‑Party Caching
MethodCache delivers exceptional performance with microsecond-level cache hits: 🚀 Cache speedup: 8276x faster than no caching
Operation | Small Model (1 item) | Medium Model (1 item) | Large Model (1 item) |
---|---|---|---|
No Caching | 1.2 ms | N/A | N/A |
Cache Miss | 1.3 ms | N/A | N/A |
Cache Hit | 145 ns | N/A | N/A |
Cache HitCold | 245 ns | N/A | N/A |
Cache Invalidation | 89 ns | N/A | N/A |
📊 Benchmarks run on .NET 9.0 with BenchmarkDotNet. Results from December 20, 2024.
- Cache Hits: Sub-microsecond response times for cached data
- Memory Efficient: Minimal memory allocations during cache operations
- Scalable: Consistent performance across different data sizes
- Zero-Overhead: Negligible impact when caching is disabled
- Method Chaining: No performance penalty - compiles to same efficient code as callback-based API
Generator | Use Case | Performance | Key Format |
---|---|---|---|
FastHashKeyGenerator |
High-throughput scenarios | Fastest (~50ns) | MethodName_hash |
JsonKeyGenerator |
Development/debugging | Medium (~200ns) | MethodName:param1:value1:param2:value2 |
MessagePackKeyGenerator |
Complex objects | Fast (~100ns) | MethodName_binary_hash |
graph TB
A[Attribute / Fluent Config] --> B[Roslyn Source Generator]
B --> C[Generated Decorator]
C --> D[ICacheManager]
D --> E[In-Memory L1]
D --> F[Redis L2]
F --> G[Multi-Region / Compression / Locks]
subgraph Observability
C --> H[Metrics]
D --> I[Analyzers]
end
Configuration precedence:
- Runtime overrides
- Startup fluent/config builders
- Attribute groups and defaults
Package | Description |
---|---|
MethodCache.Core |
Core abstractions, in-memory cache manager, attributes. |
MethodCache.SourceGenerator |
Roslyn generator emitting decorators and fluent registry. |
MethodCache.Analyzers |
Roslyn analyzers (MC0001–MC0004) ensuring safe usage. |
MethodCache.Providers.Redis |
Redis provider with hybrid orchestration, compression, locking. |
MethodCache.ETags |
HTTP ETag integration layered on MethodCache. |
- Configuration Guide - Comprehensive configuration options
- Fluent API Specification - Complete fluent API reference
- Method Chaining Examples - NEW! Real-world method chaining patterns
- Key Generator Selection Guide - Choose the right key generator
- Simplified API Examples - FluentCache-like simplicity with MethodCache power
- Third‑Party Caching Scenarios - Cache external libraries
- Sample App - Working examples
- Demo Project - Configuration-driven demonstrations
We welcome issues, ideas, and pull requests. Please read the contribution guidelines (coming soon) and ensure dotnet format
plus the test suite (dotnet test MethodCache.sln
) passes before submitting.
Built with ❤️ for the .NET community.