Custom Providers
Extend Athena.Cache with custom cache providers, serialization methods, and key generation strategies to meet specific requirements.
Custom Cache Providers
Create custom cache implementations for specialized storage backends.
Base Cache Provider Interface
public interface ICacheProvider
{
Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default);
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default);
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default);
Task ClearAsync(CancellationToken cancellationToken = default);
Task<IEnumerable<string>> GetKeysAsync(string pattern = "*", CancellationToken cancellationToken = default);
}
public abstract class BaseCacheProvider : ICacheProvider
{
protected readonly ICacheSerializer _serializer;
protected readonly ILogger _logger;
protected readonly CacheProviderOptions _options;
protected BaseCacheProvider(
ICacheSerializer serializer,
ILogger logger,
CacheProviderOptions options)
{
_serializer = serializer;
_logger = logger;
_options = options;
}
public abstract Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default);
public abstract Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default);
public abstract Task RemoveAsync(string key, CancellationToken cancellationToken = default);
public abstract Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default);
public abstract Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default);
public abstract Task ClearAsync(CancellationToken cancellationToken = default);
public abstract Task<IEnumerable<string>> GetKeysAsync(string pattern = "*", CancellationToken cancellationToken = default);
protected virtual string NormalizeKey(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Cache key cannot be null or empty", nameof(key));
return _options.KeyPrefix + key;
}
protected virtual void LogOperation(string operation, string key, bool success, TimeSpan duration)
{
if (_options.EnableLogging)
{
_logger.LogDebug("Cache {Operation} for key {Key}: {Success} in {Duration}ms",
operation, key, success ? "Success" : "Failed", duration.TotalMilliseconds);
}
}
}
File System Cache Provider
public class FileSystemCacheProvider : BaseCacheProvider
{
private readonly string _cacheDirectory;
private readonly SemaphoreSlim _semaphore;
public FileSystemCacheProvider(
ICacheSerializer serializer,
ILogger<FileSystemCacheProvider> logger,
FileSystemCacheOptions options)
: base(serializer, logger, options)
{
_cacheDirectory = options.CacheDirectory ?? Path.Combine(Path.GetTempPath(), "AthenaCache");
_semaphore = new SemaphoreSlim(options.MaxConcurrentOperations, options.MaxConcurrentOperations);
EnsureDirectoryExists();
}
public override async Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var normalizedKey = NormalizeKey(key);
var filePath = GetFilePath(normalizedKey);
await _semaphore.WaitAsync(cancellationToken);
try
{
if (!File.Exists(filePath))
{
LogOperation("GET", key, false, stopwatch.Elapsed);
return default(T);
}
var cacheItem = await ReadCacheItemAsync<T>(filePath, cancellationToken);
if (cacheItem.IsExpired)
{
// Clean up expired item
File.Delete(filePath);
LogOperation("GET", key, false, stopwatch.Elapsed);
return default(T);
}
LogOperation("GET", key, true, stopwatch.Elapsed);
return cacheItem.Value;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting cache item with key {Key}", key);
LogOperation("GET", key, false, stopwatch.Elapsed);
return default(T);
}
finally
{
_semaphore.Release();
}
}
public override async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var normalizedKey = NormalizeKey(key);
var filePath = GetFilePath(normalizedKey);
await _semaphore.WaitAsync(cancellationToken);
try
{
var cacheItem = new FileCacheItem<T>
{
Value = value,
CreatedAt = DateTimeOffset.UtcNow,
ExpiresAt = expiration.HasValue ? DateTimeOffset.UtcNow.Add(expiration.Value) : null
};
await WriteCacheItemAsync(filePath, cacheItem, cancellationToken);
LogOperation("SET", key, true, stopwatch.Elapsed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting cache item with key {Key}", key);
LogOperation("SET", key, false, stopwatch.Elapsed);
throw;
}
finally
{
_semaphore.Release();
}
}
public override async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var normalizedKey = NormalizeKey(key);
var filePath = GetFilePath(normalizedKey);
await _semaphore.WaitAsync(cancellationToken);
try
{
if (File.Exists(filePath))
{
File.Delete(filePath);
LogOperation("REMOVE", key, true, stopwatch.Elapsed);
}
else
{
LogOperation("REMOVE", key, false, stopwatch.Elapsed);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing cache item with key {Key}", key);
LogOperation("REMOVE", key, false, stopwatch.Elapsed);
throw;
}
finally
{
_semaphore.Release();
}
}
public override async Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default)
{
var keys = await GetKeysAsync(pattern, cancellationToken);
var tasks = keys.Select(key => RemoveAsync(key, cancellationToken));
await Task.WhenAll(tasks);
}
public override async Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default)
{
var normalizedKey = NormalizeKey(key);
var filePath = GetFilePath(normalizedKey);
if (!File.Exists(filePath)) return false;
try
{
var cacheItem = await ReadCacheItemAsync<object>(filePath, cancellationToken);
if (cacheItem.IsExpired)
{
File.Delete(filePath);
return false;
}
return true;
}
catch
{
return false;
}
}
public override async Task ClearAsync(CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
if (Directory.Exists(_cacheDirectory))
{
Directory.Delete(_cacheDirectory, true);
EnsureDirectoryExists();
}
}
finally
{
_semaphore.Release();
}
}
public override async Task<IEnumerable<string>> GetKeysAsync(string pattern = "*", CancellationToken cancellationToken = default)
{
if (!Directory.Exists(_cacheDirectory))
return Enumerable.Empty<string>();
var searchPattern = pattern.Replace("*", "*.cache");
var files = Directory.GetFiles(_cacheDirectory, searchPattern);
return files.Select(f => Path.GetFileNameWithoutExtension(f))
.Where(key => key.StartsWith(_options.KeyPrefix))
.Select(key => key.Substring(_options.KeyPrefix.Length));
}
private string GetFilePath(string key)
{
var safeKey = string.Join("_", key.Split(Path.GetInvalidFileNameChars()));
return Path.Combine(_cacheDirectory, $"{safeKey}.cache");
}
private void EnsureDirectoryExists()
{
if (!Directory.Exists(_cacheDirectory))
{
Directory.CreateDirectory(_cacheDirectory);
}
}
private async Task<FileCacheItem<T>> ReadCacheItemAsync<T>(string filePath, CancellationToken cancellationToken)
{
using var stream = File.OpenRead(filePath);
var json = await JsonSerializer.DeserializeAsync<FileCacheItem<T>>(stream, cancellationToken: cancellationToken);
return json;
}
private async Task WriteCacheItemAsync<T>(string filePath, FileCacheItem<T> item, CancellationToken cancellationToken)
{
using var stream = File.Create(filePath);
await JsonSerializer.SerializeAsync(stream, item, cancellationToken: cancellationToken);
}
}
public class FileCacheItem<T>
{
public T Value { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? ExpiresAt { get; set; }
public bool IsExpired => ExpiresAt.HasValue && DateTimeOffset.UtcNow > ExpiresAt.Value;
}
public class FileSystemCacheOptions : CacheProviderOptions
{
public string CacheDirectory { get; set; }
public int MaxConcurrentOperations { get; set; } = 10;
}
Database Cache Provider
public class DatabaseCacheProvider : BaseCacheProvider
{
private readonly IDbConnection _connection;
private readonly DatabaseCacheOptions _dbOptions;
public DatabaseCacheProvider(
IDbConnection connection,
ICacheSerializer serializer,
ILogger<DatabaseCacheProvider> logger,
DatabaseCacheOptions options)
: base(serializer, logger, options)
{
_connection = connection;
_dbOptions = options;
InitializeDatabase();
}
public override async Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var normalizedKey = NormalizeKey(key);
try
{
var sql = @"
SELECT Value, ExpiresAt
FROM CacheItems
WHERE CacheKey = @Key AND (ExpiresAt IS NULL OR ExpiresAt > @Now)";
var result = await _connection.QueryFirstOrDefaultAsync(sql, new
{
Key = normalizedKey,
Now = DateTimeOffset.UtcNow
});
if (result == null)
{
LogOperation("GET", key, false, stopwatch.Elapsed);
return default(T);
}
var value = _serializer.Deserialize<T>(result.Value);
LogOperation("GET", key, true, stopwatch.Elapsed);
return value;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting cache item with key {Key}", key);
LogOperation("GET", key, false, stopwatch.Elapsed);
return default(T);
}
}
public override async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var normalizedKey = NormalizeKey(key);
var serializedValue = _serializer.Serialize(value);
var expiresAt = expiration.HasValue ? DateTimeOffset.UtcNow.Add(expiration.Value) : (DateTimeOffset?)null;
try
{
var sql = @"
INSERT INTO CacheItems (CacheKey, Value, CreatedAt, ExpiresAt)
VALUES (@Key, @Value, @CreatedAt, @ExpiresAt)
ON DUPLICATE KEY UPDATE
Value = @Value,
CreatedAt = @CreatedAt,
ExpiresAt = @ExpiresAt";
await _connection.ExecuteAsync(sql, new
{
Key = normalizedKey,
Value = serializedValue,
CreatedAt = DateTimeOffset.UtcNow,
ExpiresAt = expiresAt
});
LogOperation("SET", key, true, stopwatch.Elapsed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting cache item with key {Key}", key);
LogOperation("SET", key, false, stopwatch.Elapsed);
throw;
}
}
public override async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var normalizedKey = NormalizeKey(key);
try
{
var sql = "DELETE FROM CacheItems WHERE CacheKey = @Key";
var affected = await _connection.ExecuteAsync(sql, new { Key = normalizedKey });
LogOperation("REMOVE", key, affected > 0, stopwatch.Elapsed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing cache item with key {Key}", key);
LogOperation("REMOVE", key, false, stopwatch.Elapsed);
throw;
}
}
public override async Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default)
{
var sql = "DELETE FROM CacheItems WHERE CacheKey LIKE @Pattern";
var dbPattern = pattern.Replace("*", "%");
await _connection.ExecuteAsync(sql, new { Pattern = dbPattern });
}
public override async Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default)
{
var normalizedKey = NormalizeKey(key);
var sql = @"
SELECT COUNT(1)
FROM CacheItems
WHERE CacheKey = @Key AND (ExpiresAt IS NULL OR ExpiresAt > @Now)";
var count = await _connection.QueryFirstAsync<int>(sql, new
{
Key = normalizedKey,
Now = DateTimeOffset.UtcNow
});
return count > 0;
}
public override async Task ClearAsync(CancellationToken cancellationToken = default)
{
var sql = "DELETE FROM CacheItems";
await _connection.ExecuteAsync(sql);
}
public override async Task<IEnumerable<string>> GetKeysAsync(string pattern = "*", CancellationToken cancellationToken = default)
{
var sql = "SELECT CacheKey FROM CacheItems WHERE CacheKey LIKE @Pattern";
var dbPattern = pattern.Replace("*", "%");
var keys = await _connection.QueryAsync<string>(sql, new { Pattern = dbPattern });
return keys.Where(key => key.StartsWith(_options.KeyPrefix))
.Select(key => key.Substring(_options.KeyPrefix.Length));
}
private void InitializeDatabase()
{
var createTableSql = @"
CREATE TABLE IF NOT EXISTS CacheItems (
CacheKey VARCHAR(500) PRIMARY KEY,
Value LONGTEXT NOT NULL,
CreatedAt DATETIME NOT NULL,
ExpiresAt DATETIME NULL,
INDEX idx_expires (ExpiresAt)
)";
_connection.Execute(createTableSql);
}
}
public class DatabaseCacheOptions : CacheProviderOptions
{
public string ConnectionString { get; set; }
public string TableName { get; set; } = "CacheItems";
}
Custom Serializers
Implement custom serialization strategies for specific data types or performance requirements.
Binary Serializer
public class BinarySerializer : ICacheSerializer
{
private readonly BinarySerializerOptions _options;
public BinarySerializer(BinarySerializerOptions options)
{
_options = options;
}
public byte[] Serialize<T>(T obj)
{
if (obj == null) return null;
using var stream = new MemoryStream();
if (_options.UseCompression)
{
using var gzipStream = new GZipStream(stream, CompressionLevel.Fastest);
using var writer = new BinaryWriter(gzipStream);
SerializeObject(writer, obj);
}
else
{
using var writer = new BinaryWriter(stream);
SerializeObject(writer, obj);
}
return stream.ToArray();
}
public T Deserialize<T>(byte[] data)
{
if (data == null || data.Length == 0) return default(T);
using var stream = new MemoryStream(data);
if (_options.UseCompression)
{
using var gzipStream = new GZipStream(stream, CompressionMode.Decompress);
using var reader = new BinaryReader(gzipStream);
return (T)DeserializeObject(reader, typeof(T));
}
else
{
using var reader = new BinaryReader(stream);
return (T)DeserializeObject(reader, typeof(T));
}
}
private void SerializeObject(BinaryWriter writer, object obj)
{
var type = obj.GetType();
// Write type information
writer.Write(type.AssemblyQualifiedName);
// Handle primitive types
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime) || type == typeof(DateTimeOffset))
{
SerializePrimitive(writer, obj);
return;
}
// Handle collections
if (obj is IEnumerable enumerable && type != typeof(string))
{
SerializeCollection(writer, enumerable);
return;
}
// Handle complex objects using reflection
SerializeComplexObject(writer, obj);
}
private object DeserializeObject(BinaryReader reader, Type expectedType)
{
var typeName = reader.ReadString();
var actualType = Type.GetType(typeName);
if (actualType != expectedType && !expectedType.IsAssignableFrom(actualType))
{
throw new InvalidOperationException($"Type mismatch: expected {expectedType}, got {actualType}");
}
// Handle primitive types
if (actualType.IsPrimitive || actualType == typeof(string) || actualType == typeof(DateTime) || actualType == typeof(DateTimeOffset))
{
return DeserializePrimitive(reader, actualType);
}
// Handle collections
if (typeof(IEnumerable).IsAssignableFrom(actualType) && actualType != typeof(string))
{
return DeserializeCollection(reader, actualType);
}
// Handle complex objects
return DeserializeComplexObject(reader, actualType);
}
private void SerializePrimitive(BinaryWriter writer, object obj)
{
switch (obj)
{
case bool b: writer.Write(b); break;
case byte b: writer.Write(b); break;
case sbyte sb: writer.Write(sb); break;
case short s: writer.Write(s); break;
case ushort us: writer.Write(us); break;
case int i: writer.Write(i); break;
case uint ui: writer.Write(ui); break;
case long l: writer.Write(l); break;
case ulong ul: writer.Write(ul); break;
case float f: writer.Write(f); break;
case double d: writer.Write(d); break;
case decimal dec: writer.Write(dec); break;
case char c: writer.Write(c); break;
case string str: writer.Write(str ?? string.Empty); break;
case DateTime dt: writer.Write(dt.ToBinary()); break;
case DateTimeOffset dto:
writer.Write(dto.DateTime.ToBinary());
writer.Write(dto.Offset.Ticks);
break;
}
}
private object DeserializePrimitive(BinaryReader reader, Type type)
{
return type.Name switch
{
nameof(Boolean) => reader.ReadBoolean(),
nameof(Byte) => reader.ReadByte(),
nameof(SByte) => reader.ReadSByte(),
nameof(Int16) => reader.ReadInt16(),
nameof(UInt16) => reader.ReadUInt16(),
nameof(Int32) => reader.ReadInt32(),
nameof(UInt32) => reader.ReadUInt32(),
nameof(Int64) => reader.ReadInt64(),
nameof(UInt64) => reader.ReadUInt64(),
nameof(Single) => reader.ReadSingle(),
nameof(Double) => reader.ReadDouble(),
nameof(Decimal) => reader.ReadDecimal(),
nameof(Char) => reader.ReadChar(),
nameof(String) => reader.ReadString(),
nameof(DateTime) => DateTime.FromBinary(reader.ReadInt64()),
nameof(DateTimeOffset) => new DateTimeOffset(DateTime.FromBinary(reader.ReadInt64()), new TimeSpan(reader.ReadInt64())),
_ => throw new NotSupportedException($"Type {type} is not supported for primitive deserialization")
};
}
}
public class BinarySerializerOptions
{
public bool UseCompression { get; set; } = true;
public bool IncludeTypeInformation { get; set; } = true;
public bool UseNetworkByteOrder { get; set; } = false;
}
Protocol Buffer Serializer
public class ProtocolBufferSerializer : ICacheSerializer
{
private readonly ProtobufSerializerOptions _options;
public ProtocolBufferSerializer(ProtobufSerializerOptions options)
{
_options = options;
}
public byte[] Serialize<T>(T obj)
{
if (obj == null) return null;
using var stream = new MemoryStream();
if (_options.UseCompression)
{
using var gzipStream = new GZipStream(stream, CompressionLevel.Fastest);
Serializer.Serialize(gzipStream, obj);
}
else
{
Serializer.Serialize(stream, obj);
}
return stream.ToArray();
}
public T Deserialize<T>(byte[] data)
{
if (data == null || data.Length == 0) return default(T);
using var stream = new MemoryStream(data);
if (_options.UseCompression)
{
using var gzipStream = new GZipStream(stream, CompressionMode.Decompress);
return Serializer.Deserialize<T>(gzipStream);
}
else
{
return Serializer.Deserialize<T>(stream);
}
}
}
public class ProtobufSerializerOptions
{
public bool UseCompression { get; set; } = false;
public bool PreserveReferences { get; set; } = false;
}
Custom Key Generators
Create custom cache key generation strategies.
Hashed Key Generator
public class HashedKeyGenerator : ICacheKeyGenerator
{
private readonly HashedKeyOptions _options;
public HashedKeyGenerator(HashedKeyOptions options)
{
_options = options;
}
public string GenerateKey(ControllerActionDescriptor actionDescriptor, object[] parameters)
{
var baseKey = GenerateBaseKey(actionDescriptor, parameters);
if (baseKey.Length <= _options.MaxKeyLength)
{
return baseKey;
}
// Hash long keys
return GenerateHashedKey(baseKey);
}
private string GenerateBaseKey(ControllerActionDescriptor actionDescriptor, object[] parameters)
{
var keyBuilder = new StringBuilder();
// Add controller name
keyBuilder.Append(GetControllerName(actionDescriptor.ControllerName));
keyBuilder.Append(':');
// Add action name
keyBuilder.Append(GetActionName(actionDescriptor.ActionName));
// Add parameters
if (parameters?.Length > 0)
{
keyBuilder.Append(':');
keyBuilder.Append(GetParametersHash(parameters));
}
return keyBuilder.ToString();
}
private string GenerateHashedKey(string originalKey)
{
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(originalKey));
var hash = Convert.ToBase64String(hashBytes);
// Make hash URL-safe
hash = hash.Replace('+', '-').Replace('/', '_').TrimEnd('=');
return $"hash:{hash}";
}
private string GetControllerName(string fullControllerName)
{
var name = fullControllerName.EndsWith("Controller")
? fullControllerName[..^10] // Remove "Controller" suffix
: fullControllerName;
return _options.UseShortNames ? GetShortName(name) : name;
}
private string GetActionName(string actionName)
{
return _options.UseShortNames ? GetShortName(actionName) : actionName;
}
private string GetParametersHash(object[] parameters)
{
var parameterStrings = parameters.Select(p => SerializeParameter(p));
var combined = string.Join("|", parameterStrings);
using var sha1 = SHA1.Create();
var hashBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(combined));
return Convert.ToHexString(hashBytes)[..8]; // Use first 8 characters
}
private string SerializeParameter(object parameter)
{
return parameter switch
{
null => "null",
string s => s,
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
_ => JsonSerializer.Serialize(parameter)
};
}
private string GetShortName(string name)
{
// Generate short names for common patterns
return name switch
{
"User" or "Users" => "Usr",
"Product" or "Products" => "Prd",
"Order" or "Orders" => "Ord",
"Customer" or "Customers" => "Cst",
"Category" or "Categories" => "Cat",
"Get" or "GetAll" or "GetList" => "G",
"Create" or "Post" or "Add" => "C",
"Update" or "Put" or "Edit" => "U",
"Delete" or "Remove" => "D",
_ when name.Length > 10 => name[..3] + name.GetHashCode().ToString("x")[..2],
_ => name
};
}
}
public class HashedKeyOptions
{
public int MaxKeyLength { get; set; } = 100;
public bool UseShortNames { get; set; } = true;
public bool IncludeNamespace { get; set; } = false;
public string KeyPrefix { get; set; } = "";
}
Hierarchical Key Generator
public class HierarchicalKeyGenerator : ICacheKeyGenerator
{
private readonly HierarchicalKeyOptions _options;
public string GenerateKey(ControllerActionDescriptor actionDescriptor, object[] parameters)
{
var segments = new List<string>();
// Add namespace if configured
if (_options.IncludeNamespace)
{
segments.Add(GetNamespace(actionDescriptor));
}
// Add controller
segments.Add(GetControllerSegment(actionDescriptor));
// Add action
segments.Add(GetActionSegment(actionDescriptor));
// Add parameter segments
if (parameters?.Length > 0)
{
segments.AddRange(GetParameterSegments(actionDescriptor, parameters));
}
return string.Join(_options.Separator, segments);
}
private string GetNamespace(ControllerActionDescriptor actionDescriptor)
{
return actionDescriptor.ControllerTypeInfo.Namespace?.Split('.').LastOrDefault() ?? "Default";
}
private string GetControllerSegment(ControllerActionDescriptor actionDescriptor)
{
var controllerName = actionDescriptor.ControllerName;
return _options.ControllerNaming switch
{
NamingConvention.PascalCase => controllerName,
NamingConvention.CamelCase => ToCamelCase(controllerName),
NamingConvention.KebabCase => ToKebabCase(controllerName),
NamingConvention.SnakeCase => ToSnakeCase(controllerName),
_ => controllerName.ToLowerInvariant()
};
}
private string GetActionSegment(ControllerActionDescriptor actionDescriptor)
{
var actionName = actionDescriptor.ActionName;
return _options.ActionNaming switch
{
NamingConvention.PascalCase => actionName,
NamingConvention.CamelCase => ToCamelCase(actionName),
NamingConvention.KebabCase => ToKebabCase(actionName),
NamingConvention.SnakeCase => ToSnakeCase(actionName),
_ => actionName.ToLowerInvariant()
};
}
private IEnumerable<string> GetParameterSegments(ControllerActionDescriptor actionDescriptor, object[] parameters)
{
var parameterInfos = actionDescriptor.MethodInfo.GetParameters();
for (int i = 0; i < Math.Min(parameterInfos.Length, parameters.Length); i++)
{
var paramInfo = parameterInfos[i];
var paramValue = parameters[i];
if (ShouldIncludeParameter(paramInfo, paramValue))
{
yield return FormatParameterValue(paramInfo.Name, paramValue);
}
}
}
private bool ShouldIncludeParameter(ParameterInfo paramInfo, object value)
{
// Skip complex objects unless specifically configured
if (value != null && !IsSimpleType(value.GetType()))
{
return _options.IncludeComplexParameters;
}
// Skip null values unless configured
if (value == null)
{
return _options.IncludeNullParameters;
}
return true;
}
private string FormatParameterValue(string paramName, object value)
{
if (value == null) return "null";
if (_options.IncludeParameterNames)
{
return $"{paramName}{_options.ParameterValueSeparator}{value}";
}
return value.ToString();
}
private bool IsSimpleType(Type type)
{
return type.IsPrimitive ||
type == typeof(string) ||
type == typeof(DateTime) ||
type == typeof(DateTimeOffset) ||
type == typeof(Guid) ||
type.IsEnum ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0]));
}
private string ToCamelCase(string input)
{
if (string.IsNullOrEmpty(input)) return input;
return char.ToLowerInvariant(input[0]) + input[1..];
}
private string ToKebabCase(string input)
{
return Regex.Replace(input, "([a-z])([A-Z])", "$1-$2").ToLowerInvariant();
}
private string ToSnakeCase(string input)
{
return Regex.Replace(input, "([a-z])([A-Z])", "$1_$2").ToLowerInvariant();
}
}
public class HierarchicalKeyOptions
{
public string Separator { get; set; } = ":";
public string ParameterValueSeparator { get; set; } = "=";
public bool IncludeNamespace { get; set; } = false;
public bool IncludeParameterNames { get; set; } = false;
public bool IncludeComplexParameters { get; set; } = false;
public bool IncludeNullParameters { get; set; } = false;
public NamingConvention ControllerNaming { get; set; } = NamingConvention.PascalCase;
public NamingConvention ActionNaming { get; set; } = NamingConvention.PascalCase;
}
public enum NamingConvention
{
PascalCase,
CamelCase,
KebabCase,
SnakeCase,
LowerCase
}
Registration and Configuration
Register custom providers with dependency injection.
Custom Provider Registration
// Extension method for easy registration
public static class CustomProviderExtensions
{
public static IServiceCollection AddFileSystemCache(
this IServiceCollection services,
Action<FileSystemCacheOptions> configureOptions = null)
{
var options = new FileSystemCacheOptions();
configureOptions?.Invoke(options);
services.AddSingleton(options);
services.AddSingleton<ICacheSerializer, JsonCacheSerializer>();
services.AddSingleton<ICacheProvider, FileSystemCacheProvider>();
return services;
}
public static IServiceCollection AddDatabaseCache(
this IServiceCollection services,
string connectionString,
Action<DatabaseCacheOptions> configureOptions = null)
{
var options = new DatabaseCacheOptions { ConnectionString = connectionString };
configureOptions?.Invoke(options);
services.AddSingleton(options);
services.AddSingleton<IDbConnection>(provider => new MySqlConnection(connectionString));
services.AddSingleton<ICacheSerializer, JsonCacheSerializer>();
services.AddSingleton<ICacheProvider, DatabaseCacheProvider>();
return services;
}
public static IServiceCollection AddCustomKeyGenerator<T>(
this IServiceCollection services)
where T : class, ICacheKeyGenerator
{
services.AddSingleton<ICacheKeyGenerator, T>();
return services;
}
public static IServiceCollection AddCustomSerializer<T>(
this IServiceCollection services)
where T : class, ICacheSerializer
{
services.AddSingleton<ICacheSerializer, T>();
return services;
}
}
// Usage in Program.cs
builder.Services.AddFileSystemCache(options =>
{
options.CacheDirectory = "/app/cache";
options.KeyPrefix = "myapp:";
options.EnableLogging = true;
});
builder.Services.AddCustomKeyGenerator<HashedKeyGenerator>();
builder.Services.AddCustomSerializer<ProtocolBufferSerializer>();
Multi-Provider Setup
public class MultiTierCacheProvider : ICacheProvider
{
private readonly ICacheProvider _l1Cache; // Fast, small cache (memory)
private readonly ICacheProvider _l2Cache; // Slower, larger cache (Redis/Database)
private readonly MultiTierOptions _options;
public async Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
{
// Try L1 cache first
var result = await _l1Cache.GetAsync<T>(key, cancellationToken);
if (result != null) return result;
// Try L2 cache
result = await _l2Cache.GetAsync<T>(key, cancellationToken);
if (result != null)
{
// Promote to L1 cache
await _l1Cache.SetAsync(key, result, _options.L1Expiration, cancellationToken);
}
return result;
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
{
// Set in both caches
var l1Expiration = _options.L1Expiration ?? expiration;
var l2Expiration = expiration;
await Task.WhenAll(
_l1Cache.SetAsync(key, value, l1Expiration, cancellationToken),
_l2Cache.SetAsync(key, value, l2Expiration, cancellationToken)
);
}
// Other methods follow similar pattern...
}
For integration and testing:
- Testing Strategies - Test custom providers
- Installation - Setup and configuration
- Performance Tuning - Optimize custom implementations