/* * dbMango * * Copyright 2025 Deutsche Bank AG * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Concurrent; namespace Rms.Risk.Mango.Pivot.Core.Models; /// /// A thread-safe dictionary with elements that expire after a specified duration. /// Accessing an element renews its expiration time. /// /// The type of the keys in the dictionary. /// The type of the values in the dictionary. public class ExpiringConcurrentDictionary : IDisposable where TValue : class where TKey : notnull { private readonly ConcurrentDictionary _dictionary = new(); private readonly TimeSpan _expirationDuration; private readonly bool _shouldDispose; private readonly Func? _elementFactory; private readonly Timer _cleanupTimer; private bool _disposed; /// /// Initializes a new instance of the class. /// /// The duration after which elements expire. /// The factory method to create elements for missing keys. /// The interval at which expired elements are removed. /// Indicates whether elements should be disposed when removed. public ExpiringConcurrentDictionary(Func elementFactory, TimeSpan expirationDuration, TimeSpan cleanupInterval, bool shouldDispose = true) : this(expirationDuration, cleanupInterval, shouldDispose) { _elementFactory = elementFactory ?? throw new ArgumentNullException(nameof(elementFactory)); } /// /// Initializes a new instance of the class. /// Element factory must be provided later via . /// /// The duration after which elements expire. /// The interval at which expired elements are removed. /// Indicates whether elements should be disposed when removed. public ExpiringConcurrentDictionary(TimeSpan expirationDuration, TimeSpan cleanupInterval, bool shouldDispose = true) { _expirationDuration = expirationDuration; _shouldDispose = shouldDispose; _elementFactory = null; _cleanupTimer = new(_ => RemoveExpiredElements(), null, cleanupInterval, cleanupInterval); } ~ExpiringConcurrentDictionary() { Dispose(false); } /// /// Gets or adds an element by key. If the element exists and is not expired, its expiration is renewed. /// If the element does not exist or is expired, a new element is created using the factory method. /// /// The key of the element. /// The factory method to create the element if it does not exist or is expired. /// The element associated with the key. public TValue GetOrAdd(TKey key, Func? elementFactory = null) { while (true) { var now = DateTime.UtcNow; if (_dictionary.TryGetValue(key, out var entry)) { if (entry.Expiration > now) { // Renew expiration and return the value _dictionary[key] = (entry.Value, now.Add(_expirationDuration)); return entry.Value; } else { // Remove expired entry RemoveEntry(key, entry.Value); } } // Create a new value using the factory method var factory = elementFactory ?? _elementFactory; if (factory == null) { throw new InvalidOperationException("Element factory is not specified."); } var newValue = factory(key); var newEntry = (newValue, now.Add(_expirationDuration)); if (_dictionary.TryAdd(key, newEntry)) { return newValue; } } } public void Clear() { var values = _dictionary.Values.ToList(); _dictionary.Clear(); foreach (var entry in values) { DisposeIfNecessary(entry.Value); } } /// /// Removes expired elements from the dictionary. /// If elements implement , they are disposed upon removal. /// public void RemoveExpiredElements() { var now = DateTime.UtcNow; foreach (var key in _dictionary.Keys) { if (_dictionary.TryGetValue(key, out var entry) && entry.Expiration <= now) { RemoveEntry(key, entry.Value); } } } /// /// Removes an element by key. /// If the element implements , it is disposed upon removal. /// /// The key of the element to remove. /// True if the element was removed; otherwise, false. public bool TryRemove(TKey key) { if (_dictionary.TryRemove(key, out var entry)) { DisposeIfNecessary(entry.Value); return true; } return false; } /// /// Disposes the dictionary and its elements if they implement . /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _cleanupTimer.Dispose(); foreach (var entry in _dictionary.Values) { DisposeIfNecessary(entry.Value); } _dictionary.Clear(); } _disposed = true; } } private void RemoveEntry(TKey key, TValue value) { if (_dictionary.TryRemove(key, out _)) { DisposeIfNecessary(value); } } private void DisposeIfNecessary(TValue value) { if (_shouldDispose && value is IDisposable disposable) { disposable.Dispose(); } } /// /// Determines whether the dictionary contains the specified key. /// /// The key to locate in the dictionary. /// True if the dictionary contains the key; otherwise, false. public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); /// /// Gets the count of elements in the dictionary. /// public int Count => _dictionary.Count; }