/*
* 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;
}