From ffed92e85c89e27da7378a10364d9fcbeefcb955 Mon Sep 17 00:00:00 2001 From: Efrain Marin Date: Mon, 19 May 2025 10:29:52 -0600 Subject: [PATCH 1/4] feat: updated caching support - feat: Added memory caching support - feat: refactored dependency injection methods --- Core.BluePrint.Packages.sln | 2 +- .../Configuration/RegisterBlueprint.cs | 35 +++++--- ...edisCacheProvider.cs => ICacheProvider.cs} | 4 +- ...s.csproj => Core.Blueprint.Caching.csproj} | 1 + Core.Blueprint.Redis/MemoryCacheProvider.cs | 86 +++++++++++++++++++ Core.Blueprint.Redis/RedisCacheProvider.cs | 5 +- 6 files changed, 116 insertions(+), 17 deletions(-) rename Core.Blueprint.Redis/Contracts/{IRedisCacheProvider.cs => ICacheProvider.cs} (96%) rename Core.Blueprint.Redis/{Core.Blueprint.Redis.csproj => Core.Blueprint.Caching.csproj} (90%) create mode 100644 Core.Blueprint.Redis/MemoryCacheProvider.cs diff --git a/Core.BluePrint.Packages.sln b/Core.BluePrint.Packages.sln index 7a136ec..0c6eb67 100644 --- a/Core.BluePrint.Packages.sln +++ b/Core.BluePrint.Packages.sln @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Blueprint.KeyVault", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Mongo", "Core.Blueprint.Mongo\Core.Blueprint.Mongo.csproj", "{27A8E3E1-D613-4D5B-8105-485699409F1E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Redis", "Core.Blueprint.Redis\Core.Blueprint.Redis.csproj", "{11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Caching", "Core.Blueprint.Redis\Core.Blueprint.Caching.csproj", "{11F2AA11-FB98-4A33-AEE4-CD49588D2FE1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Blueprint.Storage", "Core.Blueprint.Storage\Core.Blueprint.Storage.csproj", "{636E4520-79F9-46C8-990D-08F2D24A151C}" EndProject diff --git a/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs b/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs index 3aeb596..7ebdcb0 100644 --- a/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs +++ b/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Configuration; +using Core.Blueprint.Caching; +using Core.Blueprint.Caching.Contracts; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,23 +19,32 @@ namespace Core.Blueprint.Redis.Configuration /// The updated service collection. public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration) { - // Retrieve the Redis connection string from the configuration. - // Get Redis configuration section - var redisConnectionString = configuration.GetSection("ConnectionStrings:Redis").Value; - if (string.IsNullOrEmpty(redisConnectionString)) + // TODO for the following variable we'll need to add in the appsettings.json the following config: "UseRedisCache": true, + bool useRedis = configuration.GetValue("UseRedisCache"); + //TODO decide wheter to use appsettings or the following ENV variable + useRedis = Environment.GetEnvironmentVariable("CORE_BLUEPRINT_PACKAGES_USE_REDIS")?.ToLower() == "true"; + + if (useRedis) { - throw new InvalidOperationException("Redis connection is not configured."); + var redisConnectionString = configuration.GetSection("ConnectionStrings:Redis").Value; + if (string.IsNullOrEmpty(redisConnectionString)) + { + throw new InvalidOperationException("Redis connection is not configured."); + } + + services.AddSingleton(provider => + new RedisCacheProvider(redisConnectionString, provider.GetRequiredService>())); + } + else + { + services.AddMemoryCache(); + services.AddSingleton(); } - // Register RedisCacheProvider - services.AddSingleton(provider => - new RedisCacheProvider(redisConnectionString, provider.GetRequiredService>())); - - // Get CacheSettings and register with the ICacheSettings interface var cacheSettings = configuration.GetSection("CacheSettings").Get(); if (cacheSettings == null) { - throw new InvalidOperationException("Redis CacheSettings section is not configured."); + throw new InvalidOperationException("CacheSettings section is not configured."); } services.AddSingleton(cacheSettings); diff --git a/Core.Blueprint.Redis/Contracts/IRedisCacheProvider.cs b/Core.Blueprint.Redis/Contracts/ICacheProvider.cs similarity index 96% rename from Core.Blueprint.Redis/Contracts/IRedisCacheProvider.cs rename to Core.Blueprint.Redis/Contracts/ICacheProvider.cs index f9a7b5e..cf0a323 100644 --- a/Core.Blueprint.Redis/Contracts/IRedisCacheProvider.cs +++ b/Core.Blueprint.Redis/Contracts/ICacheProvider.cs @@ -1,9 +1,9 @@ -namespace Core.Blueprint.Redis +namespace Core.Blueprint.Caching.Contracts { /// /// Interface for managing Redis cache operations. /// - public interface IRedisCacheProvider + public interface ICacheProvider { /// /// Retrieves a cache item by its key. diff --git a/Core.Blueprint.Redis/Core.Blueprint.Redis.csproj b/Core.Blueprint.Redis/Core.Blueprint.Caching.csproj similarity index 90% rename from Core.Blueprint.Redis/Core.Blueprint.Redis.csproj rename to Core.Blueprint.Redis/Core.Blueprint.Caching.csproj index e1322ba..8d17004 100644 --- a/Core.Blueprint.Redis/Core.Blueprint.Redis.csproj +++ b/Core.Blueprint.Redis/Core.Blueprint.Caching.csproj @@ -8,6 +8,7 @@ + diff --git a/Core.Blueprint.Redis/MemoryCacheProvider.cs b/Core.Blueprint.Redis/MemoryCacheProvider.cs new file mode 100644 index 0000000..91beb2a --- /dev/null +++ b/Core.Blueprint.Redis/MemoryCacheProvider.cs @@ -0,0 +1,86 @@ +using Core.Blueprint.Caching.Contracts; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Caching.Memory; +using System.Text.Json; + +namespace Core.Blueprint.Caching +{ + public sealed class MemoryCacheProvider : ICacheProvider + { + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + public MemoryCacheProvider(IMemoryCache cache, ILogger logger) + { + _cache = cache; + _logger = logger; + } + + public ValueTask GetAsync(string key) + { + if (_cache.TryGetValue(key, out var value)) + { + if (value is TEntity typedValue) + { + return ValueTask.FromResult(typedValue); + } + + try + { + var json = value?.ToString(); + var deserialized = JsonSerializer.Deserialize(json); + return ValueTask.FromResult(deserialized); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error deserializing cache value for key {Key}", key); + } + } + + return ValueTask.FromResult(default(TEntity)); + } + + public ValueTask SetAsync(string key, TEntity value, TimeSpan? expiry = null) + { + var options = new MemoryCacheEntryOptions(); + if (expiry.HasValue) + { + options.SetAbsoluteExpiration(expiry.Value); + } + + _cache.Set(key, value, options); + return ValueTask.CompletedTask; + } + + public ValueTask RemoveAsync(string key) + { + _cache.Remove(key); + return ValueTask.CompletedTask; + } + + public ValueTask ExistsAsync(string key) + { + return ValueTask.FromResult(_cache.TryGetValue(key, out _)); + } + + public ValueTask RefreshAsync(string key, TimeSpan? expiry = null) + { + // MemoryCache does not support sliding expiration refresh like Redis, + // so we must re-set the value manually if required. + + if (_cache.TryGetValue(key, out var value)) + { + _cache.Remove(key); + + var options = new MemoryCacheEntryOptions(); + if (expiry.HasValue) + { + options.SetAbsoluteExpiration(expiry.Value); + } + + _cache.Set(key, value, options); + } + + return ValueTask.CompletedTask; + } + } +} diff --git a/Core.Blueprint.Redis/RedisCacheProvider.cs b/Core.Blueprint.Redis/RedisCacheProvider.cs index 525e310..7715f44 100644 --- a/Core.Blueprint.Redis/RedisCacheProvider.cs +++ b/Core.Blueprint.Redis/RedisCacheProvider.cs @@ -1,14 +1,15 @@ using Azure.Identity; +using Core.Blueprint.Caching.Contracts; using Microsoft.Extensions.Logging; using StackExchange.Redis; using System.Text.Json; -namespace Core.Blueprint.Redis +namespace Core.Blueprint.Caching { /// /// Redis cache provider for managing cache operations. /// - public sealed class RedisCacheProvider : IRedisCacheProvider + public sealed class RedisCacheProvider : ICacheProvider { private IDatabase _cacheDatabase = null!; private readonly ILogger _logger; -- 2.49.1 From 398ca3d7b6c30cddcb4da543c745121d72e26ae7 Mon Sep 17 00:00:00 2001 From: Efrain Marin Date: Mon, 19 May 2025 10:32:59 -0600 Subject: [PATCH 2/4] fix: updated namespaces - code cleanup, removed unused usings --- Core.Blueprint.Redis/Adapters/CacheSettings.cs | 8 +------- Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs | 4 ++-- Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs | 8 ++------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Core.Blueprint.Redis/Adapters/CacheSettings.cs b/Core.Blueprint.Redis/Adapters/CacheSettings.cs index 0d357b4..2be3840 100644 --- a/Core.Blueprint.Redis/Adapters/CacheSettings.cs +++ b/Core.Blueprint.Redis/Adapters/CacheSettings.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Core.Blueprint.Redis +namespace Core.Blueprint.Caching.Adapters { public interface ICacheSettings { diff --git a/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs b/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs index 7ebdcb0..ec42cda 100644 --- a/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs +++ b/Core.Blueprint.Redis/Configuration/RegisterBlueprint.cs @@ -1,10 +1,10 @@ -using Core.Blueprint.Caching; +using Core.Blueprint.Caching.Adapters; using Core.Blueprint.Caching.Contracts; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Core.Blueprint.Redis.Configuration +namespace Core.Blueprint.Caching.Configuration { /// /// Provides extension methods for registering Redis-related services in the DI container. diff --git a/Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs b/Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs index f7f53cb..6d9e374 100644 --- a/Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs +++ b/Core.Blueprint.Redis/Helpers/RedisCacheKeyHelper.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace Core.Blueprint.Redis.Helpers +namespace Core.Blueprint.Caching.Helpers { /// /// Helper class for generating consistent and normalized cache keys. -- 2.49.1 From d2a8ced972b85da1992b6d7e6697d6620e06b471 Mon Sep 17 00:00:00 2001 From: Sergio Matias Urquin Date: Mon, 19 May 2025 13:23:00 -0600 Subject: [PATCH 3/4] Upgrade nugets --- Core.Blueprint.Redis/Core.Blueprint.Caching.csproj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Core.Blueprint.Redis/Core.Blueprint.Caching.csproj b/Core.Blueprint.Redis/Core.Blueprint.Caching.csproj index 8d17004..2b15ab8 100644 --- a/Core.Blueprint.Redis/Core.Blueprint.Caching.csproj +++ b/Core.Blueprint.Redis/Core.Blueprint.Caching.csproj @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + -- 2.49.1 From b90bb23f27247b0928119d9c68bd886ef1bcb671 Mon Sep 17 00:00:00 2001 From: Oscar Morales Date: Mon, 19 May 2025 14:12:32 -0600 Subject: [PATCH 4/4] Add FindOnePipelineAsync method --- .../Contracts/ICollectionRepository.cs | 8 ++++++++ .../Repositories/CollectionRepository.cs | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs b/Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs index 644452d..92dd967 100644 --- a/Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs +++ b/Core.Blueprint.Mongo/Contracts/ICollectionRepository.cs @@ -148,5 +148,13 @@ namespace Core.Blueprint.Mongo /// An expression used to filter the documents to delete. /// A representing the asynchronous operation. Task DeleteManyAsync(Expression> filterExpression); + + /// + /// Executes an aggregation pipeline and returns the first document in the result asynchronously. + /// + /// The type of the output document you expect from the pipeline. + /// The aggregation pipeline definition to execute. + /// The first document from the aggregation result, or null if none found. + Task FindOnePipelineAsync(PipelineDefinition pipeline); } } diff --git a/Core.Blueprint.Mongo/Repositories/CollectionRepository.cs b/Core.Blueprint.Mongo/Repositories/CollectionRepository.cs index 9214251..3f5f6a0 100644 --- a/Core.Blueprint.Mongo/Repositories/CollectionRepository.cs +++ b/Core.Blueprint.Mongo/Repositories/CollectionRepository.cs @@ -248,5 +248,16 @@ namespace Core.Blueprint.Mongo { return Task.Run(() => _collection.DeleteManyAsync(filterExpression)); } + + /// + /// Executes an aggregation pipeline and returns the first document in the result asynchronously. + /// + /// The type of the output document you expect from the pipeline. + /// The aggregation pipeline definition to execute. + /// The first document from the aggregation result, or null if none found. + public virtual Task FindOnePipelineAsync(PipelineDefinition pipeline) + { + return Task.Run(() => _collection.Aggregate(pipeline).FirstOrDefaultAsync()); + } } } -- 2.49.1