diff --git a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs index 7da59a28..08f3c297 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs @@ -112,6 +112,58 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() (await second).Should().Be(result); winnerCount.Should().Be(1); + } + + [Fact] + public void WhenValueNotCreatedHashCodeIsZero() + { + new AsyncAtomicFactory() + .GetHashCode() + .Should().Be(0); + } + + [Fact] + public void WhenValueCreatedHashCodeIsValueHashCode() + { + new AsyncAtomicFactory(1) + .GetHashCode() + .Should().Be(1); + } + + [Fact] + public void WhenValueNotCreatedEqualsFalse() + { + var a = new AsyncAtomicFactory(); + var b = new AsyncAtomicFactory(); + + a.Equals(b).Should().BeFalse(); + } + + [Fact] + public void WhenOtherValueNotCreatedEqualsFalse() + { + var a = new AsyncAtomicFactory(1); + var b = new AsyncAtomicFactory(); + + a.Equals(b).Should().BeFalse(); + } + + [Fact] + public void WhenArgNullEqualsFalse() + { + new AsyncAtomicFactory(1) + .Equals(null) + .Should().BeFalse(); + } + + [Fact] + public void WhenArgObjectValuesAreSameEqualsTrue() + { + object other = new AsyncAtomicFactory(1); + + new AsyncAtomicFactory(1) + .Equals(other) + .Should().BeTrue(); } } } diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs index 8fd427e3..d2b58d96 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs @@ -15,11 +15,17 @@ namespace BitFaster.Caching.UnitTests.Atomic public class AtomicFactoryAsyncCacheTests { private const int capacity = 6; - private readonly AtomicFactoryAsyncCache cache = new(new ConcurrentLru>(capacity)); + private readonly ConcurrentLru> innerCache = new(capacity); + private readonly AtomicFactoryAsyncCache cache; private List> removedItems = new(); private List> updatedItems = new(); + public AtomicFactoryAsyncCacheTests() + { + cache = new(innerCache); + } + [Fact] public void WhenInnerCacheIsNullCtorThrows() { @@ -250,8 +256,53 @@ public async Task WhenFactoryThrowsEmptyKeyIsNotEnumerable() catch { } cache.Keys.Count().Should().Be(0); + } + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenRemovedValueIsReturned() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1, out var value); + + value.Should().Be(1); } + [Fact] + public void WhenNotRemovedValueIsDefault() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(2, out var value); + + value.Should().Be(0); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesntMatchDontRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 2)).Should().BeFalse(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesMatchThenRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueIsNotCreatedDoesNotRemove() + { + // seed the inner cache with an not yet created value + this.innerCache.AddOrUpdate(1, new AsyncAtomicFactory()); + + // try to remove with the default value (0) + this.cache.TryRemove(new KeyValuePair(1, 0)).Should().BeFalse(); + } +#endif + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) { this.removedItems.Add(e); diff --git a/BitFaster.Caching.UnitTests/CacheTests.cs b/BitFaster.Caching.UnitTests/CacheTests.cs index 1840e301..5cdeafa0 100644 --- a/BitFaster.Caching.UnitTests/CacheTests.cs +++ b/BitFaster.Caching.UnitTests/CacheTests.cs @@ -1,119 +1,141 @@ - -using System; + +using System; using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Moq; -using Xunit; - -namespace BitFaster.Caching.UnitTests -{ - // Tests for interface default implementations. - public class CacheTests - { -// backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func, int> evaluate = (k, f) => f(k); - cache.Setup(c => c.GetOrAdd(It.IsAny(), It.IsAny>())).Returns(evaluate); - - cache.Object.GetOrAdd( - 1, - (k, a) => k + a, - 2).Should().Be(3); +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Xunit; + +namespace BitFaster.Caching.UnitTests +{ + // Tests for interface default implementations. + public class CacheTests + { +// backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func, int> evaluate = (k, f) => f(k); + cache.Setup(c => c.GetOrAdd(It.IsAny(), It.IsAny>())).Returns(evaluate); + + cache.Object.GetOrAdd( + 1, + (k, a) => k + a, + 2).Should().Be(3); + } + + [Fact] + public void WhenCacheInterfaceDefaultTryRemoveKeyThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(1, out var value); }; + + tryRemove.Should().Throw(); + } + + [Fact] + public void WhenCacheInterfaceDefaultTryRemoveKeyValueThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(new KeyValuePair(1, 1)); }; + + tryRemove.Should().Throw(); + } + + [Fact] + public async Task WhenAsyncCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func>, ValueTask> evaluate = (k, f) => new ValueTask(f(k)); + cache.Setup(c => c.GetOrAddAsync(It.IsAny(), It.IsAny>>())).Returns(evaluate); + + var r = await cache.Object.GetOrAddAsync( + 1, + (k, a) => Task.FromResult(k + a), + 2); + + r.Should().Be(3); + } + + [Fact] + public void WhenAsyncCacheInterfaceDefaultTryRemoveKeyThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(1, out var value); }; + + tryRemove.Should().Throw(); + } + + [Fact] + public void WhenAsyncCacheInterfaceDefaultTryRemoveKeyValueThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(new KeyValuePair(1, 1)); }; + + tryRemove.Should().Throw(); } - [Fact] - public void WhenCacheInterfaceDefaultTryRemoveKeyThrows() - { - var cache = new Mock>(); - cache.CallBase = true; - - Action tryRemove = () => { cache.Object.TryRemove(1, out var value); }; - - tryRemove.Should().Throw(); + [Fact] + public void WhenScopedCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func>, Lifetime> evaluate = (k, f) => + { + var scope = f(k); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + return lifetime; + }; + + cache.Setup(c => c.ScopedGetOrAdd(It.IsAny(), It.IsAny>>())).Returns(evaluate); + + var l = cache.Object.ScopedGetOrAdd( + 1, + (k, a) => new Scoped(new Disposable(k + a)), + 2); + + l.Value.State.Should().Be(3); } - [Fact] - public void WhenCacheInterfaceDefaultTryRemoveKeyValueThrows() - { - var cache = new Mock>(); - cache.CallBase = true; - - Action tryRemove = () => { cache.Object.TryRemove(new KeyValuePair(1, 1)); }; - - tryRemove.Should().Throw(); - } - - [Fact] - public async Task WhenAsyncCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func>, ValueTask> evaluate = (k, f) => new ValueTask(f(k)); - cache.Setup(c => c.GetOrAddAsync(It.IsAny(), It.IsAny>>())).Returns(evaluate); - - var r = await cache.Object.GetOrAddAsync( - 1, - (k, a) => Task.FromResult(k + a), - 2); - - r.Should().Be(3); - } - - [Fact] - public void WhenScopedCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func>, Lifetime> evaluate = (k, f) => - { - var scope = f(k); - scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); - return lifetime; - }; - - cache.Setup(c => c.ScopedGetOrAdd(It.IsAny(), It.IsAny>>())).Returns(evaluate); - - var l = cache.Object.ScopedGetOrAdd( - 1, - (k, a) => new Scoped(new Disposable(k + a)), - 2); - - l.Value.State.Should().Be(3); - } - - [Fact] - public async Task WhenScopedAsyncCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func>>, ValueTask>> evaluate = async (k, f) => - { - var scope = await f(k); - scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); - return lifetime; - }; - - cache - .Setup(c => c.ScopedGetOrAddAsync(It.IsAny(), It.IsAny>>>())) - .Returns(evaluate); - - var lifetime = await cache.Object.ScopedGetOrAddAsync( - 1, - (k, a) => Task.FromResult(new Scoped(new Disposable(k + a))), - 2); - - lifetime.Value.State.Should().Be(3); - } -#endif - } -} + [Fact] + public async Task WhenScopedAsyncCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func>>, ValueTask>> evaluate = async (k, f) => + { + var scope = await f(k); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + return lifetime; + }; + + cache + .Setup(c => c.ScopedGetOrAddAsync(It.IsAny(), It.IsAny>>>())) + .Returns(evaluate); + + var lifetime = await cache.Object.ScopedGetOrAddAsync( + 1, + (k, a) => Task.FromResult(new Scoped(new Disposable(k + a))), + 2); + + lifetime.Value.State.Should().Be(3); + } +#endif + } +} diff --git a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs index c8ff84a4..4feb3ee0 100644 --- a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -12,7 +13,7 @@ namespace BitFaster.Caching.Atomic /// The type of the key. /// The type of the value. [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public sealed class AsyncAtomicFactory + public sealed class AsyncAtomicFactory : IEquatable> { private Initializer initializer; @@ -92,6 +93,34 @@ public V ValueIfCreated } } + /// + public override bool Equals(object obj) + { + return Equals(obj as AsyncAtomicFactory); + } + + /// + public bool Equals(AsyncAtomicFactory other) + { + if (other is null || !IsValueCreated || !other.IsValueCreated) + { + return false; + } + + return EqualityComparer.Default.Equals(ValueIfCreated, other.ValueIfCreated); + } + + /// + public override int GetHashCode() + { + if (!IsValueCreated) + { + return 0; + } + + return ValueIfCreated.GetHashCode(); + } + private async ValueTask CreateValueAsync(K key, TFactory valueFactory) where TFactory : struct, IAsyncValueFactory { var init = Volatile.Read(ref initializer); diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 38e2cb18..5d3e94ba 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -109,7 +109,36 @@ public bool TryGet(K key, out V value) public bool TryRemove(K key) { return cache.TryRemove(key); + } + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + /// + /// + ///If the value factory is still executing, returns false. + /// + public bool TryRemove(KeyValuePair item) + { + var kvp = new KeyValuePair>(item.Key, new AsyncAtomicFactory(item.Value)); + return cache.TryRemove(kvp); + } + + /// + /// + /// If the value factory is still executing, the default value will be returned. + /// + public bool TryRemove(K key, out V value) + { + if (cache.TryRemove(key, out var atomic)) + { + value = atomic.ValueIfCreated; + return true; + } + + value = default; + return false; } +#endif /// public bool TryUpdate(K key, V value) diff --git a/BitFaster.Caching/IAsyncCache.cs b/BitFaster.Caching/IAsyncCache.cs index 6f4210a6..59299d60 100644 --- a/BitFaster.Caching/IAsyncCache.cs +++ b/BitFaster.Caching/IAsyncCache.cs @@ -64,7 +64,22 @@ public interface IAsyncCache : IEnumerable> /// The factory function used to asynchronously generate a value for the key. /// An argument value to pass into valueFactory. /// A task that represents the asynchronous GetOrAdd operation. - ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) => this.GetOrAddAsync(key, k => valueFactory(k, factoryArgument)); + ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) => this.GetOrAddAsync(key, k => valueFactory(k, factoryArgument)); + + /// + /// Attempts to remove and return the value that has the specified key. + /// + /// The key of the element to remove. + /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key, out V value) => throw new NotSupportedException(); + + /// + /// Attempts to remove the specified key value pair. + /// + /// The item to remove. + /// true if the item was removed successfully; otherwise, false. + bool TryRemove(KeyValuePair item) => throw new NotSupportedException(); #endif ///