Standard Implementation of REN In Memory Cache Service

RENInMemoryCacheService is a service that allows you to handle cache operations in memorily. Here is the standard implementation:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Ren.Kit.CacheKit.Abstractions;

namespace Ren.Kit.CacheKit.Services
{
    public class RENInMemoryCacheService : IRENCacheService
    {
        private readonly IMemoryCache _cache;
        private MemoryCacheEntryOptions _defaultOptions;

        private int _defaultAbsoluteExpirationHours = 12;
        private int _defaultSlidingExpirationMinutes = 30;
        private bool _useDefaultAbsoluteExpirationWhenNull = true;

        public RENInMemoryCacheService(IMemoryCache cache)
            : this(cache, null)
        {
        }

        public RENInMemoryCacheService(IMemoryCache cache, IOptions<RENCacheKitOptions>? options = null)
        {
            _cache = cache;

            var opt = options?.Value?.CacheConfiguration;

            if (opt is not null)
            {
                _useDefaultAbsoluteExpirationWhenNull =
                    opt.UseDefaultAbsoluteExpirationWhenNull;

                var timeConfig = opt.InMemoryConfiguration?.TimeConfiguration;
                if (timeConfig is not null)
                {
                    _defaultAbsoluteExpirationHours =
                        timeConfig.AbsoluteExpirationInHours;

                    _defaultSlidingExpirationMinutes =
                        timeConfig.SlidingExpirationInMinutes;
                }
            }

            _defaultOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromHours(_defaultAbsoluteExpirationHours),

                SlidingExpiration =
                    TimeSpan.FromMinutes(_defaultSlidingExpirationMinutes)
            };
        }

        /// <inheritdoc />
        public virtual T? Get<T>(string cacheKey)
        {
            if (_cache.TryGetValue(cacheKey, out var value) && value is T typed)
                return typed;
            return default;
        }

        /// <inheritdoc />
        public virtual Task<T?> GetAsync<T>(string cacheKey, CancellationToken cancellationToken = default)
            => Task.FromResult(Get<T>(cacheKey));

        /// <inheritdoc />
        public virtual string? Get(string cacheKey)
            => _cache.TryGetValue(cacheKey, out var value) ? value?.ToString() : null;

        /// <inheritdoc />
        public virtual Task<string?> GetAsync(string cacheKey, CancellationToken cancellationToken = default)
            => Task.FromResult(Get(cacheKey));

        /// <inheritdoc />
        public virtual void Set<T>(string cacheKey, T data, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null)
        {
            TimeSpan? abs =
                absoluteExpiration
                ?? (_useDefaultAbsoluteExpirationWhenNull ? _defaultOptions.AbsoluteExpirationRelativeToNow : null);

            TimeSpan? sliding = slidingExpiration;

            var options = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = abs,
                SlidingExpiration = sliding
            };

            _cache.Set(cacheKey, data!, options);
        }

        /// <inheritdoc />
        public virtual Task SetAsync<T>(string cacheKey, T data, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default)
        {
            Set(cacheKey, data, absoluteExpiration, slidingExpiration);
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public virtual void Remove(string cacheKey)
        {
            _cache.Remove(cacheKey);
        }

        /// <inheritdoc />
        public virtual Task RemoveAsync(string cacheKey, CancellationToken cancellationToken = default)
        {
            Remove(cacheKey);
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public virtual void Clear()
            => throw new NotSupportedException("IMemoryCache does not natively support clearing all cache entries. Implement key tracking if needed.");

        /// <inheritdoc />
        public virtual Task ClearAsync(CancellationToken cancellationToken = default)
        {
            Clear();
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public virtual async Task<T?> GetOrCreateAsync<T>(
            string cacheKey,
            Func<CancellationToken, Task<T?>> factory,
            TimeSpan? absoluteExpiration = null,
            TimeSpan? slidingExpiration = null,
            CancellationToken cancellationToken = default)
        {
            if (_cache.TryGetValue(cacheKey, out var cachedObj) && cachedObj is T value)
                return value;

            value = await factory(cancellationToken).ConfigureAwait(false);
            if (value is not null)
                Set(cacheKey, value, absoluteExpiration, slidingExpiration);

            return value;
        }

        /// <inheritdoc />
        public virtual Task<bool> ExistsAsync(string cacheKey, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrWhiteSpace(cacheKey))
                return Task.FromResult(false);

            return Task.FromResult(_cache.TryGetValue(cacheKey, out _));
        }

        /// <inheritdoc />
        public virtual Task<bool> RefreshAsync(string cacheKey, TimeSpan? absoluteExpiration = null, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrWhiteSpace(cacheKey))
                return Task.FromResult(false);

            if (!_cache.TryGetValue(cacheKey, out var value) || value is null)
                return Task.FromResult(false);

            TimeSpan? abs =
                absoluteExpiration
                ?? (_useDefaultAbsoluteExpirationWhenNull ? _defaultOptions.AbsoluteExpirationRelativeToNow : null);

            if (value is byte[] bytes)
            {
                SetBytes(cacheKey, bytes, abs);
                return Task.FromResult(true);
            }

            _cache.Set(cacheKey, value, new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = abs,
                SlidingExpiration = _defaultOptions.SlidingExpiration
            });

            return Task.FromResult(true);
        }

        /// <inheritdoc />
        public virtual Task<long> RemoveManyAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (keys is null)
                return Task.FromResult(0L);

            long removed = 0;

            foreach (var k in keys)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (string.IsNullOrWhiteSpace(k)) continue;

                if (_cache.TryGetValue(k, out _))
                {
                    _cache.Remove(k);
                    removed++;
                }
            }

            return Task.FromResult(removed);
        }

        /// <inheritdoc />
        public virtual Task<long> IncrementAsync(string cacheKey, long by = 1, TimeSpan? absoluteExpiration = null, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrWhiteSpace(cacheKey))
                return Task.FromResult(0L);

            TimeSpan? abs =
                absoluteExpiration
                ?? (_useDefaultAbsoluteExpirationWhenNull ? _defaultOptions.AbsoluteExpirationRelativeToNow : null);

            lock (_cache)
            {
                long current = 0;

                if (_cache.TryGetValue(cacheKey, out var obj))
                {
                    if (obj is long l) current = l;
                    else if (obj is int i) current = i;
                }

                var next = checked(current + by);

                _cache.Set(cacheKey, next, new MemoryCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = abs,
                    SlidingExpiration = _defaultOptions.SlidingExpiration
                });

                return Task.FromResult(next);
            }
        }

        /// <inheritdoc />
        public Task HashSetAsync<T>(string key, string field, T value, TimeSpan? absoluteExpiration = null)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task<T?> HashGetAsync<T>(string key, string field)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task<bool> HashDeleteFieldAsync(string key, string field)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task<HashSet<string>> HashGetAllFieldsAsync(string key)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task<Dictionary<string, T>> HashGetAllAsync<T>(string key)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task<Dictionary<string, T?>> HashGetManyAsync<T>(string key, IEnumerable<string> fields, CancellationToken cancellationToken = default)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task HashSetManyAsync<T>(string key, IReadOnlyCollection<KeyValuePair<string, T>> items, TimeSpan? absoluteExpiration = null, CancellationToken cancellationToken = default)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public virtual byte[]? GetBytes(string cacheKey)
        {
            if (_cache.TryGetValue(cacheKey, out var value) && value is byte[] bytes)
                return bytes;
            return null;
        }

        /// <inheritdoc />
        public virtual Task<byte[]?> GetBytesAsync(string cacheKey, CancellationToken cancellationToken = default)
            => Task.FromResult(GetBytes(cacheKey));

        /// <inheritdoc />
        public virtual void SetBytes(string cacheKey, byte[] data, TimeSpan? absoluteExpiration = null)
        {
            TimeSpan? abs =
                absoluteExpiration
                ?? (_useDefaultAbsoluteExpirationWhenNull ? _defaultOptions.AbsoluteExpirationRelativeToNow : null);

            _cache.Set(cacheKey, data, new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = abs
            });
        }

        /// <inheritdoc />
        public virtual Task SetBytesAsync(string cacheKey, byte[] data, TimeSpan? absoluteExpiration = null, CancellationToken cancellationToken = default)
        {
            SetBytes(cacheKey, data, absoluteExpiration);
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public Task HashSetBytesAsync(string key, string field, byte[] value, TimeSpan? absoluteExpiration = null)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task<byte[]?> HashGetBytesAsync(string key, string field)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public Task<Dictionary<string, byte[]>> HashGetAllBytesAsync(string key)
            => throw new NotSupportedException("Hash operations are not supported by RENInMemoryCacheService.");

        /// <inheritdoc />
        public virtual Task<Dictionary<string, byte[]?>> GetBytesManyAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (keys is null)
                return Task.FromResult(new Dictionary<string, byte[]?>(0, StringComparer.Ordinal));

            var arr = keys
                .Where(k => !string.IsNullOrWhiteSpace(k))
                .Distinct(StringComparer.Ordinal)
                .ToArray();

            if (arr.Length == 0)
                return Task.FromResult(new Dictionary<string, byte[]?>(0, StringComparer.Ordinal));

            var dict = new Dictionary<string, byte[]?>(arr.Length, StringComparer.Ordinal);

            foreach (var key in arr)
            {
                cancellationToken.ThrowIfCancellationRequested();

                if (_cache.TryGetValue(key, out var value) && value is byte[] bytes)
                    dict[key] = bytes;
                else
                    dict[key] = null;
            }

            return Task.FromResult(dict);
        }

        /// <inheritdoc />
        public virtual Task SetBytesManyAsync(IReadOnlyCollection<KeyValuePair<string, byte[]>> items, TimeSpan? absoluteExpiration = null, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (items is null || items.Count == 0)
                return Task.CompletedTask;

            foreach (var kv in items)
            {
                cancellationToken.ThrowIfCancellationRequested();

                if (string.IsNullOrWhiteSpace(kv.Key))
                    continue;

                if (kv.Value is null || kv.Value.Length == 0)
                    continue;

                SetBytes(kv.Key, kv.Value, absoluteExpiration);
            }

            return Task.CompletedTask;
        }
    }
}

Here, you can see all methods are defined as virtual which allows you to override them if necessary according to your needs. To use this pre-defined methods, you need register them in Program.cs file:

Here is the content of RegisterRENCacheAccessHelpers:

You can use IRENCacheService like this:

circle-info

This code automatically uses in memory cache thanks to our registerations!

Last updated