diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 3ff5812..1d7bed6 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 8.0.x
+ dotnet-version: 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..77c852e
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+ latest
+ enable
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..e47931e
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,13 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastCache.Benchmarks/FastCache.Benchmarks.csproj b/FastCache.Benchmarks/FastCache.Benchmarks.csproj
index 5de6907..6d62d89 100644
--- a/FastCache.Benchmarks/FastCache.Benchmarks.csproj
+++ b/FastCache.Benchmarks/FastCache.Benchmarks.csproj
@@ -1,19 +1,18 @@
-
- Exe
- net10.0
- enable
- enable
-
+
+ Exe
+ net10.0
+ enable
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
diff --git a/FastCache.Benchmarks/Program.cs b/FastCache.Benchmarks/Program.cs
index 273654e..456e86f 100644
--- a/FastCache.Benchmarks/Program.cs
+++ b/FastCache.Benchmarks/Program.cs
@@ -9,7 +9,7 @@
[ShortRunJob, MemoryDiagnoser]
public class BenchMark
{
- private static FastCache _cache = new FastCache(600_000);
+ private static FastCache _cache = new(600_000);
private static ConcurrentDictionary _dict = new();
private static DateTime _dtPlus10Mins = DateTime.Now.AddMinutes(10);
diff --git a/FastCache.sln b/FastCache.sln
deleted file mode 100644
index 4f5a0f4..0000000
--- a/FastCache.sln
+++ /dev/null
@@ -1,37 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.3.32901.215
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastCache", "FastCache\FastCache.csproj", "{3DD23A5F-11D4-43E4-8BD0-1354FBD154CF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{0CD62E2D-1D7F-43A7-AA85-EB0C6F0F69EA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastCache.Benchmarks", "FastCache.Benchmarks\FastCache.Benchmarks.csproj", "{DD603B8C-5216-4079-B6B2-5AA54ED1B1F3}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {3DD23A5F-11D4-43E4-8BD0-1354FBD154CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3DD23A5F-11D4-43E4-8BD0-1354FBD154CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3DD23A5F-11D4-43E4-8BD0-1354FBD154CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3DD23A5F-11D4-43E4-8BD0-1354FBD154CF}.Release|Any CPU.Build.0 = Release|Any CPU
- {0CD62E2D-1D7F-43A7-AA85-EB0C6F0F69EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0CD62E2D-1D7F-43A7-AA85-EB0C6F0F69EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0CD62E2D-1D7F-43A7-AA85-EB0C6F0F69EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0CD62E2D-1D7F-43A7-AA85-EB0C6F0F69EA}.Release|Any CPU.Build.0 = Release|Any CPU
- {DD603B8C-5216-4079-B6B2-5AA54ED1B1F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DD603B8C-5216-4079-B6B2-5AA54ED1B1F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DD603B8C-5216-4079-B6B2-5AA54ED1B1F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DD603B8C-5216-4079-B6B2-5AA54ED1B1F3}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {B73D44CA-57C0-4F81-87D4-D2D72ED26C51}
- EndGlobalSection
-EndGlobal
diff --git a/FastCache.slnx b/FastCache.slnx
new file mode 100644
index 0000000..f90f74d
--- /dev/null
+++ b/FastCache.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FastCache/FastCache.cs b/FastCache/FastCache.cs
index eb57e60..c95e3fd 100644
--- a/FastCache/FastCache.cs
+++ b/FastCache/FastCache.cs
@@ -1,387 +1,382 @@
-using System;
using System.Collections;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-namespace Jitbit.Utils
+namespace Jitbit.Utils;
+
+internal static class FastCacheStatics
{
- internal static class FastCacheStatics
- {
- internal static readonly SemaphoreSlim GlobalStaticLock = new(1); //moved this static field to separate class, otherwise a static field in a generic class is not a true singleton
- }
+ internal static readonly SemaphoreSlim GlobalStaticLock = new(1); //moved this static field to separate class, otherwise a static field in a generic class is not a true singleton
+}
+
+///
+/// faster MemoryCache alternative. basically a concurrent dictionary with expiration
+///
+public class FastCache : IEnumerable>, IDisposable where TKey : notnull
+{
+ private readonly ConcurrentDictionary _dict = new();
+
+ private readonly Lock _lock = new();
+ private readonly Timer _cleanUpTimer;
+ private readonly EvictionCallback _itemEvicted;
+
+ ///
+ /// Callback (RUNS ON THREAD POOL!) when an item is evicted from the cache.
+ ///
+ ///
+ public delegate void EvictionCallback(TKey key);
///
- /// faster MemoryCache alternative. basically a concurrent dictionary with expiration
+ /// Initializes a new empty instance of
///
- public class FastCache : IEnumerable>, IDisposable
+ /// cleanup interval in milliseconds, default is 10000
+ /// Optional callback (RUNS ON THREAD POOL!) when an item is evicted from the cache
+ public FastCache(int cleanupJobInterval = 10000, EvictionCallback itemEvicted = null)
{
- private readonly ConcurrentDictionary _dict = new ConcurrentDictionary();
+ _itemEvicted = itemEvicted;
+ _cleanUpTimer = new Timer(s => { _ = EvictExpiredJob(); }, null, cleanupJobInterval, cleanupJobInterval);
+ }
- private readonly Lock _lock = new();
- private readonly Timer _cleanUpTimer;
- private readonly EvictionCallback _itemEvicted;
+ private async Task EvictExpiredJob()
+ {
+ //if an applicaiton has many-many instances of FastCache objects, make sure the timer-based
+ //cleanup jobs don't clash with each other, i.e. there are no clean-up jobs running in parallel
+ //so we don't waste CPU resources, because cleanup is a busy-loop that iterates a collection and does calculations
+ //so we use a lock to "throttle" the job and make it serial
+ //HOWEVER, we still allow the user to execute eviction explicitly
- ///
- /// Callback (RUNS ON THREAD POOL!) when an item is evicted from the cache.
- ///
- ///
- public delegate void EvictionCallback(TKey key);
+ //use Semaphore instead of a "lock" to free up thread, otherwise - possible thread starvation
- ///
- /// Initializes a new empty instance of
- ///
- /// cleanup interval in milliseconds, default is 10000
- /// Optional callback (RUNS ON THREAD POOL!) when an item is evicted from the cache
- public FastCache(int cleanupJobInterval = 10000, EvictionCallback itemEvicted = null)
+ await FastCacheStatics.GlobalStaticLock.WaitAsync()
+ .ConfigureAwait(false);
+ try
{
- _itemEvicted = itemEvicted;
- _cleanUpTimer = new Timer(s => { _ = EvictExpiredJob(); }, null, cleanupJobInterval, cleanupJobInterval);
+ EvictExpired();
}
+ finally { FastCacheStatics.GlobalStaticLock.Release(); }
+ }
- private async Task EvictExpiredJob()
+ ///
+ /// Cleans up expired items (dont' wait for the background job)
+ /// There's rarely a need to execute this method, b/c getting an item checks TTL anyway.
+ ///
+ public void EvictExpired()
+ {
+ //Eviction already started by another thread? forget it, lets move on
+ if (_lock.TryEnter()) //use the new System.Threading.Lock class for faster locking in .NET9+
{
- //if an applicaiton has many-many instances of FastCache objects, make sure the timer-based
- //cleanup jobs don't clash with each other, i.e. there are no clean-up jobs running in parallel
- //so we don't waste CPU resources, because cleanup is a busy-loop that iterates a collection and does calculations
- //so we use a lock to "throttle" the job and make it serial
- //HOWEVER, we still allow the user to execute eviction explicitly
-
- //use Semaphore instead of a "lock" to free up thread, otherwise - possible thread starvation
-
- await FastCacheStatics.GlobalStaticLock.WaitAsync()
- .ConfigureAwait(false);
+ List evictedKeys = null; // Batch eviction callbacks
try
{
- EvictExpired();
- }
- finally { FastCacheStatics.GlobalStaticLock.Release(); }
- }
+ //cache current tick count in a var to prevent calling it every iteration inside "IsExpired()" in a tight loop.
+ //On a 10000-items cache this allows us to slice 30 microseconds: 330 vs 360 microseconds which is 10% faster
+ //On a 50000-items cache it's even more: 2.057ms vs 2.817ms which is 35% faster!!
+ //the bigger the cache the bigger the win
+ var currTime = Environment.TickCount64;
- ///
- /// Cleans up expired items (dont' wait for the background job)
- /// There's rarely a need to execute this method, b/c getting an item checks TTL anyway.
- ///
- public void EvictExpired()
- {
- //Eviction already started by another thread? forget it, lets move on
- if (_lock.TryEnter()) //use the new System.Threading.Lock class for faster locking in .NET9+
- {
- List evictedKeys = null; // Batch eviction callbacks
- try
+ foreach (var p in _dict)
{
- //cache current tick count in a var to prevent calling it every iteration inside "IsExpired()" in a tight loop.
- //On a 10000-items cache this allows us to slice 30 microseconds: 330 vs 360 microseconds which is 10% faster
- //On a 50000-items cache it's even more: 2.057ms vs 2.817ms which is 35% faster!!
- //the bigger the cache the bigger the win
- var currTime = Environment.TickCount64;
-
- foreach (var p in _dict)
+ if (p.Value.IsExpired(currTime)) //call IsExpired with "currTime" to avoid calling Environment.TickCount64 multiple times
{
- if (p.Value.IsExpired(currTime)) //call IsExpired with "currTime" to avoid calling Environment.TickCount64 multiple times
+ if (_dict.TryRemove(p) && _itemEvicted != null) // collect key for later batch processing (only if callback exists)
{
- if (_dict.TryRemove(p) && _itemEvicted != null) // collect key for later batch processing (only if callback exists)
- {
- evictedKeys ??= new List(); //lazy initialize the list
- evictedKeys.Add(p.Key);
- }
+ evictedKeys ??= new List(); //lazy initialize the list
+ evictedKeys.Add(p.Key);
}
}
}
- finally
- {
+ }
+ finally
+ {
_lock.Exit();
- }
-
- // Trigger batched eviction callbacks outside the loop to prevent flooding the thread pool
- OnEviction(evictedKeys);
}
+
+ // Trigger batched eviction callbacks outside the loop to prevent flooding the thread pool
+ OnEviction(evictedKeys);
}
+ }
- ///
- /// Returns total count, including expired items too, if they were not yet cleaned by the eviction job
- ///
- public int Count => _dict.Count;
+ ///
+ /// Returns total count, including expired items too, if they were not yet cleaned by the eviction job
+ ///
+ public int Count => _dict.Count;
- ///
- /// Removes all items from the cache
- ///
- public void Clear() => _dict.Clear();
+ ///
+ /// Removes all items from the cache
+ ///
+ public void Clear() => _dict.Clear();
- ///
- /// Adds an item to cache if it does not exist, updates the existing item otherwise. Updating an item resets its TTL, essentially "sliding expiration".
- ///
- /// The key to add
- /// The value to add
- /// TTL of the item
- public void AddOrUpdate(TKey key, TValue value, TimeSpan ttl)
- {
- var ttlValue = new TtlValue(value, ttl);
+ ///
+ /// Adds an item to cache if it does not exist, updates the existing item otherwise. Updating an item resets its TTL, essentially "sliding expiration".
+ ///
+ /// The key to add
+ /// The value to add
+ /// TTL of the item
+ public void AddOrUpdate(TKey key, TValue value, TimeSpan ttl)
+ {
+ var ttlValue = new TtlValue(value, ttl);
- _dict.AddOrUpdate(key, static (_, c) => c, static (_, _, c) => c, ttlValue);
- }
+ _dict.AddOrUpdate(key, static (_, c) => c, static (_, _, c) => c, ttlValue);
+ }
- ///
- /// Factory pattern overload. Adds an item to cache if it does not exist, updates the existing item otherwise. Updating an item resets its TTL, essentially "sliding expiration".
- ///
- /// The key to add or update
- /// The factory function used to generate the item for the key
- /// The factory function used to update the item for the key
- /// TTL of the item
- public void AddOrUpdate(TKey key, Func addValueFactory, Func updateValueFactory, TimeSpan ttl)
- {
- _dict.AddOrUpdate(key,
- addValueFactory: k => new TtlValue(addValueFactory(k), ttl),
- updateValueFactory: (k, v) => new TtlValue(updateValueFactory(k, v.Value), ttl));
- }
+ ///
+ /// Factory pattern overload. Adds an item to cache if it does not exist, updates the existing item otherwise. Updating an item resets its TTL, essentially "sliding expiration".
+ ///
+ /// The key to add or update
+ /// The factory function used to generate the item for the key
+ /// The factory function used to update the item for the key
+ /// TTL of the item
+ public void AddOrUpdate(TKey key, Func addValueFactory, Func updateValueFactory, TimeSpan ttl)
+ {
+ _dict.AddOrUpdate(key,
+ addValueFactory: k => new TtlValue(addValueFactory(k), ttl),
+ updateValueFactory: (k, v) => new TtlValue(updateValueFactory(k, v.Value), ttl));
+ }
- ///
- /// Attempts to get a value by key
- ///
- /// The key to get
- /// When method returns, contains the object with the key if found, otherwise default value of the type
- /// True if value exists, otherwise false
- public bool TryGet(TKey key, out TValue value)
- {
- value = default(TValue);
+ ///
+ /// Attempts to get a value by key
+ ///
+ /// The key to get
+ /// When method returns, contains the object with the key if found, otherwise default value of the type
+ /// True if value exists, otherwise false
+ public bool TryGet(TKey key, out TValue value)
+ {
+ value = default;
- if (!_dict.TryGetValue(key, out TtlValue ttlValue))
- return false; //not found
+ if (!_dict.TryGetValue(key, out TtlValue ttlValue))
+ return false; //not found
- if (ttlValue.IsExpired()) //found but expired
- {
- var kv = new KeyValuePair(key, ttlValue);
-
- //secret atomic removal method (only if both key and value match condition
- //https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
- //so that we don't need any locks!! woohoo
- _dict.TryRemove(kv);
-
- /* EXPLANATION:
- * when an item was "found but is expired" - we need to treat as "not found" and discard it.
- * One solution is to use a lock
- * so that the three steps "exist? expired? remove!" are performed atomically.
- * Otherwise another tread might chip in, and ADD a non-expired item with the same key while we're evicting it.
- * And we'll be removing a non-expired key that was just added.
- *
- * BUT instead of using locks we can remove by key AND value. So if another thread has just rushed in
- * and added another item with the same key - that other item won't be removed.
- *
- * basically, instead of doing this
- *
- * lock {
- * exists?
- * expired?
- * remove by key!
- * }
- *
- * we do this
- *
- * exists? (if yes returns the value)
- * expired?
- * remove by key AND value
- *
- * If another thread has modified the value - it won't remove it.
- *
- * Locks suck becasue add extra 50ns to benchmark, so it becomes 110ns instead of 70ns which sucks.
- * So - no locks then!!!
- *
- * */
+ if (ttlValue.IsExpired()) //found but expired
+ {
+ var kv = new KeyValuePair(key, ttlValue);
+
+ //secret atomic removal method (only if both key and value match condition
+ //https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
+ //so that we don't need any locks!! woohoo
+ _dict.TryRemove(kv);
+
+ /* EXPLANATION:
+ * when an item was "found but is expired" - we need to treat as "not found" and discard it.
+ * One solution is to use a lock
+ * so that the three steps "exist? expired? remove!" are performed atomically.
+ * Otherwise another tread might chip in, and ADD a non-expired item with the same key while we're evicting it.
+ * And we'll be removing a non-expired key that was just added.
+ *
+ * BUT instead of using locks we can remove by key AND value. So if another thread has just rushed in
+ * and added another item with the same key - that other item won't be removed.
+ *
+ * basically, instead of doing this
+ *
+ * lock {
+ * exists?
+ * expired?
+ * remove by key!
+ * }
+ *
+ * we do this
+ *
+ * exists? (if yes returns the value)
+ * expired?
+ * remove by key AND value
+ *
+ * If another thread has modified the value - it won't remove it.
+ *
+ * Locks suck becasue add extra 50ns to benchmark, so it becomes 110ns instead of 70ns which sucks.
+ * So - no locks then!!!
+ *
+ * */
+
+ OnEviction(key);
+
+ return false;
+ }
- OnEviction(key);
+ value = ttlValue.Value;
+ return true;
+ }
- return false;
- }
+ ///
+ /// Attempts to add a key/value item
+ ///
+ /// The key to add
+ /// The value to add
+ /// TTL of the item
+ /// True if value was added, otherwise false (already exists)
+ public bool TryAdd(TKey key, TValue value, TimeSpan ttl)
+ {
+ if (TryGet(key, out _))
+ return false;
- value = ttlValue.Value;
- return true;
- }
+ return _dict.TryAdd(key, new TtlValue(value, ttl));
+ }
- ///
- /// Attempts to add a key/value item
- ///
- /// The key to add
- /// The value to add
- /// TTL of the item
- /// True if value was added, otherwise false (already exists)
- public bool TryAdd(TKey key, TValue value, TimeSpan ttl)
- {
- if (TryGet(key, out _))
- return false;
+ private TValue GetOrAddCore(TKey key, Func valueFactory, TimeSpan ttl)
+ {
+ bool wasAdded = false; //flag to indicate "add vs get". TODO: wrap in ref type some day to avoid captures/closures
+ var ttlValue = _dict.GetOrAdd(
+ key,
+ (_) =>
+ {
+ wasAdded = true;
+ return new TtlValue(valueFactory(), ttl);
+ });
- return _dict.TryAdd(key, new TtlValue(value, ttl));
+ //if the item is expired, update value and TTL
+ //since TtlValue is a reference type we can update its properties in-place, instead of removing and re-adding to the dictionary (extra lookups)
+ if (!wasAdded) //performance hack: skip expiration check if a brand item was just added
+ {
+ if (ttlValue.ModifyIfExpired(valueFactory, ttl))
+ OnEviction(key);
}
- private TValue GetOrAddCore(TKey key, Func valueFactory, TimeSpan ttl)
- {
- bool wasAdded = false; //flag to indicate "add vs get". TODO: wrap in ref type some day to avoid captures/closures
- var ttlValue = _dict.GetOrAdd(
- key,
- (_) =>
- {
- wasAdded = true;
- return new TtlValue(valueFactory(), ttl);
- });
+ return ttlValue.Value;
+ }
- //if the item is expired, update value and TTL
- //since TtlValue is a reference type we can update its properties in-place, instead of removing and re-adding to the dictionary (extra lookups)
- if (!wasAdded) //performance hack: skip expiration check if a brand item was just added
- {
- if (ttlValue.ModifyIfExpired(valueFactory, ttl))
- OnEviction(key);
- }
+ ///
+ /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
+ ///
+ /// The key to add
+ /// The factory function used to generate the item for the key
+ /// TTL of the item
+ public TValue GetOrAdd(TKey key, Func valueFactory, TimeSpan ttl)
+ => GetOrAddCore(key, () => valueFactory(key), ttl);
- return ttlValue.Value;
- }
+ ///
+ /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
+ ///
+ /// The key to add
+ /// The factory function used to generate the item for the key
+ /// TTL of the item
+ /// Argument value to pass into valueFactory
+ public TValue GetOrAdd(TKey key, Func valueFactory, TimeSpan ttl, TArg factoryArgument)
+ => GetOrAddCore(key, () => valueFactory(key, factoryArgument), ttl);
- ///
- /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
- ///
- /// The key to add
- /// The factory function used to generate the item for the key
- /// TTL of the item
- public TValue GetOrAdd(TKey key, Func valueFactory, TimeSpan ttl)
- => GetOrAddCore(key, () => valueFactory(key), ttl);
+ ///
+ /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
+ ///
+ /// The key to add
+ /// The value to add
+ /// TTL of the item
+ public TValue GetOrAdd(TKey key, TValue value, TimeSpan ttl)
+ => GetOrAddCore(key, () => value, ttl);
- ///
- /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
- ///
- /// The key to add
- /// The factory function used to generate the item for the key
- /// TTL of the item
- /// Argument value to pass into valueFactory
- public TValue GetOrAdd(TKey key, Func valueFactory, TimeSpan ttl, TArg factoryArgument)
- => GetOrAddCore(key, () => valueFactory(key, factoryArgument), ttl);
+ ///
+ /// Tries to remove item with the specified key
+ ///
+ /// The key of the element to remove
+ public void Remove(TKey key)
+ {
+ _dict.TryRemove(key, out _);
+ }
- ///
- /// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing value if the key exists.
- ///
- /// The key to add
- /// The value to add
- /// TTL of the item
- public TValue GetOrAdd(TKey key, TValue value, TimeSpan ttl)
- => GetOrAddCore(key, () => value, ttl);
+ ///
+ /// Tries to remove item with the specified key, also returns the object removed in an "out" var
+ ///
+ /// The key of the element to remove
+ /// Contains the object removed or the default value if not found
+ public bool TryRemove(TKey key, out TValue value)
+ {
+ bool res = _dict.TryRemove(key, out var ttlValue) && !ttlValue.IsExpired();
+ value = res ? ttlValue.Value : default;
+ return res;
+ }
- ///
- /// Tries to remove item with the specified key
- ///
- /// The key of the element to remove
- public void Remove(TKey key)
+ ///
+ public IEnumerator> GetEnumerator()
+ {
+ var currTime = Environment.TickCount64; //save to a var to prevent multiple calls to Environment.TickCount64
+ foreach (var kvp in _dict)
{
- _dict.TryRemove(key, out _);
+ if (!kvp.Value.IsExpired(currTime))
+ yield return new KeyValuePair(kvp.Key, kvp.Value.Value);
}
+ }
- ///
- /// Tries to remove item with the specified key, also returns the object removed in an "out" var
- ///
- /// The key of the element to remove
- /// Contains the object removed or the default value if not found
- public bool TryRemove(TKey key, out TValue value)
- {
- bool res = _dict.TryRemove(key, out var ttlValue) && !ttlValue.IsExpired();
- value = res ? ttlValue.Value : default(TValue);
- return res;
- }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ private void OnEviction(TKey key)
+ {
+ if (_itemEvicted == null) return;
- ///
- public IEnumerator> GetEnumerator()
+ Task.Run(() => //run on thread pool to avoid blocking
{
- var currTime = Environment.TickCount64; //save to a var to prevent multiple calls to Environment.TickCount64
- foreach (var kvp in _dict)
+ try
{
- if (!kvp.Value.IsExpired(currTime))
- yield return new KeyValuePair(kvp.Key, kvp.Value.Value);
+ _itemEvicted(key);
}
- }
+ catch { } //to prevent any exceptions from crashing the thread
+ });
+ }
- IEnumerator IEnumerable.GetEnumerator()
- {
- return this.GetEnumerator();
- }
+ // same as OnEviction(TKey) but for batching
+ private void OnEviction(List keys)
+ {
+ if (keys == null || keys.Count == 0) return;
+ if (_itemEvicted == null) return;
- private void OnEviction(TKey key)
+ Task.Run(() => //run on thread pool to avoid blocking
{
- if (_itemEvicted == null) return;
-
- Task.Run(() => //run on thread pool to avoid blocking
+ try
{
- try
+ foreach (var key in keys)
{
_itemEvicted(key);
}
- catch { } //to prevent any exceptions from crashing the thread
- });
- }
-
- // same as OnEviction(TKey) but for batching
- private void OnEviction(List keys)
- {
- if (keys == null || keys.Count == 0) return;
- if (_itemEvicted == null) return;
+ }
+ catch { } //to prevent any exceptions from crashing the thread
+ });
+ }
- Task.Run(() => //run on thread pool to avoid blocking
- {
- try
- {
- foreach (var key in keys)
- {
- _itemEvicted(key);
- }
- }
- catch { } //to prevent any exceptions from crashing the thread
- });
- }
+ private class TtlValue
+ {
+ public TValue Value { get; private set; }
+ private long TickCountWhenToKill;
- private class TtlValue
+ public TtlValue(TValue value, TimeSpan ttl)
{
- public TValue Value { get; private set; }
- private long TickCountWhenToKill;
-
- public TtlValue(TValue value, TimeSpan ttl)
- {
- Value = value;
- TickCountWhenToKill = Environment.TickCount64 + (long)ttl.TotalMilliseconds;
- }
+ Value = value;
+ TickCountWhenToKill = Environment.TickCount64 + (long)ttl.TotalMilliseconds;
+ }
- public bool IsExpired() => IsExpired(Environment.TickCount64);
+ public bool IsExpired() => IsExpired(Environment.TickCount64);
- //use an overload instead of optional param to avoid extra IF's
- public bool IsExpired(long currTime) => currTime > TickCountWhenToKill;
+ //use an overload instead of optional param to avoid extra IF's
+ public bool IsExpired(long currTime) => currTime > TickCountWhenToKill;
- ///
- /// Updates the value and TTL only if the item is expired
- ///
- /// True if the item expired and was updated, otherwise false
- public bool ModifyIfExpired(Func newValueFactory, TimeSpan newTtl)
+ ///
+ /// Updates the value and TTL only if the item is expired
+ ///
+ /// True if the item expired and was updated, otherwise false
+ public bool ModifyIfExpired(Func newValueFactory, TimeSpan newTtl)
+ {
+ var ticks = Environment.TickCount64; //save to a var to prevent multiple calls to Environment.TickCount64
+ if (IsExpired(ticks)) //if expired - update the value and TTL
{
- var ticks = Environment.TickCount64; //save to a var to prevent multiple calls to Environment.TickCount64
- if (IsExpired(ticks)) //if expired - update the value and TTL
- {
- TickCountWhenToKill = ticks + (long)newTtl.TotalMilliseconds; //update the expiration time first for better concurrency
- Value = newValueFactory();
- return true;
- }
- return false;
+ TickCountWhenToKill = ticks + (long)newTtl.TotalMilliseconds; //update the expiration time first for better concurrency
+ Value = newValueFactory();
+ return true;
}
+ return false;
}
+ }
- //IDispisable members
- private bool _disposedValue;
- ///
- public void Dispose() => Dispose(true);
- ///
- protected virtual void Dispose(bool disposing)
+ //IDisposable members
+ private bool _disposedValue;
+ ///
+ public void Dispose() => Dispose(true);
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
{
- if (!_disposedValue)
+ if (disposing)
{
- if (disposing)
- {
- _cleanUpTimer.Dispose();
- }
-
- _disposedValue = true;
+ _cleanUpTimer.Dispose();
}
+
+ _disposedValue = true;
}
}
}
diff --git a/FastCache/FastCache.csproj b/FastCache/FastCache.csproj
index b8d7060..e2444a4 100644
--- a/FastCache/FastCache.csproj
+++ b/FastCache/FastCache.csproj
@@ -1,38 +1,37 @@
-
- net6.0;net8.0;net9.0;net10.0
- latest
- Jitbit.FastCache
- FastCache
- Alex from Jitbit
- FastCache
- https://github.com/jitbit/fastcache
- README.md
- https://github.com/jitbit/fastcache
- LICENSE
- 1.1.3
- cache;caching;MemoryCache
- Fastest in-memory cache for .NET
- True
-
+
+ net6.0;net8.0;net9.0;net10.0
+ Jitbit.FastCache
+ FastCache
+ Alex from Jitbit
+ FastCache
+ https://github.com/jitbit/fastcache
+ README.md
+ https://github.com/jitbit/fastcache
+ LICENSE
+ 1.1.3
+ cache;caching;MemoryCache
+ Fastest in-memory cache for .NET
+ True
+
-
-
- True
- \
-
-
- True
- \
-
-
+
+
+ True
+ \
+
+
+ True
+ \
+
+
-
-
- all
- analyzers
-
-
+
+
+ all
+ analyzers
+
+
diff --git a/LICENSE b/LICENSE
index f1ed971..577d60d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2022 Jitbit (the company behind "Jitbit Helpdesk" software)
+Copyright (c) 2026 Jitbit (the company behind "Jitbit Helpdesk" software)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/UnitTests/EvictionCallbackTests.cs b/UnitTests/EvictionCallbackTests.cs
index d7952c5..54748ae 100644
--- a/UnitTests/EvictionCallbackTests.cs
+++ b/UnitTests/EvictionCallbackTests.cs
@@ -1,4 +1,3 @@
-using System;
using Jitbit.Utils;
namespace UnitTests;
diff --git a/UnitTests/TestHelper.cs b/UnitTests/TestHelper.cs
index aadd42a..6179192 100644
--- a/UnitTests/TestHelper.cs
+++ b/UnitTests/TestHelper.cs
@@ -1,26 +1,25 @@
[assembly: Parallelize(Workers = 3, Scope = ExecutionScope.ClassLevel)]
-namespace UnitTests
+namespace UnitTests;
+
+internal class TestHelper
{
- internal class TestHelper
+ public static async Task RunConcurrently(int numThreads, Action action)
{
- public static async Task RunConcurrently(int numThreads, Action action)
- {
- var tasks = new Task[numThreads];
- ManualResetEvent m = new ManualResetEvent(false);
+ var tasks = new Task[numThreads];
+ ManualResetEvent m = new ManualResetEvent(false);
- for (int i = 0; i < numThreads; i++)
+ for (int i = 0; i < numThreads; i++)
+ {
+ tasks[i] = Task.Run(() =>
{
- tasks[i] = Task.Run(() =>
- {
- m.WaitOne(); //dont start just yet
- action();
- });
- }
+ m.WaitOne(); //dont start just yet
+ action();
+ });
+ }
- m.Set(); //off we go
+ m.Set(); //off we go
- await Task.WhenAll(tasks);
- }
+ await Task.WhenAll(tasks);
}
}
diff --git a/UnitTests/UnitTests.cs b/UnitTests/UnitTests.cs
index e5f4169..f89b031 100644
--- a/UnitTests/UnitTests.cs
+++ b/UnitTests/UnitTests.cs
@@ -1,248 +1,246 @@
using Jitbit.Utils;
+namespace UnitTests;
-namespace UnitTests
+[TestClass]
+public class UnitTests
{
- [TestClass]
- public class UnitTests
+ [TestMethod]
+ public async Task TestGetSetCleanup()
{
- [TestMethod]
- public async Task TestGetSetCleanup()
- {
- using var _cache = new FastCache(cleanupJobInterval: 200); //add "using" to stop cleanup timer, to prevent cleanup job from clashing with other tests
- _cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(100));
- Assert.IsTrue(_cache.TryGet(42, out int v));
- Assert.AreEqual(42, v);
+ using var _cache = new FastCache(cleanupJobInterval: 200); //add "using" to stop cleanup timer, to prevent cleanup job from clashing with other tests
+ _cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(100));
+ Assert.IsTrue(_cache.TryGet(42, out int v));
+ Assert.AreEqual(42, v);
- await Task.Delay(300);
- Assert.AreEqual(0, _cache.Count); //cleanup job has run?
- }
+ await Task.Delay(300);
+ Assert.AreEqual(0, _cache.Count); //cleanup job has run?
+ }
- [TestMethod]
- public async Task TestEviction()
+ [TestMethod]
+ public async Task TestEviction()
+ {
+ var list = new List>();
+ for (int i = 0; i < 20; i++)
{
- var list = new List>();
- for (int i = 0; i < 20; i++)
- {
- var cache = new FastCache(cleanupJobInterval: 200);
- cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(100));
- list.Add(cache);
- }
- await Task.Delay(300);
-
- for (int i = 0; i < 20; i++)
- {
- Assert.AreEqual(0, list[i].Count); //cleanup job has run?
- }
-
- //cleanup
- for (int i = 0; i < 20; i++)
- {
- list[i].Dispose();
- }
+ var cache = new FastCache(cleanupJobInterval: 200);
+ cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(100));
+ list.Add(cache);
}
+ await Task.Delay(300);
- [TestMethod]
- public async Task Shortdelay()
+ for (int i = 0; i < 20; i++)
{
- var cache = new FastCache();
- cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(500));
-
- await Task.Delay(50);
-
- Assert.IsTrue(cache.TryGet(42, out int result)); //not evicted
- Assert.AreEqual(42, result);
+ Assert.AreEqual(0, list[i].Count); //cleanup job has run?
}
- [TestMethod]
- public async Task TestWithDefaultJobInterval()
+ //cleanup
+ for (int i = 0; i < 20; i++)
{
- var _cache2 = new FastCache();
- _cache2.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
- Assert.IsTrue(_cache2.TryGet("42", out _));
- await Task.Delay(150);
- Assert.IsFalse(_cache2.TryGet("42", out _));
+ list[i].Dispose();
}
+ }
- [TestMethod]
- public void TestRemove()
- {
- var cache = new FastCache();
- cache.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
- cache.Remove("42");
- Assert.IsFalse(cache.TryGet("42", out _));
- }
+ [TestMethod]
+ public async Task Shortdelay()
+ {
+ var cache = new FastCache();
+ cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(500));
- [TestMethod]
- public void TestTryRemove()
- {
- var cache = new FastCache();
- cache.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
- var res = cache.TryRemove("42", out int value);
- Assert.IsTrue(res && value == 42);
- Assert.IsFalse(cache.TryGet("42", out _));
-
- //now try remove non-existing item
- res = cache.TryRemove("blabblah", out value);
- Assert.IsFalse(res);
- Assert.AreEqual(0, value);
- }
+ await Task.Delay(50);
- [TestMethod]
- public async Task TestTryRemoveWithTtl()
- {
- var cache = new FastCache();
- cache.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
- await Task.Delay(120); //let the item expire
+ Assert.IsTrue(cache.TryGet(42, out int result)); //not evicted
+ Assert.AreEqual(42, result);
+ }
- var res = cache.TryRemove("42", out int value);
- Assert.IsFalse(res);
- Assert.AreEqual(0, value);
- }
+ [TestMethod]
+ public async Task TestWithDefaultJobInterval()
+ {
+ var _cache2 = new FastCache();
+ _cache2.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
+ Assert.IsTrue(_cache2.TryGet("42", out _));
+ await Task.Delay(150);
+ Assert.IsFalse(_cache2.TryGet("42", out _));
+ }
- [TestMethod]
- public async Task TestTryAdd()
- {
- var cache = new FastCache();
- Assert.IsTrue(cache.TryAdd("42", 42, TimeSpan.FromMilliseconds(100)));
- Assert.IsFalse(cache.TryAdd("42", 42, TimeSpan.FromMilliseconds(100)));
+ [TestMethod]
+ public void TestRemove()
+ {
+ var cache = new FastCache();
+ cache.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
+ cache.Remove("42");
+ Assert.IsFalse(cache.TryGet("42", out _));
+ }
- await Task.Delay(120); //wait for it to expire
+ [TestMethod]
+ public void TestTryRemove()
+ {
+ var cache = new FastCache();
+ cache.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
+ var res = cache.TryRemove("42", out int value);
+ Assert.IsTrue(res && value == 42);
+ Assert.IsFalse(cache.TryGet("42", out _));
+
+ //now try remove non-existing item
+ res = cache.TryRemove("blabblah", out value);
+ Assert.IsFalse(res);
+ Assert.AreEqual(0, value);
+ }
- Assert.IsTrue(cache.TryAdd("42", 42, TimeSpan.FromMilliseconds(100)));
- }
+ [TestMethod]
+ public async Task TestTryRemoveWithTtl()
+ {
+ var cache = new FastCache();
+ cache.AddOrUpdate("42", 42, TimeSpan.FromMilliseconds(100));
+ await Task.Delay(120); //let the item expire
- [TestMethod]
- public async Task TestGetOrAdd()
- {
- var cache = new FastCache();
- cache.GetOrAdd("key", k => 1024, TimeSpan.FromMilliseconds(100));
- Assert.AreEqual(1024, cache.GetOrAdd("key", k => 1025, TimeSpan.FromMilliseconds(100))); //old value
- Assert.IsTrue(cache.TryGet("key", out int res) && res == 1024); //another way to retrieve
- await Task.Delay(110);
-
- Assert.IsFalse(cache.TryGet("key", out _)); //expired
-
- //now try non-factory overloads
- Assert.AreEqual(123321, cache.GetOrAdd("key123", 123321, TimeSpan.FromMilliseconds(100)));
- Assert.AreEqual(123321, cache.GetOrAdd("key123", -1, TimeSpan.FromMilliseconds(100))); //still old value
- await Task.Delay(110);
- Assert.AreEqual(-1, cache.GetOrAdd("key123", -1, TimeSpan.FromMilliseconds(100))); //new value
- }
+ var res = cache.TryRemove("42", out int value);
+ Assert.IsFalse(res);
+ Assert.AreEqual(0, value);
+ }
+ [TestMethod]
+ public async Task TestTryAdd()
+ {
+ var cache = new FastCache();
+ Assert.IsTrue(cache.TryAdd("42", 42, TimeSpan.FromMilliseconds(100)));
+ Assert.IsFalse(cache.TryAdd("42", 42, TimeSpan.FromMilliseconds(100)));
- [TestMethod]
- public async Task TestGetOrAddExpiration()
- {
- var cache = new FastCache();
- cache.GetOrAdd("key", k => 1024, TimeSpan.FromMilliseconds(100));
+ await Task.Delay(120); //wait for it to expire
- Assert.AreEqual(1024, cache.GetOrAdd("key", k => 1025, TimeSpan.FromMilliseconds(100))); //old value
- Assert.IsTrue(cache.TryGet("key", out int res) && res == 1024); //another way to retrieve
-
- await Task.Delay(110); //let the item expire
+ Assert.IsTrue(cache.TryAdd("42", 42, TimeSpan.FromMilliseconds(100)));
+ }
- Assert.AreEqual(1025, cache.GetOrAdd("key", k => 1025, TimeSpan.FromMilliseconds(100))); //new value
- Assert.IsTrue(cache.TryGet("key", out res) && res == 1025); //another way to retrieve
- }
+ [TestMethod]
+ public async Task TestGetOrAdd()
+ {
+ var cache = new FastCache();
+ cache.GetOrAdd("key", k => 1024, TimeSpan.FromMilliseconds(100));
+ Assert.AreEqual(1024, cache.GetOrAdd("key", k => 1025, TimeSpan.FromMilliseconds(100))); //old value
+ Assert.IsTrue(cache.TryGet("key", out int res) && res == 1024); //another way to retrieve
+ await Task.Delay(110);
+
+ Assert.IsFalse(cache.TryGet("key", out _)); //expired
+
+ //now try non-factory overloads
+ Assert.AreEqual(123321, cache.GetOrAdd("key123", 123321, TimeSpan.FromMilliseconds(100)));
+ Assert.AreEqual(123321, cache.GetOrAdd("key123", -1, TimeSpan.FromMilliseconds(100))); //still old value
+ await Task.Delay(110);
+ Assert.AreEqual(-1, cache.GetOrAdd("key123", -1, TimeSpan.FromMilliseconds(100))); //new value
+ }
- [TestMethod]
- public async Task TestGetOrAddWithArg()
- {
- var cache = new FastCache();
- cache.GetOrAdd("key", (k, arg) => 1024 + arg.Length, TimeSpan.FromMilliseconds(100), "test123");
- Assert.IsTrue(cache.TryGet("key", out int res) && res == 1031);
-
- //eviction
- await Task.Delay(110);
- Assert.IsFalse(cache.TryGet("key", out _));
-
- //now try without "TryGet"
- Assert.AreEqual(24, cache.GetOrAdd("key2", (k, arg) => 21 + arg.Length, TimeSpan.FromMilliseconds(100), "123"));
- Assert.AreEqual(24, cache.GetOrAdd("key2", (k, arg) => 2211 + arg.Length, TimeSpan.FromMilliseconds(100), "123"));
- await Task.Delay(110);
- Assert.AreEqual(2214, cache.GetOrAdd("key2", (k, arg) => 2211 + arg.Length, TimeSpan.FromMilliseconds(100), "123"));
- }
- [TestMethod]
- public void TestClear()
- {
- var cache = new FastCache();
- cache.GetOrAdd("key", k => 1024, TimeSpan.FromSeconds(100));
+ [TestMethod]
+ public async Task TestGetOrAddExpiration()
+ {
+ var cache = new FastCache();
+ cache.GetOrAdd("key", k => 1024, TimeSpan.FromMilliseconds(100));
- cache.Clear();
+ Assert.AreEqual(1024, cache.GetOrAdd("key", k => 1025, TimeSpan.FromMilliseconds(100))); //old value
+ Assert.IsTrue(cache.TryGet("key", out int res) && res == 1024); //another way to retrieve
+
+ await Task.Delay(110); //let the item expire
- Assert.IsFalse(cache.TryGet("key", out int res));
- }
+ Assert.AreEqual(1025, cache.GetOrAdd("key", k => 1025, TimeSpan.FromMilliseconds(100))); //new value
+ Assert.IsTrue(cache.TryGet("key", out res) && res == 1025); //another way to retrieve
+ }
- [TestMethod]
- public async Task TestTryAddAtomicness()
- {
- int i = 0;
-
- var cache = new FastCache();
- cache.TryAdd(42, 42, TimeSpan.FromMilliseconds(50)); //add item with short TTL
+ [TestMethod]
+ public async Task TestGetOrAddWithArg()
+ {
+ var cache = new FastCache();
+ cache.GetOrAdd("key", (k, arg) => 1024 + arg.Length, TimeSpan.FromMilliseconds(100), "test123");
+ Assert.IsTrue(cache.TryGet("key", out int res) && res == 1031);
+
+ //eviction
+ await Task.Delay(110);
+ Assert.IsFalse(cache.TryGet("key", out _));
+
+ //now try without "TryGet"
+ Assert.AreEqual(24, cache.GetOrAdd("key2", (k, arg) => 21 + arg.Length, TimeSpan.FromMilliseconds(100), "123"));
+ Assert.AreEqual(24, cache.GetOrAdd("key2", (k, arg) => 2211 + arg.Length, TimeSpan.FromMilliseconds(100), "123"));
+ await Task.Delay(110);
+ Assert.AreEqual(2214, cache.GetOrAdd("key2", (k, arg) => 2211 + arg.Length, TimeSpan.FromMilliseconds(100), "123"));
+ }
- await Task.Delay(100); //wait for tha value to expire
+ [TestMethod]
+ public void TestClear()
+ {
+ var cache = new FastCache();
+ cache.GetOrAdd("key", k => 1024, TimeSpan.FromSeconds(100));
- await TestHelper.RunConcurrently(20, () => {
- if (cache.TryAdd(42, 42, TimeSpan.FromSeconds(1)))
- i++;
- });
+ cache.Clear();
- Assert.AreEqual(1, i);
- }
+ Assert.IsFalse(cache.TryGet("key", out int res));
+ }
- //this text can occasionally fail becasue factory is not guaranteed to be called only once. only panic if it fails ALL THE TIME
- [TestMethod]
- public async Task TestGetOrAddAtomicNess()
- {
- int i = 0;
+ [TestMethod]
+ public async Task TestTryAddAtomicness()
+ {
+ int i = 0;
+
+ var cache = new FastCache();
+ cache.TryAdd(42, 42, TimeSpan.FromMilliseconds(50)); //add item with short TTL
- var cache = new FastCache();
-
- cache.GetOrAdd(42, 42, TimeSpan.FromMilliseconds(100));
+ await Task.Delay(100); //wait for tha value to expire
- await Task.Delay(110); //wait for tha value to expire
+ await TestHelper.RunConcurrently(20, () => {
+ if (cache.TryAdd(42, 42, TimeSpan.FromSeconds(1)))
+ i++;
+ });
- await TestHelper.RunConcurrently(20, () => {
- cache.GetOrAdd(42, k => { return ++i; }, TimeSpan.FromSeconds(1));
- });
+ Assert.AreEqual(1, i);
+ }
- //test that only the first value was added
- cache.TryGet(42, out i);
- Assert.AreEqual(1, i);
- }
+ //this text can occasionally fail becasue factory is not guaranteed to be called only once. only panic if it fails ALL THE TIME
+ [TestMethod]
+ public async Task TestGetOrAddAtomicNess()
+ {
+ int i = 0;
- [TestMethod]
- public async Task Enumerator()
- {
- var cache = new FastCache(); //now with default cleanup interval
- cache.GetOrAdd("key", k => 1024, TimeSpan.FromMilliseconds(100));
+ var cache = new FastCache();
+
+ cache.GetOrAdd(42, 42, TimeSpan.FromMilliseconds(100));
- Assert.AreEqual(1024, cache.FirstOrDefault().Value);
+ await Task.Delay(110); //wait for tha value to expire
- await Task.Delay(110);
+ await TestHelper.RunConcurrently(20, () => {
+ cache.GetOrAdd(42, k => { return ++i; }, TimeSpan.FromSeconds(1));
+ });
- Assert.IsFalse(cache.Any());
- }
+ //test that only the first value was added
+ cache.TryGet(42, out i);
+ Assert.AreEqual(1, i);
+ }
- [TestMethod]
- public async Task TestTtlExtended()
- {
- var _cache = new FastCache();
- _cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(300));
+ [TestMethod]
+ public async Task Enumerator()
+ {
+ var cache = new FastCache(); //now with default cleanup interval
+ cache.GetOrAdd("key", k => 1024, TimeSpan.FromMilliseconds(100));
- await Task.Delay(50);
- Assert.IsTrue(_cache.TryGet(42, out int result)); //not evicted
- Assert.AreEqual(42, result);
+ Assert.AreEqual(1024, cache.FirstOrDefault().Value);
- _cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(300));
+ await Task.Delay(110);
- await Task.Delay(250);
+ Assert.IsFalse(cache.Any());
+ }
- Assert.IsTrue(_cache.TryGet(42, out int result2)); //still not evicted
- Assert.AreEqual(42, result2);
- }
+ [TestMethod]
+ public async Task TestTtlExtended()
+ {
+ var _cache = new FastCache();
+ _cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(300));
+
+ await Task.Delay(50);
+ Assert.IsTrue(_cache.TryGet(42, out int result)); //not evicted
+ Assert.AreEqual(42, result);
+
+ _cache.AddOrUpdate(42, 42, TimeSpan.FromMilliseconds(300));
+
+ await Task.Delay(250);
+
+ Assert.IsTrue(_cache.TryGet(42, out int result2)); //still not evicted
+ Assert.AreEqual(42, result2);
}
}
\ No newline at end of file
diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj
index e8f6d25..055f837 100644
--- a/UnitTests/UnitTests.csproj
+++ b/UnitTests/UnitTests.csproj
@@ -1,21 +1,20 @@
-
- net10.0
- enable
- enable
+
+ net10.0
+ enable
+ false
+ true
+
- false
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/UnitTests/UnitTests2.cs b/UnitTests/UnitTests2.cs
index a68b05a..44216c1 100644
--- a/UnitTests/UnitTests2.cs
+++ b/UnitTests/UnitTests2.cs
@@ -2,252 +2,251 @@
//some more unit tests. Thanks Claude! :))
-namespace UnitTests
+namespace UnitTests;
+
+[TestClass]
+public class UnitTests2
{
- [TestClass]
- public class UnitTests2
+ [TestMethod]
+ public void AddOrUpdate_NewItem_AddsSuccessfully()
+ {
+ // Arrange
+ var cache = new FastCache();
+
+ // Act
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+ bool exists = cache.TryGet("key1", out int value);
+
+ // Assert
+ Assert.IsTrue(exists);
+ Assert.AreEqual(42, value);
+ }
+
+ [TestMethod]
+ public void AddOrUpdate_ExistingItem_UpdatesSuccessfully()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+
+ // Act
+ cache.AddOrUpdate("key1", 43, TimeSpan.FromMinutes(1));
+ bool exists = cache.TryGet("key1", out int value);
+
+ // Assert
+ Assert.IsTrue(exists);
+ Assert.AreEqual(43, value);
+ }
+
+ [TestMethod]
+ public async Task TryGet_ExpiredItem_ReturnsFalse()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMilliseconds(100));
+
+ // Act
+ await Task.Delay(200); // Wait for expiration
+ bool exists = cache.TryGet("key1", out int value);
+
+ // Assert
+ Assert.IsFalse(exists);
+ Assert.AreEqual(default(int), value);
+ }
+
+ [TestMethod]
+ public void TryAdd_NewItem_ReturnsTrue()
+ {
+ // Arrange
+ var cache = new FastCache();
+
+ // Act
+ bool added = cache.TryAdd("key1", 42, TimeSpan.FromMinutes(1));
+
+ // Assert
+ Assert.IsTrue(added);
+ Assert.IsTrue(cache.TryGet("key1", out int value));
+ Assert.AreEqual(42, value);
+ }
+
+ [TestMethod]
+ public void TryAdd_ExistingItem_ReturnsFalse()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+
+ // Act
+ bool added = cache.TryAdd("key1", 43, TimeSpan.FromMinutes(1));
+
+ // Assert
+ Assert.IsFalse(added);
+ Assert.IsTrue(cache.TryGet("key1", out int value));
+ Assert.AreEqual(42, value); // Original value should remain
+ }
+
+ [TestMethod]
+ public void GetOrAdd_NewItem_AddsAndReturnsValue()
+ {
+ // Arrange
+ var cache = new FastCache();
+
+ // Act
+ int value = cache.GetOrAdd("key1", k => 42, TimeSpan.FromMinutes(1));
+
+ // Assert
+ Assert.AreEqual(42, value);
+ Assert.IsTrue(cache.TryGet("key1", out int retrieved));
+ Assert.AreEqual(42, retrieved);
+ }
+
+ [TestMethod]
+ public void GetOrAdd_ExistingNonExpiredItem_ReturnsExistingValue()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+
+ // Act
+ int value = cache.GetOrAdd("key1", k => 43, TimeSpan.FromMinutes(1));
+
+ // Assert
+ Assert.AreEqual(42, value); // Should return existing value
+ }
+
+ [TestMethod]
+ public async Task GetOrAdd_ExistingExpiredItem_ReturnsNewValue()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMilliseconds(100));
+ await Task.Delay(200); // Wait for expiration
+
+ // Act
+ int value = cache.GetOrAdd("key1", k => 43, TimeSpan.FromMinutes(1));
+
+ // Assert
+ Assert.AreEqual(43, value); // Should return new value
+ }
+
+ [TestMethod]
+ public void GetOrAddWithArg_NewItem_AddsAndReturnsValue()
+ {
+ // Arrange
+ var cache = new FastCache();
+ int multiplier = 2;
+
+ // Act
+ int value = cache.GetOrAdd("key1", (k, m) => 21 * m, TimeSpan.FromMinutes(1), multiplier);
+
+ // Assert
+ Assert.AreEqual(42, value);
+ }
+
+ [TestMethod]
+ public void Remove_ExistingItem_RemovesSuccessfully()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+
+ // Act
+ cache.Remove("key1");
+
+ // Assert
+ Assert.IsFalse(cache.TryGet("key1", out _));
+ }
+
+ [TestMethod]
+ public void TryRemove_ExistingItem_RemovesAndReturnsValue()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+
+ // Act
+ bool removed = cache.TryRemove("key1", out int value);
+
+ // Assert
+ Assert.IsTrue(removed);
+ Assert.AreEqual(42, value);
+ Assert.IsFalse(cache.TryGet("key1", out _));
+ }
+
+ [TestMethod]
+ public void Clear_RemovesAllItems()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+ cache.AddOrUpdate("key2", 43, TimeSpan.FromMinutes(1));
+
+ // Act
+ cache.Clear();
+
+ // Assert
+ Assert.AreEqual(0, cache.Count);
+ Assert.IsFalse(cache.TryGet("key1", out _));
+ Assert.IsFalse(cache.TryGet("key2", out _));
+ }
+
+ [TestMethod]
+ public void Enumeration_ReturnsOnlyNonExpiredItems()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
+ cache.AddOrUpdate("key2", 43, TimeSpan.FromMilliseconds(1));
+ Thread.Sleep(50); // Wait for second item to expire
+
+ // Act
+ var items = cache.ToList();
+
+ // Assert
+ Assert.HasCount(1, items);
+ Assert.AreEqual(42, items[0].Value);
+ Assert.AreEqual("key1", items[0].Key);
+ }
+
+ [TestMethod]
+ public async Task EvictExpired_RemovesExpiredItems()
+ {
+ // Arrange
+ var cache = new FastCache();
+ cache.AddOrUpdate("key1", 42, TimeSpan.FromMilliseconds(100));
+ cache.AddOrUpdate("key2", 43, TimeSpan.FromMinutes(1));
+
+ // Act
+ await Task.Delay(200); // Wait for first item to expire
+ cache.EvictExpired();
+
+ // Assert
+ Assert.IsFalse(cache.TryGet("key1", out _));
+ Assert.IsTrue(cache.TryGet("key2", out int value));
+ Assert.AreEqual(43, value);
+ }
+
+ [TestMethod]
+ public void AddOrUpdate_WithFactory()
{
- [TestMethod]
- public void AddOrUpdate_NewItem_AddsSuccessfully()
- {
- // Arrange
- var cache = new FastCache();
-
- // Act
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
- bool exists = cache.TryGet("key1", out int value);
-
- // Assert
- Assert.IsTrue(exists);
- Assert.AreEqual(42, value);
- }
-
- [TestMethod]
- public void AddOrUpdate_ExistingItem_UpdatesSuccessfully()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
-
- // Act
- cache.AddOrUpdate("key1", 43, TimeSpan.FromMinutes(1));
- bool exists = cache.TryGet("key1", out int value);
-
- // Assert
- Assert.IsTrue(exists);
- Assert.AreEqual(43, value);
- }
-
- [TestMethod]
- public async Task TryGet_ExpiredItem_ReturnsFalse()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMilliseconds(100));
-
- // Act
- await Task.Delay(200); // Wait for expiration
- bool exists = cache.TryGet("key1", out int value);
-
- // Assert
- Assert.IsFalse(exists);
- Assert.AreEqual(default(int), value);
- }
-
- [TestMethod]
- public void TryAdd_NewItem_ReturnsTrue()
- {
- // Arrange
- var cache = new FastCache();
-
- // Act
- bool added = cache.TryAdd("key1", 42, TimeSpan.FromMinutes(1));
-
- // Assert
- Assert.IsTrue(added);
- Assert.IsTrue(cache.TryGet("key1", out int value));
- Assert.AreEqual(42, value);
- }
-
- [TestMethod]
- public void TryAdd_ExistingItem_ReturnsFalse()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
-
- // Act
- bool added = cache.TryAdd("key1", 43, TimeSpan.FromMinutes(1));
-
- // Assert
- Assert.IsFalse(added);
- Assert.IsTrue(cache.TryGet("key1", out int value));
- Assert.AreEqual(42, value); // Original value should remain
- }
-
- [TestMethod]
- public void GetOrAdd_NewItem_AddsAndReturnsValue()
- {
- // Arrange
- var cache = new FastCache();
-
- // Act
- int value = cache.GetOrAdd("key1", k => 42, TimeSpan.FromMinutes(1));
-
- // Assert
- Assert.AreEqual(42, value);
- Assert.IsTrue(cache.TryGet("key1", out int retrieved));
- Assert.AreEqual(42, retrieved);
- }
-
- [TestMethod]
- public void GetOrAdd_ExistingNonExpiredItem_ReturnsExistingValue()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
-
- // Act
- int value = cache.GetOrAdd("key1", k => 43, TimeSpan.FromMinutes(1));
-
- // Assert
- Assert.AreEqual(42, value); // Should return existing value
- }
-
- [TestMethod]
- public async Task GetOrAdd_ExistingExpiredItem_ReturnsNewValue()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMilliseconds(100));
- await Task.Delay(200); // Wait for expiration
-
- // Act
- int value = cache.GetOrAdd("key1", k => 43, TimeSpan.FromMinutes(1));
-
- // Assert
- Assert.AreEqual(43, value); // Should return new value
- }
-
- [TestMethod]
- public void GetOrAddWithArg_NewItem_AddsAndReturnsValue()
- {
- // Arrange
- var cache = new FastCache();
- int multiplier = 2;
-
- // Act
- int value = cache.GetOrAdd("key1", (k, m) => 21 * m, TimeSpan.FromMinutes(1), multiplier);
-
- // Assert
- Assert.AreEqual(42, value);
- }
-
- [TestMethod]
- public void Remove_ExistingItem_RemovesSuccessfully()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
-
- // Act
- cache.Remove("key1");
-
- // Assert
- Assert.IsFalse(cache.TryGet("key1", out _));
- }
-
- [TestMethod]
- public void TryRemove_ExistingItem_RemovesAndReturnsValue()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
-
- // Act
- bool removed = cache.TryRemove("key1", out int value);
-
- // Assert
- Assert.IsTrue(removed);
- Assert.AreEqual(42, value);
- Assert.IsFalse(cache.TryGet("key1", out _));
- }
-
- [TestMethod]
- public void Clear_RemovesAllItems()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
- cache.AddOrUpdate("key2", 43, TimeSpan.FromMinutes(1));
-
- // Act
- cache.Clear();
-
- // Assert
- Assert.AreEqual(0, cache.Count);
- Assert.IsFalse(cache.TryGet("key1", out _));
- Assert.IsFalse(cache.TryGet("key2", out _));
- }
-
- [TestMethod]
- public void Enumeration_ReturnsOnlyNonExpiredItems()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMinutes(1));
- cache.AddOrUpdate("key2", 43, TimeSpan.FromMilliseconds(1));
- Thread.Sleep(50); // Wait for second item to expire
-
- // Act
- var items = cache.ToList();
-
- // Assert
- Assert.HasCount(1, items);
- Assert.AreEqual(42, items[0].Value);
- Assert.AreEqual("key1", items[0].Key);
- }
-
- [TestMethod]
- public async Task EvictExpired_RemovesExpiredItems()
- {
- // Arrange
- var cache = new FastCache();
- cache.AddOrUpdate("key1", 42, TimeSpan.FromMilliseconds(100));
- cache.AddOrUpdate("key2", 43, TimeSpan.FromMinutes(1));
-
- // Act
- await Task.Delay(200); // Wait for first item to expire
- cache.EvictExpired();
-
- // Assert
- Assert.IsFalse(cache.TryGet("key1", out _));
- Assert.IsTrue(cache.TryGet("key2", out int value));
- Assert.AreEqual(43, value);
- }
-
- [TestMethod]
- public void AddOrUpdate_WithFactory()
- {
- // Arrange
- var cache = new FastCache();
- int callCount = 0;
-
- // Act
- cache.AddOrUpdate("key1", _ => { callCount++; return 42; }, (_, _) => { callCount++; return 43; }, TimeSpan.FromMinutes(1));
- bool exists = cache.TryGet("key1", out int value);
-
- // Assert
- Assert.IsTrue(exists);
- Assert.AreEqual(42, value);
- Assert.AreEqual(1, callCount); // Factory should be called exactly once
-
- callCount = 0;
- cache.AddOrUpdate("key1", _ => { callCount++; return 44; }, (_, _) => { callCount++; return 45; }, TimeSpan.FromMinutes(1));
- exists = cache.TryGet("key1", out value);
- Assert.IsTrue(exists);
- Assert.AreEqual(45, value);
- Assert.AreEqual(1, callCount); // Factory should be called exactly once
- }
+ // Arrange
+ var cache = new FastCache();
+ int callCount = 0;
+
+ // Act
+ cache.AddOrUpdate("key1", _ => { callCount++; return 42; }, (_, _) => { callCount++; return 43; }, TimeSpan.FromMinutes(1));
+ bool exists = cache.TryGet("key1", out int value);
+
+ // Assert
+ Assert.IsTrue(exists);
+ Assert.AreEqual(42, value);
+ Assert.AreEqual(1, callCount); // Factory should be called exactly once
+
+ callCount = 0;
+ cache.AddOrUpdate("key1", _ => { callCount++; return 44; }, (_, _) => { callCount++; return 45; }, TimeSpan.FromMinutes(1));
+ exists = cache.TryGet("key1", out value);
+ Assert.IsTrue(exists);
+ Assert.AreEqual(45, value);
+ Assert.AreEqual(1, callCount); // Factory should be called exactly once
}
}
\ No newline at end of file