Cache Invalidation
Cache invalidation determines when cached data should be removed or refreshed. Athena.Cache provides multiple invalidation strategies to ensure your cache stays in sync with your data.
Table-based Invalidation
The most common pattern is to invalidate cache when database tables change.
Basic Usage
[HttpGet]
[AthenaCache(ExpirationMinutes = 30)]
[CacheInvalidateOn("Users")] // This cache is tagged with "Users"
public async Task<UserDto[]> GetUsers()
{
return await _userService.GetUsersAsync();
}
[HttpPost]
[CacheInvalidateOn("Users")] // This clears all "Users" tagged caches
public async Task<UserDto> CreateUser([FromBody] CreateUserRequest request)
{
return await _userService.CreateUserAsync(request);
}
When CreateUser
is called, it automatically clears the cache from GetUsers
.
Multiple Table Dependencies
[HttpGet("user-profiles")]
[AthenaCache(ExpirationMinutes = 45)]
[CacheInvalidateOn("Users")] // Depends on Users table
[CacheInvalidateOn("UserProfiles")] // Also depends on UserProfiles table
public async Task<UserProfileDto[]> GetUserProfiles()
{
return await _userService.GetUserProfilesAsync();
}
[HttpPut("{id}/profile")]
[CacheInvalidateOn("UserProfiles")] // Only clears UserProfiles caches
public async Task UpdateUserProfile(int id, [FromBody] UpdateProfileRequest request)
{
await _userService.UpdateUserProfileAsync(id, request);
}
[HttpDelete("{id}")]
[CacheInvalidateOn("Users")] // Clears both Users and UserProfiles caches
[CacheInvalidateOn("UserProfiles")] // because user deletion affects both tables
public async Task DeleteUser(int id)
{
await _userService.DeleteUserAsync(id);
}
Pattern-based Invalidation
For more granular control, use pattern-based invalidation to target specific cache keys.
Wildcard Patterns
[HttpPut("{id}")]
[CacheInvalidateOn("Users", InvalidationType.Pattern, "user_*")]
public async Task UpdateUser(int id, [FromBody] UpdateUserRequest request)
{
await _userService.UpdateUserAsync(id, request);
// Clears: "user_123", "user_456", "user_profile_123", etc.
}
Specific Key Patterns
[HttpPut("{id}/avatar")]
[CacheInvalidateOn("Users", InvalidationType.Pattern, "user_{id}_*")]
public async Task UpdateUserAvatar(int id, [FromBody] AvatarRequest request)
{
await _userService.UpdateUserAvatarAsync(id, request);
// Clears: "user_123_profile", "user_123_details", etc.
// Preserves: "user_456_profile", "users_list", etc.
}
Complex Patterns
[HttpPost("{userId}/orders")]
[CacheInvalidateOn("Orders", InvalidationType.Pattern, "user_{userId}_orders*")]
[CacheInvalidateOn("Orders", InvalidationType.Pattern, "orders_summary_*")]
public async Task CreateOrder(int userId, [FromBody] CreateOrderRequest request)
{
await _orderService.CreateOrderAsync(userId, request);
// Clears user-specific order caches and summary caches
}
Exact Key Invalidation
Invalidate specific cache keys when you know exactly what to clear.
[HttpPut("user/{id}/status")]
[CacheInvalidateOn("Users", InvalidationType.Exact, "active_users_count")]
[CacheInvalidateOn("Users", InvalidationType.Exact, "user_statistics")]
public async Task UpdateUserStatus(int id, [FromBody] StatusUpdateRequest request)
{
await _userService.UpdateUserStatusAsync(id, request);
// Only clears the specific keys: "active_users_count" and "user_statistics"
}
Programmatic Invalidation
For scenarios where attributes aren’t enough, use programmatic invalidation.
Using ICacheInvalidator
[ApiController]
public class AdminController : ControllerBase
{
private readonly ICacheInvalidator _cacheInvalidator;
private readonly IUserService _userService;
public AdminController(ICacheInvalidator cacheInvalidator, IUserService userService)
{
_cacheInvalidator = cacheInvalidator;
_userService = userService;
}
[HttpPost("bulk-update-users")]
public async Task<IActionResult> BulkUpdateUsers([FromBody] BulkUpdateRequest request)
{
await _userService.BulkUpdateUsersAsync(request);
// Clear multiple table caches
await _cacheInvalidator.InvalidateByTableAsync("Users");
await _cacheInvalidator.InvalidateByTableAsync("UserProfiles");
await _cacheInvalidator.InvalidateByTableAsync("UserStatistics");
return Ok();
}
[HttpPost("clear-cache/{pattern}")]
public async Task<IActionResult> ClearCacheByPattern(string pattern)
{
await _cacheInvalidator.InvalidateByPatternAsync(pattern);
return Ok($"Cleared caches matching pattern: {pattern}");
}
}
Conditional Invalidation
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(
int id,
[FromBody] UpdateUserRequest request,
[FromServices] ICacheInvalidator cacheInvalidator)
{
var updatedUser = await _userService.UpdateUserAsync(id, request);
// Conditional invalidation based on what changed
if (request.EmailChanged)
{
await cacheInvalidator.InvalidateByPatternAsync($"user_{id}_*");
}
if (request.RoleChanged)
{
await cacheInvalidator.InvalidateByTableAsync("UserRoles");
await cacheInvalidator.InvalidateByTableAsync("Permissions");
}
return Ok(updatedUser);
}
Convention-based Invalidation
Enable automatic invalidation based on controller naming conventions.
Configuration
// Program.cs
builder.Services.AddAthenaCacheComplete(options =>
{
options.Convention.EnableConventionBasedInvalidation = true;
options.Convention.ControllerSuffix = "Controller";
options.Convention.TableSuffix = "";
});
Automatic Behavior
// With convention enabled:
public class UsersController : ControllerBase
{
[HttpGet]
[AthenaCache] // Automatically adds [CacheInvalidateOn("Users")]
public async Task<UserDto[]> GetUsers() { ... }
[HttpPost] // Automatically adds [CacheInvalidateOn("Users")]
public async Task<UserDto> CreateUser([FromBody] CreateUserRequest request) { ... }
}
// Override convention when needed:
[HttpGet("external")]
[AthenaCache]
[NoConventionInvalidation] // Skip automatic invalidation
public async Task<ExternalDataDto> GetExternalData() { ... }
Distributed Invalidation
When using Redis, invalidation works across all application instances.
Cross-instance Invalidation
// Instance A
[HttpPost]
[CacheInvalidateOn("Products")]
public async Task<ProductDto> CreateProduct([FromBody] CreateProductRequest request)
{
var product = await _productService.CreateProductAsync(request);
// This invalidation is automatically propagated to Instance B, C, etc.
return product;
}
Manual Cross-instance Invalidation
[ApiController]
public class CacheManagementController : ControllerBase
{
private readonly IDistributedCacheInvalidator _distributedInvalidator;
[HttpDelete("distributed/table/{tableName}")]
public async Task<IActionResult> InvalidateTableAcrossInstances(string tableName)
{
await _distributedInvalidator.InvalidateByTableAsync(tableName);
return Ok($"Invalidated {tableName} across all instances");
}
[HttpDelete("distributed/pattern/{pattern}")]
public async Task<IActionResult> InvalidatePatternAcrossInstances(string pattern)
{
await _distributedInvalidator.InvalidateByPatternAsync(pattern);
return Ok($"Invalidated pattern {pattern} across all instances");
}
}
Batch Invalidation
Efficiently invalidate multiple caches in a single operation.
[HttpPost("reorganize-departments")]
public async Task<IActionResult> ReorganizeDepartments(
[FromBody] ReorganizeRequest request,
[FromServices] ICacheInvalidator cacheInvalidator)
{
await _organizationService.ReorganizeDepartmentsAsync(request);
// Batch invalidation
await cacheInvalidator.InvalidateMultipleAsync(new[]
{
("Departments", InvalidationType.Table),
("Users", InvalidationType.Table),
("employee_*", InvalidationType.Pattern),
("department_hierarchy", InvalidationType.Exact)
});
return Ok();
}
Invalidation Events
Subscribe to invalidation events for monitoring and debugging.
// Program.cs
builder.Services.AddAthenaCacheComplete(options =>
{
options.Events.OnCacheInvalidated = (context) =>
{
var logger = context.ServiceProvider.GetService<ILogger<Program>>();
logger?.LogInformation("Cache invalidated: {InvalidationType} - {Target}",
context.InvalidationType, context.Target);
return Task.CompletedTask;
};
});
Custom Invalidation Handlers
public class CacheInvalidationHandler
{
private readonly ILogger<CacheInvalidationHandler> _logger;
private readonly IMetricsCollector _metrics;
public async Task HandleInvalidation(CacheInvalidationContext context)
{
// Log invalidation
_logger.LogInformation("Invalidated: {Type} - {Target}",
context.InvalidationType, context.Target);
// Record metrics
await _metrics.RecordInvalidationAsync(context.InvalidationType, context.Target);
// Custom business logic
if (context.Target == "Users")
{
await NotifyUserManagementSystemAsync();
}
}
}
Best Practices
1. Use Table-based for Most Cases
// Good: Clear and predictable
[CacheInvalidateOn("Users")]
[CacheInvalidateOn("UserProfiles")]
// Avoid: Too many specific patterns
[CacheInvalidateOn("Users", InvalidationType.Pattern, "user_123_*")]
[CacheInvalidateOn("Users", InvalidationType.Pattern, "user_456_*")]
// ... (better to use table-based)
2. Be Specific with Patterns
// Good: Specific pattern
[CacheInvalidateOn("Orders", InvalidationType.Pattern, "user_{userId}_orders*")]
// Avoid: Too broad pattern
[CacheInvalidateOn("Orders", InvalidationType.Pattern, "*")] // Clears everything!
3. Group Related Invalidations
// Good: Group related invalidations together
[HttpDelete("{id}")]
[CacheInvalidateOn("Users")]
[CacheInvalidateOn("UserProfiles")]
[CacheInvalidateOn("UserStatistics")]
public async Task DeleteUser(int id) { ... }
4. Use Conditional Invalidation for Complex Scenarios
// Good: Only invalidate what actually changed
public async Task UpdateUserProfile(int userId, UpdateProfileRequest request)
{
var changes = await _userService.UpdateUserProfileAsync(userId, request);
if (changes.ProfileChanged)
await _cacheInvalidator.InvalidateByTableAsync("UserProfiles");
if (changes.PreferencesChanged)
await _cacheInvalidator.InvalidateByTableAsync("UserPreferences");
}
Troubleshooting
Cache Not Being Invalidated
- Check invalidation tags match:
[CacheInvalidateOn("Users")] // Tag: "Users" [CacheInvalidateOn("User")] // Tag: "User" - different!
- Verify invalidation methods are called:
// Make sure the invalidating action is actually executed [HttpPost] [CacheInvalidateOn("Users")] public async Task CreateUser(...) { // If this throws an exception, invalidation won't happen await _userService.CreateUserAsync(...); }
- Check distributed invalidation setup:
// Ensure IDistributedCacheInvalidator is registered for Redis scenarios builder.Services.AddAthenaCacheRedisComplete(...);
Invalidation Logging
Enable invalidation logging to debug issues:
builder.Services.AddAthenaCacheComplete(options =>
{
options.Logging.LogCacheInvalidation = true;
});
For more advanced scenarios, see:
- Redis Setup - Distributed invalidation
- Monitoring - Track invalidation events
- Performance - Optimize invalidation performance