I created cache management structure for Asp.Net Core and Redis. But i dont know is this best practices or bad.
First one is Store my entity keys class is
public class CacheKeys
{
#region User Cache Keys
public const string GetUserKey = "user__{0}";
public const string GetUserListKey = "user__list";
#endregion
}
Second one is Create Cache Key String utility class is
public class CacheKeyUtility
{
private readonly string EnvironmentName;
public CacheKeyUtility(string environmentName)
{
EnvironmentName = environmentName ?? throw new ArgumentNullException(nameof(environmentName));
}
public string GetUserCacheKey(UserCacheType userCacheType, object? id = null) =>
userCacheType switch
{
UserCacheType.One => GetCacheKey(CacheKeys.GetUserKey, id.ToString()),
_ => GetCacheKey(CacheKeys.GetUserListKey)
};
private string GetCacheKey(string keyFormat, params string[]? formatParameters)
{
string result = $"{EnvironmentName.ToLower()}__{keyFormat.ToLower()}";
if (formatParameters != null)
{
result = string.Format(result, formatParameters);
}
return result;
}
}
CacheKeyUtility class has Singleton DI and EnvironmentName value in appsetting.json. DI Code is
public static IServiceCollection AddMyServiceLifeCycles(this IServiceCollection services, IConfiguration Configuration)
{
string environmentName = Configuration["Environment:EnvironmentName"];
services.AddSingleton(new CacheKeyUtility(environmentName));
services.AddScoped<IRedisCacheService, RedisCacheService>();
services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddScoped<IUserRepository, UserRepository>();
services.AddTransient<CustomExceptionHandler>();
services.AddTransient<IHashService, HashService>();
services.AddTransient<ITokenService, TokenService>();
services.AddTransient(typeof(ILogHelper<>), typeof(LogHelper<>));
return services;
}
Usage Example is
public class GetAllUserHandler : BaseHandler<GetAllUserRequest, GenericResponse<GetAllUserResponse>, GetAllUserHandler>, IRequestHandler<GetAllUserRequest, GenericResponse<GetAllUserResponse>>
{
private readonly IUserRepository _userRepository;
private readonly IRedisCacheService _redisCacheService;
private readonly CacheKeyUtility CacheKeyUtility;
public GetAllUserHandler(IHttpContextAccessor httpContextAccessor, IEnumerable<FluentValidation.IValidator<GetAllUserRequest>> validators, ILogHelper<GetAllUserHandler> logHelper, IUserRepository userRepository, IRedisCacheService redisCacheService, CacheKeyUtility cacheKeyUtility) : base(httpContextAccessor, validators, logHelper)
{
_userRepository = userRepository;
_redisCacheService = redisCacheService;
CacheKeyUtility = cacheKeyUtility;
}
public async Task<GenericResponse<GetAllUserResponse>> Handle(GetAllUserRequest request, CancellationToken cancellationToken)
{
await CheckValidate(request);
try
{
var response = new GetAllUserResponse();
string cacheKey = CacheKeyUtility.GetUserCacheKey(Shared.Enums.CacheEnums.UserCacheType.List);
var cacheUserList= await _redisCacheService.GetAsync<IEnumerable<UserDataModel>>(cacheKey);
if (cacheUserList != null && cacheUserList.Any())
{
response.UserList = cacheUserList;
response.TotalCount = cacheUserList.Count();
return GenericResponse<GetAllUserResponse>.Success(200, response);
}
var query = _userRepository.GetUserList(request.Query);
var data = await query.Select(x => new UserDataModel
{
FirstName = x.FirstName,
LastName = x.LastName,
Gsm = x.Gsm,
Id = x.Id,
Mail = x.Mail,
}).TryPagination(request.PageCount, request.PageNumber).ToListWithNoLockAsync();
await _redisCacheService.SetAsync(cacheKey, data);
response.TotalCount = await query.CountAsync();
response.UserList = data;
return GenericResponse<GetAllUserResponse>.Success(200, response);
}
catch (Exception ex)
{
_logHelper.LogError(ex);
return GenericResponse<GetAllUserResponse>.Error(500, ex.Message);
}
}
}
I think this structure is good. My question is "What are the bad points of this structure?"
1 Answer 1
you can always use nameof
to get the class name, for instance nameof(User)
will return User
string. So, you can depend on System.Type.Name
and suffix to it the other values if you will. This would be better to avoid magic strings, and also it's supported by intellisense, and also is generics friendly, so you could do typeof(T).Name
.
If you have an idea of unifying the key name of all keys by using the templates, then you could also work a way to unify the Key Format
of all keys, so you have a template design that all keys will be applied on, no matter what custom template was provided, it will always have the same positions of all arguments, this would give a better handling for the key and add more customization without losing the key template maintainability.
Anyhow, for the implementation, you can use abstractions and inheritance to give more options, and free room to adopt and expand if necessary.
We can finalize CacheKeys
and CacheKeyUtility
into one abstraction, and then from this abstraction we can implement the concrete classes as much as needed. one way of doing it is combining them into CacheKey
and use generics. here is an example :
public interface ICacheKey
{
string EnvironmentName { get; }
object Value { get; }
string FormatTemplate { get; }
IEnumerable<string> Parameters { get; }
}
public abstract class CacheKey<T> : ICacheKey
{
private const string _defaultTemplate = "{0}__{1}";
public string EnvironmentName { get; }
public string FormatTemplate { get; }
public object Value { get; }
public IEnumerable<string> Parameters { get; }
public CacheKey(string environmentName, object value, IEnumerable<string> parameters) : this(environmentName, _defaultTemplate, value, parameters) { }
public CacheKey(string environmentName, string formatTemplate, object value, IEnumerable<string> parameters)
{
EnvironmentName = environmentName ?? throw new ArgumentNullException(nameof(environmentName));
Value = value ?? string.Empty;
FormatTemplate = formatTemplate ?? _defaultTemplate;
Parameters = parameters;
}
/// <summary>
/// When called, it will return the formatted key
/// </summary>
/// <returns></returns>
public override string ToString()
{
var result = string.Format(FormatTemplate, typeof(T).Name, Value);
if (Parameters != null)
{
result = string.Format(result, Parameters);
}
return result.ToLowerInvariant();
}
}
having this base abstraction, we can do this :
public class UserCacheKey : CacheKey<User>
{
private const string _defaultEnvironmentName = "ExampleEnvironment";
public UserCacheKey(object value) : this(value, null) { }
public UserCacheKey(object value, IEnumerable<string> parameters) : base(_defaultEnvironmentName, value, parameters) { }
}
from there we can use ICacheKey
on other caches and use ToString
to get the formatted key. You can then extend it as needed.
-
\$\begingroup\$ Thanks for reply @iSR5 . This reply enugh for me. I will edit my key structure using your suggestion \$\endgroup\$filiphasan– filiphasan2022年07月02日 08:15:05 +00:00Commented Jul 2, 2022 at 8:15
enum
why don't you just either use anenum
orconst string
mixing both would be unnecessary for this kind of work. you could also use thenameof
as key as well, but this you would need to pass the name of a class. in this case, you may able to use generics as well. \$\endgroup\$