1
\$\begingroup\$

I created cache management structure for Asp.Net Core and Redis. But i dont know is this best practices or bad.

My Github Repo

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?"

\$\endgroup\$
2
  • \$\begingroup\$ some thoughts : have you thought about using abstraction and inheritance, to be able to reuse it on other caches as well ?. Also, since you're using enum why don't you just either use an enum or const string mixing both would be unnecessary for this kind of work. you could also use the nameof 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\$
    – iSR5
    Jun 28, 2022 at 13:31
  • \$\begingroup\$ @iSR5 Sounds like an answer to me. \$\endgroup\$
    – pacmaninbw
    Jun 28, 2022 at 13:47

1 Answer 1

0
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for reply @iSR5 . This reply enugh for me. I will edit my key structure using your suggestion \$\endgroup\$
    – filiphasan
    Jul 2, 2022 at 8:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.