diff --git a/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs index 603cec33..bdafe20e 100644 --- a/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs @@ -9,12 +9,12 @@ namespace BitFaster.Caching.UnitTests.Lru { - public class ClassicLruTests - { - private const int capacity = 3; + public class ClassicLruTests + { + private const int capacity = 3; - private ClassicLru lru = new ClassicLru(1, capacity, EqualityComparer.Default); - ValueFactory valueFactory = new ValueFactory(); + private ClassicLru lru = new ClassicLru(1, capacity, EqualityComparer.Default); + ValueFactory valueFactory = new ValueFactory(); [Fact] public void WhenConcurrencyIsLessThan1CtorThrows() @@ -49,137 +49,137 @@ public void ConstructAddAndRetrieveWithDefaultCtorReturnsValue() } [Fact] - public void WhenItemIsAddedCountIsCorrect() - { - lru.Count.Should().Be(0); - lru.GetOrAdd(1, valueFactory.Create); - lru.Count.Should().Be(1); - } - - [Fact] - public void WhenItemExistsTryGetReturnsValueAndTrue() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - result.Should().Be(true); - value.Should().Be("1"); - } - - [Fact] - public void WhenItemDoesNotExistTryGetReturnsNullAndFalse() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(2, out var value); - - result.Should().Be(false); - value.Should().BeNull(); - } - - [Fact] - public void WhenItemIsAddedThenRetrievedHitRatioIsHalf() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - lru.HitRatio.Should().Be(0.5); - } - - [Fact] - public void WhenKeyIsRequestedItIsCreatedAndCached() - { - var result1 = lru.GetOrAdd(1, valueFactory.Create); - var result2 = lru.GetOrAdd(1, valueFactory.Create); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() - { - var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); - var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public void WhenDifferentKeysAreRequestedValueIsCreatedForEach() - { - var result1 = lru.GetOrAdd(1, valueFactory.Create); - var result2 = lru.GetOrAdd(2, valueFactory.Create); - - valueFactory.timesCalled.Should().Be(2); - - result1.Should().Be("1"); - result2.Should().Be("2"); - } - - [Fact] - public async Task WhenDifferentKeysAreRequesteValueIsCreatedForEachAsync() - { - var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); - var result2 = await lru.GetOrAddAsync(2, valueFactory.CreateAsync).ConfigureAwait(false); - - valueFactory.timesCalled.Should().Be(2); - - result1.Should().Be("1"); - result2.Should().Be("2"); - } - - [Fact] - public void WhenMoreKeysRequestedThanCapacityCountDoesNotIncrease() - { - for (int i = 0; i < capacity + 1; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - } - - lru.Count.Should().Be(capacity); - valueFactory.timesCalled.Should().Be(capacity + 1); - } - - [Fact] - public async Task WhenMoreKeysRequestedThanCapacityCountDoesNotIncreaseAsync() - { - for (int i = 0; i < capacity + 1; i++) - { - await lru.GetOrAddAsync(i, valueFactory.CreateAsync); - } - - lru.Count.Should().Be(capacity); - valueFactory.timesCalled.Should().Be(capacity + 1); - } - - [Fact] - public void WhenMoreKeysRequestedThanCapacityOldestItemIsEvicted() - { - // request 10 items, LRU is now full - for (int i = 0; i < capacity; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - } - - valueFactory.timesCalled.Should().Be(capacity); - - // request 0, now item 1 is to be evicted - lru.GetOrAdd(0, valueFactory.Create); - valueFactory.timesCalled.Should().Be(capacity); - - // request next item after last, verify value factory was called - lru.GetOrAdd(capacity, valueFactory.Create); - valueFactory.timesCalled.Should().Be(capacity + 1); - - // request 0, verify value factory not called - lru.GetOrAdd(0, valueFactory.Create); - valueFactory.timesCalled.Should().Be(capacity + 1); - - // request 1, verify value factory is called (and it was therefore not cached) - lru.GetOrAdd(1, valueFactory.Create); - valueFactory.timesCalled.Should().Be(capacity + 2); - } + public void WhenItemIsAddedCountIsCorrect() + { + lru.Count.Should().Be(0); + lru.GetOrAdd(1, valueFactory.Create); + lru.Count.Should().Be(1); + } + + [Fact] + public void WhenItemExistsTryGetReturnsValueAndTrue() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + result.Should().Be(true); + value.Should().Be("1"); + } + + [Fact] + public void WhenItemDoesNotExistTryGetReturnsNullAndFalse() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(2, out var value); + + result.Should().Be(false); + value.Should().BeNull(); + } + + [Fact] + public void WhenItemIsAddedThenRetrievedHitRatioIsHalf() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + lru.HitRatio.Should().Be(0.5); + } + + [Fact] + public void WhenKeyIsRequestedItIsCreatedAndCached() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create); + var result2 = lru.GetOrAdd(1, valueFactory.Create); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); + var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public void WhenDifferentKeysAreRequestedValueIsCreatedForEach() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create); + var result2 = lru.GetOrAdd(2, valueFactory.Create); + + valueFactory.timesCalled.Should().Be(2); + + result1.Should().Be("1"); + result2.Should().Be("2"); + } + + [Fact] + public async Task WhenDifferentKeysAreRequesteValueIsCreatedForEachAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); + var result2 = await lru.GetOrAddAsync(2, valueFactory.CreateAsync).ConfigureAwait(false); + + valueFactory.timesCalled.Should().Be(2); + + result1.Should().Be("1"); + result2.Should().Be("2"); + } + + [Fact] + public void WhenMoreKeysRequestedThanCapacityCountDoesNotIncrease() + { + for (int i = 0; i < capacity + 1; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + lru.Count.Should().Be(capacity); + valueFactory.timesCalled.Should().Be(capacity + 1); + } + + [Fact] + public async Task WhenMoreKeysRequestedThanCapacityCountDoesNotIncreaseAsync() + { + for (int i = 0; i < capacity + 1; i++) + { + await lru.GetOrAddAsync(i, valueFactory.CreateAsync); + } + + lru.Count.Should().Be(capacity); + valueFactory.timesCalled.Should().Be(capacity + 1); + } + + [Fact] + public void WhenMoreKeysRequestedThanCapacityOldestItemIsEvicted() + { + // request 10 items, LRU is now full + for (int i = 0; i < capacity; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + valueFactory.timesCalled.Should().Be(capacity); + + // request 0, now item 1 is to be evicted + lru.GetOrAdd(0, valueFactory.Create); + valueFactory.timesCalled.Should().Be(capacity); + + // request next item after last, verify value factory was called + lru.GetOrAdd(capacity, valueFactory.Create); + valueFactory.timesCalled.Should().Be(capacity + 1); + + // request 0, verify value factory not called + lru.GetOrAdd(0, valueFactory.Create); + valueFactory.timesCalled.Should().Be(capacity + 1); + + // request 1, verify value factory is called (and it was therefore not cached) + lru.GetOrAdd(1, valueFactory.Create); + valueFactory.timesCalled.Should().Be(capacity + 2); + } [Fact] public void WhenValueExpiresItIsDisposed() @@ -212,31 +212,31 @@ public async Task WhenValueExpiresAsyncItIsDisposed() } [Fact] - public void WhenKeyDoesNotExistTryGetReturnsFalse() - { - lru.GetOrAdd(1, valueFactory.Create); + public void WhenKeyDoesNotExistTryGetReturnsFalse() + { + lru.GetOrAdd(1, valueFactory.Create); - lru.TryGet(2, out var result).Should().Be(false); - } + lru.TryGet(2, out var result).Should().Be(false); + } - [Fact] - public void WhenKeyExistsTryGetReturnsTrueAndOutValueIsCorrect() - { - lru.GetOrAdd(1, valueFactory.Create); + [Fact] + public void WhenKeyExistsTryGetReturnsTrueAndOutValueIsCorrect() + { + lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - result.Should().Be(true); - value.Should().Be("1"); - } + bool result = lru.TryGet(1, out var value); + result.Should().Be(true); + value.Should().Be("1"); + } - [Fact] - public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() + [Fact] + public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() { - lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); - lru.TryRemove(1).Should().BeTrue(); - lru.TryGet(1, out var value).Should().BeFalse(); - } + lru.TryRemove(1).Should().BeTrue(); + lru.TryGet(1, out var value).Should().BeFalse(); + } [Fact] public void WhenItemIsRemovedItIsDisposed() @@ -251,11 +251,11 @@ public void WhenItemIsRemovedItIsDisposed() } [Fact] - public void WhenKeyDoesNotExistTryRemoveReturnsFalse() - { - lru.GetOrAdd(1, valueFactory.Create); + public void WhenKeyDoesNotExistTryRemoveReturnsFalse() + { + lru.GetOrAdd(1, valueFactory.Create); - lru.TryRemove(2).Should().BeFalse(); - } - } + lru.TryRemove(2).Should().BeFalse(); + } + } } diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index 61928e8f..49dd38aa 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -10,20 +10,20 @@ namespace BitFaster.Caching.UnitTests.Lru { - public class ConcurrentLruTests - { - private readonly ITestOutputHelper testOutputHelper; - private const int hotCap = 3; - private const int warmCap = 3; - private const int coldCap = 3; + public class ConcurrentLruTests + { + private readonly ITestOutputHelper testOutputHelper; + private const int hotCap = 3; + private const int warmCap = 3; + private const int coldCap = 3; - private ConcurrentLru lru = new ConcurrentLru(1, hotCap + warmCap + coldCap, EqualityComparer.Default); - private ValueFactory valueFactory = new ValueFactory(); + private ConcurrentLru lru = new ConcurrentLru(1, hotCap + warmCap + coldCap, EqualityComparer.Default); + private ValueFactory valueFactory = new ValueFactory(); - public ConcurrentLruTests(ITestOutputHelper testOutputHelper) - { - this.testOutputHelper = testOutputHelper; - } + public ConcurrentLruTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } [Fact] public void WhenConcurrencyIsLessThan1CtorThrows() @@ -58,307 +58,307 @@ public void ConstructAddAndRetrieveWithDefaultCtorReturnsValue() } [Fact] - public void WhenItemIsAddedCountIsCorrect() - { - lru.Count.Should().Be(0); - lru.GetOrAdd(1, valueFactory.Create); - lru.Count.Should().Be(1); - } - - [Fact] - public async Task WhenItemIsAddedCountIsCorrectAsync() - { - lru.Count.Should().Be(0); - await lru.GetOrAddAsync(0, valueFactory.CreateAsync).ConfigureAwait(false); - lru.Count.Should().Be(1); - } - - [Fact] - public void WhenItemExistsTryGetReturnsValueAndTrue() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - result.Should().Be(true); - value.Should().Be("1"); - } - - [Fact] - public void WhenItemDoesNotExistTryGetReturnsNullAndFalse() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(2, out var value); - - result.Should().Be(false); - value.Should().BeNull(); - } - - [Fact] - public void WhenItemIsAddedThenRetrievedHitRatioIsHalf() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - lru.HitRatio.Should().Be(0.5); - } - - [Fact] - public void WhenKeyIsRequestedItIsCreatedAndCached() - { - var result1 = lru.GetOrAdd(1, valueFactory.Create); - var result2 = lru.GetOrAdd(1, valueFactory.Create); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() - { - var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); - var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public void WhenDifferentKeysAreRequestedValueIsCreatedForEach() - { - var result1 = lru.GetOrAdd(1, valueFactory.Create); - var result2 = lru.GetOrAdd(2, valueFactory.Create); - - valueFactory.timesCalled.Should().Be(2); - - result1.Should().Be("1"); - result2.Should().Be("2"); - } - - [Fact] - public async Task WhenDifferentKeysAreRequesteValueIsCreatedForEachAsync() - { - var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); - var result2 = await lru.GetOrAddAsync(2, valueFactory.CreateAsync).ConfigureAwait(false); - - valueFactory.timesCalled.Should().Be(2); - - result1.Should().Be("1"); - result2.Should().Be("2"); - } - - [Fact] - public void WhenValuesAreNotReadAndMoreKeysRequestedThanCapacityCountDoesNotIncrease() - { - int hotColdCapacity = hotCap + coldCap; - for (int i = 0; i < hotColdCapacity + 1; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - } - - lru.Count.Should().Be(hotColdCapacity); - valueFactory.timesCalled.Should().Be(hotColdCapacity + 1); - } - - [Fact] - public void WhenValuesAreReadAndMoreKeysRequestedThanCapacityCountIsBounded() - { - int capacity = hotCap + coldCap + warmCap; - for (int i = 0; i < capacity + 1; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - - // touch items already cached when they are still in hot - if (i > 0) - { - lru.GetOrAdd(i - 1, valueFactory.Create); - } - } - - lru.Count.Should().Be(capacity); - valueFactory.timesCalled.Should().Be(capacity + 1); - } - - [Fact] - public void WhenKeysAreContinuouslyRequestedInTheOrderTheyAreAddedCountIsBounded() - { - int capacity = hotCap + coldCap + warmCap; - for (int i = 0; i < capacity + 10; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - - // Touch all items already cached in hot, warm and cold. - // This is worst case scenario, since we touch them in the exact order they - // were added. - for (int j = 0; j < i; j++) - { - lru.GetOrAdd(j, valueFactory.Create); - } - - testOutputHelper.WriteLine($"Total: {lru.Count} Hot: {lru.HotCount} Warm: {lru.WarmCount} Cold: {lru.ColdCount}"); - lru.Count.Should().BeLessOrEqualTo(capacity + 1); - } - } - - [Fact] - public void WhenValueIsNotTouchedAndExpiresFromHotValueIsBumpedToCold() - { - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - - lru.HotCount.Should().Be(3); - lru.WarmCount.Should().Be(0); - lru.ColdCount.Should().Be(1); - } - - [Fact] - public void WhenValueIsTouchedAndExpiresFromHotValueIsBumpedToWarm() - { - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(0, valueFactory.Create); - - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - - lru.HotCount.Should().Be(3); - lru.WarmCount.Should().Be(1); - lru.ColdCount.Should().Be(0); - } - - [Fact] - public void WhenValueIsTouchedAndExpiresFromColdItIsBumpedToWarm() - { - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - - // touch 0 while it is in cold - lru.GetOrAdd(0, valueFactory.Create); - - lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - - lru.HotCount.Should().Be(3); - lru.WarmCount.Should().Be(1); - lru.ColdCount.Should().Be(3); - } - - [Fact] - public void WhenValueIsNotTouchedAndExpiresFromColdItIsRemoved() - { - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - - // insert 7, 0th item will expire from cold - lru.HotCount.Should().Be(3); - lru.WarmCount.Should().Be(0); - lru.ColdCount.Should().Be(3); - - lru.TryGet(0, out var value).Should().Be(false); - } - - [Fact] - public void WhenValueIsNotTouchedAndExpiresFromWarmValueIsBumpedToCold() - { - // first 4 values are touched in hot, promote to warm - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - - // 3 values added to hot fill warm - lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - - lru.HotCount.Should().Be(3); - lru.WarmCount.Should().Be(3); - lru.ColdCount.Should().Be(1); - } - - [Fact] - public void WhenValueIsTouchedAndExpiresFromWarmValueIsBumpedBackIntoWarm() - { - // first 4 values are touched in hot, promote to warm - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - - // touch 0 while it is warm - lru.GetOrAdd(0, valueFactory.Create); - - // 3 values added to hot fill warm. Only 0 is touched. - lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - - // When warm fills, 2 items are processed. 1 is promoted back into warm, and 1 into cold. - lru.HotCount.Should().Be(3); - lru.WarmCount.Should().Be(3); - lru.ColdCount.Should().Be(1); - } - - [Fact] - public void WhenValueExpiresItIsDisposed() - { - var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); - var disposableValueFactory = new DisposableValueFactory(); - - for (int i = 0; i < 5; i++) - { - lruOfDisposable.GetOrAdd(i, disposableValueFactory.Create); - } - - disposableValueFactory.Items[0].IsDisposed.Should().BeTrue(); - disposableValueFactory.Items[1].IsDisposed.Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryRemove(1).Should().BeTrue(); - lru.TryGet(1, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenItemIsRemovedItIsDisposed() - { - var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); - var disposableValueFactory = new DisposableValueFactory(); - - lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); - lruOfDisposable.TryRemove(1); - - disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); - } - - [Fact] - public void WhenKeyDoesNotExistTryRemoveReturnsFalse() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryRemove(2).Should().BeFalse(); - } + public void WhenItemIsAddedCountIsCorrect() + { + lru.Count.Should().Be(0); + lru.GetOrAdd(1, valueFactory.Create); + lru.Count.Should().Be(1); + } + + [Fact] + public async Task WhenItemIsAddedCountIsCorrectAsync() + { + lru.Count.Should().Be(0); + await lru.GetOrAddAsync(0, valueFactory.CreateAsync).ConfigureAwait(false); + lru.Count.Should().Be(1); + } + + [Fact] + public void WhenItemExistsTryGetReturnsValueAndTrue() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + result.Should().Be(true); + value.Should().Be("1"); + } + + [Fact] + public void WhenItemDoesNotExistTryGetReturnsNullAndFalse() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(2, out var value); + + result.Should().Be(false); + value.Should().BeNull(); + } + + [Fact] + public void WhenItemIsAddedThenRetrievedHitRatioIsHalf() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + lru.HitRatio.Should().Be(0.5); + } + + [Fact] + public void WhenKeyIsRequestedItIsCreatedAndCached() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create); + var result2 = lru.GetOrAdd(1, valueFactory.Create); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); + var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public void WhenDifferentKeysAreRequestedValueIsCreatedForEach() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create); + var result2 = lru.GetOrAdd(2, valueFactory.Create); + + valueFactory.timesCalled.Should().Be(2); + + result1.Should().Be("1"); + result2.Should().Be("2"); + } + + [Fact] + public async Task WhenDifferentKeysAreRequesteValueIsCreatedForEachAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); + var result2 = await lru.GetOrAddAsync(2, valueFactory.CreateAsync).ConfigureAwait(false); + + valueFactory.timesCalled.Should().Be(2); + + result1.Should().Be("1"); + result2.Should().Be("2"); + } + + [Fact] + public void WhenValuesAreNotReadAndMoreKeysRequestedThanCapacityCountDoesNotIncrease() + { + int hotColdCapacity = hotCap + coldCap; + for (int i = 0; i < hotColdCapacity + 1; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + lru.Count.Should().Be(hotColdCapacity); + valueFactory.timesCalled.Should().Be(hotColdCapacity + 1); + } + + [Fact] + public void WhenValuesAreReadAndMoreKeysRequestedThanCapacityCountIsBounded() + { + int capacity = hotCap + coldCap + warmCap; + for (int i = 0; i < capacity + 1; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + + // touch items already cached when they are still in hot + if (i > 0) + { + lru.GetOrAdd(i - 1, valueFactory.Create); + } + } + + lru.Count.Should().Be(capacity); + valueFactory.timesCalled.Should().Be(capacity + 1); + } + + [Fact] + public void WhenKeysAreContinuouslyRequestedInTheOrderTheyAreAddedCountIsBounded() + { + int capacity = hotCap + coldCap + warmCap; + for (int i = 0; i < capacity + 10; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + + // Touch all items already cached in hot, warm and cold. + // This is worst case scenario, since we touch them in the exact order they + // were added. + for (int j = 0; j < i; j++) + { + lru.GetOrAdd(j, valueFactory.Create); + } + + testOutputHelper.WriteLine($"Total: {lru.Count} Hot: {lru.HotCount} Warm: {lru.WarmCount} Cold: {lru.ColdCount}"); + lru.Count.Should().BeLessOrEqualTo(capacity + 1); + } + } + + [Fact] + public void WhenValueIsNotTouchedAndExpiresFromHotValueIsBumpedToCold() + { + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + + lru.HotCount.Should().Be(3); + lru.WarmCount.Should().Be(0); + lru.ColdCount.Should().Be(1); + } + + [Fact] + public void WhenValueIsTouchedAndExpiresFromHotValueIsBumpedToWarm() + { + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(0, valueFactory.Create); + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + + lru.HotCount.Should().Be(3); + lru.WarmCount.Should().Be(1); + lru.ColdCount.Should().Be(0); + } + + [Fact] + public void WhenValueIsTouchedAndExpiresFromColdItIsBumpedToWarm() + { + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + + // touch 0 while it is in cold + lru.GetOrAdd(0, valueFactory.Create); + + lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + + lru.HotCount.Should().Be(3); + lru.WarmCount.Should().Be(1); + lru.ColdCount.Should().Be(3); + } + + [Fact] + public void WhenValueIsNotTouchedAndExpiresFromColdItIsRemoved() + { + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + + // insert 7, 0th item will expire from cold + lru.HotCount.Should().Be(3); + lru.WarmCount.Should().Be(0); + lru.ColdCount.Should().Be(3); + + lru.TryGet(0, out var value).Should().Be(false); + } + + [Fact] + public void WhenValueIsNotTouchedAndExpiresFromWarmValueIsBumpedToCold() + { + // first 4 values are touched in hot, promote to warm + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + + // 3 values added to hot fill warm + lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + + lru.HotCount.Should().Be(3); + lru.WarmCount.Should().Be(3); + lru.ColdCount.Should().Be(1); + } + + [Fact] + public void WhenValueIsTouchedAndExpiresFromWarmValueIsBumpedBackIntoWarm() + { + // first 4 values are touched in hot, promote to warm + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + + // touch 0 while it is warm + lru.GetOrAdd(0, valueFactory.Create); + + // 3 values added to hot fill warm. Only 0 is touched. + lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + + // When warm fills, 2 items are processed. 1 is promoted back into warm, and 1 into cold. + lru.HotCount.Should().Be(3); + lru.WarmCount.Should().Be(3); + lru.ColdCount.Should().Be(1); + } + + [Fact] + public void WhenValueExpiresItIsDisposed() + { + var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + + for (int i = 0; i < 5; i++) + { + lruOfDisposable.GetOrAdd(i, disposableValueFactory.Create); + } + + disposableValueFactory.Items[0].IsDisposed.Should().BeTrue(); + disposableValueFactory.Items[1].IsDisposed.Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryRemove(1).Should().BeTrue(); + lru.TryGet(1, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenItemIsRemovedItIsDisposed() + { + var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + + lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); + lruOfDisposable.TryRemove(1); + + disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenKeyDoesNotExistTryRemoveReturnsFalse() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryRemove(2).Should().BeFalse(); + } [Fact] public void WhenRepeatedlyAddingAndRemovingSameValueLruRemainsInConsistentState() @@ -373,5 +373,5 @@ public void WhenRepeatedlyAddingAndRemovingSameValueLruRemainsInConsistentState( lru.TryRemove(1); } } - } + } } diff --git a/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs b/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs index f9624cd8..1aa61229 100644 --- a/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs +++ b/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs @@ -5,20 +5,20 @@ namespace BitFaster.Caching.UnitTests.Lru { - public class ValueFactory - { - public int timesCalled; + public class ValueFactory + { + public int timesCalled; - public string Create(int key) - { - timesCalled++; - return key.ToString(); - } + public string Create(int key) + { + timesCalled++; + return key.ToString(); + } - public Task CreateAsync(int key) - { - timesCalled++; - return Task.FromResult(key.ToString()); - } - } + public Task CreateAsync(int key) + { + timesCalled++; + return Task.FromResult(key.ToString()); + } + } } diff --git a/BitFaster.Caching.UnitTests/SingletonCacheTests.cs b/BitFaster.Caching.UnitTests/SingletonCacheTests.cs index 5018aaf8..fa233043 100644 --- a/BitFaster.Caching.UnitTests/SingletonCacheTests.cs +++ b/BitFaster.Caching.UnitTests/SingletonCacheTests.cs @@ -6,136 +6,136 @@ namespace BitFaster.Caching.UnitTests { - public class SingletonCacheTests - { - [Fact] - public void AcquireWithSameKeyUsingCustomComparerReturnsSameLifetime() - { - var cache = new SingletonCache(1, 3, StringComparer.OrdinalIgnoreCase); - - var lifetime1 = cache.Acquire("foo"); - var lifetime2 = cache.Acquire("FOO"); - lifetime1.Value.Should().BeSameAs(lifetime2.Value); - lifetime1.Dispose(); - lifetime2.Dispose(); - } - - [Fact] - public void AcquireWithSameKeyReturnsSameLifetime() - { - var cache = new SingletonCache(); - - var lifetime1 = cache.Acquire("Foo"); - var lifetime2 = cache.Acquire("Foo"); - lifetime1.Value.Should().BeSameAs(lifetime2.Value); - lifetime1.Dispose(); - lifetime2.Dispose(); - } - - [Fact] - public void AcquireReleaseAcquireReturnsDifferentValue() - { - var cache = new SingletonCache(); - - var lifetime1 = cache.Acquire("Foo"); - lifetime1.Dispose(); - - var lifetime2 = cache.Acquire("Foo"); - lifetime2.Dispose(); - - lifetime1.Value.Should().NotBeSameAs(lifetime2.Value); - } - - [Fact] - public async Task AcquireWithSameKeyOnTwoDifferentThreadsReturnsSameValue() - { - var cache = new SingletonCache(); - - EventWaitHandle event1 = new EventWaitHandle(false, EventResetMode.AutoReset); - EventWaitHandle event2 = new EventWaitHandle(false, EventResetMode.AutoReset); - - Lifetime lifetime1 = null; + public class SingletonCacheTests + { + [Fact] + public void AcquireWithSameKeyUsingCustomComparerReturnsSameLifetime() + { + var cache = new SingletonCache(1, 3, StringComparer.OrdinalIgnoreCase); + + var lifetime1 = cache.Acquire("foo"); + var lifetime2 = cache.Acquire("FOO"); + lifetime1.Value.Should().BeSameAs(lifetime2.Value); + lifetime1.Dispose(); + lifetime2.Dispose(); + } + + [Fact] + public void AcquireWithSameKeyReturnsSameLifetime() + { + var cache = new SingletonCache(); + + var lifetime1 = cache.Acquire("Foo"); + var lifetime2 = cache.Acquire("Foo"); + lifetime1.Value.Should().BeSameAs(lifetime2.Value); + lifetime1.Dispose(); + lifetime2.Dispose(); + } + + [Fact] + public void AcquireReleaseAcquireReturnsDifferentValue() + { + var cache = new SingletonCache(); + + var lifetime1 = cache.Acquire("Foo"); + lifetime1.Dispose(); + + var lifetime2 = cache.Acquire("Foo"); + lifetime2.Dispose(); + + lifetime1.Value.Should().NotBeSameAs(lifetime2.Value); + } + + [Fact] + public async Task AcquireWithSameKeyOnTwoDifferentThreadsReturnsSameValue() + { + var cache = new SingletonCache(); + + EventWaitHandle event1 = new EventWaitHandle(false, EventResetMode.AutoReset); + EventWaitHandle event2 = new EventWaitHandle(false, EventResetMode.AutoReset); + + Lifetime lifetime1 = null; Lifetime lifetime2 = null; - Task task1 = Task.Run(() => - { - event1.WaitOne(); - lifetime1 = cache.Acquire("Foo"); - event2.Set(); - - event1.WaitOne(); - lifetime1.Dispose(); - event2.Set(); - }); - - Task task2 = Task.Run(() => - { - event1.Set(); - event2.WaitOne(); - lifetime2 = cache.Acquire("Foo"); - - event1.Set(); - event2.WaitOne(); - lifetime2.Dispose(); - }); - - await Task.WhenAll(task1, task2); - - lifetime1.Value.Should().BeSameAs(lifetime2.Value); - } - - [Fact] - public async Task AcquireWithSameKeyOnManyDifferentThreadsReturnsSameValue() - { - int count = 0; - - var cache = new SingletonCache(); - - int maxConcurrency = Environment.ProcessorCount + 1; - Task[] tasks = new Task[maxConcurrency]; - for (int concurrency = 0; concurrency < maxConcurrency; concurrency++) - { - tasks[concurrency] = Task.Run(() => - { - for (int i = 0; i < 100000; i++) - { - using (var lifetime = cache.Acquire("Foo")) - { - lock (lifetime.Value) - { - int result = Interlocked.Increment(ref count); - result.Should().Be(1); - Interlocked.Decrement(ref count); - } - } - } - }); - } - - await Task.WhenAll(tasks); - } - - [Fact] - public void WhenValueIsDisposableItIsDisposedWhenReleased() - { - var cache = new SingletonCache(); - - using (var lifetime = cache.Acquire("Foo")) - { - DisposeTest.WasDisposed.Should().BeFalse(); - } - - DisposeTest.WasDisposed.Should().BeTrue(); - } - - public class DisposeTest : IDisposable + Task task1 = Task.Run(() => + { + event1.WaitOne(); + lifetime1 = cache.Acquire("Foo"); + event2.Set(); + + event1.WaitOne(); + lifetime1.Dispose(); + event2.Set(); + }); + + Task task2 = Task.Run(() => + { + event1.Set(); + event2.WaitOne(); + lifetime2 = cache.Acquire("Foo"); + + event1.Set(); + event2.WaitOne(); + lifetime2.Dispose(); + }); + + await Task.WhenAll(task1, task2); + + lifetime1.Value.Should().BeSameAs(lifetime2.Value); + } + + [Fact] + public async Task AcquireWithSameKeyOnManyDifferentThreadsReturnsSameValue() + { + int count = 0; + + var cache = new SingletonCache(); + + int maxConcurrency = Environment.ProcessorCount + 1; + Task[] tasks = new Task[maxConcurrency]; + for (int concurrency = 0; concurrency < maxConcurrency; concurrency++) + { + tasks[concurrency] = Task.Run(() => + { + for (int i = 0; i < 100000; i++) + { + using (var lifetime = cache.Acquire("Foo")) + { + lock (lifetime.Value) + { + int result = Interlocked.Increment(ref count); + result.Should().Be(1); + Interlocked.Decrement(ref count); + } + } + } + }); + } + + await Task.WhenAll(tasks); + } + + [Fact] + public void WhenValueIsDisposableItIsDisposedWhenReleased() + { + var cache = new SingletonCache(); + + using (var lifetime = cache.Acquire("Foo")) + { + DisposeTest.WasDisposed.Should().BeFalse(); + } + + DisposeTest.WasDisposed.Should().BeTrue(); + } + + public class DisposeTest : IDisposable { - public static bool WasDisposed { get; set; } + public static bool WasDisposed { get; set; } public void Dispose() { - WasDisposed = true; + WasDisposed = true; } } - } + } } diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj index a0cf0e13..c10b5c59 100644 --- a/BitFaster.Caching/BitFaster.Caching.csproj +++ b/BitFaster.Caching/BitFaster.Caching.csproj @@ -8,7 +8,7 @@ High performance, thread-safe in-memory caching primitives for .NET. LICENSE true - 0.9.3 + 0.9.4 Copyright © Alex Peck 2020 https://github.com/bitfaster/BitFaster.Caching diff --git a/BitFaster.Caching/Lru/IPolicy.cs b/BitFaster.Caching/Lru/IPolicy.cs index 504d2e39..8989bd72 100644 --- a/BitFaster.Caching/Lru/IPolicy.cs +++ b/BitFaster.Caching/Lru/IPolicy.cs @@ -6,18 +6,18 @@ namespace BitFaster.Caching.Lru { - public interface IPolicy where I : LruItem - { - I CreateItem(K key, V value); + public interface IPolicy where I : LruItem + { + I CreateItem(K key, V value); - void Touch(I item); + void Touch(I item); - bool ShouldDiscard(I item); + bool ShouldDiscard(I item); - ItemDestination RouteHot(I item); + ItemDestination RouteHot(I item); - ItemDestination RouteWarm(I item); + ItemDestination RouteWarm(I item); - ItemDestination RouteCold(I item); - } + ItemDestination RouteCold(I item); + } } diff --git a/BitFaster.Caching/Lru/ItemDestination.cs b/BitFaster.Caching/Lru/ItemDestination.cs index 7a25cfa9..0741e246 100644 --- a/BitFaster.Caching/Lru/ItemDestination.cs +++ b/BitFaster.Caching/Lru/ItemDestination.cs @@ -6,10 +6,10 @@ namespace BitFaster.Caching.Lru { - public enum ItemDestination - { - Warm, - Cold, - Remove - } + public enum ItemDestination + { + Warm, + Cold, + Remove + } } diff --git a/BitFaster.Caching/Lru/LruItem.cs b/BitFaster.Caching/Lru/LruItem.cs index 0afdf523..624da55b 100644 --- a/BitFaster.Caching/Lru/LruItem.cs +++ b/BitFaster.Caching/Lru/LruItem.cs @@ -6,26 +6,26 @@ namespace BitFaster.Caching.Lru { - public class LruItem - { - private bool wasAccessed; - private bool wasRemoved; + public class LruItem + { + private volatile bool wasAccessed; + private volatile bool wasRemoved; public LruItem(K k, V v) - { - this.Key = k; - this.Value = v; - } + { + this.Key = k; + this.Value = v; + } - public readonly K Key; + public readonly K Key; - public readonly V Value; + public readonly V Value; - public bool WasAccessed - { - get => this.wasAccessed; - set => this.wasAccessed = value; - } + public bool WasAccessed + { + get => this.wasAccessed; + set => this.wasAccessed = value; + } public bool WasRemoved { diff --git a/BitFaster.Caching/Lru/LruPolicy.cs b/BitFaster.Caching/Lru/LruPolicy.cs index 8874bfe8..3e68020e 100644 --- a/BitFaster.Caching/Lru/LruPolicy.cs +++ b/BitFaster.Caching/Lru/LruPolicy.cs @@ -7,60 +7,60 @@ namespace BitFaster.Caching.Lru { - /// - /// Discards the least recently used items first. - /// - public readonly struct LruPolicy : IPolicy> - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public LruItem CreateItem(K key, V value) - { - return new LruItem(key, value); - } + /// + /// Discards the least recently used items first. + /// + public readonly struct LruPolicy : IPolicy> + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LruItem CreateItem(K key, V value) + { + return new LruItem(key, value); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Touch(LruItem item) - { - item.WasAccessed = true; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LruItem item) + { + item.WasAccessed = true; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShouldDiscard(LruItem item) - { - return false; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldDiscard(LruItem item) + { + return false; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ItemDestination RouteHot(LruItem item) - { - if (item.WasAccessed) - { - return ItemDestination.Warm; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteHot(LruItem item) + { + if (item.WasAccessed) + { + return ItemDestination.Warm; + } - return ItemDestination.Cold; - } + return ItemDestination.Cold; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ItemDestination RouteWarm(LruItem item) - { - if (item.WasAccessed) - { - return ItemDestination.Warm; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteWarm(LruItem item) + { + if (item.WasAccessed) + { + return ItemDestination.Warm; + } - return ItemDestination.Cold; - } + return ItemDestination.Cold; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ItemDestination RouteCold(LruItem item) - { - if (item.WasAccessed) - { - return ItemDestination.Warm; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteCold(LruItem item) + { + if (item.WasAccessed) + { + return ItemDestination.Warm; + } - return ItemDestination.Remove; - } - } + return ItemDestination.Remove; + } + } } diff --git a/BitFaster.Caching/ReferenceCount.cs b/BitFaster.Caching/ReferenceCount.cs index 4ce8555f..fbb55e14 100644 --- a/BitFaster.Caching/ReferenceCount.cs +++ b/BitFaster.Caching/ReferenceCount.cs @@ -4,67 +4,67 @@ namespace BitFaster.Caching { - /// - /// A reference counting class suitable for use with compare and swap algorithms. - /// - /// The value type. - public class ReferenceCount - { - private readonly TValue value; - private readonly int count; + /// + /// A reference counting class suitable for use with compare and swap algorithms. + /// + /// The value type. + public class ReferenceCount + { + private readonly TValue value; + private readonly int count; - public ReferenceCount(TValue value) - { - this.value = value; - this.count = 1; - } + public ReferenceCount(TValue value) + { + this.value = value; + this.count = 1; + } - private ReferenceCount(TValue value, int referenceCount) - { - this.value = value; - this.count = referenceCount; - } + private ReferenceCount(TValue value, int referenceCount) + { + this.value = value; + this.count = referenceCount; + } - public TValue Value - { - get - { - return this.value; - } - } + public TValue Value + { + get + { + return this.value; + } + } - public int Count - { - get - { - return this.count; - } - } + public int Count + { + get + { + return this.count; + } + } - public override int GetHashCode() - { - return this.value.GetHashCode() ^ this.count; - } + public override int GetHashCode() + { + return this.value.GetHashCode() ^ this.count; + } - public override bool Equals(object obj) - { - ReferenceCount refCount = obj as ReferenceCount; - return refCount != null && refCount.Value != null && refCount.Value.Equals(this.value) && refCount.count == this.count; - } + public override bool Equals(object obj) + { + ReferenceCount refCount = obj as ReferenceCount; + return refCount != null && refCount.Value != null && refCount.Value.Equals(this.value) && refCount.count == this.count; + } - public ReferenceCount IncrementCopy() - { - if (this.count <= 0 && this.value is IDisposable) - { - throw new ObjectDisposedException($"{typeof(TValue).Name} is disposed."); - } + public ReferenceCount IncrementCopy() + { + if (this.count <= 0 && this.value is IDisposable) + { + throw new ObjectDisposedException($"{typeof(TValue).Name} is disposed."); + } - return new ReferenceCount(this.value, this.count + 1); - } + return new ReferenceCount(this.value, this.count + 1); + } - public ReferenceCount DecrementCopy() - { - return new ReferenceCount(this.value, this.count - 1); - } - } + public ReferenceCount DecrementCopy() + { + return new ReferenceCount(this.value, this.count - 1); + } + } }