From 919838ed7f230a043eee6c3f74da991c8a0b35a5 Mon Sep 17 00:00:00 2001 From: Harpreet Singh Date: Fri, 27 Jun 2025 10:41:41 +1000 Subject: [PATCH 1/9] feat: update cachemanage and cachereposiotry implementations to async. --- .../Cache/CouchDBCacheRepository.cs | 371 ++++++-- .../Cache/RedisCacheRepository.cs | 415 +++++++-- Src/CrispyWaffle/Cache/CacheManager.cs | 878 ++++++++++++++---- Src/CrispyWaffle/Cache/ICacheRepository.cs | 38 +- .../Cache/MemoryCacheRepository.cs | 140 ++- 5 files changed, 1497 insertions(+), 345 deletions(-) diff --git a/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs b/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs index f480684e..fd156ea1 100644 --- a/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs +++ b/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using CouchDB.Driver; +using CouchDB.Driver.Extensions; using CrispyWaffle.Cache; using CrispyWaffle.CouchDB.Utils.Communications; using CrispyWaffle.Log; @@ -42,31 +44,32 @@ public class CouchDBCacheRepository : ICacheRepository, IDisposable /// Finally, it converts the filtered results to a list and returns the count of those documents. /// This is useful for determining how many valid instances of a specific document type exist in the database. /// - public int GetDocCount() - where T : CouchDBCacheDocument => - ResolveDatabase().Where(x => x.Id != null).ToList().Count; + public async Task GetDocCount() + where T : CouchDBCacheDocument + { + var db = await ResolveDatabase().ConfigureAwait(false); + return db.Where(x => x.Id != null).ToList().Count; + } - /// - public void Clear() => Clear(); + /// + /// Clears all documents from the cache database. + /// + /// A task representing the asynchronous clear operation. + /// Thrown if ShouldPropagateExceptions is true and an error occurs during deletion. + public async Task Clear() => await Clear(); /// - /// Clears all documents of type from the CouchDB database. + /// Clears all documents of the specified type from the cache database. /// - /// The type of documents to be cleared, which must inherit from . - /// - /// This method retrieves all documents of the specified type from the database that have a non-null Id. - /// It then creates a list of asynchronous delete tasks for each document and waits for all tasks to complete. - /// If an exception occurs during the process, it checks whether exceptions should be propagated or handled. - /// If propagation is not desired, the exception is logged using the . - /// This method does not return any value and modifies the state of the database by removing documents. - /// - /// Thrown when an error occurs during the deletion process, unless exceptions are suppressed. - public void Clear() + /// The document type to clear, must inherit from CouchDBCacheDocument. + /// A task representing the asynchronous clear operation. + /// Thrown if ShouldPropagateExceptions is true and an error occurs during deletion. + public async Task Clear() where T : CouchDBCacheDocument { try { - var db = ResolveDatabase(); + var db = await ResolveDatabase().ConfigureAwait(false); var docs = db.Where(x => x.Id != null).ToList(); var tasks = new List(docs.Count); @@ -89,14 +92,18 @@ public void Clear() } /// - public T Get(string key) + public async ValueTask GetAsync(string key, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (!typeof(CouchDBCacheDocument).IsAssignableFrom(typeof(T))) { return default; } - return (T)(object)GetSpecific(key); + var result = await GetSpecificAsync(key); + + return (T)(object)result; } /// @@ -110,18 +117,22 @@ public T Get(string key) /// /// /// This method checks if the specified type is assignable from . - /// If it is, it calls the method to retrieve the cached document associated with the provided keys. + /// If it is, it calls the method to retrieve the cached document associated with the provided keys. /// If the type is not assignable, it returns the default value for that type, which could be null for reference types or zero for numeric types. /// This method is useful for retrieving cached data in a type-safe manner, ensuring that only compatible types are processed. /// - public T Get(string key, string subKey) + public async ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (!typeof(CouchDBCacheDocument).IsAssignableFrom(typeof(T))) { return default; } - return (T)(object)GetSpecific(key, subKey); + var result = await GetSpecificAsync(key, subKey, cancellationToken); + + return (T)(object)result; } /// @@ -129,23 +140,33 @@ public T Get(string key, string subKey) /// /// Type T with base type . /// A uniquely identifiable key to get document from the specified database. + /// Token to cancel the operation. /// The document if found. + /// Thrown if the operation is cancelled. /// Thrown in case the operation fails. - public T GetSpecific(string key) + public async Task GetSpecificAsync(string key, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { try { - var doc = ResolveDatabase().Where(x => x.Key == key).FirstOrDefault(); + cancellationToken.ThrowIfCancellationRequested(); + + var clientDb = await ResolveDatabase().ConfigureAwait(false); + var doc = await clientDb.FindAsync(key); if (doc != default && doc.ExpiresAt != default && doc.ExpiresAt <= DateTime.UtcNow) { - RemoveSpecific(key); + await RemoveSpecificAsync(key, cancellationToken).ConfigureAwait(false); return default; } return doc; } + catch (OperationCanceledException) + { + LogConsumer.Warning($"Operation cancelled while getting document with key: {key}"); + throw; + } catch (Exception e) { if (ShouldPropagateExceptions) @@ -156,41 +177,46 @@ public T GetSpecific(string key) LogConsumer.Handle(e); } - throw new InvalidOperationException($"Unable to get the item with key: {key}"); + throw new InvalidOperationException( + $"Unable to get the item with key: {key}" + ); } /// - /// Retrieves a specific document from the CouchDB cache based on the provided key and subKey. + /// Retrieves a specific cached document by key and subkey, removing it if expired. /// - /// The type of the document to retrieve, which must inherit from . - /// The primary key used to identify the document. - /// The secondary key used to further specify the document. - /// The document of type if found and not expired; otherwise, returns default for the type. - /// - /// This method queries the database for a document that matches the specified and . - /// If a matching document is found, it checks whether the document has expired by comparing its expiration time with the current UTC time. - /// If the document has expired, it is removed from the cache, and the method returns default. - /// If an exception occurs during the database operation, it is either logged or propagated based on the ShouldPropagateExceptions flag. - /// If no document is found and no exceptions are thrown, an is thrown indicating that the item could not be retrieved. - /// - /// Thrown when unable to retrieve the item with the specified key and sub key. - public T GetSpecific(string key, string subKey) + /// The document type to retrieve, must inherit from CouchDBCacheDocument. + /// The primary cache key. + /// The secondary cache key. + /// Token to cancel the operation. + /// The cached document if found and not expired; otherwise null. + /// Thrown if the operation is cancelled. + /// Thrown if ShouldPropagateExceptions is true and an error occurs. + public async Task GetSpecificAsync(string key, string subKey, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { try { - var doc = ResolveDatabase() + cancellationToken.ThrowIfCancellationRequested(); + + var client = await ResolveDatabase().ConfigureAwait(false); + var doc = client .Where(x => x.Key == key && x.SubKey == subKey) .FirstOrDefault(); if (doc != default && doc.ExpiresAt != default && doc.ExpiresAt <= DateTime.UtcNow) { - RemoveSpecific(key, subKey); + await RemoveSpecificAsync(key, subKey, cancellationToken).ConfigureAwait(false); return default; } return doc; } + catch (OperationCanceledException) + { + LogConsumer.Warning($"Operation cancelled while getting document with key and subkey: {key} {subKey}"); + throw; + } catch (Exception e) { if (ShouldPropagateExceptions) @@ -207,42 +233,76 @@ public T GetSpecific(string key, string subKey) } /// - public void Remove(string key) => RemoveSpecific(key); + /// A task representing the asynchronous operation. + /// Token to cancel the operation. + /// Key to be removed. + public async Task RemoveAsync(string key, CancellationToken cancellationToken = default) + { + await RemoveSpecificAsync(key, cancellationToken).ConfigureAwait(false); + } /// /// Removes a specific entry from the cache based on the provided key and subKey. /// /// The primary key associated with the entry to be removed. /// The secondary key associated with the entry to be removed. + /// Token to cancel the operation. /// /// This method calls the generic method RemoveSpecific with the type CouchDBCacheDocument to remove the entry /// identified by the combination of and . /// It is important to ensure that both keys are correctly specified to successfully remove the intended entry from the cache. /// If the specified entry does not exist, no action will be taken, and no exceptions will be thrown. /// - public void Remove(string key, string subKey) => - RemoveSpecific(key, subKey); + /// A task representing the asynchronous operation + public async Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default) + { + await RemoveSpecificAsync(key, subKey, cancellationToken).ConfigureAwait(false); + } + /// /// Removes from a class specified database instead of the general database. /// /// Type T with base type . /// A uniquely identifiable key to remove document from the specified database. - public void RemoveSpecific(string key) + /// Token to cancel the operation. + /// A task representing the asynchronous operation. + public async Task RemoveSpecificAsync(string key, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { + + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("Key cannot be null, empty, or whitespace.", nameof(key)); + } + try { - var db = _connector.CouchDBClient.GetDatabase(); - var doc = db.Where(x => x.Key == key).FirstOrDefault(); + cancellationToken.ThrowIfCancellationRequested(); + + // Using synchronous database resolution if async version not available + var db = await ResolveDatabase().ConfigureAwait(false); + var doc = await db.FindAsync(key).ConfigureAwait(false); - if (doc != default) + if (doc != null) { - db.DeleteAsync(doc).Wait(); + await db.DeleteAsync(doc).ConfigureAwait(false); + LogConsumer.Info($"Successfully removed document with key: {key}"); } + else + { + LogConsumer.Info($"Document with key: {key} not found for removal"); + } + } + catch (OperationCanceledException) + { + LogConsumer.Warning($"Operation cancelled while removing document with key: {key}"); + throw; } catch (Exception e) { + LogConsumer.Error($"Failed to remove document with key: {key}", e); + if (ShouldPropagateExceptions) { throw; @@ -258,6 +318,7 @@ public void RemoveSpecific(string key) /// The type of the CouchDB document, which must inherit from . /// The key of the document to be removed. /// The subKey of the document to be removed. + /// Token to cancel the operation /// /// This method retrieves a document from the CouchDB database that matches the specified /// and . If a matching document is found, it is deleted asynchronously from the database. @@ -265,21 +326,44 @@ public void RemoveSpecific(string key) /// . If exceptions are not propagated, they are logged using the /// . This method does not return any value. /// - public void RemoveSpecific(string key, string subKey) + /// A task representing the asynchronous operation. + /// Thrown when key is null, empty, or whitespace. + /// Thrown when the operation is cancelled. + + public async Task RemoveSpecificAsync(string key, string subKey, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("Key cannot be null, empty, or whitespace.", nameof(key)); + } + try { - var db = _connector.CouchDBClient.GetDatabase(); + cancellationToken.ThrowIfCancellationRequested(); + + var db = await ResolveDatabase().ConfigureAwait(false); var doc = db.Where(x => x.Key == key && x.SubKey == subKey).FirstOrDefault(); - if (doc != default) + if (doc != null) + { + await db.DeleteAsync(doc).ConfigureAwait(false); + LogConsumer.Info($"Successfully removed document with key: {key}"); + } + else { - db.DeleteAsync(doc).Wait(); + LogConsumer.Info($"Document with key: {key} not found for removal"); } } + catch (OperationCanceledException) + { + LogConsumer.Warning($"Operation cancelled while removing document with key: {key}"); + throw; + } catch (Exception e) { + LogConsumer.Error($"Failed to remove document with key: {key}", e); + if (ShouldPropagateExceptions) { throw; @@ -290,14 +374,14 @@ public void RemoveSpecific(string key, string subKey) } /// - public void Set(T value, string key, TimeSpan? ttl = null) + public async ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) { if (!typeof(CouchDBCacheDocument).IsAssignableFrom(typeof(T))) { return; } - SetSpecific((CouchDBCacheDocument)(object)value, key, ttl); + await SetSpecificAsync((CouchDBCacheDocument)(object)value, key, ttl, cancellationToken); } /// @@ -307,20 +391,43 @@ public void Set(T value, string key, TimeSpan? ttl = null) /// The value to be set in the cache. /// The primary key under which the value is stored. /// The sub-key under which the value is stored. + /// Token to cancel the operation. /// /// This method checks if the provided type is assignable from . /// If it is not, the method returns without performing any action. /// If the type is valid, it calls the method to set the value in the cache. /// This allows for type-safe caching of documents that inherit from . /// - public void Set(T value, string key, string subKey) + /// Thrown when the operation is cancelled. + /// A ValueTask representing the asynchronous operation. + public async ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) { - if (!typeof(CouchDBCacheDocument).IsAssignableFrom(typeof(T))) + try { - return; + cancellationToken.ThrowIfCancellationRequested(); + if (!typeof(CouchDBCacheDocument).IsAssignableFrom(typeof(T))) + { + return; + } + + await SetSpecificAsync((CouchDBCacheDocument)(object)value, key, subKey, cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + // Always propagate cancellation + LogConsumer.Warning($"Operation cancelled while setting document with key: {key}, subKey: {subKey}"); + throw; + } + catch (Exception ex) + { + // Log and wrap with context + LogConsumer.Error($"Failed to set cache item with key: '{key}', subKey: '{subKey}', type: {typeof(T).Name}", ex); - SetSpecific((CouchDBCacheDocument)(object)value, key, subKey); + if (ShouldPropagateExceptions) + { + throw; + } + } } /// @@ -330,11 +437,13 @@ public void Set(T value, string key, string subKey) /// The value of type T to be persisted. /// A uniquely identifiable key to remove document from the specified database. /// How long the value should be stored. - public void SetSpecific(T value, string key, TimeSpan? ttl = null) + /// A ValueTask representing the asynchronous operation + public async Task SetSpecificAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { try { + cancellationToken.ThrowIfCancellationRequested(); value.Key = key; if (ttl != null) @@ -343,7 +452,8 @@ public void SetSpecific(T value, string key, TimeSpan? ttl = null) value.ExpiresAt = DateTime.UtcNow.Add(ttl.Value); } - ResolveDatabase().CreateAsync(value).Wait(); + var clientDB = await ResolveDatabase().ConfigureAwait(false); + await clientDB.CreateAsync(value); } catch (Exception e) { @@ -363,6 +473,7 @@ public void SetSpecific(T value, string key, TimeSpan? ttl = null) /// The CouchDBCacheDocument instance to be updated. /// The key to be set for the CouchDBCacheDocument. /// The subKey to be set for the CouchDBCacheDocument. + /// Token to cancel the operation /// /// This method assigns the provided and to the specified /// of type . It then attempts to create or update the @@ -371,15 +482,25 @@ public void SetSpecific(T value, string key, TimeSpan? ttl = null) /// is rethrown; otherwise, it is logged using the LogConsumer. /// /// Thrown when an error occurs during the database operation, unless exceptions are suppressed. - public void SetSpecific(T value, string key, string subKey) + /// Thrown when the operation is cancelled + /// A task representing the asynchronous operation + public async Task SetSpecificAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { try { - value.Key = key; - value.SubKey = subKey; + cancellationToken.ThrowIfCancellationRequested(); + value.Key = key.Trim(); + value.SubKey = subKey.Trim(); - ResolveDatabase().CreateOrUpdateAsync(value).Wait(); + var clientDB = await ResolveDatabase().ConfigureAwait(false); + await clientDB.CreateOrUpdateAsync(value).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Always propagate cancellation + LogConsumer.Warning($"Operation cancelled while setting document with key: {key}, subKey: {subKey}"); + throw; } catch (Exception e) { @@ -393,53 +514,92 @@ public void SetSpecific(T value, string key, string subKey) } /// - public bool TryGet(string key, out T value) + public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default) { - var get = Get(key); + try + { + cancellationToken.ThrowIfCancellationRequested(); + var get = await GetAsync(key, cancellationToken); - if (get != default) + if (get != default) + { + var value = (T)(object)get; + return (true, value); + } + } + catch (OperationCanceledException) + { + // Always propagate cancellation + LogConsumer.Warning($"Operation cancelled while setting document with key: {key}"); + return (false, default(T)); + } + catch (Exception e) { - value = (T)(object)get; - return true; + if (ShouldPropagateExceptions) + { + throw; + } + + LogConsumer.Handle(e); } - value = default; - return false; + return (false, default(T)); } /// - /// Tries to retrieve a value associated with the specified keys from the cache. + /// Attempts to retrieve a cached document by key and subkey. /// /// The type of the value to retrieve. /// The primary key used to access the cached value. /// The secondary key used to access the cached value. - /// When this method returns, contains the retrieved value if successful; otherwise, the default value for type . - /// True if the value was found and retrieved successfully; otherwise, false. + /// Token to cancel the operation. + /// + /// A tuple containing Success (true if found and retrieved) and Value (the document cast to T or default(T)). + /// /// /// This method attempts to fetch a cached document from a data store using a combination of a primary key and a secondary key. - /// If the document is found, it is cast to the specified type and returned through the out parameter . - /// If no document is found, is set to its default value, and the method returns false. + /// If the document is found, it is cast to the specified type and returned./>. /// This is useful for safely attempting to retrieve values without throwing exceptions if the keys do not exist in the cache. /// - public bool TryGet(string key, string subKey, out T value) + /// Thrown if the operation is cancelled. + /// Thrown if ShouldPropagateExceptions is true and an error occurs. + public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default) { - var get = Get(key, subKey); - - if (get != default) + try { - value = (T)(object)get; - return true; + cancellationToken.ThrowIfCancellationRequested(); + var get = await GetAsync(key, subKey, cancellationToken); + if (get != default) + { + var value = (T)(object)get; + return (true, value); + } } + catch (OperationCanceledException) + { + // Always propagate cancellation + LogConsumer.Warning($"Operation cancelled while setting document with key and subkey: {key} {subKey}"); + throw; + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } - value = default; - return false; + LogConsumer.Handle(e); + } + + return (false, default(T)); } /// /// Retrieves the Time-To-Live (TTL) value for a specified cache key. /// /// The key for which the TTL value is to be retrieved. - /// The TimeSpan representing the TTL for the specified . + /// Token to cancel the operation. + /// The TTL of the cached document, or TimeSpan.Zero if not found or no TTL set /// /// This method accesses a CouchDB document associated with the provided /// and retrieves its Time-To-Live (TTL) value. The TTL indicates the duration for which the @@ -448,7 +608,36 @@ public bool TryGet(string key, string subKey, out T value) /// This method assumes that the key exists in the cache; if it does not, the behavior will depend /// on the implementation of the Get method. /// - public TimeSpan TTL(string key) => Get(key).TTL; + /// Thrown when the operation is cancelled + public async Task TTLAsync(string key, CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + var document = await GetAsync(key, cancellationToken).ConfigureAwait(false); + + return document?.TTL ?? TimeSpan.Zero; + } + catch (OperationCanceledException) + { + // Always propagate cancellation + LogConsumer.Warning($"Operation cancelled while setting document with key: {key}"); + throw; + } + catch (Exception ex) + { + LogConsumer.Error($"Failed to get TTL for key: '{key}'", ex); + + if (ShouldPropagateExceptions) + { + throw; + } + + // Return default value on error + return TimeSpan.Zero; + } + } /// /// Resolves a CouchDatabase instance for the specified database name or defaults to the type name if none is provided. @@ -462,7 +651,7 @@ public bool TryGet(string key, string subKey, out T value) /// ensuring that it handles database creation and retrieval efficiently. The use of generics allows for flexibility in specifying the type of documents /// that will be stored in the database, making this method suitable for various CouchDBCacheDocument types. /// - private CouchDatabase ResolveDatabase(string dbName = default) + private async Task> ResolveDatabase(string dbName = default) where T : CouchDBCacheDocument { if (string.IsNullOrEmpty(dbName)) @@ -470,8 +659,8 @@ private CouchDatabase ResolveDatabase(string dbName = default) dbName = $"{typeof(T).Name.ToLowerInvariant()}s"; } - return !_connector.CouchDBClient.GetDatabasesNamesAsync().Result.Contains(dbName) - ? _connector.CouchDBClient.CreateDatabaseAsync().Result + return !(await _connector.CouchDBClient.GetDatabasesNamesAsync()).Contains(dbName) + ? await _connector.CouchDBClient.CreateDatabaseAsync(dbName) : _connector.CouchDBClient.GetDatabase(); } diff --git a/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs index 59d39012..e608ef22 100644 --- a/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs +++ b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Threading; +using System.Threading.Tasks; using CrispyWaffle.Cache; using CrispyWaffle.Log; using CrispyWaffle.Redis.Utils.Communications; @@ -145,6 +147,7 @@ public void RemoveFromDatabase(string key, int databaseNumber) => /// true if [should propagate exceptions]; otherwise, false. public bool ShouldPropagateExceptions { get; set; } + /// /// Sets the specified value. /// @@ -152,19 +155,50 @@ public void RemoveFromDatabase(string key, int databaseNumber) => /// The value. /// The key. /// The TTL. - public void Set(T value, string key, TimeSpan? ttl = null) + /// To cancel operation. + /// A valuetask representing the asynchronous operation. + public async ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) { try { + // Throw if cancellation is requested before starting + cancellationToken.ThrowIfCancellationRequested(); + + Task cacheOperation; + if (ttl.HasValue) { - _cacheClient.Db0.AddAsync(key, value, ttl.Value).Wait(); + cacheOperation = _cacheClient.Db0.AddAsync(key, value, ttl.Value); + } + else + { + cacheOperation = _cacheClient.Db0.AddAsync(key, value); + } + + // If your cache client doesn't support cancellation tokens directly, + // you can use Task.WhenAny with a cancellation task + if (!cancellationToken.CanBeCanceled) + { + await cacheOperation.ConfigureAwait(false); } else { - _cacheClient.Db0.AddAsync(key, value).Wait(); + var cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken); + var completedTask = await Task.WhenAny(cacheOperation, cancellationTask).ConfigureAwait(false); + + if (completedTask == cancellationTask) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + await cacheOperation.ConfigureAwait(false); } } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions + throw; + } catch (Exception e) { if (ShouldPropagateExceptions) @@ -183,18 +217,83 @@ public void Set(T value, string key, TimeSpan? ttl = null) /// The value. /// The key. /// The sub key. - public void Set(T value, string key, string subKey) + /// To cancel operation. + /// /// + /// A ValueTask that represents the asynchronous hash field set operation. The task completes when: + /// - The hash field has been successfully updated in Redis, OR + /// - An exception occurs during the cache operation, OR + /// - The operation is cancelled via the cancellation token + /// The returned ValueTask has no result value (void equivalent for async operations). + /// + /// Completion scenarios: + /// • Success: Task completes successfully when hash field is updated + /// • Cancellation: Task throws OperationCanceledException if cancelled before or during execution + /// • Cache errors: Task may complete silently or throw exception based on ShouldPropagateExceptions setting + /// • Network errors: Task may complete silently or throw exception based on ShouldPropagateExceptions setting + /// + /// The ValueTask should always be awaited or its result checked to ensure proper exception handling. + /// + public async ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key cannot be null or empty", nameof(key)); + } + + if (string.IsNullOrEmpty(subKey)) + { + throw new ArgumentException("SubKey cannot be null or empty", nameof(subKey)); + } + + if (EqualityComparer.Default.Equals(value, default(T)) && !typeof(T).IsValueType) + { + throw new ArgumentNullException(nameof(value)); + } + try { - var allValues = _cacheClient.Db0.ExistsAsync(key).Result - ? _cacheClient.Db0.HashGetAllAsync(key, CommandFlags.PreferReplica).Result - : new Dictionary(); + // Early cancellation check + cancellationToken.ThrowIfCancellationRequested(); + + // Check if the hash key exists + var keyExists = await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false); + + // Get existing hash values if key exists, otherwise create new dictionary + IDictionary allValues; + if (keyExists) + { + // Check for cancellation before expensive operation + cancellationToken.ThrowIfCancellationRequested(); + + allValues = await _cacheClient.Db0.HashGetAllAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); + } + else + { + allValues = new Dictionary(); + } + + // Update the specific sub-key with the new value allValues[subKey] = value; - _cacheClient.Db0.HashSetAsync(key, allValues, CommandFlags.FireAndForget).Wait(); + + // Check for cancellation before final write operation + cancellationToken.ThrowIfCancellationRequested(); + + // Save the updated hash back to Redis + await _cacheClient.Db0.HashSetAsync(key, allValues, CommandFlags.FireAndForget).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; + } + catch (ArgumentException) + { + // Re-throw argument validation exceptions + throw; } catch (Exception e) { + // Handle cache/network exceptions according to configuration if (ShouldPropagateExceptions) { throw; @@ -209,19 +308,62 @@ public void Set(T value, string key, string subKey) /// /// The type of object (the object will be cast to this type). /// The key. - /// T. + /// Token to monitor for cancellation requests. default if no cancellation is required; + /// otherwise, a that can be used to cancel the operation. + /// Thrown when key is null or empty. /// "Unable to get the item with key. - public T Get(string key) + /// Thrown when the operation is cancelled. + /// + /// A ValueTask that represents the asynchronous cache get operation. The task result contains: + /// - The cached value of type T if the key exists in cache + /// + /// The returned ValueTask will complete with the cached value when: + /// • Success: Key exists and value is successfully retrieved from cache + /// • Exception: Key doesn't exist in cache (throws InvalidOperationException) + /// • Exception: Cache operation fails and ShouldPropagateExceptions is true + /// • Exception: Cache operation fails and ShouldPropagateExceptions is false (throws InvalidOperationException after logging) + /// • Cancellation: Operation is cancelled via the cancellation token (throws OperationCanceledException) + /// + /// The ValueTask should always be awaited to ensure proper exception handling. + /// + public async ValueTask GetAsync(string key, CancellationToken cancellationToken = default) { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key cannot be null or empty", nameof(key)); + } + try { - if (_cacheClient.Db0.ExistsAsync(key).Result) + // Early cancellation check + cancellationToken.ThrowIfCancellationRequested(); + + // Check if the key exists in cache + var keyExists = await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false); + + if (keyExists) { - return _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).Result; + // Check for cancellation before expensive get operation + cancellationToken.ThrowIfCancellationRequested(); + + // Retrieve the value from cache + var value = await _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); + return value; } } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; + } + catch (ArgumentException) + { + // Re-throw argument validation exceptions + throw; + } catch (Exception e) { + // Handle cache/network exceptions according to configuration if (ShouldPropagateExceptions) { throw; @@ -230,6 +372,7 @@ public T Get(string key) HandleException(e); } + // Key doesn't exist or operation failed with exception suppression throw new InvalidOperationException( string.Format(CultureInfo.CurrentCulture, "Unable to get the item with key {0}", key) ); @@ -241,21 +384,62 @@ public T Get(string key) /// The type of the document. /// The key. /// The sub key. - /// T. + /// Token to monitor for cancellation requests. default if no cancellation is required; + /// otherwise, a that can be used to cancel the operation. + /// + /// A ValueTask that represents the asynchronous hash field get operation. The task result contains: + /// - The cached value of type T if the hash field exists in cache + /// + /// The returned ValueTask will complete with the cached value when: + /// • Success: Hash field exists and value is successfully retrieved from cache + /// • Exception: Hash field doesn't exist in cache (throws InvalidOperationException) + /// • Exception: Cache operation fails and ShouldPropagateExceptions is true + /// • Exception: Cache operation fails and ShouldPropagateExceptions is false (throws InvalidOperationException after logging) + /// • Cancellation: Operation is cancelled via the cancellation token (throws OperationCanceledException) + /// + /// The ValueTask should always be awaited to ensure proper exception handling. + /// + /// Thrown when key or subKey is null or empty. /// Unable to get the item with key and sub key. - public T Get(string key, string subKey) + /// Thrown when the operation is cancelled. + /// + public async ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default) { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key cannot be null or empty", nameof(key)); + } + try { - if (_cacheClient.Db0.HashExistsAsync(key, subKey, CommandFlags.PreferReplica).Result) + // Early cancellation check + cancellationToken.ThrowIfCancellationRequested(); + + // Check if the key exists in cache + var keyExists = await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false); + + if (keyExists) { - return _cacheClient - .Db0.HashGetAsync(key, subKey, CommandFlags.PreferReplica) - .Result; + // Check for cancellation before expensive get operation + cancellationToken.ThrowIfCancellationRequested(); + + // Retrieve the value from cache + return await _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); } } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; + } + catch (ArgumentException) + { + // Re-throw argument validation exceptions + throw; + } catch (Exception e) { + // Handle cache/network exceptions according to configuration if (ShouldPropagateExceptions) { throw; @@ -264,13 +448,9 @@ public T Get(string key, string subKey) HandleException(e); } + // Key doesn't exist or operation failed with exception suppression throw new InvalidOperationException( - string.Format( - CultureInfo.CurrentCulture, - "Unable to get the item with key {0} and sub key {1}", - key, - subKey - ) + string.Format(CultureInfo.CurrentCulture, "Unable to get the item with key {0}", key) ); } @@ -280,69 +460,174 @@ public T Get(string key, string subKey) /// /// The type of object (the object will be cast to this type). /// The key. - /// The value. - /// Returns True if the object with the key exists, false otherwise. - public bool TryGet(string key, out T value) + /// Token to monitor for cancellation requests. default if no cancellation is required; + /// otherwise, a that can be used to cancel the operation. + /// + /// A ValueTask that represents the asynchronous try-get operation. The task result contains: + /// - A touple with Success=true and the cached value if the key exists + /// - A touple with Success=false and default(T) if the key doesn't exist or operation fails + /// + /// The returned ValueTask will complete when: + /// • Success: Key exists and value is successfully retrieved from cache + /// • Success: Key doesn't exist (returns false with default value) + /// • Exception: Cache operation fails and ShouldPropagateExceptions is true + /// • Exception: Operation is cancelled via the cancellation token (throws OperationCanceledException) + /// + /// When ShouldPropagateExceptions is false, cache errors are logged and the method returns false. + /// + /// Thrown when key is null or empty. + /// Thrown when the operation is cancelled. + public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default) { - value = default; + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key cannot be null or empty", nameof(key)); + } + try { - value = _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).Result; - return _cacheClient.Db0.ExistsAsync(key).Result; + // Early cancellation check + cancellationToken.ThrowIfCancellationRequested(); + + // More efficient: Check existence first, then get value if it exists + var keyExists = await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false); + + if (keyExists) + { + // Check for cancellation before expensive get operation + cancellationToken.ThrowIfCancellationRequested(); + + // Retrieve the value from cache + var value = await _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); + return (Success: true, Value: value); + } + + // Key doesn't exist + return (Success: false, Value: default(T)); + } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; + } + catch (ArgumentException) + { + // Re-throw argument validation exceptions + throw; } catch (Exception e) { + // Handle cache/network exceptions according to configuration if (ShouldPropagateExceptions) { throw; } HandleException(e); - } - return false; + // Return failed result when exceptions are suppressed + return (Success: false, Value: default(T)); + } } /// - /// Tries the get. + /// Asynchronously attempts to get a value from the cache using the specified key. + /// This method returns both the success status and the value in a single result. /// - /// The type of the document. - /// The key. - /// The sub key. - /// The value. - /// true if get the key, false otherwise. - public bool TryGet(string key, string subKey, out T value) + /// The type of value to retrieve from cache + /// The cache key used to identify the cached value. Cannot be null or empty. + /// The subkey. + /// Token to monitor for cancellation requests. default if no cancellation is required; + /// otherwise, a that can be used to cancel the operation. + /// + /// A ValueTask that represents the asynchronous try-get operation. The task result contains: + /// - A touple with Success=true and the cached value if the key exists + /// - A touple with Success=false and default(T) if the key doesn't exist or operation fails + /// + /// The returned ValueTask will complete when: + /// • Success: Key exists and value is successfully retrieved from cache + /// • Success: Key doesn't exist (returns false with default value) + /// • Exception: Cache operation fails and ShouldPropagateExceptions is true + /// • Exception: Operation is cancelled via the cancellation token (throws OperationCanceledException) + /// + /// When ShouldPropagateExceptions is false, cache errors are logged and the method returns false. + /// + /// Thrown when key is null or empty. + /// Thrown when the operation is cancelled. + public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default) { - value = default; + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException("Key cannot be null or empty", nameof(key)); + } + try { - value = _cacheClient - .Db0.HashGetAsync(key, subKey, CommandFlags.PreferReplica) - .Result; - return _cacheClient.Db0.HashExistsAsync(key, subKey).Result; + // Early cancellation check + cancellationToken.ThrowIfCancellationRequested(); + + // More efficient: Check existence first, then get value if it exists + var hashFieldExists = await _cacheClient.Db0.HashExistsAsync(key, subKey).ConfigureAwait(false); + + if (hashFieldExists) + { + // Check for cancellation before expensive get operation + cancellationToken.ThrowIfCancellationRequested(); + + // Retrieve the value from cache + var value = await _cacheClient.Db0.HashGetAsync(key, subKey, CommandFlags.PreferReplica).ConfigureAwait(false); + return (Success: true, Value: value); + } + + // Key doesn't exist + return (Success: false, Value: default(T)); + } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; + } + catch (ArgumentException) + { + // Re-throw argument validation exceptions + throw; } catch (Exception e) { + // Handle cache/network exceptions according to configuration if (ShouldPropagateExceptions) { throw; } HandleException(e); - } - return false; + // Return failed result when exceptions are suppressed + return (Success: false, Value: default(T)); + } } /// /// Removes the specified key from the cache. /// /// The key. - public void Remove(string key) + /// Token to monitor for cancellation requests. default if no cancellation is required; + /// otherwise, a that can be used to cancel the operation. + /// A ValueTask that represents the asynchronous call. + /// + /// Thrown when the operation is cancelled. + public async Task RemoveAsync(string key, CancellationToken cancellationToken = default) { try { - _cacheClient.Db0.RemoveAsync(key).Wait(); + cancellationToken.ThrowIfCancellationRequested(); + + await _cacheClient.Db0.RemoveAsync(key, CommandFlags.FireAndForget).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; } catch (Exception e) { @@ -356,15 +641,24 @@ public void Remove(string key) } /// - /// Removes the specified key. + /// Removes the specified key from the cache. /// /// The key. - /// The sub key. - public void Remove(string key, string subKey) + /// The subkey. + /// Token to monitor for cancellation requests. default if no cancellation is required; + /// otherwise, a that can be used to cancel the operation. + /// A ValueTask that represents the asynchronous call. + /// Thrown when the operation is cancelled. + public async Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default) { try { - _cacheClient.Db0.HashDeleteAsync(key, subKey, CommandFlags.FireAndForget).Wait(); + await _cacheClient.Db0.HashDeleteAsync(key, subKey, CommandFlags.FireAndForget).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; } catch (Exception e) { @@ -380,14 +674,23 @@ public void Remove(string key, string subKey) /// /// Returns the time to live of the specified key. /// - /// The key. + /// The key. + /// Tp cancel operation. + /// A representing the asynchronous operation. /// The timespan until this key is expired from the cache or 0 if it's already expired or doesn't exists. - public TimeSpan TTL(string key) + /// Thrown when the operation is cancelled. + public async Task TTLAsync(string key, CancellationToken cancellationToken = default) { try { - return _cacheClient.Db0.Database.KeyTimeToLive(key, CommandFlags.PreferReplica) - ?? TimeSpan.Zero; + cancellationToken.ThrowIfCancellationRequested(); + var ttl = await _cacheClient.Db0.Database.KeyTimeToLiveAsync(key, CommandFlags.PreferReplica); + return ttl ?? TimeSpan.Zero; + } + catch (OperationCanceledException) + { + // Re-throw cancellation exceptions to preserve cancellation semantics + throw; } catch (Exception e) { diff --git a/Src/CrispyWaffle/Cache/CacheManager.cs b/Src/CrispyWaffle/Cache/CacheManager.cs index 15878fbd..2c4e1ee1 100644 --- a/Src/CrispyWaffle/Cache/CacheManager.cs +++ b/Src/CrispyWaffle/Cache/CacheManager.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using CrispyWaffle.Composition; using CrispyWaffle.Log; +using Newtonsoft.Json.Linq; namespace CrispyWaffle.Cache; @@ -32,6 +37,11 @@ public static class CacheManager /// private static bool _isMemoryRepositoryInList; + /// + /// Default timeout. + /// + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + /// /// Adds a new cache repository of the specified type with an automatically assigned priority. /// @@ -106,13 +116,38 @@ public static int AddRepository(ICacheRepository repository, int priority) /// The type of the value to cache. /// The value to cache. /// The key under which to store the value. - public static void Set(T value, [Localizable(false)] string key) + /// Cancel. + /// If cancelled. + /// void. + public static async ValueTask SetAsync(T value, [Localizable(false)] string key, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + LogConsumer.Trace("Adding {0} to {1} cache repositories", key, _repositories.Count); - foreach (var repository in _repositories.Values) + + var tasks = _repositories.Values.Select(async repository => { - repository.Set(value, key); - } + try + { + await repository.SetAsync(value, key, cancellationToken: cancellationToken); + return true; + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + return false; + } + catch (Exception ex) + { + LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + return false; + } + }); + + var results = await Task.WhenAll(tasks); + var successCount = results.Count(r => r); + + LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count); } /// @@ -122,22 +157,48 @@ public static void Set(T value, [Localizable(false)] string key) /// The value to cache. /// The key under which to store the value. /// The sub key for additional categorization. - public static void Set( + /// Cancel. + /// If cancelled. + /// void. + public static async ValueTask SetAsync( T value, [Localizable(false)] string key, - [Localizable(false)] string subKey + [Localizable(false)] string subKey, + CancellationToken cancellationToken = default ) { + cancellationToken.ThrowIfCancellationRequested(); + LogConsumer.Trace( "Adding {0}/{2} to {1} cache repositories", key, _repositories.Count, subKey ); - foreach (var repository in _repositories.Values) + + var tasks = _repositories.Values.Select(async repository => { - repository.Set(value, key, subKey); - } + try + { + await repository.SetAsync(value, key, subKey, cancellationToken: cancellationToken); + return true; + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + return false; + } + catch (Exception ex) + { + LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + return false; + } + }); + + var results = await Task.WhenAll(tasks); + var successCount = results.Count(r => r); + + LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count); } /// @@ -147,18 +208,42 @@ public static void Set( /// The value to cache. /// The key under which to store the value. /// The time-to-live for the cached value. - public static void Set(T value, [Localizable(false)] string key, TimeSpan ttl) + /// Cancel. + /// If cancelled. + /// void. + public static async ValueTask SetAsync(T value, [Localizable(false)] string key, TimeSpan ttl, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + LogConsumer.Trace( "Adding {0} to {1} cache repositories with TTL of {2:g}", key, _repositories.Count, ttl ); - foreach (var repository in _repositories.Values) + var tasks = _repositories.Values.Select(async repository => { - repository.Set(value, key, ttl); - } + try + { + await repository.SetAsync(value, key, ttl, cancellationToken: cancellationToken); + return true; + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + return false; + } + catch (Exception ex) + { + LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + return false; + } + }); + + var results = await Task.WhenAll(tasks); + var successCount = results.Count(r => r); + + LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count); } /// @@ -168,12 +253,18 @@ public static void Set(T value, [Localizable(false)] string key, TimeSpan ttl /// The type of the value to cache. /// The value to cache. /// The key under which to store the value. + /// Cancel. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static void SetTo( + /// If cancelled. + /// void. + public static async ValueTask SetToAsync( TValue value, - [Localizable(false)] string key + [Localizable(false)] string key, + CancellationToken cancellationToken = default ) { + cancellationToken.ThrowIfCancellationRequested(); + var type = typeof(TCacheRepository); LogConsumer.Trace("Adding {0} to repository of type {1}", key, type.FullName); var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; @@ -184,7 +275,18 @@ public static void SetTo( ); } - repository.Set(value, key); + try + { + await repository.SetAsync(value, key, cancellationToken: cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + } } /// @@ -195,13 +297,19 @@ public static void SetTo( /// The value to cache. /// The key under which to store the value. /// The sub key for additional categorization. + /// Cancel. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static void SetTo( + /// If cancelled. + /// void. + public static async ValueTask SetToAsync( TValue value, [Localizable(false)] string key, - [Localizable(false)] string subKey + [Localizable(false)] string subKey, + CancellationToken cancellationToken = default ) { + cancellationToken.ThrowIfCancellationRequested(); + var type = typeof(TCacheRepository); LogConsumer.Trace("Adding {0}/{2} to repository of type {1}", key, type.FullName, subKey); var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; @@ -212,7 +320,18 @@ public static void SetTo( ); } - repository.Set(value, key, subKey); + try + { + await repository.SetAsync(value, key, subKey, cancellationToken: cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + } } /// @@ -223,13 +342,19 @@ public static void SetTo( /// The value to cache. /// The key under which to store the value. /// The time-to-live for this key. + /// Cancel. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static void SetTo( + /// If cancelled. + /// void. + public static async ValueTask SetToAsync( TValue value, [Localizable(false)] string key, - TimeSpan ttl + TimeSpan ttl, + CancellationToken cancellationToken = default ) { + cancellationToken.ThrowIfCancellationRequested(); + var type = typeof(TCacheRepository); LogConsumer.Trace( "Adding {0} to repository of type {1} with TTL of {2:g}", @@ -245,7 +370,18 @@ TimeSpan ttl ); } - repository.Set(value, key, ttl); + try + { + await repository.SetAsync(value, key, ttl, cancellationToken: cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + } } /// @@ -257,14 +393,20 @@ TimeSpan ttl /// The key under which to store the value. /// The sub key of the cached value. /// The time-to-live for this key. + /// Cancel. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static void SetTo( + /// If cancelled. + /// void. + public static async ValueTask SetToAsync( TValue value, [Localizable(false)] string key, [Localizable(false)] string subKey, - TimeSpan ttl + TimeSpan ttl, + CancellationToken cancellationToken = default ) { + cancellationToken.ThrowIfCancellationRequested(); + var type = typeof(TCacheRepository); LogConsumer.Trace( "Adding {0}/{2} to repository of type {1} with TTL of {2:g}", @@ -281,7 +423,18 @@ TimeSpan ttl ); } - repository.Set(value, key, subKey); + try + { + await repository.SetAsync(value, key, subKey, cancellationToken: cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + } } /// @@ -289,28 +442,91 @@ TimeSpan ttl /// /// The type of the value to retrieve. /// The key of the cached value. + /// Cancellation token. /// The retrieved value. /// Unable to get the item with key {key}. - public static T Get([Localizable(false)] string key) + /// Operation cancelled. + public static async ValueTask GetAsync([Localizable(false)] string key, CancellationToken cancellationToken = default) { - LogConsumer.Trace( - "Getting {0} from any of {1} cache repositories", - key, - _repositories.Count - ); - foreach (var repository in _repositories.Values) + try { - if (!repository.TryGet(key, out T value)) - { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); + + LogConsumer.Trace( + "Getting {0} from any of {1} cache repositories", + key, + _repositories.Count + ); + + // Create timeout for the entire operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + var errors = new List<(string Repository, Exception Error)>(); + + foreach (var repository in _repositories.Values) { - SetTo(value, key); - } + try + { + combinedCts.Token.ThrowIfCancellationRequested(); + + var repositoryName = repository.GetType().Name; + var repositoryStopwatch = Stopwatch.StartNew(); + + LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); + + // Try to get value from repository + var result = await repository.GetAsync(key, combinedCts.Token); + + repositoryStopwatch.Stop(); + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); + + // If found in non-memory repository, promote to memory cache + if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + { + _ = Task.Run( + async () => + { + try + { + await SetToAsync(result, key, CancellationToken.None); + LogConsumer.Debug("Promoted key {0} to memory cache", key); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); + } + }, cancellationToken); + } + + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + return result; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // User cancelled - stop immediately + LogConsumer.Info("Get operation cancelled by user for key {0}", key); + throw; + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + // Timeout occurred + LogConsumer.Error("Get operation timed out for key {0}", key); + throw new TimeoutException($"Get operation timed out for key {key}"); + } + catch (Exception ex) + { + var repositoryName = repository.GetType().Name; + errors.Add((repositoryName, ex)); - return value; + LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + throw; } throw new InvalidOperationException($"Unable to get the item with key {key}"); @@ -324,32 +540,86 @@ public static T Get([Localizable(false)] string key) /// The sub key of the cached value. /// The retrieved value. /// Unable to get the item with key {key} and sub key {subKey}. - public static T Get([Localizable(false)] string key, [Localizable(false)] string subKey) + public static async ValueTask GetAsync([Localizable(false)] string key, [Localizable(false)] string subKey, CancellationToken cancellationToken = default) { - LogConsumer.Trace( - "Getting {0}/{2} from any of {1} cache repositories", - key, - _repositories.Count, - subKey - ); - foreach (var repository in _repositories.Values) + try { - if (!repository.TryGet(key, subKey, out T value)) - { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); + + LogConsumer.Trace("Getting {0}/{2} from any of {1} cache repositories", key, _repositories.Count, subKey); + + // Create timeout for the entire operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + var errors = new List<(string Repository, Exception Error)>(); + + foreach (var repository in _repositories.Values) { - SetTo(value, key, subKey); - } + try + { + combinedCts.Token.ThrowIfCancellationRequested(); + + var repositoryName = repository.GetType().Name; + var repositoryStopwatch = Stopwatch.StartNew(); + + LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); - return value; + // Try to get value from repository + var result = await repository.GetAsync(key, subKey, combinedCts.Token); + + repositoryStopwatch.Stop(); + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); + + // If found in non-memory repository, promote to memory cache + if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + { + _ = Task.Run( + async () => + { + try + { + await SetToAsync(result, key, subKey, CancellationToken.None); + LogConsumer.Debug("Promoted key {0} to memory cache", key); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); + } + }, cancellationToken); + } + + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + return result; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // User cancelled - stop immediately + LogConsumer.Info("Get operation cancelled by user for key {0}", key); + throw; + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + // Timeout occurred + LogConsumer.Error("Get operation timed out for key {0}", key); + throw new TimeoutException($"Get operation timed out for key {key}"); + } + catch (Exception ex) + { + var repositoryName = repository.GetType().Name; + errors.Add((repositoryName, ex)); + + LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + throw; } - throw new InvalidOperationException( - $"Unable to get the item with key {key} and sub key {subKey}" - ); + throw new InvalidOperationException($"Unable to get the item with key {key}"); } /// @@ -358,21 +628,45 @@ public static T Get([Localizable(false)] string key, [Localizable(false)] str /// The type of the cache repository to retrieve the value from. /// The type of the value to retrieve. /// The key of the cached value. + /// Cancelation token. /// The retrieved value. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static TValue GetFrom([Localizable(false)] string key) + public static async ValueTask GetFrom([Localizable(false)] string key, CancellationToken cancellationToken = default) { - var type = typeof(TCacheRepository); - LogConsumer.Trace("Getting {0} from repository {1}", key, type.FullName); - var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; - if (repository == null) + try { - throw new InvalidOperationException( - $"The repository of type {type.FullName} isn't available in the repositories providers list" - ); - } + cancellationToken.ThrowIfCancellationRequested(); - return repository.Get(key); + var type = typeof(TCacheRepository); + + LogConsumer.Trace("Getting {0} from repository {1}", key, type.FullName); + var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; + if (repository == null) + { + throw new InvalidOperationException( + $"The repository of type {type.FullName} isn't available in the repositories providers list" + ); + } + + // Create timeout for the operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + var result = await repository.GetAsync(key, combinedCts.Token); + + return result; + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + throw; + } + catch (OperationCanceledException) + { + var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + LogConsumer.Error(error); + throw new TimeoutException(error); + } } /// @@ -382,24 +676,50 @@ public static TValue GetFrom([Localizable(false)] stri /// The type of the value to retrieve. /// The key of the cached value. /// The sub key of the cached value. + /// Cancelation token. /// The retrieved value. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static TValue GetFrom( + /// Operation cancelled. + public static async ValueTask GetFromAsync( [Localizable(false)] string key, - [Localizable(false)] string subKey + [Localizable(false)] string subKey, + CancellationToken cancellationToken = default ) { - var type = typeof(TCacheRepository); - LogConsumer.Trace("Getting {0}/{2} from repository {1}", key, type.FullName, subKey); - var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; - if (repository == null) + try { - throw new InvalidOperationException( - $"The repository of type {type.FullName} isn't available in the repositories providers list" - ); - } + cancellationToken.ThrowIfCancellationRequested(); + + var type = typeof(TCacheRepository); - return repository.Get(key, subKey); + LogConsumer.Trace("Getting {0} from repository {1}", key, type.FullName); + var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; + if (repository == null) + { + throw new InvalidOperationException( + $"The repository of type {type.FullName} isn't available in the repositories providers list" + ); + } + + // Create timeout for the operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + var result = await repository.GetAsync(key, subKey, combinedCts.Token); + + return result; + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + throw; + } + catch (OperationCanceledException) + { + var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + LogConsumer.Error(error); + throw new TimeoutException(error); + } } /// @@ -407,29 +727,86 @@ public static TValue GetFrom( /// /// The type of the value to retrieve. /// The key of the cached value. - /// The retrieved value, if found. + /// Cancelation token. + /// Operation cancelled. /// true if the value was found; otherwise, false. - public static bool TryGet([Localizable(false)] string key, out T value) + public static async ValueTask TryGetAsync([Localizable(false)] string key, CancellationToken cancellationToken = default) { - LogConsumer.Trace( - "Trying to get {0} from any of {1} repositories", - key, - _repositories.Count - ); - value = default; - foreach (var repository in _repositories.Values) + try { - if (!repository.TryGet(key, out value)) - { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); + + LogConsumer.Trace("Trying to get {0} from any of {1} repositories",key,_repositories.Count); - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + // Create timeout for the entire operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + var errors = new List<(string Repository, Exception Error)>(); + + foreach (var repository in _repositories.Values) { - SetTo(value, key); - } + try + { + combinedCts.Token.ThrowIfCancellationRequested(); + + var repositoryName = repository.GetType().Name; + var repositoryStopwatch = Stopwatch.StartNew(); + + LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); + + // Try to get value from repository + var result = await repository.GetAsync(key, combinedCts.Token); + + repositoryStopwatch.Stop(); + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); + + // If found in non-memory repository, promote to memory cache + if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + { + _ = Task.Run( + async () => + { + try + { + await SetToAsync(result, key, CancellationToken.None); + LogConsumer.Debug("Promoted key {0} to memory cache", key); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); + } + }, cancellationToken); + } + + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + return true; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // User cancelled - stop immediately + LogConsumer.Info("Get operation cancelled by user for key {0}", key); + throw; + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + // Timeout occurred + LogConsumer.Error("Get operation timed out for key {0}", key); + throw new TimeoutException($"Get operation timed out for key {key}"); + } + catch (Exception ex) + { + var repositoryName = repository.GetType().Name; + errors.Add((repositoryName, ex)); - return true; + LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + throw; } return false; @@ -441,34 +818,89 @@ public static bool TryGet([Localizable(false)] string key, out T value) /// The type of the value to retrieve. /// The key of the cached value. /// The sub key of the cached value. - /// The retrieved value, if found. + /// Cancel token. /// true if the value was found; otherwise, false. - public static bool TryGet( + public static async ValueTask TryGetAsync( [Localizable(false)] string key, [Localizable(false)] string subKey, - out T value + CancellationToken cancellationToken = default ) { - LogConsumer.Trace( - "Trying to get {0}/{2} from any of {1} repositories", - key, - _repositories.Count, - subKey - ); - value = default; - foreach (var repository in _repositories.Values) + try { - if (!repository.TryGet(key, subKey, out value)) - { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); + + LogConsumer.Trace("Trying to get {0}/{2} from any of {1} repositories", key, _repositories.Count, subKey); - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + // Create timeout for the entire operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + var errors = new List<(string Repository, Exception Error)>(); + + foreach (var repository in _repositories.Values) { - SetTo(value, key, subKey); - } + try + { + combinedCts.Token.ThrowIfCancellationRequested(); + + var repositoryName = repository.GetType().Name; + var repositoryStopwatch = Stopwatch.StartNew(); - return true; + LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); + + // Try to get value from repository + var result = await repository.GetAsync(key, subKey, combinedCts.Token); + + repositoryStopwatch.Stop(); + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); + + // If found in non-memory repository, promote to memory cache + if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + { + _ = Task.Run( + async () => + { + try + { + await SetToAsync(result, key, subKey, CancellationToken.None); + LogConsumer.Debug("Promoted key {0} to memory cache", key); + } + catch (Exception ex) + { + LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); + } + }, cancellationToken); + } + + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + return true; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // User cancelled - stop immediately + LogConsumer.Info("Get operation cancelled by user for key {0}", key); + throw; + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + // Timeout occurred + LogConsumer.Error("Get operation timed out for key {0}", key); + throw new TimeoutException($"Get operation timed out for key {key}"); + } + catch (Exception ex) + { + var repositoryName = repository.GetType().Name; + errors.Add((repositoryName, ex)); + + LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("Cache operation was cancelled"); + throw; } return false; @@ -479,7 +911,7 @@ out T value /// /// The key of the cached value. /// The TTL of the cached value, or TimeSpan.Zero if not found. - public static TimeSpan TTL([Localizable(false)] string key) + public static async Task TTL([Localizable(false)] string key) { LogConsumer.Trace( "Trying to get TTL of key {0} from {1} repositories", @@ -489,7 +921,7 @@ public static TimeSpan TTL([Localizable(false)] string key) var result = new TimeSpan(0); foreach (var repository in _repositories.Values) { - var currentResult = repository.TTL(key); + var currentResult = await repository.TTLAsync(key); if (currentResult == result) { continue; @@ -505,12 +937,58 @@ public static TimeSpan TTL([Localizable(false)] string key) /// Removes a cached value by key from all repositories. /// /// The key of the cached value to remove. - public static void Remove([Localizable(false)] string key) + /// Cancel token. + /// Completed task. + public static async ValueTask Remove([Localizable(false)] string key, CancellationToken cancellationToken = default) { - LogConsumer.Trace("Removing key {0} from {1} repositories", key, _repositories.Count); - foreach (var repository in _repositories.Values) + try + { + cancellationToken.ThrowIfCancellationRequested(); + + LogConsumer.Trace("Removing key {0} from {1} repositories", key, _repositories.Count); + + // Create timeout for the entire operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + var errors = new List<(string Repository, Exception Error)>(); + + foreach (var repository in _repositories.Values) + { + try + { + combinedCts.Token.ThrowIfCancellationRequested(); + + var repositoryName = repository.GetType().Name; + + LogConsumer.Debug("Attempting to delete key {0} from repository {1}", key, repositoryName); + + // Try to remove value from repository + await repository.RemoveAsync(key, combinedCts.Token); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // User cancelled - stop immediately + LogConsumer.Info("Get operation cancelled by user for key {0}", key); + throw; + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + // Timeout occurred + LogConsumer.Error("Get operation timed out for key {0}", key); + throw new TimeoutException($"Get operation timed out for key {key}"); + } + catch (Exception ex) + { + var repositoryName = repository.GetType().Name; + errors.Add((repositoryName, ex)); + } + } + } + catch (OperationCanceledException) { - repository.Remove(key); + Console.WriteLine("Cache operation was cancelled"); + throw; } } @@ -519,17 +997,58 @@ public static void Remove([Localizable(false)] string key) /// /// The key of the cached value to remove. /// The sub key of the cached value to remove. - public static void Remove([Localizable(false)] string key, [Localizable(false)] string subKey) + /// Cancel token. + /// Completed task. + public static async ValueTask Remove([Localizable(false)] string key, [Localizable(false)] string subKey, CancellationToken cancellationToken = default) { - LogConsumer.Trace( - "Removing key {0} and sub key {2} from {1} repositories", - key, - _repositories.Count, - subKey - ); - foreach (var repository in _repositories.Values) + try + { + cancellationToken.ThrowIfCancellationRequested(); + + LogConsumer.Trace("Removing key {0} and sub key {2} from {1} repositories", key ,_repositories.Count, subKey); + + // Create timeout for the entire operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + var errors = new List<(string Repository, Exception Error)>(); + + foreach (var repository in _repositories.Values) + { + try + { + combinedCts.Token.ThrowIfCancellationRequested(); + + var repositoryName = repository.GetType().Name; + + LogConsumer.Debug("Attempting to delete key {0} from repository {1}", key, repositoryName); + + // Try to remove value from repository + await repository.RemoveAsync(key, subKey, combinedCts.Token); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // User cancelled - stop immediately + LogConsumer.Info("Get operation cancelled by user for key {0}", key); + throw; + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + // Timeout occurred + LogConsumer.Error("Get operation timed out for key {0}", key); + throw new TimeoutException($"Get operation timed out for key {key}"); + } + catch (Exception ex) + { + var repositoryName = repository.GetType().Name; + errors.Add((repositoryName, ex)); + } + } + } + catch (OperationCanceledException) { - repository.Remove(key, subKey); + Console.WriteLine("Cache operation was cancelled"); + throw; } } @@ -538,20 +1057,44 @@ public static void Remove([Localizable(false)] string key, [Localizable(false)] /// /// The type of the cache repository. /// The key of the cached value. + /// Cancel token. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static void RemoveFrom([Localizable(false)] string key) + /// The operation get cancelled. + /// Completed task. + public static async ValueTask RemoveFrom([Localizable(false)] string key, CancellationToken cancellationToken = default) { - var type = typeof(TCacheRepository); - LogConsumer.Trace("Removing key {0} from {1} repository", key, _repositories.Count); - var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; - if (repository == null) + try { - throw new InvalidOperationException( - $"The repository of type {type.FullName} isn't available in the repositories providers list" - ); - } + cancellationToken.ThrowIfCancellationRequested(); + + var type = typeof(TCacheRepository); + + LogConsumer.Trace("Removing key {0} from {1} repository", key, _repositories.Count); + var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; + if (repository == null) + { + throw new InvalidOperationException( + $"The repository of type {type.FullName} isn't available in the repositories providers list" + ); + } - repository.Remove(key); + // Create timeout for the operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + await repository.RemoveAsync(key, combinedCts.Token); + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + throw; + } + catch (OperationCanceledException) + { + var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + LogConsumer.Error(error); + throw new TimeoutException(error); + } } /// @@ -560,27 +1103,48 @@ public static void RemoveFrom([Localizable(false)] string key) /// The type of the cache repository. /// The key of the cached value. /// The sub key of the cached value to remove. + /// Cancel token. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static void RemoveFrom( + /// The operation get cancelled. + /// Completed task. + public static async ValueTask RemoveFrom( [Localizable(false)] string key, - [Localizable(false)] string subKey + [Localizable(false)] string subKey, + CancellationToken cancellationToken = default ) { - var type = typeof(TCacheRepository); - LogConsumer.Trace( - "Removing key {0} and sub key {2} from {1} repository", - key, - _repositories.Count, - subKey - ); - var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; - if (repository == null) + + try { - throw new InvalidOperationException( - $"The repository of type {type.FullName} isn't available in the repositories providers list" - ); - } + cancellationToken.ThrowIfCancellationRequested(); - repository.Remove(key, subKey); + var type = typeof(TCacheRepository); + + LogConsumer.Trace("Removing key {0} and sub key {2} from {1} repository", key, _repositories.Count, subKey); + var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; + if (repository == null) + { + throw new InvalidOperationException( + $"The repository of type {type.FullName} isn't available in the repositories providers list" + ); + } + + // Create timeout for the operation + using var timeoutCts = new CancellationTokenSource(DefaultTimeout); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + await repository.RemoveAsync(key, subKey, combinedCts.Token); + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + throw; + } + catch (OperationCanceledException) + { + var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + LogConsumer.Error(error); + throw new TimeoutException(error); + } } } diff --git a/Src/CrispyWaffle/Cache/ICacheRepository.cs b/Src/CrispyWaffle/Cache/ICacheRepository.cs index 0a93e2d5..c3592f1f 100644 --- a/Src/CrispyWaffle/Cache/ICacheRepository.cs +++ b/Src/CrispyWaffle/Cache/ICacheRepository.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace CrispyWaffle.Cache; @@ -22,12 +24,14 @@ public interface ICacheRepository /// The value. /// The key. /// (Optional)The time to live for this key. + /// Cancel operation. + /// returns TaskValue. [System.Diagnostics.CodeAnalysis.SuppressMessage( "Naming", "CA1716:Identifiers should not match keywords", Justification = "Design choice." )] - void Set(T value, string key, TimeSpan? ttl = null); + ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default); /// /// Sets the specified value. @@ -36,18 +40,21 @@ public interface ICacheRepository /// The value. /// The key. /// The sub key. + /// To cancel the operation. + /// returns TaskValue. [System.Diagnostics.CodeAnalysis.SuppressMessage( "Naming", "CA1716:Identifiers should not match keywords", Justification = "Design choice." )] - void Set(T value, string key, string subKey); + ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default); /// /// Gets the object with the specified key. /// /// The type of object to return. /// The key. + /// Token to cancel request. /// The object as . /// Throws when the object with the specified key doesn't exists. [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -55,7 +62,7 @@ public interface ICacheRepository "CA1716:Identifiers should not match keywords", Justification = "Design choice." )] - T Get(string key); + ValueTask GetAsync(string key, CancellationToken cancellationToken = default); /// /// Gets the specified key. @@ -63,13 +70,14 @@ public interface ICacheRepository /// The type of object to return. /// The key. /// The sub key. + /// Token to cancel request. /// The object as . [System.Diagnostics.CodeAnalysis.SuppressMessage( "Naming", "CA1716:Identifiers should not match keywords", Justification = "Design choice." )] - T Get(string key, string subKey); + ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default); /// /// Tries to get a value based on its key, if exists return true, else false. @@ -77,9 +85,9 @@ public interface ICacheRepository /// /// The type of object to return if found. /// The key. - /// The value. + /// Cancel token. /// Returns True if the object with the key exists, false otherwise. - bool TryGet(string key, out T value); + ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default); /// /// Tries the get. @@ -87,32 +95,38 @@ public interface ICacheRepository /// The type of object to return if found. /// The key. /// The sub key. - /// The value. + /// Cancel token. /// Success info of the get, as a bool. - bool TryGet(string key, string subKey, out T value); + ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default); /// /// Removes the specified key from the cache. /// /// The key. - void Remove(string key); + /// Cancel token. + /// Completed Task. + Task RemoveAsync(string key, CancellationToken cancellationToken = default); /// /// Removes the specified key. /// /// The key. /// The sub key. - void Remove(string key, string subKey); + /// Cancel token. + /// Completed Task. + Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default); /// /// Returns the time to live of the specified key. /// /// The key. + /// Token to cancel the operation. /// The timespan until this key is expired from the cache or 0 if it's already expired or doesn't exists. - TimeSpan TTL(string key); + Task TTLAsync(string key, CancellationToken cancellationToken = default); /// /// Clears this instance. /// - void Clear(); + /// Completed Task. + Task Clear(); } diff --git a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs index b23982e4..175d6c4b 100644 --- a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs +++ b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace CrispyWaffle.Cache; @@ -35,9 +38,22 @@ public class MemoryCacheRepository : ICacheRepository /// The value. /// The key. /// This would be the TTL parameter, but it's not implemented in this type of cache (memory). Maybe in further version... + /// Cancellation token. /// The dictionary already contains the maximum number of elements. - public void Set(T value, string key, TimeSpan? ttl = null) => + /// If cancelled. + /// The Value. + public ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_data.Count >= int.MaxValue) + { + throw new OverflowException("The dictionary already contains the maximum number of elements."); + } + _data.AddOrUpdate(key, value, (_, _) => value); + return default(ValueTask); + } /// /// Sets the specified value. @@ -46,11 +62,18 @@ public void Set(T value, string key, TimeSpan? ttl = null) => /// The value. /// The key. /// The sub key. + /// Cancel. /// The dictionary already contains the maximum number of elements. - public void Set(T value, string key, string subKey) + /// If cancelled. + /// ValuTask. + public ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) { + + cancellationToken.ThrowIfCancellationRequested(); + var finalKey = $"{key}-{subKey}"; _hash.AddOrUpdate(finalKey, value, (_, _) => value); + return default(ValueTask); } /// @@ -58,10 +81,15 @@ public void Set(T value, string key, string subKey) /// /// The type of object (the object will be cast to this type). /// The key. + /// to cancel. /// The object as The type parameter. /// Throws when the object with the specified key doesn't exist. - public T Get(string key) + /// If cancelled. + public async ValueTask GetAsync(string key, CancellationToken cancellationToken = default) { + + cancellationToken.ThrowIfCancellationRequested(); + if (!_data.TryGetValue(key, out var value)) { throw new InvalidOperationException($"Unable to get the item with key {key}"); @@ -76,72 +104,115 @@ public T Get(string key) /// The type parameter. /// The key. /// The sub key. - /// T. + /// to cancel. /// Unable to get the item with key {key} and sub key {subKey}. - public T Get(string key, string subKey) + /// If cancelled. + /// Invalid cast. + /// T. + public ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + var finalKey = $"{key}-{subKey}"; - if (!_hash.TryGetValue(finalKey, out var value)) + if (!_hash.TryGetValue(finalKey, out var temp)) { throw new InvalidOperationException( $"Unable to get the item with key {key} and sub key {subKey}" ); } - return (T)value; + try + { + var value = (T)temp; + return new ValueTask(value); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Unable to convert the item with key {key} and sub key {subKey}"); + } } /// - /// Tries to get a value based on its key, if exists return true, else false. - /// The out parameter value is the object requested. + /// Attempts to retrieve a value from the cache by key. /// /// The type of object (the object will be cast to this type). /// The key. - /// The value. - /// Returns True if the object with the key exists, false otherwise. - public bool TryGet(string key, out T value) + /// Token to cancel the operation. + /// If cancelled. + /// Invalid cast of value. + /// + /// A tuple containing Success (true if found and castable to T) and Value (the cached item or default(T)). + /// + public ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default) { - value = default; + cancellationToken.ThrowIfCancellationRequested(); + if (!_data.TryGetValue(key, out var temp)) { - return false; + return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); } - value = (T)temp; - return true; + try + { + var value = (T)temp; + return new ValueTask<(bool Success, T Value)>((Success: true, Value: value)); + } + catch (InvalidCastException) + { + return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); + } } /// - /// Tries the get. + /// Attempts to retrieve a value from the cache by key. /// /// The type parameter. /// The key. - /// The sub key. - /// The value. - /// true if able to get the key and sub key, false otherwise. - public bool TryGet(string key, string subKey, out T value) + /// The sub key. + /// Cancel token. + /// If cancelled. + /// Invalid cast of value. + /// + /// A tuple containing Success (true if found and castable to T) and Value (the cached item or default(T)). + /// + public ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default) { - value = default; + cancellationToken.ThrowIfCancellationRequested(); + var finalKey = $"{key}-{subKey}"; if (!_hash.TryGetValue(finalKey, out var temp)) { - return false; + return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); ; } - value = (T)temp; - return true; + try + { + var value = (T)temp; + return new ValueTask<(bool Success, T Value)>((Success: true, Value: value)); ; + } + catch (InvalidCastException) + { + return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); ; + } } /// /// Removes the specified key from the cache. /// /// The key. - public void Remove(string key) + /// Cancel token. + /// If cancelled. + /// Completed Task. + public Task RemoveAsync(string key, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (_data.ContainsKey(key)) { _data.TryRemove(key, out _); } + + return Task.CompletedTask; } /// @@ -149,31 +220,42 @@ public void Remove(string key) /// /// The key. /// The sub key. - public void Remove(string key, string subKey) + /// Cancel token. + /// Completed Task. + public Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); var finalKey = $"{key}-{subKey}"; if (_data.ContainsKey(finalKey)) { _hash.TryRemove(finalKey, out _); } + + return Task.CompletedTask; } /// /// Returns the time to live of the specified key. /// /// The key. + /// Token to cancel the operation /// /// The timespan until this key is expired from the cache or 0 if it's already expired or doesn't exist. /// As Memory Cache does not implement TTL or expire mechanism, this will always return 0, even if the key exists. /// - public TimeSpan TTL(string key) => new TimeSpan(0); + public Task TTLAsync(string key, CancellationToken cancellationToken = default) + { + return Task.FromResult(TimeSpan.Zero); + } /// /// Clears this instance. /// - public void Clear() + /// Completed Task. + public Task Clear() { _data.Clear(); _hash.Clear(); + return Task.CompletedTask; } } From 0cd7449595a67455083f2f3dbaf3f089d2a44d86 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Fri, 27 Jun 2025 00:54:05 +0000 Subject: [PATCH 2/9] [CodeFactor] Apply fixes to commit 919838e --- .../Cache/CouchDBCacheRepository.cs | 4 +--- Src/CrispyWaffle/Cache/CacheManager.cs | 3 +-- Src/CrispyWaffle/Cache/MemoryCacheRepository.cs | 10 ++++------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs b/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs index fd156ea1..c8a9f0d9 100644 --- a/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs +++ b/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -270,7 +270,6 @@ public async Task RemoveAsync(string key, string subKey, CancellationToken cance public async Task RemoveSpecificAsync(string key, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { - if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentException("Key cannot be null, empty, or whitespace.", nameof(key)); @@ -329,7 +328,6 @@ public async Task RemoveSpecificAsync(string key, CancellationToken cancellat /// A task representing the asynchronous operation. /// Thrown when key is null, empty, or whitespace. /// Thrown when the operation is cancelled. - public async Task RemoveSpecificAsync(string key, string subKey, CancellationToken cancellationToken = default) where T : CouchDBCacheDocument { diff --git a/Src/CrispyWaffle/Cache/CacheManager.cs b/Src/CrispyWaffle/Cache/CacheManager.cs index 2c4e1ee1..489712ee 100644 --- a/Src/CrispyWaffle/Cache/CacheManager.cs +++ b/Src/CrispyWaffle/Cache/CacheManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; @@ -1113,7 +1113,6 @@ public static async ValueTask RemoveFrom( CancellationToken cancellationToken = default ) { - try { cancellationToken.ThrowIfCancellationRequested(); diff --git a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs index 175d6c4b..5c5f0bcc 100644 --- a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs +++ b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; @@ -68,7 +68,6 @@ public ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, Cancella /// ValuTask. public ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); var finalKey = $"{key}-{subKey}"; @@ -87,7 +86,6 @@ public ValueTask SetAsync(T value, string key, string subKey, CancellationTok /// If cancelled. public async ValueTask GetAsync(string key, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); if (!_data.TryGetValue(key, out var value)) @@ -182,17 +180,17 @@ public ValueTask GetAsync(string key, string subKey, CancellationToken can var finalKey = $"{key}-{subKey}"; if (!_hash.TryGetValue(finalKey, out var temp)) { - return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); ; + return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); } try { var value = (T)temp; - return new ValueTask<(bool Success, T Value)>((Success: true, Value: value)); ; + return new ValueTask<(bool Success, T Value)>((Success: true, Value: value)); } catch (InvalidCastException) { - return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); ; + return new ValueTask<(bool Success, T Value)>((Success: false, Value: default(T))); } } From 60f982fc24db19d639bbcfd3f2b845d586c9d208 Mon Sep 17 00:00:00 2001 From: Harpreet Singh Date: Mon, 30 Jun 2025 12:10:24 +1000 Subject: [PATCH 3/9] Feat: updated test cases and fixed minor changes. --- .../Cache/RedisCacheRepository.cs | 5 +- .../Communications/SmtpMailer.cs | 16 +++-- Src/CrispyWaffle/Cache/CacheManager.cs | 25 ++++--- .../Cache/MemoryCacheRepository.cs | 2 +- .../Cache/CouchDBCacheRepositoryTests.cs | 60 +++++++++-------- .../Cache/CacheManagerTests.cs | 67 ++++++++++--------- .../Cache/CouchDBCacheRepositoryTests.cs | 19 ++++-- .../Cache/MemoryCacheRepositoryTests.cs | 22 +++--- 8 files changed, 122 insertions(+), 94 deletions(-) diff --git a/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs index e608ef22..c3caec9f 100644 --- a/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs +++ b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs @@ -708,11 +708,12 @@ public async Task TTLAsync(string key, CancellationToken cancellationT /// /// Clears this instance. /// - public void Clear() + /// A representing the asynchronous operation. + public async Task Clear() { try { - _cacheClient.Db0.FlushDbAsync().Wait(); + await _cacheClient.Db0.FlushDbAsync(); } catch (Exception e) { diff --git a/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs b/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs index 8c26221c..dae53d66 100644 --- a/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs +++ b/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs @@ -8,6 +8,7 @@ using System.Net.Mime; using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; using CrispyWaffle.Cache; using CrispyWaffle.Configuration; @@ -329,7 +330,8 @@ public async Task SendAsync() } catch (Exception e) { - if (!HandleExtension(e, cacheKey)) + var result = await HandleExtension(e, cacheKey); + if (!result) { throw; } @@ -343,7 +345,11 @@ public async Task SendAsync() /// A representing the asynchronous operation. private async Task SendInternalAsync(string cacheKey) { - if (CacheManager.TryGet(cacheKey, out bool exists) && exists) + + (bool exists, _) = await CacheManager.TryGetAsync(cacheKey, CancellationToken.None); + + + if (exists) { LogConsumer.Trace("E-mail sending disabled due {0}", "network error"); return; @@ -366,8 +372,8 @@ private async Task SendInternalAsync(string cacheKey) /// /// The exception that occurred. /// The cache key to prevent repeated failures. - /// if the exception was handled; otherwise, . - private static bool HandleExtension(Exception e, string cacheKey) + /// A representing the asynchronous operation. + private static async Task HandleExtension(Exception e, string cacheKey) { TelemetryAnalytics.TrackMetric("SMTPError", e.Message); if ( @@ -376,7 +382,7 @@ private static bool HandleExtension(Exception e, string cacheKey) || e.Message.IndexOf(@"5.0.3", StringComparison.InvariantCultureIgnoreCase) != -1 ) { - CacheManager.Set(true, cacheKey, new TimeSpan(0, 15, 0)); + await CacheManager.SetAsync(true, cacheKey, new TimeSpan(0, 15, 0)); return true; } diff --git a/Src/CrispyWaffle/Cache/CacheManager.cs b/Src/CrispyWaffle/Cache/CacheManager.cs index 2c4e1ee1..513e2c6e 100644 --- a/Src/CrispyWaffle/Cache/CacheManager.cs +++ b/Src/CrispyWaffle/Cache/CacheManager.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using System.Xml.Linq; using CrispyWaffle.Composition; using CrispyWaffle.Log; using Newtonsoft.Json.Linq; @@ -502,6 +503,10 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); return result; } + catch (InvalidOperationException) + { + throw new InvalidOperationException($"Unable to get the item with key {key}"); + } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // User cancelled - stop immediately @@ -566,7 +571,7 @@ public static async ValueTask GetAsync([Localizable(false)] string key, [L LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); // Try to get value from repository - var result = await repository.GetAsync(key, subKey, combinedCts.Token); + var result = await repository.GetAsync(key, subKey, combinedCts.Token); repositoryStopwatch.Stop(); LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); @@ -729,8 +734,10 @@ public static async ValueTask GetFromAsync( /// The key of the cached value. /// Cancelation token. /// Operation cancelled. - /// true if the value was found; otherwise, false. - public static async ValueTask TryGetAsync([Localizable(false)] string key, CancellationToken cancellationToken = default) + /// + /// A tuple containing Success (true if found and castable to T) and Value (the cached item or default(T)). + /// + public static async ValueTask<(bool Success, T Value)> TryGetAsync([Localizable(false)] string key, CancellationToken cancellationToken = default) { try { @@ -780,7 +787,7 @@ public static async ValueTask TryGetAsync([Localizable(false)] string k } LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); - return true; + return (true, result); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -809,7 +816,7 @@ public static async ValueTask TryGetAsync([Localizable(false)] string k throw; } - return false; + return (false, default(T)); } /// @@ -911,17 +918,17 @@ public static async ValueTask TryGetAsync( /// /// The key of the cached value. /// The TTL of the cached value, or TimeSpan.Zero if not found. - public static async Task TTL([Localizable(false)] string key) + public static async Task TTLAsync([Localizable(false)] string key) { LogConsumer.Trace( "Trying to get TTL of key {0} from {1} repositories", key, _repositories.Count ); - var result = new TimeSpan(0); + TimeSpan result = new TimeSpan(0); foreach (var repository in _repositories.Values) { - var currentResult = await repository.TTLAsync(key); + TimeSpan currentResult = await repository.TTLAsync(key); if (currentResult == result) { continue; @@ -930,7 +937,7 @@ public static async Task TTL([Localizable(false)] string key) return currentResult; } - return new TimeSpan(0); + return result; // Default TTL if not found in any repository } /// diff --git a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs index 175d6c4b..55f71071 100644 --- a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs +++ b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs @@ -168,7 +168,7 @@ public ValueTask GetAsync(string key, string subKey, CancellationToken can /// /// The type parameter. /// The key. - /// The sub key. + /// The sub key. /// Cancel token. /// If cancelled. /// Invalid cast of value. diff --git a/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs b/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs index aa1c1b4a..1b820213 100644 --- a/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs +++ b/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs @@ -24,83 +24,85 @@ public CouchDBCacheRepositoryTests() } [Fact] - public void GetAndSetCouchDocTest() + public async Task GetAndSetAsyncCouchDocTest() { var doc = new CouchDBCacheDocument(); - _repository.Set(doc, Guid.NewGuid().ToString()); + await _repository.SetAsync(doc, Guid.NewGuid().ToString()); - var docDB = _repository.Get(doc.Key); + var docDB = await _repository.GetAsync(doc.Key); Assert.True(doc.Key == docDB.Key); - _repository.Remove(doc.Key); + await _repository.RemoveAsync(doc.Key); } [Fact] - public void GetAndSetSpecificTest() + public async Task GetAndSetSpecificAsyncTest() { var docOne = new Car("MakerOne"); - _repository.SetSpecific(docOne, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + await _repository.SetSpecificAsync(docOne, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); var docTwo = new Car("MakerTwo"); - _repository.SetSpecific(docTwo, Guid.NewGuid().ToString()); + await _repository.SetSpecificAsync(docTwo, Guid.NewGuid().ToString()); - var docDB = _repository.GetSpecific(docOne.Key); + var docDB = await _repository.GetSpecificAsync(docOne.Key); Assert.True( docOne.Key == docDB.Key && docOne.SubKey == docDB.SubKey && docOne.Maker == "MakerOne" ); - docDB = _repository.GetSpecific(docTwo.Key); + docDB = await _repository.GetSpecificAsync(docTwo.Key); Assert.True(docTwo.Key == docDB.Key && docTwo.Maker == "MakerTwo"); - _repository.RemoveSpecific(docOne.Key); - _repository.RemoveSpecific(docTwo.Key); + await _repository.RemoveSpecificAsync(docOne.Key); + await _repository.RemoveSpecificAsync(docTwo.Key); } [Fact] - public void RemoveCouchDocTest() + public async Task RemoveAsyncCouchDocTest() { var doc = new CouchDBCacheDocument(); - _repository.Set(doc, Guid.NewGuid().ToString()); + await _repository.SetAsync(doc, Guid.NewGuid().ToString()); - _repository.Remove(doc.Key); + await _repository.RemoveAsync(doc.Key); - var docDB = _repository.Get(doc.Key); + var docDB = await _repository.GetAsync(doc.Key); Assert.True(docDB == default); } [Fact] - public void RemoveSpecificTest() + public async Task RemoveSpecificAsyncTest() { var doc = new Car("Maker"); - _repository.SetSpecific(doc, Guid.NewGuid().ToString()); + await _repository.SetSpecificAsync(doc, Guid.NewGuid().ToString()); - _repository.RemoveSpecific(doc.Key); + await _repository.RemoveSpecificAsync(doc.Key); - var docDB = _repository.Get(doc.Key); + var docDB = await _repository.GetAsync(doc.Key); Assert.True(docDB == default); } [Fact] - public void DatabaseClearTest() + public async Task DatabaseClearTest() { - _repository.Set(new CouchDBCacheDocument(), Guid.NewGuid().ToString()); - _repository.Set(new CouchDBCacheDocument(), Guid.NewGuid().ToString()); - _repository.Set(new CouchDBCacheDocument(), Guid.NewGuid().ToString()); - _repository.Set(new CouchDBCacheDocument(), Guid.NewGuid().ToString()); + var task1 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); + var task2 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); + var task3 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); + var task4 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); - _repository.Clear(); + await Task.WhenAll(task1, task2, task3, task4); - var count = _repository.GetDocCount(); + await _repository.Clear(); + + var count = await _repository.GetDocCount(); Assert.True(count == 0); } @@ -110,14 +112,14 @@ public async Task TTLGetTest() { var doc = new CouchDBCacheDocument() { Key = Guid.NewGuid().ToString() }; - _repository.Set(new CouchDBCacheDocument(), doc.Key, new TimeSpan(0, 0, 5)); - var fromDB = _repository.Get(doc.Key); + await _repository.SetAsync(new CouchDBCacheDocument(), doc.Key, new TimeSpan(0, 0, 5)); + var fromDB = await _repository.GetAsync(doc.Key); Assert.True(doc.Key == fromDB.Key); await Task.Delay(6000); - fromDB = _repository.Get(doc.Key); + fromDB = await _repository.GetAsync(doc.Key); Assert.True(fromDB == null); } diff --git a/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs b/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs index 63f0ac25..c9341b44 100644 --- a/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs +++ b/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using CrispyWaffle.Cache; using FluentAssertions; using NSubstitute; @@ -45,22 +47,22 @@ public void AddRepositoryShouldAddRepositoryWithSpecifiedPriority() } [Fact] - public void SetShouldSetValueInAllRepositories() + public async Task SetAsyncShouldSetValueInAllRepositories() { // Arrange var key = "testKey"; var value = new { Name = "Test" }; - CacheManager.AddRepository(_mockCacheRepository); + CacheManager.AddRepository(_mockCacheRepository); // Act - CacheManager.Set(value, key); + await CacheManager.SetAsync(value, key); // Assert - _mockCacheRepository.Received().Set(value, key); + await _mockCacheRepository.Received(1).SetAsync(value, key); } [Fact] - public void SetShouldSetValueInRepositoriesWithTTL() + public async Task SetAsyncShouldSetValueInRepositoriesWithTTL() { // Arrange var key = "testKey"; @@ -69,72 +71,73 @@ public void SetShouldSetValueInRepositoriesWithTTL() CacheManager.AddRepository(_mockCacheRepository); // Act - CacheManager.Set(value, key, ttl); + await CacheManager.SetAsync(value, key, ttl); // Assert - _mockCacheRepository.Received().Set(value, key, ttl); + await _mockCacheRepository.Received(1).SetAsync(value, key, ttl); } [Fact] - public void GetShouldThrowWhenItemNotFound() + public async Task SetAsyncShouldSetValueWithSubKey() { // Arrange var key = "testKey"; - _mockCacheRepository.TryGet(key, out Arg.Any()).Returns(false); + var subKey = "testSubKey"; + var value = new { Name = "Test" }; + CacheManager.AddRepository(_mockCacheRepository); // Act - Action act = () => CacheManager.Get(key); + await CacheManager.SetAsync(value, key, subKey); // Assert - act.Should() - .Throw() - .WithMessage("Unable to get the item with key testKey"); + await _mockCacheRepository.Received(1).SetAsync(value, key, subKey, Arg.Any()); } [Fact] - public void SetToShouldThrowWhenRepositoryNotFound() + public async Task GetAsyncShouldThrowWhenItemNotFound() { // Arrange var key = "testKey"; - var value = new { Name = "Test" }; CacheManager.AddRepository(_mockCacheRepository); - // Act - Action act = () => CacheManager.SetTo(value, key); + Func act = async () => await CacheManager.GetAsync(key); // Assert - act.Should() - .Throw() - .WithMessage( - "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list" - ); + await act.Should() + .ThrowAsync() + .WithMessage("Unable to get the item with key testKey"); } [Fact] - public void RemoveShouldRemoveKeyFromAllRepositories() + public async Task SetToShouldThrowWhenRepositoryNotFound() { // Arrange var key = "testKey"; + var value = new { Name = "Test" }; CacheManager.AddRepository(_mockCacheRepository); // Act - CacheManager.Remove(key); + Func act = async () => await CacheManager.SetToAsync(value, key); // Assert - _mockCacheRepository.Received().Remove(key); - } + await act.Should() + .ThrowAsync() + .WithMessage( + "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list" + ); + } [Fact] - public void TTLShouldReturnCorrectTTLFromRepositories() + public async Task TTLShouldReturnCorrectTTLFromRepositories() { // Arrange var key = "testKey"; var expectedTTL = TimeSpan.FromMinutes(10); - CacheManager.AddRepository(_mockCacheRepository); - _mockCacheRepository.TTL(key).Returns(expectedTTL); + CacheManager.AddRepository(_mockCacheRepository); + await CacheManager.SetAsync(new { Name = "Test" }, key, expectedTTL, CancellationToken.None); // Set value with TTL // Act - var result = CacheManager.TTL(key); + var result = await CacheManager.TTLAsync(key); // Assert result.Should().Be(expectedTTL); @@ -148,11 +151,11 @@ public void RemoveFromShouldThrowWhenRepositoryNotFound() CacheManager.AddRepository(_mockCacheRepository); // Act - Action act = () => CacheManager.RemoveFrom(key); + Func act = async () => await CacheManager.RemoveFrom(key); // Assert act.Should() - .Throw() + .ThrowAsync() .WithMessage( "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list" ); diff --git a/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs b/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs index 0d6b4b4f..6985c868 100644 --- a/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs +++ b/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using CouchDB.Driver; using CrispyWaffle.CouchDB.Cache; using CrispyWaffle.CouchDB.Utils.Communications; @@ -24,40 +25,46 @@ public CouchDBCacheRepositoryTests() } [Fact] - public void SetToDatabaseShouldStoreValue() + public async Task SetToDatabaseShouldStoreValue() { // Arrange var key = "test-key"; var value = "test-value"; // Act - _repository.Set(value, key); + await _repository.SetAsync(value, key); // Assert } [Fact] - public void GetFromDatabaseShouldReturnStoredValue() + public async Task GetFromDatabaseShouldReturnStoredValue() { // Arrange var key = "test-key"; // Act - var actualValue = _repository.Get(key); + var actualValue = await _repository.GetAsync(key); // Assert actualValue.Should().BeNull(); } [Fact] - public void RemoveFromDatabaseShouldRemoveValue() + public async Task RemoveFromDatabaseShouldRemoveValue() { // Arrange var key = "test-key"; + var value = "test-value"; + + // Act + await _repository.SetAsync(value, key); // Act - _repository.Remove(key); + await _repository.RemoveAsync(key); + (bool success, _)= await _repository.TryGetAsync(key); // Assert + success.Should().Be(false); } } diff --git a/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs b/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs index cd49453b..6576e5e7 100644 --- a/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs +++ b/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using CrispyWaffle.Cache; using FluentAssertions; using Xunit; @@ -12,46 +13,47 @@ public class MemoryCacheRepositoryTests public MemoryCacheRepositoryTests() => _repository = new MemoryCacheRepository(); [Fact] - public void SetShouldStoreValue() + public async Task SetAsyncShouldStoreValue() { // Arrange var key = "test-key"; var value = "test-value"; - _repository.Set(value, key); + await _repository.SetAsync(value, key); // Act - var actualValue = _repository.Get(key); + var actualValue = await _repository.GetAsync(key); // Assert actualValue.Should().Be(value); } [Fact] - public void GetShouldReturnStoredValue() + public async Task GetShouldReturnStoredValue() { // Arrange var key = "test-key"; var expectedValue = "test-value"; - _repository.Set(expectedValue, key); + await _repository.SetAsync(expectedValue, key); // Act - var actualValue = _repository.Get(key); + var actualValue = await _repository.GetAsync(key); // Assert actualValue.Should().Be(expectedValue); } [Fact] - public void RemoveShouldRemoveStoredValue() + public async Task RemoveShouldRemoveStoredValue() { // Arrange var key = "test-key"; - _repository.Set("test-value", key); - _repository.Remove(key); + var Task1 = _repository.SetAsync("test-value", key).AsTask(); + var Task2 = _repository.RemoveAsync(key); + await Task.WhenAll(Task1, Task2); // Act - var exception = Assert.Throws(() => _repository.Get(key) + var exception = Assert.ThrowsAsync(async () => await _repository.GetAsync(key) ); // Assert From 8394e7cf1467410113fa62a42431963ad0fa0880 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Mon, 30 Jun 2025 02:15:28 +0000 Subject: [PATCH 4/9] [CodeFactor] Apply fixes --- Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs b/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs index dae53d66..68a04b2d 100644 --- a/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs +++ b/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -345,7 +345,6 @@ public async Task SendAsync() /// A representing the asynchronous operation. private async Task SendInternalAsync(string cacheKey) { - (bool exists, _) = await CacheManager.TryGetAsync(cacheKey, CancellationToken.None); From e5d61cc74a8cb6f2619b383227a3b12a9ddecde5 Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sat, 4 Jul 2026 03:55:20 +0100 Subject: [PATCH 5/9] refactor: switch to TryGetAsync for cache retrieval Modify CacheManager to use TryGetAsync for retrieving cache items, enabling operations to return success status along with the cached value. This refactoring improves error handling and ensures that exceptions only occur when explicitly thrown, such as during timeouts or cancellation. Update tests to reflect new TTL handling and remove redundant operations. Enhance settings.json to autoselect organization for Snyk, ensuring configurations are aligned for automation and security purposes. --- .vscode/settings.json | 3 +- Src/CrispyWaffle/Cache/CacheManager.cs | 44 ++++++++++++++----- .../Cache/CacheManagerTests.cs | 3 +- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 804a4175..a69c5d00 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,6 @@ "sonarlint.connectedMode.project": { "connectionId": "guibranco", "projectKey": "guibranco_CrispyWaffle" - } + }, + "snyk.advanced.autoSelectOrganization": true } \ No newline at end of file diff --git a/Src/CrispyWaffle/Cache/CacheManager.cs b/Src/CrispyWaffle/Cache/CacheManager.cs index 94737fb3..40e413e2 100644 --- a/Src/CrispyWaffle/Cache/CacheManager.cs +++ b/Src/CrispyWaffle/Cache/CacheManager.cs @@ -477,9 +477,16 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); // Try to get value from repository - var result = await repository.GetAsync(key, combinedCts.Token); + var (success, result) = await repository.TryGetAsync(key, combinedCts.Token); repositoryStopwatch.Stop(); + + if (!success) + { + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + continue; + } + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache @@ -500,13 +507,8 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca }, cancellationToken); } - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); return result; } - catch (InvalidOperationException) - { - throw new InvalidOperationException($"Unable to get the item with key {key}"); - } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // User cancelled - stop immediately @@ -571,9 +573,16 @@ public static async ValueTask GetAsync([Localizable(false)] string key, [L LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); // Try to get value from repository - var result = await repository.GetAsync(key, subKey, combinedCts.Token); + var (success, result) = await repository.TryGetAsync(key, subKey, combinedCts.Token); repositoryStopwatch.Stop(); + + if (!success) + { + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + continue; + } + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache @@ -594,7 +603,6 @@ public static async ValueTask GetAsync([Localizable(false)] string key, [L }, cancellationToken); } - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); return result; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) @@ -763,9 +771,16 @@ public static async ValueTask GetFromAsync( LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); // Try to get value from repository - var result = await repository.GetAsync(key, combinedCts.Token); + var (success, result) = await repository.TryGetAsync(key, combinedCts.Token); repositoryStopwatch.Stop(); + + if (!success) + { + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + continue; + } + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache @@ -786,7 +801,6 @@ public static async ValueTask GetFromAsync( }, cancellationToken); } - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); return (true, result); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) @@ -857,9 +871,16 @@ public static async ValueTask TryGetAsync( LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); // Try to get value from repository - var result = await repository.GetAsync(key, subKey, combinedCts.Token); + var (success, result) = await repository.TryGetAsync(key, subKey, combinedCts.Token); repositoryStopwatch.Stop(); + + if (!success) + { + LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + continue; + } + LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache @@ -880,7 +901,6 @@ public static async ValueTask TryGetAsync( }, cancellationToken); } - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); return true; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) diff --git a/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs b/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs index c9341b44..a2c3c8bf 100644 --- a/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs +++ b/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs @@ -133,7 +133,8 @@ public async Task TTLShouldReturnCorrectTTLFromRepositories() // Arrange var key = "testKey"; var expectedTTL = TimeSpan.FromMinutes(10); - CacheManager.AddRepository(_mockCacheRepository); + CacheManager.AddRepository(_mockCacheRepository); + _mockCacheRepository.TTLAsync(key, Arg.Any()).Returns(expectedTTL); await CacheManager.SetAsync(new { Name = "Test" }, key, expectedTTL, CancellationToken.None); // Set value with TTL // Act From 2fe1340bca8ae2e972f27b9fa6f19b358cb683ff Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sat, 4 Jul 2026 03:57:31 +0100 Subject: [PATCH 6/9] Format files --- .config/dotnet-tools.json | 5 +- .../Cache/CouchDBCacheRepository.cs | 122 +++-- .../Cache/RedisCacheRepository.cs | 94 +++- .../Communications/SmtpMailer.cs | 1 - Src/CrispyWaffle/Cache/CacheManager.cs | 444 ++++++++++++++---- Src/CrispyWaffle/Cache/ICacheRepository.cs | 31 +- .../Cache/MemoryCacheRepository.cs | 47 +- .../Cache/CouchDBCacheRepositoryTests.cs | 22 +- .../Cache/CacheManagerTests.cs | 20 +- .../Cache/CouchDBCacheRepositoryTests.cs | 2 +- .../Cache/MemoryCacheRepositoryTests.cs | 3 +- 11 files changed, 627 insertions(+), 164 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 924a7719..6e2e244c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "csharpier": { - "version": "1.2.6", + "version": "1.3.0", "commands": [ "csharpier" - ] + ], + "rollForward": false } } } \ No newline at end of file diff --git a/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs b/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs index c8a9f0d9..7100f063 100644 --- a/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs +++ b/Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs @@ -121,7 +121,11 @@ public async ValueTask GetAsync(string key, CancellationToken cancellation /// If the type is not assignable, it returns the default value for that type, which could be null for reference types or zero for numeric types. /// This method is useful for retrieving cached data in a type-safe manner, ensuring that only compatible types are processed. /// - public async ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async ValueTask GetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -144,7 +148,10 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok /// The document if found. /// Thrown if the operation is cancelled. /// Thrown in case the operation fails. - public async Task GetSpecificAsync(string key, CancellationToken cancellationToken = default) + public async Task GetSpecificAsync( + string key, + CancellationToken cancellationToken = default + ) where T : CouchDBCacheDocument { try @@ -177,9 +184,7 @@ public async Task GetSpecificAsync(string key, CancellationToken cancellat LogConsumer.Handle(e); } - throw new InvalidOperationException( - $"Unable to get the item with key: {key}" - ); + throw new InvalidOperationException($"Unable to get the item with key: {key}"); } /// @@ -192,7 +197,11 @@ public async Task GetSpecificAsync(string key, CancellationToken cancellat /// The cached document if found and not expired; otherwise null. /// Thrown if the operation is cancelled. /// Thrown if ShouldPropagateExceptions is true and an error occurs. - public async Task GetSpecificAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async Task GetSpecificAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) where T : CouchDBCacheDocument { try @@ -200,9 +209,7 @@ public async Task GetSpecificAsync(string key, string subKey, Cancellation cancellationToken.ThrowIfCancellationRequested(); var client = await ResolveDatabase().ConfigureAwait(false); - var doc = client - .Where(x => x.Key == key && x.SubKey == subKey) - .FirstOrDefault(); + var doc = client.Where(x => x.Key == key && x.SubKey == subKey).FirstOrDefault(); if (doc != default && doc.ExpiresAt != default && doc.ExpiresAt <= DateTime.UtcNow) { @@ -214,7 +221,9 @@ public async Task GetSpecificAsync(string key, string subKey, Cancellation } catch (OperationCanceledException) { - LogConsumer.Warning($"Operation cancelled while getting document with key and subkey: {key} {subKey}"); + LogConsumer.Warning( + $"Operation cancelled while getting document with key and subkey: {key} {subKey}" + ); throw; } catch (Exception e) @@ -238,7 +247,8 @@ public async Task GetSpecificAsync(string key, string subKey, Cancellation /// Key to be removed. public async Task RemoveAsync(string key, CancellationToken cancellationToken = default) { - await RemoveSpecificAsync(key, cancellationToken).ConfigureAwait(false); + await RemoveSpecificAsync(key, cancellationToken) + .ConfigureAwait(false); } /// @@ -254,12 +264,16 @@ public async Task RemoveAsync(string key, CancellationToken cancellationToken = /// If the specified entry does not exist, no action will be taken, and no exceptions will be thrown. /// /// A task representing the asynchronous operation - public async Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async Task RemoveAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { - await RemoveSpecificAsync(key, subKey, cancellationToken).ConfigureAwait(false); + await RemoveSpecificAsync(key, subKey, cancellationToken) + .ConfigureAwait(false); } - /// /// Removes from a class specified database instead of the general database. /// @@ -267,7 +281,10 @@ public async Task RemoveAsync(string key, string subKey, CancellationToken cance /// A uniquely identifiable key to remove document from the specified database. /// Token to cancel the operation. /// A task representing the asynchronous operation. - public async Task RemoveSpecificAsync(string key, CancellationToken cancellationToken = default) + public async Task RemoveSpecificAsync( + string key, + CancellationToken cancellationToken = default + ) where T : CouchDBCacheDocument { if (string.IsNullOrWhiteSpace(key)) @@ -328,7 +345,11 @@ public async Task RemoveSpecificAsync(string key, CancellationToken cancellat /// A task representing the asynchronous operation. /// Thrown when key is null, empty, or whitespace. /// Thrown when the operation is cancelled. - public async Task RemoveSpecificAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async Task RemoveSpecificAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) where T : CouchDBCacheDocument { if (string.IsNullOrWhiteSpace(key)) @@ -339,7 +360,7 @@ public async Task RemoveSpecificAsync(string key, string subKey, Cancellation try { cancellationToken.ThrowIfCancellationRequested(); - + var db = await ResolveDatabase().ConfigureAwait(false); var doc = db.Where(x => x.Key == key && x.SubKey == subKey).FirstOrDefault(); @@ -372,7 +393,12 @@ public async Task RemoveSpecificAsync(string key, string subKey, Cancellation } /// - public async ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) + public async ValueTask SetAsync( + T value, + string key, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default + ) { if (!typeof(CouchDBCacheDocument).IsAssignableFrom(typeof(T))) { @@ -398,7 +424,12 @@ public async ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, Ca /// /// Thrown when the operation is cancelled. /// A ValueTask representing the asynchronous operation. - public async ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) + public async ValueTask SetAsync( + T value, + string key, + string subKey, + CancellationToken cancellationToken = default + ) { try { @@ -408,18 +439,29 @@ public async ValueTask SetAsync(T value, string key, string subKey, Cancellat return; } - await SetSpecificAsync((CouchDBCacheDocument)(object)value, key, subKey, cancellationToken).ConfigureAwait(false); + await SetSpecificAsync( + (CouchDBCacheDocument)(object)value, + key, + subKey, + cancellationToken + ) + .ConfigureAwait(false); } catch (OperationCanceledException) { // Always propagate cancellation - LogConsumer.Warning($"Operation cancelled while setting document with key: {key}, subKey: {subKey}"); + LogConsumer.Warning( + $"Operation cancelled while setting document with key: {key}, subKey: {subKey}" + ); throw; } catch (Exception ex) { // Log and wrap with context - LogConsumer.Error($"Failed to set cache item with key: '{key}', subKey: '{subKey}', type: {typeof(T).Name}", ex); + LogConsumer.Error( + $"Failed to set cache item with key: '{key}', subKey: '{subKey}', type: {typeof(T).Name}", + ex + ); if (ShouldPropagateExceptions) { @@ -436,7 +478,12 @@ public async ValueTask SetAsync(T value, string key, string subKey, Cancellat /// A uniquely identifiable key to remove document from the specified database. /// How long the value should be stored. /// A ValueTask representing the asynchronous operation - public async Task SetSpecificAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) + public async Task SetSpecificAsync( + T value, + string key, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default + ) where T : CouchDBCacheDocument { try @@ -482,7 +529,12 @@ public async Task SetSpecificAsync(T value, string key, TimeSpan? ttl = null, /// Thrown when an error occurs during the database operation, unless exceptions are suppressed. /// Thrown when the operation is cancelled /// A task representing the asynchronous operation - public async Task SetSpecificAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) + public async Task SetSpecificAsync( + T value, + string key, + string subKey, + CancellationToken cancellationToken = default + ) where T : CouchDBCacheDocument { try @@ -497,7 +549,9 @@ public async Task SetSpecificAsync(T value, string key, string subKey, Cancel catch (OperationCanceledException) { // Always propagate cancellation - LogConsumer.Warning($"Operation cancelled while setting document with key: {key}, subKey: {subKey}"); + LogConsumer.Warning( + $"Operation cancelled while setting document with key: {key}, subKey: {subKey}" + ); throw; } catch (Exception e) @@ -512,7 +566,10 @@ public async Task SetSpecificAsync(T value, string key, string subKey, Cancel } /// - public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default) + public async ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + CancellationToken cancellationToken = default + ) { try { @@ -561,7 +618,11 @@ public async Task SetSpecificAsync(T value, string key, string subKey, Cancel /// /// Thrown if the operation is cancelled. /// Thrown if ShouldPropagateExceptions is true and an error occurs. - public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { try { @@ -576,7 +637,9 @@ public async Task SetSpecificAsync(T value, string key, string subKey, Cancel catch (OperationCanceledException) { // Always propagate cancellation - LogConsumer.Warning($"Operation cancelled while setting document with key and subkey: {key} {subKey}"); + LogConsumer.Warning( + $"Operation cancelled while setting document with key and subkey: {key} {subKey}" + ); throw; } catch (Exception e) @@ -613,7 +676,8 @@ public async Task TTLAsync(string key, CancellationToken cancellationT { cancellationToken.ThrowIfCancellationRequested(); - var document = await GetAsync(key, cancellationToken).ConfigureAwait(false); + var document = await GetAsync(key, cancellationToken) + .ConfigureAwait(false); return document?.TTL ?? TimeSpan.Zero; } diff --git a/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs index c3caec9f..6a08bfbc 100644 --- a/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs +++ b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs @@ -147,7 +147,6 @@ public void RemoveFromDatabase(string key, int databaseNumber) => /// true if [should propagate exceptions]; otherwise, false. public bool ShouldPropagateExceptions { get; set; } - /// /// Sets the specified value. /// @@ -157,7 +156,12 @@ public void RemoveFromDatabase(string key, int databaseNumber) => /// The TTL. /// To cancel operation. /// A valuetask representing the asynchronous operation. - public async ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) + public async ValueTask SetAsync( + T value, + string key, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default + ) { try { @@ -184,7 +188,8 @@ public async ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, Ca else { var cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken); - var completedTask = await Task.WhenAny(cacheOperation, cancellationTask).ConfigureAwait(false); + var completedTask = await Task.WhenAny(cacheOperation, cancellationTask) + .ConfigureAwait(false); if (completedTask == cancellationTask) { @@ -233,7 +238,12 @@ public async ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, Ca /// /// The ValueTask should always be awaited or its result checked to ensure proper exception handling. /// - public async ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) + public async ValueTask SetAsync( + T value, + string key, + string subKey, + CancellationToken cancellationToken = default + ) { if (string.IsNullOrEmpty(key)) { @@ -265,7 +275,9 @@ public async ValueTask SetAsync(T value, string key, string subKey, Cancellat // Check for cancellation before expensive operation cancellationToken.ThrowIfCancellationRequested(); - allValues = await _cacheClient.Db0.HashGetAllAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); + allValues = await _cacheClient + .Db0.HashGetAllAsync(key, CommandFlags.PreferReplica) + .ConfigureAwait(false); } else { @@ -279,7 +291,9 @@ public async ValueTask SetAsync(T value, string key, string subKey, Cancellat cancellationToken.ThrowIfCancellationRequested(); // Save the updated hash back to Redis - await _cacheClient.Db0.HashSetAsync(key, allValues, CommandFlags.FireAndForget).ConfigureAwait(false); + await _cacheClient + .Db0.HashSetAsync(key, allValues, CommandFlags.FireAndForget) + .ConfigureAwait(false); } catch (OperationCanceledException) { @@ -347,7 +361,9 @@ public async ValueTask GetAsync(string key, CancellationToken cancellation cancellationToken.ThrowIfCancellationRequested(); // Retrieve the value from cache - var value = await _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); + var value = await _cacheClient + .Db0.GetAsync(key, CommandFlags.PreferReplica) + .ConfigureAwait(false); return value; } } @@ -403,7 +419,11 @@ public async ValueTask GetAsync(string key, CancellationToken cancellation /// Unable to get the item with key and sub key. /// Thrown when the operation is cancelled. /// - public async ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async ValueTask GetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { if (string.IsNullOrEmpty(key)) { @@ -424,7 +444,9 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok cancellationToken.ThrowIfCancellationRequested(); // Retrieve the value from cache - return await _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); + return await _cacheClient + .Db0.GetAsync(key, CommandFlags.PreferReplica) + .ConfigureAwait(false); } } catch (OperationCanceledException) @@ -460,7 +482,7 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok /// /// The type of object (the object will be cast to this type). /// The key. - /// Token to monitor for cancellation requests. default if no cancellation is required; + /// Token to monitor for cancellation requests. default if no cancellation is required; /// otherwise, a that can be used to cancel the operation. /// /// A ValueTask that represents the asynchronous try-get operation. The task result contains: @@ -477,7 +499,10 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok /// /// Thrown when key is null or empty. /// Thrown when the operation is cancelled. - public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default) + public async ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + CancellationToken cancellationToken = default + ) { if (string.IsNullOrEmpty(key)) { @@ -498,7 +523,9 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok cancellationToken.ThrowIfCancellationRequested(); // Retrieve the value from cache - var value = await _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).ConfigureAwait(false); + var value = await _cacheClient + .Db0.GetAsync(key, CommandFlags.PreferReplica) + .ConfigureAwait(false); return (Success: true, Value: value); } @@ -543,18 +570,22 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok /// A ValueTask that represents the asynchronous try-get operation. The task result contains: /// - A touple with Success=true and the cached value if the key exists /// - A touple with Success=false and default(T) if the key doesn't exist or operation fails - /// + /// /// The returned ValueTask will complete when: /// • Success: Key exists and value is successfully retrieved from cache /// • Success: Key doesn't exist (returns false with default value) /// • Exception: Cache operation fails and ShouldPropagateExceptions is true /// • Exception: Operation is cancelled via the cancellation token (throws OperationCanceledException) - /// + /// /// When ShouldPropagateExceptions is false, cache errors are logged and the method returns false. /// /// Thrown when key is null or empty. /// Thrown when the operation is cancelled. - public async ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { if (string.IsNullOrEmpty(key)) { @@ -567,7 +598,9 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok cancellationToken.ThrowIfCancellationRequested(); // More efficient: Check existence first, then get value if it exists - var hashFieldExists = await _cacheClient.Db0.HashExistsAsync(key, subKey).ConfigureAwait(false); + var hashFieldExists = await _cacheClient + .Db0.HashExistsAsync(key, subKey) + .ConfigureAwait(false); if (hashFieldExists) { @@ -575,7 +608,9 @@ public async ValueTask GetAsync(string key, string subKey, CancellationTok cancellationToken.ThrowIfCancellationRequested(); // Retrieve the value from cache - var value = await _cacheClient.Db0.HashGetAsync(key, subKey, CommandFlags.PreferReplica).ConfigureAwait(false); + var value = await _cacheClient + .Db0.HashGetAsync(key, subKey, CommandFlags.PreferReplica) + .ConfigureAwait(false); return (Success: true, Value: value); } @@ -622,7 +657,9 @@ public async Task RemoveAsync(string key, CancellationToken cancellationToken = { cancellationToken.ThrowIfCancellationRequested(); - await _cacheClient.Db0.RemoveAsync(key, CommandFlags.FireAndForget).ConfigureAwait(false); + await _cacheClient + .Db0.RemoveAsync(key, CommandFlags.FireAndForget) + .ConfigureAwait(false); } catch (OperationCanceledException) { @@ -649,11 +686,17 @@ public async Task RemoveAsync(string key, CancellationToken cancellationToken = /// otherwise, a that can be used to cancel the operation. /// A ValueTask that represents the asynchronous call. /// Thrown when the operation is cancelled. - public async Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default) + public async Task RemoveAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { try { - await _cacheClient.Db0.HashDeleteAsync(key, subKey, CommandFlags.FireAndForget).ConfigureAwait(false); + await _cacheClient + .Db0.HashDeleteAsync(key, subKey, CommandFlags.FireAndForget) + .ConfigureAwait(false); } catch (OperationCanceledException) { @@ -674,17 +717,20 @@ public async Task RemoveAsync(string key, string subKey, CancellationToken cance /// /// Returns the time to live of the specified key. /// - /// The key. + /// The key. /// Tp cancel operation. /// A representing the asynchronous operation. /// The timespan until this key is expired from the cache or 0 if it's already expired or doesn't exists. - /// Thrown when the operation is cancelled. + /// Thrown when the operation is cancelled. public async Task TTLAsync(string key, CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); - var ttl = await _cacheClient.Db0.Database.KeyTimeToLiveAsync(key, CommandFlags.PreferReplica); + var ttl = await _cacheClient.Db0.Database.KeyTimeToLiveAsync( + key, + CommandFlags.PreferReplica + ); return ttl ?? TimeSpan.Zero; } catch (OperationCanceledException) @@ -713,7 +759,7 @@ public async Task Clear() { try { - await _cacheClient.Db0.FlushDbAsync(); + await _cacheClient.Db0.FlushDbAsync(); } catch (Exception e) { diff --git a/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs b/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs index 68a04b2d..b10b2cdb 100644 --- a/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs +++ b/Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs @@ -347,7 +347,6 @@ private async Task SendInternalAsync(string cacheKey) { (bool exists, _) = await CacheManager.TryGetAsync(cacheKey, CancellationToken.None); - if (exists) { LogConsumer.Trace("E-mail sending disabled due {0}", "network error"); diff --git a/Src/CrispyWaffle/Cache/CacheManager.cs b/Src/CrispyWaffle/Cache/CacheManager.cs index 40e413e2..9accf7d0 100644 --- a/Src/CrispyWaffle/Cache/CacheManager.cs +++ b/Src/CrispyWaffle/Cache/CacheManager.cs @@ -120,7 +120,11 @@ public static int AddRepository(ICacheRepository repository, int priority) /// Cancel. /// If cancelled. /// void. - public static async ValueTask SetAsync(T value, [Localizable(false)] string key, CancellationToken cancellationToken = default) + public static async ValueTask SetAsync( + T value, + [Localizable(false)] string key, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -140,7 +144,12 @@ public static async ValueTask SetAsync(T value, [Localizable(false)] string k } catch (Exception ex) { - LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + LogConsumer.Error( + "Failed to set {0} in repository {1}: {2}", + key, + repository.GetType().Name, + ex.Message + ); return false; } }); @@ -148,7 +157,12 @@ public static async ValueTask SetAsync(T value, [Localizable(false)] string k var results = await Task.WhenAll(tasks); var successCount = results.Count(r => r); - LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count); + LogConsumer.Info( + "Successfully set {0} in {1} out of {2} repositories", + key, + successCount, + _repositories.Count + ); } /// @@ -191,7 +205,12 @@ public static async ValueTask SetAsync( } catch (Exception ex) { - LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + LogConsumer.Error( + "Failed to set {0} in repository {1}: {2}", + key, + repository.GetType().Name, + ex.Message + ); return false; } }); @@ -199,7 +218,12 @@ public static async ValueTask SetAsync( var results = await Task.WhenAll(tasks); var successCount = results.Count(r => r); - LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count); + LogConsumer.Info( + "Successfully set {0} in {1} out of {2} repositories", + key, + successCount, + _repositories.Count + ); } /// @@ -212,7 +236,12 @@ public static async ValueTask SetAsync( /// Cancel. /// If cancelled. /// void. - public static async ValueTask SetAsync(T value, [Localizable(false)] string key, TimeSpan ttl, CancellationToken cancellationToken = default) + public static async ValueTask SetAsync( + T value, + [Localizable(false)] string key, + TimeSpan ttl, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -236,7 +265,12 @@ public static async ValueTask SetAsync(T value, [Localizable(false)] string k } catch (Exception ex) { - LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + LogConsumer.Error( + "Failed to set {0} in repository {1}: {2}", + key, + repository.GetType().Name, + ex.Message + ); return false; } }); @@ -244,7 +278,12 @@ public static async ValueTask SetAsync(T value, [Localizable(false)] string k var results = await Task.WhenAll(tasks); var successCount = results.Count(r => r); - LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count); + LogConsumer.Info( + "Successfully set {0} in {1} out of {2} repositories", + key, + successCount, + _repositories.Count + ); } /// @@ -286,7 +325,12 @@ public static async ValueTask SetToAsync( } catch (Exception ex) { - LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + LogConsumer.Error( + "Failed to set {0} in repository {1}: {2}", + key, + repository.GetType().Name, + ex.Message + ); } } @@ -331,7 +375,12 @@ public static async ValueTask SetToAsync( } catch (Exception ex) { - LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + LogConsumer.Error( + "Failed to set {0} in repository {1}: {2}", + key, + repository.GetType().Name, + ex.Message + ); } } @@ -381,7 +430,12 @@ public static async ValueTask SetToAsync( } catch (Exception ex) { - LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + LogConsumer.Error( + "Failed to set {0} in repository {1}: {2}", + key, + repository.GetType().Name, + ex.Message + ); } } @@ -434,7 +488,12 @@ public static async ValueTask SetToAsync( } catch (Exception ex) { - LogConsumer.Error("Failed to set {0} in repository {1}: {2}", key, repository.GetType().Name, ex.Message); + LogConsumer.Error( + "Failed to set {0} in repository {1}: {2}", + key, + repository.GetType().Name, + ex.Message + ); } } @@ -447,7 +506,10 @@ public static async ValueTask SetToAsync( /// The retrieved value. /// Unable to get the item with key {key}. /// Operation cancelled. - public static async ValueTask GetAsync([Localizable(false)] string key, CancellationToken cancellationToken = default) + public static async ValueTask GetAsync( + [Localizable(false)] string key, + CancellationToken cancellationToken = default + ) { try { @@ -461,7 +523,10 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca // Create timeout for the entire operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var errors = new List<(string Repository, Exception Error)>(); @@ -474,7 +539,11 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca var repositoryName = repository.GetType().Name; var repositoryStopwatch = Stopwatch.StartNew(); - LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); + LogConsumer.Debug( + "Attempting to get key {0} from repository {1}", + key, + repositoryName + ); // Try to get value from repository var (success, result) = await repository.TryGetAsync(key, combinedCts.Token); @@ -483,28 +552,46 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca if (!success) { - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + LogConsumer.Debug( + "Key {0} not found in repository {1} ({2}ms)", + key, + repositoryName, + repositoryStopwatch.ElapsedMilliseconds + ); continue; } LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + if ( + _isMemoryRepositoryInList + && repository.GetType() != typeof(MemoryCacheRepository) + ) { _ = Task.Run( async () => - { - try - { - await SetToAsync(result, key, CancellationToken.None); - LogConsumer.Debug("Promoted key {0} to memory cache", key); - } - catch (Exception ex) { - LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); - } - }, cancellationToken); + try + { + await SetToAsync( + result, + key, + CancellationToken.None + ); + LogConsumer.Debug("Promoted key {0} to memory cache", key); + } + catch (Exception ex) + { + LogConsumer.Error( + "Failed to promote key {0} to memory cache: {1}", + key, + ex.Message + ); + } + }, + cancellationToken + ); } return result; @@ -526,7 +613,12 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca var repositoryName = repository.GetType().Name; errors.Add((repositoryName, ex)); - LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + LogConsumer.Error( + "Error getting key {0} from repository {1}: {2}", + key, + repositoryName, + ex.Message + ); } } } @@ -547,17 +639,29 @@ public static async ValueTask GetAsync([Localizable(false)] string key, Ca /// The sub key of the cached value. /// The retrieved value. /// Unable to get the item with key {key} and sub key {subKey}. - public static async ValueTask GetAsync([Localizable(false)] string key, [Localizable(false)] string subKey, CancellationToken cancellationToken = default) + public static async ValueTask GetAsync( + [Localizable(false)] string key, + [Localizable(false)] string subKey, + CancellationToken cancellationToken = default + ) { try { cancellationToken.ThrowIfCancellationRequested(); - LogConsumer.Trace("Getting {0}/{2} from any of {1} cache repositories", key, _repositories.Count, subKey); + LogConsumer.Trace( + "Getting {0}/{2} from any of {1} cache repositories", + key, + _repositories.Count, + subKey + ); // Create timeout for the entire operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var errors = new List<(string Repository, Exception Error)>(); @@ -570,37 +674,64 @@ public static async ValueTask GetAsync([Localizable(false)] string key, [L var repositoryName = repository.GetType().Name; var repositoryStopwatch = Stopwatch.StartNew(); - LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); + LogConsumer.Debug( + "Attempting to get key {0} from repository {1}", + key, + repositoryName + ); // Try to get value from repository - var (success, result) = await repository.TryGetAsync(key, subKey, combinedCts.Token); + var (success, result) = await repository.TryGetAsync( + key, + subKey, + combinedCts.Token + ); repositoryStopwatch.Stop(); if (!success) { - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + LogConsumer.Debug( + "Key {0} not found in repository {1} ({2}ms)", + key, + repositoryName, + repositoryStopwatch.ElapsedMilliseconds + ); continue; } LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + if ( + _isMemoryRepositoryInList + && repository.GetType() != typeof(MemoryCacheRepository) + ) { _ = Task.Run( async () => { try { - await SetToAsync(result, key, subKey, CancellationToken.None); + await SetToAsync( + result, + key, + subKey, + CancellationToken.None + ); LogConsumer.Debug("Promoted key {0} to memory cache", key); } catch (Exception ex) { - LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); + LogConsumer.Error( + "Failed to promote key {0} to memory cache: {1}", + key, + ex.Message + ); } - }, cancellationToken); + }, + cancellationToken + ); } return result; @@ -622,7 +753,12 @@ public static async ValueTask GetAsync([Localizable(false)] string key, [L var repositoryName = repository.GetType().Name; errors.Add((repositoryName, ex)); - LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + LogConsumer.Error( + "Error getting key {0} from repository {1}: {2}", + key, + repositoryName, + ex.Message + ); } } } @@ -644,7 +780,10 @@ public static async ValueTask GetAsync([Localizable(false)] string key, [L /// Cancelation token. /// The retrieved value. /// The repository of type {type.FullName} isn't available in the repositories providers list. - public static async ValueTask GetFrom([Localizable(false)] string key, CancellationToken cancellationToken = default) + public static async ValueTask GetFrom( + [Localizable(false)] string key, + CancellationToken cancellationToken = default + ) { try { @@ -663,7 +802,10 @@ public static async ValueTask GetFrom([Localiz // Create timeout for the operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var result = await repository.GetAsync(key, combinedCts.Token); @@ -671,12 +813,17 @@ public static async ValueTask GetFrom([Localiz } catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) { - LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + LogConsumer.Info( + "Get operation cancelled by user for key {0} from {1}", + key, + typeof(TCacheRepository).Name + ); throw; } catch (OperationCanceledException) { - var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + var error = + $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; LogConsumer.Error(error); throw new TimeoutException(error); } @@ -716,7 +863,10 @@ public static async ValueTask GetFromAsync( // Create timeout for the operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var result = await repository.GetAsync(key, subKey, combinedCts.Token); @@ -724,12 +874,17 @@ public static async ValueTask GetFromAsync( } catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) { - LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + LogConsumer.Info( + "Get operation cancelled by user for key {0} from {1}", + key, + typeof(TCacheRepository).Name + ); throw; } catch (OperationCanceledException) { - var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + var error = + $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; LogConsumer.Error(error); throw new TimeoutException(error); } @@ -745,17 +900,27 @@ public static async ValueTask GetFromAsync( /// /// A tuple containing Success (true if found and castable to T) and Value (the cached item or default(T)). /// - public static async ValueTask<(bool Success, T Value)> TryGetAsync([Localizable(false)] string key, CancellationToken cancellationToken = default) + public static async ValueTask<(bool Success, T Value)> TryGetAsync( + [Localizable(false)] string key, + CancellationToken cancellationToken = default + ) { try { cancellationToken.ThrowIfCancellationRequested(); - LogConsumer.Trace("Trying to get {0} from any of {1} repositories",key,_repositories.Count); + LogConsumer.Trace( + "Trying to get {0} from any of {1} repositories", + key, + _repositories.Count + ); // Create timeout for the entire operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var errors = new List<(string Repository, Exception Error)>(); @@ -768,7 +933,11 @@ public static async ValueTask GetFromAsync( var repositoryName = repository.GetType().Name; var repositoryStopwatch = Stopwatch.StartNew(); - LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); + LogConsumer.Debug( + "Attempting to get key {0} from repository {1}", + key, + repositoryName + ); // Try to get value from repository var (success, result) = await repository.TryGetAsync(key, combinedCts.Token); @@ -777,28 +946,46 @@ public static async ValueTask GetFromAsync( if (!success) { - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + LogConsumer.Debug( + "Key {0} not found in repository {1} ({2}ms)", + key, + repositoryName, + repositoryStopwatch.ElapsedMilliseconds + ); continue; } LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + if ( + _isMemoryRepositoryInList + && repository.GetType() != typeof(MemoryCacheRepository) + ) { _ = Task.Run( async () => { try { - await SetToAsync(result, key, CancellationToken.None); + await SetToAsync( + result, + key, + CancellationToken.None + ); LogConsumer.Debug("Promoted key {0} to memory cache", key); } catch (Exception ex) { - LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); + LogConsumer.Error( + "Failed to promote key {0} to memory cache: {1}", + key, + ex.Message + ); } - }, cancellationToken); + }, + cancellationToken + ); } return (true, result); @@ -820,7 +1007,12 @@ public static async ValueTask GetFromAsync( var repositoryName = repository.GetType().Name; errors.Add((repositoryName, ex)); - LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + LogConsumer.Error( + "Error getting key {0} from repository {1}: {2}", + key, + repositoryName, + ex.Message + ); } } } @@ -851,11 +1043,19 @@ public static async ValueTask TryGetAsync( { cancellationToken.ThrowIfCancellationRequested(); - LogConsumer.Trace("Trying to get {0}/{2} from any of {1} repositories", key, _repositories.Count, subKey); + LogConsumer.Trace( + "Trying to get {0}/{2} from any of {1} repositories", + key, + _repositories.Count, + subKey + ); // Create timeout for the entire operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var errors = new List<(string Repository, Exception Error)>(); @@ -868,37 +1068,64 @@ public static async ValueTask TryGetAsync( var repositoryName = repository.GetType().Name; var repositoryStopwatch = Stopwatch.StartNew(); - LogConsumer.Debug("Attempting to get key {0} from repository {1}", key, repositoryName); + LogConsumer.Debug( + "Attempting to get key {0} from repository {1}", + key, + repositoryName + ); // Try to get value from repository - var (success, result) = await repository.TryGetAsync(key, subKey, combinedCts.Token); + var (success, result) = await repository.TryGetAsync( + key, + subKey, + combinedCts.Token + ); repositoryStopwatch.Stop(); if (!success) { - LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds); + LogConsumer.Debug( + "Key {0} not found in repository {1} ({2}ms)", + key, + repositoryName, + repositoryStopwatch.ElapsedMilliseconds + ); continue; } LogConsumer.Info("Found key {0} in repository {1}", key, repositoryName); // If found in non-memory repository, promote to memory cache - if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository)) + if ( + _isMemoryRepositoryInList + && repository.GetType() != typeof(MemoryCacheRepository) + ) { _ = Task.Run( async () => { try { - await SetToAsync(result, key, subKey, CancellationToken.None); + await SetToAsync( + result, + key, + subKey, + CancellationToken.None + ); LogConsumer.Debug("Promoted key {0} to memory cache", key); } catch (Exception ex) { - LogConsumer.Error("Failed to promote key {0} to memory cache: {1}", key, ex.Message); + LogConsumer.Error( + "Failed to promote key {0} to memory cache: {1}", + key, + ex.Message + ); } - }, cancellationToken); + }, + cancellationToken + ); } return true; @@ -920,7 +1147,12 @@ public static async ValueTask TryGetAsync( var repositoryName = repository.GetType().Name; errors.Add((repositoryName, ex)); - LogConsumer.Error("Error getting key {0} from repository {1}: {2}", key, repositoryName, ex.Message); + LogConsumer.Error( + "Error getting key {0} from repository {1}: {2}", + key, + repositoryName, + ex.Message + ); } } } @@ -966,7 +1198,10 @@ public static async Task TTLAsync([Localizable(false)] string key) /// The key of the cached value to remove. /// Cancel token. /// Completed task. - public static async ValueTask Remove([Localizable(false)] string key, CancellationToken cancellationToken = default) + public static async ValueTask Remove( + [Localizable(false)] string key, + CancellationToken cancellationToken = default + ) { try { @@ -976,7 +1211,10 @@ public static async ValueTask Remove([Localizable(false)] string key, Cancellati // Create timeout for the entire operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var errors = new List<(string Repository, Exception Error)>(); @@ -988,7 +1226,11 @@ public static async ValueTask Remove([Localizable(false)] string key, Cancellati var repositoryName = repository.GetType().Name; - LogConsumer.Debug("Attempting to delete key {0} from repository {1}", key, repositoryName); + LogConsumer.Debug( + "Attempting to delete key {0} from repository {1}", + key, + repositoryName + ); // Try to remove value from repository await repository.RemoveAsync(key, combinedCts.Token); @@ -1026,17 +1268,29 @@ public static async ValueTask Remove([Localizable(false)] string key, Cancellati /// The sub key of the cached value to remove. /// Cancel token. /// Completed task. - public static async ValueTask Remove([Localizable(false)] string key, [Localizable(false)] string subKey, CancellationToken cancellationToken = default) + public static async ValueTask Remove( + [Localizable(false)] string key, + [Localizable(false)] string subKey, + CancellationToken cancellationToken = default + ) { try { cancellationToken.ThrowIfCancellationRequested(); - LogConsumer.Trace("Removing key {0} and sub key {2} from {1} repositories", key ,_repositories.Count, subKey); + LogConsumer.Trace( + "Removing key {0} and sub key {2} from {1} repositories", + key, + _repositories.Count, + subKey + ); // Create timeout for the entire operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); var errors = new List<(string Repository, Exception Error)>(); @@ -1048,7 +1302,11 @@ public static async ValueTask Remove([Localizable(false)] string key, [Localizab var repositoryName = repository.GetType().Name; - LogConsumer.Debug("Attempting to delete key {0} from repository {1}", key, repositoryName); + LogConsumer.Debug( + "Attempting to delete key {0} from repository {1}", + key, + repositoryName + ); // Try to remove value from repository await repository.RemoveAsync(key, subKey, combinedCts.Token); @@ -1088,7 +1346,10 @@ public static async ValueTask Remove([Localizable(false)] string key, [Localizab /// The repository of type {type.FullName} isn't available in the repositories providers list. /// The operation get cancelled. /// Completed task. - public static async ValueTask RemoveFrom([Localizable(false)] string key, CancellationToken cancellationToken = default) + public static async ValueTask RemoveFrom( + [Localizable(false)] string key, + CancellationToken cancellationToken = default + ) { try { @@ -1107,18 +1368,26 @@ public static async ValueTask RemoveFrom([Localizable(false)] // Create timeout for the operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); await repository.RemoveAsync(key, combinedCts.Token); } catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) { - LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + LogConsumer.Info( + "Get operation cancelled by user for key {0} from {1}", + key, + typeof(TCacheRepository).Name + ); throw; } catch (OperationCanceledException) { - var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + var error = + $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; LogConsumer.Error(error); throw new TimeoutException(error); } @@ -1146,7 +1415,12 @@ public static async ValueTask RemoveFrom( var type = typeof(TCacheRepository); - LogConsumer.Trace("Removing key {0} and sub key {2} from {1} repository", key, _repositories.Count, subKey); + LogConsumer.Trace( + "Removing key {0} and sub key {2} from {1} repository", + key, + _repositories.Count, + subKey + ); var repository = _repositories.SingleOrDefault(r => type == r.Value.GetType()).Value; if (repository == null) { @@ -1157,18 +1431,26 @@ public static async ValueTask RemoveFrom( // Create timeout for the operation using var timeoutCts = new CancellationTokenSource(DefaultTimeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); await repository.RemoveAsync(key, subKey, combinedCts.Token); } catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) { - LogConsumer.Info("Get operation cancelled by user for key {0} from {1}", key, typeof(TCacheRepository).Name); + LogConsumer.Info( + "Get operation cancelled by user for key {0} from {1}", + key, + typeof(TCacheRepository).Name + ); throw; } catch (OperationCanceledException) { - var error = $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; + var error = + $"Get operation timed out for key {key} from {typeof(TCacheRepository).Name}"; LogConsumer.Error(error); throw new TimeoutException(error); } diff --git a/Src/CrispyWaffle/Cache/ICacheRepository.cs b/Src/CrispyWaffle/Cache/ICacheRepository.cs index c3592f1f..abf301b9 100644 --- a/Src/CrispyWaffle/Cache/ICacheRepository.cs +++ b/Src/CrispyWaffle/Cache/ICacheRepository.cs @@ -31,7 +31,12 @@ public interface ICacheRepository "CA1716:Identifiers should not match keywords", Justification = "Design choice." )] - ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default); + ValueTask SetAsync( + T value, + string key, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default + ); /// /// Sets the specified value. @@ -47,7 +52,12 @@ public interface ICacheRepository "CA1716:Identifiers should not match keywords", Justification = "Design choice." )] - ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default); + ValueTask SetAsync( + T value, + string key, + string subKey, + CancellationToken cancellationToken = default + ); /// /// Gets the object with the specified key. @@ -77,7 +87,11 @@ public interface ICacheRepository "CA1716:Identifiers should not match keywords", Justification = "Design choice." )] - ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default); + ValueTask GetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ); /// /// Tries to get a value based on its key, if exists return true, else false. @@ -87,7 +101,10 @@ public interface ICacheRepository /// The key. /// Cancel token. /// Returns True if the object with the key exists, false otherwise. - ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default); + ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + CancellationToken cancellationToken = default + ); /// /// Tries the get. @@ -97,7 +114,11 @@ public interface ICacheRepository /// The sub key. /// Cancel token. /// Success info of the get, as a bool. - ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default); + ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ); /// /// Removes the specified key from the cache. diff --git a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs index d870a7dc..42a8d569 100644 --- a/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs +++ b/Src/CrispyWaffle/Cache/MemoryCacheRepository.cs @@ -42,13 +42,20 @@ public class MemoryCacheRepository : ICacheRepository /// The dictionary already contains the maximum number of elements. /// If cancelled. /// The Value. - public ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default) + public ValueTask SetAsync( + T value, + string key, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); if (_data.Count >= int.MaxValue) { - throw new OverflowException("The dictionary already contains the maximum number of elements."); + throw new OverflowException( + "The dictionary already contains the maximum number of elements." + ); } _data.AddOrUpdate(key, value, (_, _) => value); @@ -66,7 +73,12 @@ public ValueTask SetAsync(T value, string key, TimeSpan? ttl = null, Cancella /// The dictionary already contains the maximum number of elements. /// If cancelled. /// ValuTask. - public ValueTask SetAsync(T value, string key, string subKey, CancellationToken cancellationToken = default) + public ValueTask SetAsync( + T value, + string key, + string subKey, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -102,12 +114,16 @@ public async ValueTask GetAsync(string key, CancellationToken cancellation /// The type parameter. /// The key. /// The sub key. - /// to cancel. + /// to cancel. /// Unable to get the item with key {key} and sub key {subKey}. /// If cancelled. /// Invalid cast. /// T. - public ValueTask GetAsync(string key, string subKey, CancellationToken cancellationToken = default) + public ValueTask GetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -126,7 +142,9 @@ public ValueTask GetAsync(string key, string subKey, CancellationToken can } catch (InvalidCastException) { - throw new InvalidCastException($"Unable to convert the item with key {key} and sub key {subKey}"); + throw new InvalidCastException( + $"Unable to convert the item with key {key} and sub key {subKey}" + ); } } @@ -141,7 +159,10 @@ public ValueTask GetAsync(string key, string subKey, CancellationToken can /// /// A tuple containing Success (true if found and castable to T) and Value (the cached item or default(T)). /// - public ValueTask<(bool Success, T Value)> TryGetAsync(string key, CancellationToken cancellationToken = default) + public ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -173,7 +194,11 @@ public ValueTask GetAsync(string key, string subKey, CancellationToken can /// /// A tuple containing Success (true if found and castable to T) and Value (the cached item or default(T)). /// - public ValueTask<(bool Success, T Value)> TryGetAsync(string key, string subKey, CancellationToken cancellationToken = default) + public ValueTask<(bool Success, T Value)> TryGetAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -220,7 +245,11 @@ public Task RemoveAsync(string key, CancellationToken cancellationToken = defaul /// The sub key. /// Cancel token. /// Completed Task. - public Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default) + public Task RemoveAsync( + string key, + string subKey, + CancellationToken cancellationToken = default + ) { cancellationToken.ThrowIfCancellationRequested(); var finalKey = $"{key}-{subKey}"; diff --git a/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs b/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs index 1b820213..eb75bebd 100644 --- a/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs +++ b/Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs @@ -42,7 +42,11 @@ public async Task GetAndSetSpecificAsyncTest() { var docOne = new Car("MakerOne"); - await _repository.SetSpecificAsync(docOne, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + await _repository.SetSpecificAsync( + docOne, + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + ); var docTwo = new Car("MakerTwo"); @@ -93,10 +97,18 @@ public async Task RemoveSpecificAsyncTest() [Fact] public async Task DatabaseClearTest() { - var task1 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); - var task2 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); - var task3 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); - var task4 = _repository.SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()).AsTask(); + var task1 = _repository + .SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()) + .AsTask(); + var task2 = _repository + .SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()) + .AsTask(); + var task3 = _repository + .SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()) + .AsTask(); + var task4 = _repository + .SetAsync(new CouchDBCacheDocument(), Guid.NewGuid().ToString()) + .AsTask(); await Task.WhenAll(task1, task2, task3, task4); diff --git a/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs b/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs index a2c3c8bf..83e8e186 100644 --- a/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs +++ b/Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs @@ -52,7 +52,7 @@ public async Task SetAsyncShouldSetValueInAllRepositories() // Arrange var key = "testKey"; var value = new { Name = "Test" }; - CacheManager.AddRepository(_mockCacheRepository); + CacheManager.AddRepository(_mockCacheRepository); // Act await CacheManager.SetAsync(value, key); @@ -84,13 +84,15 @@ public async Task SetAsyncShouldSetValueWithSubKey() var key = "testKey"; var subKey = "testSubKey"; var value = new { Name = "Test" }; - CacheManager.AddRepository(_mockCacheRepository); + CacheManager.AddRepository(_mockCacheRepository); // Act await CacheManager.SetAsync(value, key, subKey); // Assert - await _mockCacheRepository.Received(1).SetAsync(value, key, subKey, Arg.Any()); + await _mockCacheRepository + .Received(1) + .SetAsync(value, key, subKey, Arg.Any()); } [Fact] @@ -117,7 +119,8 @@ public async Task SetToShouldThrowWhenRepositoryNotFound() CacheManager.AddRepository(_mockCacheRepository); // Act - Func act = async () => await CacheManager.SetToAsync(value, key); + Func act = async () => + await CacheManager.SetToAsync(value, key); // Assert await act.Should() @@ -125,7 +128,7 @@ await act.Should() .WithMessage( "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list" ); - } + } [Fact] public async Task TTLShouldReturnCorrectTTLFromRepositories() @@ -135,7 +138,12 @@ public async Task TTLShouldReturnCorrectTTLFromRepositories() var expectedTTL = TimeSpan.FromMinutes(10); CacheManager.AddRepository(_mockCacheRepository); _mockCacheRepository.TTLAsync(key, Arg.Any()).Returns(expectedTTL); - await CacheManager.SetAsync(new { Name = "Test" }, key, expectedTTL, CancellationToken.None); // Set value with TTL + await CacheManager.SetAsync( + new { Name = "Test" }, + key, + expectedTTL, + CancellationToken.None + ); // Set value with TTL // Act var result = await CacheManager.TTLAsync(key); diff --git a/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs b/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs index 6985c868..a05016a7 100644 --- a/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs +++ b/Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs @@ -62,7 +62,7 @@ public async Task RemoveFromDatabaseShouldRemoveValue() // Act await _repository.RemoveAsync(key); - (bool success, _)= await _repository.TryGetAsync(key); + (bool success, _) = await _repository.TryGetAsync(key); // Assert success.Should().Be(false); diff --git a/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs b/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs index 6576e5e7..e072bea2 100644 --- a/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs +++ b/Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs @@ -53,7 +53,8 @@ public async Task RemoveShouldRemoveStoredValue() await Task.WhenAll(Task1, Task2); // Act - var exception = Assert.ThrowsAsync(async () => await _repository.GetAsync(key) + var exception = Assert.ThrowsAsync(async () => + await _repository.GetAsync(key) ); // Assert From 69f06e92c6b4e37bdaacf077146caf3a977d2aba Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sat, 4 Jul 2026 04:15:05 +0100 Subject: [PATCH 7/9] refactor: implement SafeWriteLine to handle test output safely Replace direct calls to `_testOutputHelper.WriteLine` with a new `SafeWriteLine` method. This change addresses the issue of `InvalidOperationException` being thrown when test output occurs after the test has completed. `SafeWriteLine` wraps the write operations in a try-catch block to safely ignore these exceptions, ensuring more robust handling of log output during parallel test execution. --- Tests/CrispyWaffle.Tests/TestLogProvider.cs | 63 +++++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/Tests/CrispyWaffle.Tests/TestLogProvider.cs b/Tests/CrispyWaffle.Tests/TestLogProvider.cs index ce8ade52..9712da41 100644 --- a/Tests/CrispyWaffle.Tests/TestLogProvider.cs +++ b/Tests/CrispyWaffle.Tests/TestLogProvider.cs @@ -18,16 +18,16 @@ public TestLogProvider(ITestOutputHelper testOutputHelper) => testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper)); public void SetLevel(LogLevel level) => - _testOutputHelper.WriteLine("Set level: {0}", level.GetHumanReadableValue()); + SafeWriteLine("Set level: {0}", level.GetHumanReadableValue()); public void Fatal(string category, string message) => - _testOutputHelper.WriteLine("Fatal: {0} - {1}", category, message); + SafeWriteLine("Fatal: {0} - {1}", category, message); public void Error(string category, string message) => - _testOutputHelper.WriteLine("Error: {0} - {1}", category, message); + SafeWriteLine("Error: {0} - {1}", category, message); public void Warning(string category, string message) => - _testOutputHelper.WriteLine("Warning: {0} - {1}", category, message); + SafeWriteLine("Warning: {0} - {1}", category, message); public void Info(string category, string message) => System.Diagnostics.Trace.WriteLine($"Info: {category} - {message}"); @@ -37,7 +37,7 @@ public void Trace(string category, string message) => public void Trace(string category, string message, Exception exception) { - _testOutputHelper.WriteLine("Trace: {0} - {1}", category, message); + SafeWriteLine("Trace: {0} - {1}", category, message); WriteExceptionDetails("Trace", category, exception); } @@ -45,12 +45,12 @@ public void Trace(string category, Exception exception) => WriteExceptionDetails("Trace", category, exception); public void Debug(string category, string message) => - _testOutputHelper.WriteLine("Debug: {0} - {1}", category, message); + SafeWriteLine("Debug: {0} - {1}", category, message); public void Debug(string category, string content, string identifier) { - _testOutputHelper.WriteLine("Error: {0} - {1}", category, identifier); - _testOutputHelper.WriteLine(content); + SafeWriteLine("Error: {0} - {1}", category, identifier); + SafeWriteLine(content); } public void Debug( @@ -61,22 +61,49 @@ SerializerFormat customFormat ) where T : class, new() { - _testOutputHelper.WriteLine("Error: {0} - {1}", category, identifier); - _testOutputHelper.WriteLine((string)content.GetCustomSerializer(customFormat)); + SafeWriteLine("Error: {0} - {1}", category, identifier); + SafeWriteLine((string)content.GetCustomSerializer(customFormat)); + } + + /// + /// Writes to the test output, swallowing failures caused by the owning test having + /// already completed (e.g. this provider is stale, from a prior parallel test run). + /// + private void SafeWriteLine(string message) + { + try + { + _testOutputHelper.WriteLine(message); + } + catch (InvalidOperationException) + { + // The test that registered this provider has already finished; ignore. + } + } + + /// + /// Writes to the test output, swallowing failures caused by the owning test having + /// already completed (e.g. this provider is stale, from a prior parallel test run). + /// + private void SafeWriteLine(string format, params object[] args) + { + try + { + _testOutputHelper.WriteLine(format, args); + } + catch (InvalidOperationException) + { + // The test that registered this provider has already finished; ignore. + } } private void WriteExceptionDetails(string level, string category, Exception exception) { do { - _testOutputHelper.WriteLine("{0}: {1} - {2}", level, category, exception.Message); - _testOutputHelper.WriteLine( - "{0}: {1} - {2}", - level, - category, - exception.GetType().FullName - ); - _testOutputHelper.WriteLine("{0}: {1} - {2}", level, category, exception.StackTrace); + SafeWriteLine("{0}: {1} - {2}", level, category, exception.Message); + SafeWriteLine("{0}: {1} - {2}", level, category, exception.GetType().FullName); + SafeWriteLine("{0}: {1} - {2}", level, category, exception.StackTrace); exception = exception.InnerException; } while (exception != null); From 2ebad8a0224da1987bbc2dec120a97591b50a1ce Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sat, 4 Jul 2026 04:23:49 +0100 Subject: [PATCH 8/9] fix: restrict actions to main repo for external PRs Add conditional checks to prevent executing SonarCloud and Codecov steps on pull requests from forks. This ensures that sensitive information (such as tokens) is only used when the head repository matches the main repository, thus enhancing security by avoiding exposure on external PRs. --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index daaa046b..ab923345 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,7 @@ jobs: dotnet-version: '10.0.x' - name: 🔧 Install SonarCloud scanner + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository run: dotnet tool install --global dotnet-sonarscanner - name: 📡 Install DeepSource scanner @@ -66,6 +67,7 @@ jobs: run: dotnet restore - name: 🔍 Begin SonarCloud analysis + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository run: | dotnet sonarscanner begin \ /k:"guibranco_CrispyWaffle" \ @@ -89,12 +91,14 @@ jobs: /p:CoverletOutputFormat=opencover%2ccobertura - name: 📊 End SonarCloud analysis + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository run: dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - name: ☂️ Upload coverage to Codecov + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: token: ${{ secrets.CODECOV_TOKEN }} From 62f1260fa06c95504870fec9d270b2a28e4f9180 Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sat, 4 Jul 2026 04:30:20 +0100 Subject: [PATCH 9/9] refactor: replace async delay with thread sleep in tests Change the concurrency test's task delay from async Task.Delay to Thread.Sleep. This modification improves the test reliability by ensuring precise execution timing, as Task.Delay could be subject to task scheduler variations, affecting the accuracy of concurrency measurements. --- Tests/CrispyWaffle.Tests/Scheduler/JobRunnerTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/CrispyWaffle.Tests/Scheduler/JobRunnerTests.cs b/Tests/CrispyWaffle.Tests/Scheduler/JobRunnerTests.cs index c84d979d..ab1477d3 100644 --- a/Tests/CrispyWaffle.Tests/Scheduler/JobRunnerTests.cs +++ b/Tests/CrispyWaffle.Tests/Scheduler/JobRunnerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using CrispyWaffle.Scheduler; using Xunit; @@ -60,10 +61,10 @@ public async Task ValidateConcurrency() var runner = new JobRunner( "*", - async () => + () => { sampler.Counter++; - await Task.Delay(sleepMilliseconds); + Thread.Sleep(sleepMilliseconds); } );