Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Code Reviews

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Cache data using Redis Cache in an ASP.NET Core 3.1 project

+1
−0

This is an unanswered code review request of mine from Code Review Stack Exchange.

Overview

I have developed a small ASP.NET Core 3.1 Web API that provides information that is rarely changed (several times a day) but often read (~ 10 K / day).

For me, it is a good opportunity to toy with Redis Cache and the Stack Exchange client.

I want to achieve the following:

  • allow services to transparently get and set data through cache: just provide a key, a function to construct the data if not in cache and an expiration period
  • should be thread-safe

The code

RedisCacheService.cs - the generic service that allows other services to transparently use the Redis cache

public class RedisCacheService : IRedisCacheService
{
    private ConnectionMultiplexer Redis = null;
    private IDatabase Database = null;

    private ILoggingService Logger { get; }

    private static readonly object SyncLock = new object();

    public RedisCacheService(ILoggingService loggingService)
    {
        Logger = loggingService;
    }

    /// <summary>
    /// checks that database is initialized. If not it is initialized
    /// </summary>
    private void EnsureConnection()
    {
        if (Database == null)
        {
            lock (SyncLock)
            {
                if (Database == null)
                {
                    //TODO: use appsettings 
                    Redis = ConnectionMultiplexer.Connect("localhost");
                    Database = Redis.GetDatabase();
                }
            }
        }
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "<Pending>")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "<Pending>")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "<Pending>")]
    public async Task<T> GetInfoThroughCache<T>(string key, int expirationSeconds, Func<T> computeData)
    {
        if (computeData == null)
            throw new ArgumentException($"{nameof(computeData)} is null");

        string cachedValue;
        try
        {
            EnsureConnection();

            // checking the cache (no locking yet)
            cachedValue = await Database.StringGetAsync(key);
        }
        catch(Exception exc )
        {
            Logger.LogError(exc, $"Error getting cached value for key {key}");
            return computeData();
        }

        if (string.IsNullOrEmpty(cachedValue) || cachedValue == "nil")
        {
            lock(SyncLock)
            {
                T newData = computeData();
                string serializedData = JsonConvert.SerializeObject(newData);
                TimeSpan expires = new TimeSpan(0, 0, expirationSeconds);

                try
                {
                    bool set = Database.StringSet(key, serializedData, expires);
                }
                catch(Exception exc)
                {
                    Logger.LogError(exc, $"Error setting cached value for key {key}");
                    return computeData();
                }

                return newData;
            }
        }

        // information is cached
        try
        {
            T cacheData = JsonConvert.DeserializeObject<T>(cachedValue);
            return cacheData;
        }
        catch(Exception exc)
        {
            Logger.LogError(exc, $"Error deserializing  cached value for key {key}");
            throw;
        }
    }
}

Usage example

var questionInfo = await RedisCacheService.GetInfoThroughCache($"question_{questionId}", QuestionFullInfoCacheExpiration, () =>
    {
        var questionInfo = GetQuestionHeaderInfo(questionId);
        if (questionInfo == null)
            throw new ArgumentException($"No question info for id = {questionId}");

        GetQuestionFullInfo_QuestionComments(questionInfo);
        GetQuestionFullInfo_Answers(questionInfo);

        return questionInfo;
    });

This seems to work fine (caching spares significant database processing time), although I have not performed any parallel testing (multiple threads accessing this functionality).

Next step would be to try a faster serializer (Protobuf?).

Any thoughts? I am interested mostly in code design rather than performance aspects.

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

0 answers

Sign up to answer this question »