Last active
August 17, 2024 20:26
-
-
Save IntranetFactory/09338b44aa3c6ad7b2cab0284acfe14c to your computer and use it in GitHub Desktop.
EFCoreSecondLevelCacheInterceptor Distributed cached provider using Redis sentinel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using adenin.Platform.Configuration; | |
| using adenin.Platform.Serialization.MessagePack; | |
| using EFCoreSecondLevelCacheInterceptor; | |
| using JetBrains.Annotations; | |
| using MessagePack; | |
| using MessagePack.Formatters; | |
| using MessagePack.Resolvers; | |
| using Microsoft.AspNetCore.Hosting; | |
| using Microsoft.Extensions.Configuration; | |
| using Microsoft.Extensions.DependencyInjection; | |
| using Microsoft.Extensions.Logging; | |
| using Microsoft.Extensions.Options; | |
| using StackExchange.Redis; | |
| using System; | |
| using System.Collections.Concurrent; | |
| using System.Collections.Generic; | |
| namespace adenin.Platform.Caching; | |
| // ReSharper disable once InconsistentNaming | |
| public class EFRedisCacheProvider : IEFCacheServiceProvider | |
| { | |
| public const string CacheName = "PlatformEFFusionCache"; | |
| private readonly IServiceProvider _serviceProvider; | |
| private readonly ILogger<EFRedisCacheProvider> _redisCacheProviderLogger; | |
| private readonly ConcurrentDictionary<string, ConnectionMultiplexer> _redisConnections = new(StringComparer.Ordinal); | |
| public EFRedisCacheProvider( | |
| IOptions<EFCoreSecondLevelCacheSettings> cacheSettings, | |
| IServiceProvider serviceProvider, | |
| ILogger<EFRedisCacheProvider> redisCacheProviderLogger) | |
| { | |
| ArgumentNullException.ThrowIfNull(cacheSettings); | |
| ArgumentNullException.ThrowIfNull(serviceProvider); | |
| _serviceProvider = serviceProvider; | |
| _redisCacheProviderLogger = redisCacheProviderLogger; | |
| } | |
| public void InsertValue(EFCacheKey cacheKey, [CanBeNull] EFCachedData value, EFCachePolicy cachePolicy) | |
| { | |
| ArgumentNullException.ThrowIfNull(cacheKey); | |
| ArgumentNullException.ThrowIfNull(cachePolicy); | |
| value ??= new EFCachedData { IsNull = true }; | |
| var redisDb = GetRedisConnection().GetDatabase(); | |
| var keyHash = cacheKey.KeyHash; | |
| foreach (var rootCacheKey in cacheKey.CacheDependencies) | |
| { | |
| if (string.IsNullOrWhiteSpace(rootCacheKey)) | |
| { | |
| continue; | |
| } | |
| var expiryTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + cachePolicy.CacheTimeout.TotalMilliseconds; | |
| redisDb.SortedSetAdd(rootCacheKey, keyHash, expiryTime); | |
| } | |
| var data = Serialize(value); | |
| redisDb.StringSet(keyHash, data, cachePolicy.CacheTimeout); | |
| } | |
| public void ClearAllCachedEntries() | |
| { | |
| _redisCacheProviderLogger.LogWarning("EFFusionCacheProvider.ClearAllCachedEntries was called"); | |
| } | |
| public EFCachedData? GetValue(EFCacheKey cacheKey, EFCachePolicy cachePolicy) | |
| { | |
| ArgumentNullException.ThrowIfNull(cacheKey); | |
| var redisDb = GetRedisConnection().GetDatabase(); | |
| var maybeValue = redisDb.StringGet(cacheKey.KeyHash); | |
| return maybeValue.HasValue ? Deserialize<EFCachedData>(maybeValue) : null; | |
| } | |
| public void InvalidateCacheDependencies(EFCacheKey cacheKey) | |
| { | |
| ArgumentNullException.ThrowIfNull(cacheKey); | |
| var redisDb = GetRedisConnection().GetDatabase(); | |
| foreach (var rootCacheKey in cacheKey.CacheDependencies) | |
| { | |
| if (string.IsNullOrWhiteSpace(rootCacheKey)) | |
| { | |
| continue; | |
| } | |
| var dependencyKeys = new HashSet<string>(); | |
| foreach (var item in redisDb.SortedSetScan(rootCacheKey)) | |
| { | |
| _ = dependencyKeys.Add(item.Element); | |
| } | |
| if (dependencyKeys.Count > 0) | |
| { | |
| redisDb.KeyDelete([.. dependencyKeys]); | |
| } | |
| redisDb.KeyDelete(rootCacheKey); | |
| } | |
| } | |
| private ConnectionMultiplexer GetRedisConnection() | |
| { | |
| return _redisConnections.GetOrAdd("redis", _ => | |
| { | |
| var config = _serviceProvider.GetRequiredService<IWebHostEnvironment>(); | |
| var redisSetting = config.GetAppConfiguration().GetValue<string>("RedisConnectionString"); | |
| var hosts = redisSetting.Split(','); | |
| if (hosts.Length <= 1) | |
| { | |
| return ConnectionMultiplexer.Connect(redisSetting); | |
| } | |
| var endpoints = new EndPointCollection(); | |
| foreach (var host in hosts) | |
| { | |
| endpoints.Add(host, 26379); | |
| } | |
| var redisOptions = new ConfigurationOptions | |
| { | |
| EndPoints = endpoints, | |
| ServiceName = "mymaster", | |
| TieBreaker = "" | |
| }; | |
| return ConnectionMultiplexer.Connect(redisOptions); | |
| }); | |
| } | |
| private static byte[] Serialize<T>(T? obj) | |
| { | |
| return MessagePackSerializer.Serialize(obj, MessagePackSerializerOptions.Standard.WithResolver(CustomResolvers)); | |
| } | |
| private static T? Deserialize<T>(byte[] data) | |
| { | |
| return MessagePackSerializer.Deserialize<T>(data, MessagePackSerializerOptions.Standard.WithResolver(CustomResolvers)); | |
| } | |
| private static IFormatterResolver CustomResolvers => CompositeResolver.Create([DBNullFormatter.Instance], | |
| [ | |
| NativeDateTimeResolver.Instance, | |
| ContractlessStandardResolver.Instance, | |
| StandardResolverAllowPrivate.Instance, | |
| TypelessContractlessStandardResolver.Instance, | |
| DynamicGenericResolver.Instance | |
| ] | |
| ); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
We switched to use MessagePack for serialization. I've updated the Gist to the version we use in prod without any problems since May.