Advanced thread-safe generic wrapper for System.Runtime.Caching with comprehensive features including cache groups, dependency management, automatic population, and persistent disk storage. Key Features: • Cache groups for organized data management • Dependency relationships between cache groups • Automatic cache population with custom methods • Persistent cache storage that survives application restarts • Retrieve all items from a group with GetAllByGroup() • Comprehensive metadata and monitoring with GetAllCacheMetadata() • Detailed statistics and disk usage analysis • Multiple expiration strategies (sliding/absolute) • Auto-refresh with configurable intervals • Thread-safe operations with minimal lock contention • JSON-based serialization for persistent storage Perfect for high-performance applications requiring sophisticated caching patterns, data persistence, and comprehensive monitoring capabilities.
$ dotnet add package CacheUtilityA thread-safe, generic wrapper for System.Runtime.Caching that simplifies cache access and supports powerful caching patterns with persistent storage capabilities.
CacheUtility provides an easy-to-use abstraction over the standard .NET memory cache with additional features:
services.AddCacheLogging() — zero-config DI integrationInstall the CacheUtility NuGet package:
Install-Package CacheUtility
dotnet add package CacheUtility
<PackageReference Include="CacheUtility" Version="1.3.5" />
using CacheUtility;
// Simple caching with automatic population
var userData = Cache.Get("user_123", "users", () => GetUserFromDatabase(123));
// Enable persistent cache for specific groups
var options = new PersistentCacheOptions
{
PersistentGroups = new[] { "users", "settings" }
};
Cache.EnablePersistentCache(options);
// Cache with auto-refresh every 5 minutes
var config = Cache.Get("app_config", "settings",
TimeSpan.FromHours(1),
() => LoadConfiguration(),
TimeSpan.FromMinutes(5));
Note: All examples assume you have added the using statement:
using CacheUtility;
The most common pattern is to request an item from the cache, providing a function to generate the item if it doesn't exist:
// Basic usage with default 30-minute sliding expiration
var result = Cache.Get("MyKey", "MyGroupName", () =>
{
return MyLongRunningTask();
});
// With custom sliding expiration
var result = Cache.Get("MyKey", "MyGroupName", TimeSpan.FromHours(1), () =>
{
return MyLongRunningTask();
});
// With absolute expiration
var result = Cache.Get("MyKey", "MyGroupName", DateTime.Now.AddDays(1), () =>
{
return MyLongRunningTask();
});
// Cache user data with a sliding expiration
var userData = Cache.Get($"User_{userId}", "UserProfiles", TimeSpan.FromMinutes(30), () =>
{
return database.GetUserById(userId);
});
// Cache application settings with absolute expiration
var settings = Cache.Get("GlobalSettings", "AppConfig", DateTime.Now.AddHours(12), () =>
{
return configurationService.LoadSettings();
});
For async operations, you can use the utility with async/await:
var result = await Cache.Get("MyKey", "MyGroupName", async () =>
{
return await MyLongRunningTaskAsync();
});
Remove a specific item from the cache:
Cache.Remove("MyKey", "MyGroupName");
Remove an entire group of cached items:
Cache.RemoveGroup("MyGroupName");
Remove multiple groups:
Cache.RemoveGroup("GroupA", "GroupB", "GroupC");
Get all cached items that belong to a specific group:
var allItems = Cache.GetAllByGroup("MyGroupName");
// Iterate through all items in the group
foreach (var kvp in allItems)
{
Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}
// Access specific items if you know the key
if (allItems.ContainsKey("MySpecificKey"))
{
var specificItem = allItems["MySpecificKey"];
}
CacheUtility supports optional persistent caching, where cached data is automatically saved to disk and survives application restarts. This hybrid approach combines the speed of in-memory caching with the persistence of disk storage.
Enable persistent cache for specific groups:
// Enable persistence for specific cache groups
var options = new PersistentCacheOptions
{
PersistentGroups = new[] { "users", "settings", "products" }
};
Cache.EnablePersistentCache(options);
// Only specified groups will persist to disk
var userData = Cache.Get("userProfile", "users", () => GetUserFromDatabase(userId)); // Persisted
var tempData = Cache.Get("temp", "temporary", () => GetTempData()); // NOT persisted
Enable with custom configuration:
// Enable with custom options
Cache.EnablePersistentCache(new PersistentCacheOptions
{
BaseDirectory = @"C:\MyApp\Cache",
MaxFileSize = 50 * 1024 * 1024, // 50MB limit per file
PersistentGroups = new[] { "users", "settings" } // Only these groups persist
});
When persistent cache is enabled:
Persistent cache files are stored with a simple naming pattern:
%LOCALAPPDATA%/CacheUtility/
├── users_userProfile_123.cache # Cache data
├── users_userProfile_123.meta # Expiration metadata
├── reports_monthly_2025.cache
├── reports_monthly_2025.meta
└── settings_appConfig.cache
var options = new PersistentCacheOptions
{
BaseDirectory = @"C:\MyApp\Cache", // Custom cache directory
MaxFileSize = 10 * 1024 * 1024 // 10MB max per cached item (0 = no limit)
};
Cache.EnablePersistentCache(options);
Check if persistent cache is enabled:
bool isEnabled = Cache.IsPersistentCacheEnabled;
Get current configuration:
var options = Cache.GetPersistentCacheOptions();
if (options != null)
{
Console.WriteLine($"Cache directory: {options.BaseDirectory}");
Console.WriteLine($"Max file size: {options.MaxFileSize} bytes");
}
Get persistent cache statistics:
var stats = Cache.GetPersistentCacheStatistics();
Console.WriteLine($"Cache enabled: {stats.IsEnabled}");
Console.WriteLine($"Cache directory: {stats.BaseDirectory}");
Console.WriteLine($"Total files: {stats.TotalFiles}");
Console.WriteLine($"Cache files: {stats.CacheFiles}");
Console.WriteLine($"Meta files: {stats.MetaFiles}");
Console.WriteLine($"Orphaned files: {stats.OrphanedFiles}");
Console.WriteLine($"Total size: {stats.TotalSizeFormatted}");
Console.WriteLine($"Average file size: {stats.AverageFileSizeFormatted}");
Console.WriteLine($"Largest file: {stats.LargestFileSize:N0} bytes");
Console.WriteLine($"Smallest file: {stats.SmallestFileSize:N0} bytes");
if (stats.OldestFileTime.HasValue)
{
Console.WriteLine($"Oldest file: {stats.OldestFileTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Directory age: {stats.DirectoryAge?.Days} days");
}
if (stats.NewestFileTime.HasValue)
{
Console.WriteLine($"Newest file: {stats.NewestFileTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Last activity: {stats.TimeSinceLastActivity?.TotalMinutes:F1} minutes ago");
}
Manually clean up expired files:
// Manually trigger cleanup of expired persistent cache files
Cache.CleanupExpiredPersistentCache();
Disable persistent cache:
// Disable persistent caching (returns to memory-only)
Cache.DisablePersistentCache();
When persistent cache is enabled, cache metadata includes additional information:
var metadata = Cache.GetAllCacheMetadata();
foreach (var item in metadata)
{
Console.WriteLine($"Key: {item.CacheKey}");
Console.WriteLine($" Is Persisted: {item.IsPersisted}");
if (item.IsPersisted)
{
Console.WriteLine($" File Path: {item.PersistentFilePath}");
Console.WriteLine($" File Size: {item.PersistentFileSize:N0} bytes");
Console.WriteLine($" Last Persisted: {item.LastPersistedTime:yyyy-MM-dd HH:mm:ss}");
}
}
Persistent cache automatically manages file cleanup:
Application restart scenarios:
// Enable persistent cache for a specific group
var options = new PersistentCacheOptions
{
PersistentGroups = new[] { "user-sessions" }
};
Cache.EnablePersistentCache(options);
// Cache expensive data that should survive restarts
var expensiveData = Cache.Get("dailyReport", "reports", () => GenerateDailyReport());
// After application restart, data is automatically loaded from disk
var sameData = Cache.Get("dailyReport", "reports", () => GenerateDailyReport());
// No need to regenerate - loaded from persistent storage!
Large dataset caching:
// Cache large datasets that might not fit entirely in memory
var bigData = Cache.Get("dataset_2025", "analytics", () => LoadHugeDataset());
Cross-session data:
// Cache user preferences that should persist across sessions
var userPrefs = Cache.Get($"prefs_{userId}", "userdata", () => LoadUserPreferences(userId));
GetPersistentCacheStatistics() to monitor cache sizeMaxFileSize to prevent extremely large cache filesGet detailed metadata about cached items for monitoring, debugging, or displaying in management interfaces:
// Get metadata for all cached items
var allMetadata = Cache.GetAllCacheMetadata();
foreach (var metadata in allMetadata)
{
Console.WriteLine($"Key: {metadata.CacheKey}");
Console.WriteLine($" Group: {metadata.GroupName}");
Console.WriteLine($" Type: {metadata.DataType}");
Console.WriteLine($" Size: {metadata.EstimatedMemorySize:N0} bytes");
Console.WriteLine($" Last Refresh: {metadata.LastRefreshTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($" Refresh Interval: {metadata.RefreshInterval}");
Console.WriteLine($" Is Refreshing: {metadata.IsRefreshing}");
Console.WriteLine($" Populate Method: {metadata.PopulateMethodName ?? "Unknown"}");
// Auto-refresh information
if (metadata.NextRefreshTime.HasValue)
{
Console.WriteLine($" Next Refresh: {metadata.NextRefreshTime:yyyy-MM-dd HH:mm:ss}");
var timeUntilRefresh = metadata.NextRefreshTime.Value - DateTime.Now;
Console.WriteLine($" Time Until Refresh: {timeUntilRefresh:hh\\:mm\\:ss}");
}
// Expiration information
if (metadata.HasAbsoluteExpiration)
{
Console.WriteLine($" Absolute Expiration: {metadata.AbsoluteExpiration:yyyy-MM-dd HH:mm:ss}");
if (metadata.TimeUntilExpiration.HasValue)
{
Console.WriteLine($" Time Until Expiration: {metadata.TimeUntilExpiration.Value:hh\\:mm\\:ss}");
}
Console.WriteLine($" Is Expired: {metadata.IsExpired}");
}
if (metadata.HasSlidingExpiration)
{
Console.WriteLine($" Sliding Expiration: {metadata.SlidingExpiration}");
}
// Persistent cache information
Console.WriteLine($" Persistent Cache Enabled: {metadata.PersistentCacheEnabled}");
if (metadata.IsPersisted)
{
Console.WriteLine($" Persisted to Disk: Yes");
Console.WriteLine($" Cache File: {metadata.PersistentFilePath}");
Console.WriteLine($" Meta File: {metadata.PersistentMetaFilePath}");
Console.WriteLine($" Cache File Size: {metadata.PersistentFileSize:N0} bytes");
Console.WriteLine($" Meta File Size: {metadata.PersistentMetaFileSize:N0} bytes");
Console.WriteLine($" Total Disk Size: {metadata.TotalPersistentSize:N0} bytes");
Console.WriteLine($" Last Persisted: {metadata.LastPersistedTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($" File Age: {metadata.PersistentFileAge?.TotalHours:F1} hours");
}
else if (metadata.PersistentCacheEnabled)
{
Console.WriteLine($" Persisted to Disk: No (not yet saved)");
}
if (metadata.CollectionCount.HasValue)
{
Console.WriteLine($" Items in Collection: {metadata.CollectionCount}");
}
Console.WriteLine(); // Blank line for readability
}
// You can filter the results as needed
var userDataItems = allMetadata.Where(m => m.GroupName == "UserProfiles");
var itemsWithAutoRefresh = allMetadata.Where(m => m.NextRefreshTime.HasValue);
var persistedItems = allMetadata.Where(m => m.IsPersisted);
Each CacheItemMetadata object contains:
The PopulateMethodName property helps identify which methods are used to populate cache items:
// Direct method reference - shows actual method name
Cache.Get("key1", "group", MyDataService.LoadUserData);
// PopulateMethodName: "MyDataService.LoadUserData"
// Lambda expression - shows indicator
Cache.Get("key2", "group", () => database.GetUser(123));
// PopulateMethodName: "[Lambda/Anonymous]"
// Anonymous method - shows indicator
Cache.Get("key3", "group", delegate() { return "test"; });
// PopulateMethodName: "[Lambda/Anonymous]"
This is particularly useful for:
Clear the entire cache:
Cache.RemoveAll();
Clear the cache except for specific groups:
Cache.RemoveAllButThese(new List<string> { "CriticalData", "ApplicationSettings" });
Remove multiple items that contain specific strings:
Cache.Remove(new List<string> { "UserProfile", "123" }, "UserData");
// This will remove any cache key containing both "UserProfile" and "123"
// Cache some user data
Cache.Get("User1", "UserData", () => GetUserInfo(1));
Cache.Get("User2", "UserData", () => GetUserInfo(2));
Cache.Get("User3", "UserData", () => GetUserInfo(3));
// Get all cached items from the group
var allUsers = Cache.GetAllByGroup("UserData");
Console.WriteLine($"Found {allUsers.Count} cached users");
// Process each cached item
foreach (var user in allUsers)
{
Console.WriteLine($"User Key: {user.Key}, Data: {user.Value}");
}
CacheUtility supports automatic background refresh of cached data at specified intervals. This feature ensures your cache stays up-to-date with fresh data while maintaining high performance by serving existing data immediately, even during refresh operations.
Key benefits:
// Cache data with automatic refresh every 5 minutes
var userData = Cache.Get("user_123", "UserProfiles",
TimeSpan.FromHours(1), // Sliding expiration
() => database.GetUserById(123), // Populate method
refresh: TimeSpan.FromMinutes(5) // Refresh interval
);
// Even if GetExpensiveData() takes 10 seconds to execute,
// subsequent cache calls will return immediately with existing data
var expensiveData = Cache.Get("expensive_key", "DataGroup",
TimeSpan.FromMinutes(30),
() => GetExpensiveDataFromAPI(), // Slow operation
refresh: TimeSpan.FromMinutes(2)
);
// This call returns instantly, even if refresh is running in background
var sameData = Cache.Get("expensive_key", "DataGroup",
TimeSpan.FromMinutes(30),
() => GetExpensiveDataFromAPI(),
refresh: TimeSpan.FromMinutes(2)
);
API data caching:
var weatherData = Cache.Get($"weather_{cityId}", "WeatherCache",
TimeSpan.FromHours(2), // Cache for 2 hours max, after the cache item last has been accessed
() => weatherAPI.GetCurrentWeather(cityId),
refresh: TimeSpan.FromMinutes(15) // Refresh every 15 minutes
);
Database result caching:
var reports = Cache.Get("monthly_reports", "Reports",
TimeSpan.FromHours(4),
() => database.GenerateMonthlyReports(), // Expensive query
refresh: TimeSpan.FromHours(1) // Refresh hourly
);
Configuration data:
var config = Cache.Get("app_config", "Configuration",
TimeSpan.FromDays(1),
() => configService.LoadConfiguration(),
refresh: TimeSpan.FromMinutes(30) // Check for config updates every 30 minutes
);
CacheUtility supports optional removal callbacks that are invoked when cached items are removed from the cache. This is useful for cleanup operations, logging, or triggering dependent actions.
var result = Cache.Get("MyKey", "MyGroupName",
DateTime.Now.AddHours(1), // Either Absolute expiration
TimeSpan.FromMinutes(10), // Or Sliding expiration
CacheItemPriority.Default, // Priority
() => MyLongRunningTask(),
removedCallback: (args) => // Optional callback
{
Console.WriteLine($"Cache item removed. Key: {args.CacheItem.Key}, Reason: {args.RemovedReason}");
});
The callback provides a CacheEntryRemovedArguments object that contains:
CacheItem: The cache item that was removedRemovedReason: The reason for removal (Removed, Expired, Evicted, ChangeMonitorChanged)Common removal reasons:
Removed: Item was explicitly removedExpired: Item expired (absolute or sliding expiration)Evicted: Item was evicted due to memory pressureChangeMonitorChanged: Item was removed due to a dependency changeCleanup resources:
var fileData = Cache.Get("FileData", "Files",
TimeSpan.FromMinutes(30),
CacheItemPriority.Default,
() => LoadFileData("myfile.txt"),
removedCallback: (args) =>
{
if (args.CacheItem.Value is IDisposable disposable)
{
disposable.Dispose();
}
});
Trigger dependent operations:
var config = Cache.Get("AppConfig", "Configuration",
DateTime.Now.AddHours(12),
() => LoadConfiguration(),
removedCallback: (args) =>
{
// Refresh dependent services when configuration changes
if (args.RemovedReason == CacheEntryRemovedReason.Expired)
{
RefreshDependentServices();
}
});
Set up dependencies between cache groups so that when one group is cleared, its dependent groups are also cleared:
// Set up dependencies
Cache.SetDependencies("ParentGroup", "ChildGroup1", "ChildGroup2");
// Now when ParentGroup is removed, ChildGroup1 and ChildGroup2 will also be removed
Cache.RemoveGroup("ParentGroup");
// Set up dependencies
Cache.SetDependencies("UserData", "UserProfiles", "UserPreferences", "UserActivity");
Cache.SetDependencies("UserProfiles", "ProfilePhotos");
// Now when UserData is cleared, all dependent caches are also cleared
Cache.RemoveGroup("UserData");
// This will clear UserData, UserProfiles, ProfilePhotos, UserPreferences, and UserActivity
MaxFileSize limits to prevent extremely large cache filesGetPersistentCacheStatistics()Problem: Persistent cache files are not being created or loaded.
Solutions:
Check if persistent cache is enabled:
if (!Cache.IsPersistentCacheEnabled)
{
Cache.EnablePersistentCache();
}
Verify directory permissions:
var stats = Cache.GetPersistentCacheStatistics();
Console.WriteLine($"Cache directory: {stats.BaseDirectory}");
// Ensure your application has read/write access to this directory
Check for serialization issues:
[JsonIgnore] for non-serializable propertiesProblem: Cache operations seem slow.
Solutions:
Check if you're blocking on populate methods:
// Bad - synchronous database call
var data = Cache.Get("key", "group", () => database.GetData());
// Better - use async populate methods when available
var data = Cache.Get("key", "group", () => GetDataAsync().Result);
Monitor cache hit rates:
var metadata = Cache.GetAllCacheMetadata();
// Analyze refresh patterns and expiration times
Optimize persistent cache settings:
Cache.EnablePersistentCache(new PersistentCacheOptions
{
BaseDirectory = @"C:\FastSSD\Cache\", // Use fast storage
MaxFileSize = 1024 * 1024 // Limit file sizes
});
Problem: High memory usage from cached data.
Solutions:
Monitor cache size:
var metadata = Cache.GetAllCacheMetadata();
var totalSize = metadata.Sum(m => m.EstimatedMemorySize);
Console.WriteLine($"Total cache size: {totalSize:N0} bytes");
Use appropriate expiration strategies:
// Use sliding expiration for frequently accessed data
Cache.Get("key", "group", TimeSpan.FromMinutes(30), () => GetData());
// Use absolute expiration for time-sensitive data
Cache.Get("key", "group", DateTime.Now.AddHours(1), () => GetData());
Remove unused cache groups:
Cache.RemoveGroup("unused_group");
Problem: Persistent cache directory contains orphaned files.
Solutions:
Check for orphaned files:
var stats = Cache.GetPersistentCacheStatistics();
if (stats.OrphanedFiles > 0)
{
Console.WriteLine($"Found {stats.OrphanedFiles} orphaned files");
}
Clean up expired files:
Cache.CleanupExpiredPersistentCache();
Manual cleanup (if needed):
var options = Cache.GetPersistentCacheOptions();
if (options != null && Directory.Exists(options.BaseDirectory))
{
// Backup and clean the directory if necessary
}
If you encounter issues not covered here:
var metadata = Cache.GetAllCacheMetadata();
var stats = Cache.GetPersistentCacheStatistics();
Task.Run() to prevent blocking the main thread.// Basic get with populate method
T Get<T>(string cacheKey, string groupName, Func<T> populateMethod)
// Get with sliding expiration
T Get<T>(string cacheKey, string groupName, TimeSpan slidingExpiration, Func<T> populateMethod)
// Get with absolute expiration
T Get<T>(string cacheKey, string groupName, DateTime absoluteExpiration, Func<T> populateMethod)
// Get with auto-refresh
T Get<T>(string cacheKey, string groupName, TimeSpan slidingExpiration, Func<T> populateMethod, TimeSpan refresh)
// Get with callback
T Get<T>(string cacheKey, string groupName, TimeSpan slidingExpiration, Func<T> populateMethod, CacheEntryRemovedCallback removedCallback)
// Remove single item
void Remove(string cacheKey, string groupName)
// Remove entire group
void RemoveGroup(string groupName)
// Remove all cache items
void RemoveAll()
// Get all items from a group
IEnumerable<T> GetAllByGroup<T>(string groupName)
// Add dependency between groups
void AddGroupDependency(string dependentGroup, string parentGroup)
// Enable with defaults
void EnablePersistentCache()
// Enable with custom options
void EnablePersistentCache(PersistentCacheOptions options)
// Disable persistent cache
void DisablePersistentCache()
// Check if enabled
bool IsPersistentCacheEnabled { get; }
// Get current options
PersistentCacheOptions GetPersistentCacheOptions()
// Get comprehensive statistics
PersistentCacheStatistics GetPersistentCacheStatistics()
// Manual cleanup of expired files
void CleanupExpiredPersistentCache()
// Get metadata for all cached items
IEnumerable<CacheItemMetadata> GetAllCacheMetadata()
public class CacheItemMetadata
{
// Basic Information
public string CacheKey { get; set; }
public string GroupName { get; set; }
public string DataType { get; set; }
public long EstimatedMemorySize { get; set; }
// Refresh Information
public DateTime LastRefreshTime { get; set; }
public DateTime? LastRefreshAttempt { get; set; }
public TimeSpan RefreshInterval { get; set; }
public DateTime? NextRefreshTime { get; set; }
public bool IsRefreshing { get; set; }
public DateTime? RefreshStartTime { get; set; }
// Collection Information
public int? CollectionCount { get; set; }
// Method Information
public string PopulateMethodName { get; set; }
// Expiration Information
public DateTime AbsoluteExpiration { get; set; }
public TimeSpan SlidingExpiration { get; set; }
public bool HasAbsoluteExpiration { get; }
public bool HasSlidingExpiration { get; }
public TimeSpan? TimeUntilExpiration { get; }
public bool IsExpired { get; }
// Persistent Cache Information
public bool PersistentCacheEnabled { get; set; }
public bool IsPersisted { get; set; }
public string PersistentFilePath { get; set; }
public string PersistentMetaFilePath { get; set; }
public long PersistentFileSize { get; set; }
public long PersistentMetaFileSize { get; set; }
public long TotalPersistentSize { get; }
public DateTime? LastPersistedTime { get; set; }
public TimeSpan? PersistentFileAge { get; }
}
public class PersistentCacheStatistics
{
// Basic Information
public bool IsEnabled { get; set; }
public string BaseDirectory { get; set; }
// File Counts
public int TotalFiles { get; set; }
public int CacheFiles { get; set; }
public int MetaFiles { get; set; }
public int OrphanedFiles { get; set; }
// Size Information
public long TotalSizeBytes { get; set; }
public string TotalSizeFormatted { get; }
public long AverageFileSize { get; }
public string AverageFileSizeFormatted { get; }
public long LargestFileSize { get; set; }
public long SmallestFileSize { get; set; }
// Time Information
public DateTime? OldestFileTime { get; set; }
public DateTime? NewestFileTime { get; set; }
public TimeSpan? DirectoryAge { get; }
public TimeSpan? TimeSinceLastActivity { get; }
}
public class PersistentCacheOptions
{
// Base directory for cache files (default: %LOCALAPPDATA%/CacheUtility/)
public string BaseDirectory { get; set; }
// Maximum size for individual cache files in bytes (default: 10MB)
public long MaxFileSize { get; set; }
}
// Dispose all resources
void Dispose()
// Check if item exists in cache
bool Exists(string cacheKey, string groupName)
Based on internal testing with 1,000 cache operations:
| Operation | Memory Cache | Persistent Cache (Enabled) | Overhead |
|---|---|---|---|
| Cache Hit | ~0.001ms | ~0.001ms | 0% |
| Cache Miss (Population) | ~1.2ms | ~1.3ms | ~8% |
| Group Removal | ~0.5ms | ~2.1ms | ~320% |
| Metadata Retrieval | ~0.8ms | ~1.1ms | ~37% |
Key Findings:
Recommendations:
The CacheUtility is built on top of .NET's MemoryCache, which has built-in memory pressure detection. However, be mindful of:
All operations in CacheUtility are thread-safe. The implementation uses ReaderWriterLockSlim for efficient concurrent access and CacheLock for synchronizing modifications to the cache.
CacheUtility includes built-in diagnostic logging via Microsoft.Extensions.Logging. All key cache operations emit structured Debug-level log messages.
One line in your service registration:
builder.Services.AddCacheLogging();
This automatically connects CacheUtility to your application's logging pipeline on host startup. Works seamlessly with Serilog, NLog, or any ILoggerFactory-based provider.
For console apps or other non-DI scenarios:
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
Cache.ConfigureLogging(loggerFactory);
CacheUtility logs under the "CacheUtility" source name. Use your logging framework's namespace overrides to control verbosity per environment:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"CacheUtility": "Debug"
}
}
}
}
| Operation | Level | Example message |
|---|---|---|
| Cache hit | Trace | Cache hit: {CacheKey} in group {GroupName} |
| Cache miss | Debug | Cache miss, loading data: {CacheKey} in group {GroupName} using {MethodName} |
| Remove key | Debug | Removing cache key: {CacheKey} from group {GroupName} |
| Remove group | Debug | Removing cache group {GroupName} ({KeyCount} keys) |
| Remove all | Debug | Removing all cached items ({Count} keys registered) |
| Background refresh start | Debug | Starting background refresh for {CacheKey} |
| Background refresh done | Debug | Background refresh completed for {CacheKey} |
| Background refresh fail | Warning | Background refresh failed for {CacheKey} |
| Persistent cache enabled | Debug | Enabling persistent cache (directory: ...) |
When logging is not configured, NullLogger is used — zero overhead.