diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs index 91c83b75..71f3962b 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs @@ -18,6 +18,7 @@ public class AtomicFactoryAsyncCacheTests private readonly AtomicFactoryAsyncCache cache = new(new ConcurrentLru>(capacity)); private List> removedItems = new(); + private List> updatedItems = new(); [Fact] public void WhenInnerCacheIsNullCtorThrows() @@ -65,7 +66,7 @@ public void WhenNoInnerEventsNoOuterEvents() } [Fact] - public void WhenEventHandlerIsRegisteredItIsFired() + public void WhenRemovedEventHandlerIsRegisteredItIsFired() { this.cache.Events.Value.ItemRemoved += OnItemRemoved; @@ -75,6 +76,19 @@ public void WhenEventHandlerIsRegisteredItIsFired() this.removedItems.First().Key.Should().Be(1); } + [Fact] + public void WhenUpdatedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemUpdated += OnItemUpdated; + + this.cache.AddOrUpdate(1, 2); + this.cache.AddOrUpdate(1, 3); + + this.updatedItems.First().Key.Should().Be(1); + this.updatedItems.First().OldValue.Should().Be(2); + this.updatedItems.First().NewValue.Should().Be(3); + } + [Fact] public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() { @@ -193,5 +207,10 @@ private void OnItemRemoved(object sender, ItemRemovedEventArgs e) { this.removedItems.Add(e); } + + private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) + { + this.updatedItems.Add(e); + } } } diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs index c4cd420d..f765bfc6 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs @@ -18,6 +18,7 @@ public class AtomicFactoryCacheTests private readonly AtomicFactoryCache cache = new(new ConcurrentLru>(capacity)); private List> removedItems = new(); + private List> updatedItems = new(); [Fact] public void WhenInnerCacheIsNullCtorThrows() @@ -54,7 +55,7 @@ public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() } [Fact] - public void WhenEventHandlerIsRegisteredItIsFired() + public void WhenRemovedEventHandlerIsRegisteredItIsFired() { this.cache.Events.Value.ItemRemoved += OnItemRemoved; @@ -64,6 +65,19 @@ public void WhenEventHandlerIsRegisteredItIsFired() this.removedItems.First().Key.Should().Be(1); } + [Fact] + public void WhenUpdatedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemUpdated += OnItemUpdated; + + this.cache.AddOrUpdate(1, 2); + this.cache.AddOrUpdate(1, 3); + + this.updatedItems.First().Key.Should().Be(1); + this.updatedItems.First().OldValue.Should().Be(2); + this.updatedItems.First().NewValue.Should().Be(3); + } + [Fact] public void WhenNoInnerEventsNoOuterEvents() { @@ -193,5 +207,10 @@ private void OnItemRemoved(object sender, ItemRemovedEventArgs e) { this.removedItems.Add(e); } + + private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) + { + this.updatedItems.Add(e); + } } } diff --git a/BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs b/BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs index 95b48bf6..6da81cf6 100644 --- a/BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs +++ b/BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using BitFaster.Caching.Atomic; using FluentAssertions; using Xunit; @@ -15,6 +13,7 @@ public class CacheEventProxyBaseTests private EventProxy eventProxy; private List> removedItems = new(); + private List> updatedItems = new(); public CacheEventProxyBaseTests() { @@ -23,56 +22,107 @@ public CacheEventProxyBaseTests() } [Fact] - public void WhenEventHandlerIsRegisteredItIsFired() + public void WheRemovedEventHandlerIsRegisteredItIsFired() { this.eventProxy.ItemRemoved += OnItemRemoved; - this.testCacheEvents.Fire(1, new AtomicFactory(1), ItemRemovedReason.Removed); + this.testCacheEvents.FireRemoved(1, new AtomicFactory(1), ItemRemovedReason.Removed); this.removedItems.First().Key.Should().Be(1); } [Fact] - public void WhenEventHandlerIsAddedThenRemovedItIsNotFired() + public void WhenRemovedEventHandlerIsAddedThenRemovedItIsNotFired() { this.eventProxy.ItemRemoved += OnItemRemoved; this.eventProxy.ItemRemoved -= OnItemRemoved; - this.testCacheEvents.Fire(1, new AtomicFactory(1), ItemRemovedReason.Removed); + this.testCacheEvents.FireRemoved(1, new AtomicFactory(1), ItemRemovedReason.Removed); this.removedItems.Count.Should().Be(0); } [Fact] - public void WhenTwoEventHandlersAddedThenOneRemovedEventIsFired() + public void WhenTwoRemovedEventHandlersAddedThenOneRemovedEventIsFired() { this.eventProxy.ItemRemoved += OnItemRemoved; this.eventProxy.ItemRemoved += OnItemRemovedThrow; this.eventProxy.ItemRemoved -= OnItemRemovedThrow; - this.testCacheEvents.Fire(1, new AtomicFactory(1), ItemRemovedReason.Removed); + this.testCacheEvents.FireRemoved(1, new AtomicFactory(1), ItemRemovedReason.Removed); this.removedItems.First().Key.Should().Be(1); } + [Fact] + public void WheUpdatedEventHandlerIsRegisteredItIsFired() + { + this.eventProxy.ItemUpdated += OnItemUpdated; + + this.testCacheEvents.FireUpdated(1, new AtomicFactory(2), new AtomicFactory(3)); + + this.updatedItems.First().Key.Should().Be(1); + this.updatedItems.First().OldValue.Should().Be(2); + this.updatedItems.First().NewValue.Should().Be(3); + } + + [Fact] + public void WhenUpdatedEventHandlerIsAddedThenRemovedItIsNotFired() + { + this.eventProxy.ItemUpdated += OnItemUpdated; + this.eventProxy.ItemUpdated -= OnItemUpdated; + + this.testCacheEvents.FireUpdated(1, new AtomicFactory(2), new AtomicFactory(3)); + + this.updatedItems.Count.Should().Be(0); + } + + [Fact] + public void WhenTwoUpdatedEventHandlersAddedThenOneRemovedEventIsFired() + { + this.eventProxy.ItemUpdated += OnItemUpdated; + this.eventProxy.ItemUpdated += OnItemUpdatedThrow; + this.eventProxy.ItemUpdated -= OnItemUpdatedThrow; + + this.testCacheEvents.FireUpdated(1, new AtomicFactory(2), new AtomicFactory(3)); + + this.updatedItems.First().Key.Should().Be(1); + } + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) { this.removedItems.Add(e); } + private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) + { + this.updatedItems.Add(e); + } + private void OnItemRemovedThrow(object sender, ItemRemovedEventArgs e) { throw new Exception("Should never happen"); } + private void OnItemUpdatedThrow(object sender, ItemUpdatedEventArgs e) + { + throw new Exception("Should never happen"); + } + private class TestCacheEvents : ICacheEvents> { public event EventHandler>> ItemRemoved; + public event EventHandler>> ItemUpdated; - public void Fire(K key, AtomicFactory value, ItemRemovedReason reason) + public void FireRemoved(K key, AtomicFactory value, ItemRemovedReason reason) { ItemRemoved?.Invoke(this, new ItemRemovedEventArgs>(key, value, reason)); } + + public void FireUpdated(K key, AtomicFactory oldValue, AtomicFactory newValue) + { + ItemUpdated?.Invoke(this, new ItemUpdatedEventArgs>(key, oldValue, newValue)); + } } private class EventProxy : CacheEventProxyBase, V> @@ -86,6 +136,11 @@ protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEven { return new ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); } + + protected override ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs> inner) + { + return new ItemUpdatedEventArgs(inner.Key, inner.OldValue.ValueIfCreated, inner.NewValue.ValueIfCreated); + } } } } diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index 6fa46662..6ae2a956 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -1,13 +1,12 @@ using FluentAssertions; using BitFaster.Caching.Lru; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -using System.Collections; namespace BitFaster.Caching.UnitTests.Lru { @@ -23,12 +22,18 @@ public class ConcurrentLruTests private ValueFactory valueFactory = new ValueFactory(); private List> removedItems = new List>(); + private List> updatedItems = new List>(); private void OnLruItemRemoved(object sender, ItemRemovedEventArgs e) { removedItems.Add(e); } + private void OnLruItemUpdated(object sender, ItemUpdatedEventArgs e) + { + updatedItems.Add(e); + } + public ConcurrentLruTests(ITestOutputHelper testOutputHelper) { this.testOutputHelper = testOutputHelper; @@ -708,6 +713,55 @@ public void WhenKeyDoesNotExistAddOrUpdateMaintainsLruOrder() lru.WarmCount.Should().Be(1); // items must have been enqueued and cycled for one of them to reach the warm queue } + [Fact] + public void WhenItemExistsAddOrUpdateFiresUpdateEvent() + { + var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); + lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; + + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(2, 3); + + lruEvents.AddOrUpdate(1, 3); + + this.updatedItems.Count.Should().Be(1); + this.updatedItems[0].Key.Should().Be(1); + this.updatedItems[0].OldValue.Should().Be(2); + this.updatedItems[0].NewValue.Should().Be(3); + } + + [Fact] + public void WhenItemExistsTryUpdateFiresUpdateEvent() + { + var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); + lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; + + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(2, 3); + + lruEvents.TryUpdate(1, 3); + + this.updatedItems.Count.Should().Be(1); + this.updatedItems[0].Key.Should().Be(1); + this.updatedItems[0].OldValue.Should().Be(2); + this.updatedItems[0].NewValue.Should().Be(3); + } + + [Fact] + public void WhenItemUpdatedEventIsUnregisteredEventIsNotFired() + { + var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); + + lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; + lruEvents.Events.Value.ItemUpdated -= OnLruItemUpdated; + + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(1, 2); + + updatedItems.Count.Should().Be(0); + } + [Fact] public void WhenCacheIsEmptyClearIsNoOp() { diff --git a/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs index 2838aaf9..9b75347c 100644 --- a/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs @@ -1,8 +1,5 @@ using FluentAssertions; using BitFaster.Caching.Lru; -using System; -using System.Collections.Generic; -using System.Text; using Xunit; namespace BitFaster.Caching.UnitTests.Lru @@ -62,7 +59,7 @@ public void IncrementTotalCountIsNoOp() [Fact] public void OnItemUpdatedIsNoOp() { - counter.Invoking(c => c.OnItemUpdated(1, 2)).Should().NotThrow(); + counter.Invoking(c => c.OnItemUpdated(1, 2, 3)).Should().NotThrow(); } [Fact] @@ -72,14 +69,25 @@ public void OnItemRemovedIsNoOp() } [Fact] - public void RegisterEventHandlerIsNoOp() + public void RegisterRemovedEventHandlerIsNoOp() { counter.ItemRemoved += OnItemRemoved; counter.ItemRemoved -= OnItemRemoved; } + [Fact] + public void RegisterUpdateEventHandlerIsNoOp() + { + counter.ItemUpdated += OnItemUpdated; + counter.ItemUpdated -= OnItemUpdated; + } + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) { } + + private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) + { + } } } diff --git a/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs index eca1a39b..0c3831df 100644 --- a/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs @@ -1,8 +1,6 @@ using FluentAssertions; using BitFaster.Caching.Lru; -using System; using System.Collections.Generic; -using System.Text; using Xunit; namespace BitFaster.Caching.UnitTests.Lru @@ -66,7 +64,7 @@ public void WhenTotalCountIsZeroRatioReturnsZero() [Fact] public void WhenItemUpdatedIncrementUpdatedCount() { - telemetryPolicy.OnItemUpdated(1, 2); + telemetryPolicy.OnItemUpdated(1, 2, 3); telemetryPolicy.Updated.Should().Be(1); } @@ -116,5 +114,35 @@ public void WhenEventSourceIsSetItemRemovedEventUsesSource() eventSourceList.Should().HaveCount(1); eventSourceList[0].Should().Be(this); } + + [Fact] + public void WhenOnItemUpdatedInvokedEventIsFired() + { + List> eventList = new(); + + telemetryPolicy.ItemUpdated += (source, args) => eventList.Add(args); + + telemetryPolicy.OnItemUpdated(1, 2, 3); + + eventList.Should().HaveCount(1); + eventList[0].Key.Should().Be(1); + eventList[0].OldValue.Should().Be(2); + eventList[0].NewValue.Should().Be(3); + } + + [Fact] + public void WhenEventSourceIsSetItemUpdatedEventUsesSource() + { + List eventSourceList = new(); + + telemetryPolicy.SetEventSource(this); + + telemetryPolicy.ItemUpdated += (source, args) => eventSourceList.Add(source); + + telemetryPolicy.OnItemUpdated(1, 2, 3); + + eventSourceList.Should().HaveCount(1); + eventSourceList[0].Should().Be(this); + } } } diff --git a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs index 31714b27..0cbf1420 100644 --- a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs +++ b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs @@ -15,6 +15,7 @@ public abstract class ScopedAsyncCacheTestBase protected readonly IScopedAsyncCache cache; protected List>> removedItems = new(); + protected List>> updatedItems = new(); protected ScopedAsyncCacheTestBase(IScopedAsyncCache cache) { @@ -48,7 +49,7 @@ public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() } [Fact] - public void WhenEventHandlerIsRegisteredItIsFired() + public void WhenRemovedEventHandlerIsRegisteredItIsFired() { this.cache.Events.Value.ItemRemoved += OnItemRemoved; @@ -58,6 +59,17 @@ public void WhenEventHandlerIsRegisteredItIsFired() this.removedItems.First().Key.Should().Be(1); } + [Fact] + public void WhenUpdatedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemUpdated += OnItemUpdated; + + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.AddOrUpdate(1, new Disposable()); + + this.updatedItems.First().Key.Should().Be(1); + } + [Fact] public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() { @@ -220,5 +232,10 @@ protected void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + { + this.updatedItems.Add(e); + } } } diff --git a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs index 1908eedd..d214ae5c 100644 --- a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using BitFaster.Caching.Lru; using FluentAssertions; diff --git a/BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs b/BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs index f74e0029..89cdc9b1 100644 --- a/BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs +++ b/BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs @@ -1,9 +1,7 @@ -using System; + using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using FluentAssertions; using Xunit; @@ -15,6 +13,7 @@ public abstract class ScopedCacheTestBase protected readonly IScopedCache cache; protected List>> removedItems = new(); + protected List>> updatedItems = new(); protected ScopedCacheTestBase(IScopedCache cache) { @@ -48,7 +47,7 @@ public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() } [Fact] - public void WhenEventHandlerIsRegisteredItIsFired() + public void WhenRemovedEventHandlerIsRegisteredItIsFired() { this.cache.Events.Value.ItemRemoved += OnItemRemoved; @@ -58,6 +57,17 @@ public void WhenEventHandlerIsRegisteredItIsFired() this.removedItems.First().Key.Should().Be(1); } + [Fact] + public void WhenUpdatedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemUpdated += OnItemUpdated; + + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.AddOrUpdate(1, new Disposable()); + + this.updatedItems.First().Key.Should().Be(1); + } + [Fact] public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() { @@ -220,5 +230,10 @@ protected void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + { + this.updatedItems.Add(e); + } } } diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 9d5c4f6d..d632bdb2 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -127,6 +127,11 @@ protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEven { return new ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); } + + protected override ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs> inner) + { + return new ItemUpdatedEventArgs(inner.Key, inner.OldValue.ValueIfCreated, inner.NewValue.ValueIfCreated); + } } } } diff --git a/BitFaster.Caching/Atomic/AtomicFactoryCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryCache.cs index 328989e2..a184b86c 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryCache.cs @@ -126,6 +126,11 @@ protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEven { return new ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); } + + protected override ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs> inner) + { + return new ItemUpdatedEventArgs(inner.Key, inner.OldValue.ValueIfCreated, inner.NewValue.ValueIfCreated); + } } } } diff --git a/BitFaster.Caching/Atomic/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryScopedAsyncCache.cs index 8f389fc3..15e54b39 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryScopedAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryScopedAsyncCache.cs @@ -144,6 +144,11 @@ protected override ItemRemovedEventArgs> TranslateOnRemoved(ItemRem { return new ItemRemovedEventArgs>(inner.Key, inner.Value.ScopeIfCreated, inner.Reason); } + + protected override ItemUpdatedEventArgs> TranslateOnUpdated(ItemUpdatedEventArgs> inner) + { + return new ItemUpdatedEventArgs>(inner.Key, inner.OldValue.ScopeIfCreated, inner.NewValue.ScopeIfCreated); + } } } } diff --git a/BitFaster.Caching/Atomic/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryScopedCache.cs index 5629e27d..ec32ac5f 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryScopedCache.cs @@ -142,6 +142,11 @@ protected override ItemRemovedEventArgs> TranslateOnRemoved(ItemRem { return new ItemRemovedEventArgs>(inner.Key, inner.Value.ScopeIfCreated, inner.Reason); } + + protected override ItemUpdatedEventArgs> TranslateOnUpdated(ItemUpdatedEventArgs> inner) + { + return new ItemUpdatedEventArgs>(inner.Key, inner.OldValue.ScopeIfCreated, inner.NewValue.ScopeIfCreated); + } } } } diff --git a/BitFaster.Caching/CacheEventProxyBase.cs b/BitFaster.Caching/CacheEventProxyBase.cs index acd7b1f4..83fca73a 100644 --- a/BitFaster.Caching/CacheEventProxyBase.cs +++ b/BitFaster.Caching/CacheEventProxyBase.cs @@ -15,6 +15,8 @@ public abstract class CacheEventProxyBase : ICacheEvents> itemRemovedProxy; + private event EventHandler> itemUpdatedProxy; + /// /// Initializes a new instance of the CacheEventProxyBase class with the specified inner cache events. /// @@ -27,17 +29,24 @@ public CacheEventProxyBase(ICacheEvents events) /// public event EventHandler> ItemRemoved { - add { this.Register(value); } - remove { this.UnRegister(value); } + add { this.RegisterRemoved(value); } + remove { this.UnRegisterRemoved(value); } + } + + /// + public event EventHandler> ItemUpdated + { + add { this.RegisterUpdated(value); } + remove { this.UnRegisterUpdated(value); } } - private void Register(EventHandler> value) + private void RegisterRemoved(EventHandler> value) { itemRemovedProxy += value; events.ItemRemoved += OnItemRemoved; } - private void UnRegister(EventHandler> value) + private void UnRegisterRemoved(EventHandler> value) { this.itemRemovedProxy -= value; @@ -47,16 +56,44 @@ private void UnRegister(EventHandler> value) } } + private void RegisterUpdated(EventHandler> value) + { + itemUpdatedProxy += value; + events.ItemUpdated += OnItemUpdated; + } + + private void UnRegisterUpdated(EventHandler> value) + { + this.itemUpdatedProxy -= value; + + if (this.itemUpdatedProxy == null) + { + this.events.ItemUpdated -= OnItemUpdated; + } + } + private void OnItemRemoved(object sender, ItemRemovedEventArgs args) { itemRemovedProxy(sender, TranslateOnRemoved(args)); } + private void OnItemUpdated(object sender, ItemUpdatedEventArgs args) + { + itemUpdatedProxy(sender, TranslateOnUpdated(args)); + } + /// /// Translate the ItemRemovedEventArgs by converting the inner arg type to the outer arg type. /// /// The inner arg. /// The translated arg. protected abstract ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs inner); + + /// + /// Translate the ItemUpdatedEventArgs by converting the inner arg type to the outer arg type. + /// + /// The inner arg. + /// The translated arg. + protected abstract ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs inner); } } diff --git a/BitFaster.Caching/ICacheEvents.cs b/BitFaster.Caching/ICacheEvents.cs index 18978f1b..cef461d0 100644 --- a/BitFaster.Caching/ICacheEvents.cs +++ b/BitFaster.Caching/ICacheEvents.cs @@ -11,5 +11,10 @@ public interface ICacheEvents /// Occurs when an item is removed from the cache. /// event EventHandler> ItemRemoved; + + /// + /// Occurs when an item is updated. + /// + event EventHandler> ItemUpdated; } } diff --git a/BitFaster.Caching/ItemUpdatedEventArgs.cs b/BitFaster.Caching/ItemUpdatedEventArgs.cs new file mode 100644 index 00000000..c6b56279 --- /dev/null +++ b/BitFaster.Caching/ItemUpdatedEventArgs.cs @@ -0,0 +1,40 @@ +using System; + +namespace BitFaster.Caching +{ + /// + /// Provides data for the ItemUpdated event. + /// + /// The type of the updated item key. + /// The type of the updated item value. + public class ItemUpdatedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the ItemUpdatedEventArgs class using the specified key, old value and new value. + /// + /// The key of the item that was updated. + /// The old cache value. + /// The new cache value. + public ItemUpdatedEventArgs(K key, V oldValue, V newValue) + { + this.Key = key; + this.OldValue = oldValue; + this.NewValue = newValue; + } + + /// + /// Gets the key of the item that was updated. + /// + public K Key { get; } + + /// + /// Gets the old value of the item. + /// + public V OldValue { get; } + + /// + /// Gets the new value of the item. + /// + public V NewValue { get; } + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 2afef096..2cfa1a4c 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -281,7 +281,7 @@ public bool TryUpdate(K key, V value) V oldValue = existing.Value; existing.Value = value; this.itemPolicy.Update(existing); - this.telemetryPolicy.OnItemUpdated(existing.Key, existing.Value); + this.telemetryPolicy.OnItemUpdated(existing.Key, oldValue, existing.Value); Disposer.Dispose(oldValue); return true; @@ -715,6 +715,12 @@ public event EventHandler> ItemRemoved remove { this.lru.telemetryPolicy.ItemRemoved -= value; } } + public event EventHandler> ItemUpdated + { + add { this.lru.telemetryPolicy.ItemUpdated += value; } + remove { this.lru.telemetryPolicy.ItemUpdated -= value; } + } + public void Trim(int itemCount) { lru.Trim(itemCount); diff --git a/BitFaster.Caching/Lru/ITelemetryPolicy.cs b/BitFaster.Caching/Lru/ITelemetryPolicy.cs index 311c1ade..884923bc 100644 --- a/BitFaster.Caching/Lru/ITelemetryPolicy.cs +++ b/BitFaster.Caching/Lru/ITelemetryPolicy.cs @@ -30,8 +30,9 @@ public interface ITelemetryPolicy : ICacheMetrics, ICacheEvents /// Register the update of an item. /// /// The key. - /// The value. - void OnItemUpdated(K key, V value); + /// The old value. + /// The new value. + void OnItemUpdated(K key, V oldValue, V value); /// /// Set the event source for any events that are fired. diff --git a/BitFaster.Caching/Lru/NoTelemetryPolicy.cs b/BitFaster.Caching/Lru/NoTelemetryPolicy.cs index 4c62d951..b5aaf952 100644 --- a/BitFaster.Caching/Lru/NoTelemetryPolicy.cs +++ b/BitFaster.Caching/Lru/NoTelemetryPolicy.cs @@ -37,6 +37,14 @@ public event EventHandler> ItemRemoved remove { } } + /// + public event EventHandler> ItemUpdated + { + // no-op, nothing is registered + add { } + remove { } + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void IncrementMiss() @@ -57,7 +65,7 @@ public void OnItemRemoved(K key, V value, ItemRemovedReason reason) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void OnItemUpdated(K key, V value) + public void OnItemUpdated(K key, V oldValue, V value) { } diff --git a/BitFaster.Caching/Lru/TelemetryPolicy.cs b/BitFaster.Caching/Lru/TelemetryPolicy.cs index b7201f6a..3cb94d2f 100644 --- a/BitFaster.Caching/Lru/TelemetryPolicy.cs +++ b/BitFaster.Caching/Lru/TelemetryPolicy.cs @@ -21,6 +21,9 @@ public struct TelemetryPolicy : ITelemetryPolicy /// public event EventHandler> ItemRemoved; + /// + public event EventHandler> ItemUpdated; + /// public double HitRatio => Total == 0 ? 0 : (double)Hits / (double)Total; @@ -64,9 +67,12 @@ public void OnItemRemoved(K key, V value, ItemRemovedReason reason) } /// - public void OnItemUpdated(K key, V value) + public void OnItemUpdated(K key, V oldValue, V newValue) { this.updatedCount.Increment(); + + // passing 'this' as source boxes the struct, and is anyway the wrong object + this.ItemUpdated?.Invoke(this.eventSource, new ItemUpdatedEventArgs(key, oldValue, newValue)); } ///