diff --git a/src/CosmosCache.cs b/src/CosmosCache.cs index 66cf255..c4f4ad7 100644 --- a/src/CosmosCache.cs +++ b/src/CosmosCache.cs @@ -82,7 +82,7 @@ public byte[] Get(string key) throw new ArgumentNullException(nameof(key)); } - await this.ConnectAsync().ConfigureAwait(false); + await this.ConnectAsync(token).ConfigureAwait(false); ItemResponse cosmosCacheSessionResponse; try @@ -92,9 +92,12 @@ public byte[] Get(string key) id: key, requestOptions: null, cancellationToken: token).ConfigureAwait(false); + + this.options.DiagnosticsHandler?.Invoke(cosmosCacheSessionResponse.Diagnostics); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { + this.options.DiagnosticsHandler?.Invoke(ex.Diagnostics); return null; } @@ -122,7 +125,7 @@ public byte[] Get(string key) } cosmosCacheSessionResponse.Resource.PartitionKeyAttribute = this.options.ContainerPartitionKeyAttribute; - await this.cosmosContainer.ReplaceItemAsync( + ItemResponse replaceCacheSessionResponse = await this.cosmosContainer.ReplaceItemAsync( partitionKey: new PartitionKey(key), id: key, item: cosmosCacheSessionResponse.Resource, @@ -132,9 +135,12 @@ await this.cosmosContainer.ReplaceItemAsync( EnableContentResponseOnWrite = false, }, cancellationToken: token).ConfigureAwait(false); + + this.options.DiagnosticsHandler?.Invoke(replaceCacheSessionResponse.Diagnostics); } catch (CosmosException cosmosException) when (cosmosException.StatusCode == HttpStatusCode.PreconditionFailed) { + this.options.DiagnosticsHandler?.Invoke(cosmosException.Diagnostics); if (this.options.RetrySlidingExpirationUpdates) { // Race condition on replace, we need to get the latest version of the item @@ -164,7 +170,7 @@ public void Refresh(string key) throw new ArgumentNullException(nameof(key)); } - await this.ConnectAsync().ConfigureAwait(false); + await this.ConnectAsync(token).ConfigureAwait(false); ItemResponse cosmosCacheSessionResponse; try @@ -174,9 +180,12 @@ public void Refresh(string key) id: key, requestOptions: null, cancellationToken: token).ConfigureAwait(false); + + this.options.DiagnosticsHandler?.Invoke(cosmosCacheSessionResponse.Diagnostics); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { + this.options.DiagnosticsHandler?.Invoke(ex.Diagnostics); return; } @@ -185,7 +194,7 @@ public void Refresh(string key) try { cosmosCacheSessionResponse.Resource.PartitionKeyAttribute = this.options.ContainerPartitionKeyAttribute; - await this.cosmosContainer.ReplaceItemAsync( + ItemResponse replaceCacheSessionResponse = await this.cosmosContainer.ReplaceItemAsync( partitionKey: new PartitionKey(key), id: key, item: cosmosCacheSessionResponse.Resource, @@ -195,10 +204,13 @@ await this.cosmosContainer.ReplaceItemAsync( EnableContentResponseOnWrite = false, }, cancellationToken: token).ConfigureAwait(false); + + this.options.DiagnosticsHandler?.Invoke(replaceCacheSessionResponse.Diagnostics); } catch (CosmosException cosmosException) when (cosmosException.StatusCode == HttpStatusCode.PreconditionFailed) { // Race condition on replace, we need do not need to refresh it + this.options.DiagnosticsHandler?.Invoke(cosmosException.Diagnostics); } } } @@ -221,10 +233,10 @@ public void Remove(string key) throw new ArgumentNullException(nameof(key)); } - await this.ConnectAsync().ConfigureAwait(false); + await this.ConnectAsync(token).ConfigureAwait(false); try { - await this.cosmosContainer.DeleteItemAsync( + ItemResponse deleteCacheSessionResponse = await this.cosmosContainer.DeleteItemAsync( partitionKey: new PartitionKey(key), id: key, requestOptions: new ItemRequestOptions() @@ -232,10 +244,13 @@ await this.cosmosContainer.DeleteItemAsync( EnableContentResponseOnWrite = false, }, cancellationToken: token).ConfigureAwait(false); + + this.options.DiagnosticsHandler?.Invoke(deleteCacheSessionResponse.Diagnostics); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { // do nothing + this.options.DiagnosticsHandler?.Invoke(ex.Diagnostics); } } @@ -267,9 +282,9 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options) throw new ArgumentNullException(nameof(options)); } - await this.ConnectAsync().ConfigureAwait(false); + await this.ConnectAsync(token).ConfigureAwait(false); - await this.cosmosContainer.UpsertItemAsync( + ItemResponse setCacheSessionResponse = await this.cosmosContainer.UpsertItemAsync( partitionKey: new PartitionKey(key), item: CosmosCache.BuildCosmosCacheSession( key, @@ -281,6 +296,8 @@ await this.cosmosContainer.UpsertItemAsync( EnableContentResponseOnWrite = false, }, cancellationToken: token).ConfigureAwait(false); + + this.options.DiagnosticsHandler?.Invoke(setCacheSessionResponse.Diagnostics); } private static CosmosCacheSession BuildCosmosCacheSession(string key, byte[] content, DistributedCacheEntryOptions options, CosmosCacheOptions cosmosCacheOptions) @@ -379,7 +396,8 @@ private async Task CosmosContainerInitializeAsync() this.cosmosClient = this.GetClientInstance(); if (this.options.CreateIfNotExists) { - await this.cosmosClient.CreateDatabaseIfNotExistsAsync(this.options.DatabaseName).ConfigureAwait(false); + DatabaseResponse databaseResponse = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(this.options.DatabaseName).ConfigureAwait(false); + this.options.DiagnosticsHandler?.Invoke(databaseResponse.Diagnostics); int defaultTimeToLive = this.options.DefaultTimeToLiveInMs.HasValue && this.options.DefaultTimeToLiveInMs.Value > 0 ? this.options.DefaultTimeToLiveInMs.Value : CosmosCache.DefaultTimeToLive; @@ -387,9 +405,12 @@ private async Task CosmosContainerInitializeAsync() try { ContainerResponse existingContainer = await this.cosmosClient.GetContainer(this.options.DatabaseName, this.options.ContainerName).ReadContainerAsync().ConfigureAwait(false); + this.options.DiagnosticsHandler?.Invoke(existingContainer.Diagnostics); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { + this.options.DiagnosticsHandler?.Invoke(ex.Diagnostics); + // Container is optimized as Key-Value store excluding all properties string partitionKeyDefinition = CosmosCache.ContainerPartitionKeyPath; if (!string.IsNullOrWhiteSpace(this.options.ContainerPartitionKeyAttribute)) @@ -397,7 +418,7 @@ private async Task CosmosContainerInitializeAsync() partitionKeyDefinition = $"/{this.options.ContainerPartitionKeyAttribute}"; } - await this.cosmosClient.GetDatabase(this.options.DatabaseName).DefineContainer(this.options.ContainerName, partitionKeyDefinition) + ContainerResponse newContainer = await this.cosmosClient.GetDatabase(this.options.DatabaseName).DefineContainer(this.options.ContainerName, partitionKeyDefinition) .WithDefaultTimeToLive(defaultTimeToLive) .WithIndexingPolicy() .WithIndexingMode(IndexingMode.Consistent) @@ -408,6 +429,7 @@ await this.cosmosClient.GetDatabase(this.options.DatabaseName).DefineContainer(t .Attach() .Attach() .CreateAsync(this.options.ContainerThroughput).ConfigureAwait(false); + this.options.DiagnosticsHandler?.Invoke(newContainer.Diagnostics); } } else @@ -415,9 +437,11 @@ await this.cosmosClient.GetDatabase(this.options.DatabaseName).DefineContainer(t try { ContainerResponse existingContainer = await this.cosmosClient.GetContainer(this.options.DatabaseName, this.options.ContainerName).ReadContainerAsync().ConfigureAwait(false); + this.options.DiagnosticsHandler?.Invoke(existingContainer.Diagnostics); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { + this.options.DiagnosticsHandler?.Invoke(ex.Diagnostics); throw new InvalidOperationException($"Cannot find an existing container named {this.options.ContainerName} within database {this.options.DatabaseName}"); } } diff --git a/src/CosmosCacheOptions.cs b/src/CosmosCacheOptions.cs index 10c1541..3ef1887 100644 --- a/src/CosmosCacheOptions.cs +++ b/src/CosmosCacheOptions.cs @@ -13,6 +13,30 @@ namespace Microsoft.Extensions.Caching.Cosmos /// public class CosmosCacheOptions : IOptions { + /// + /// Delegate to receive Diagnostics from the internal Cosmos DB operations. + /// + /// An instance of result from a Cosmos DB service operation. + /// + /// + /// SomePredefinedThresholdTime) + /// { + /// Console.WriteLine(diagnostics.ToString()); + /// } + /// } + /// + /// CosmosCacheOptions options = new CosmosCacheOptions(){ + /// /* Other options */, + /// DiagnosticsHandler = captureDiagnostics + /// }; + /// ]]> + /// + /// + public delegate void DiagnosticsDelegate(CosmosDiagnostics diagnostics); + /// /// Gets or sets an instance of to build a Cosmos Client with. Either use this or provide an existing . /// @@ -68,6 +92,15 @@ public class CosmosCacheOptions : IOptions /// Default value is true. public bool RetrySlidingExpirationUpdates { get; set; } = true; + /// + /// Gets or sets a delegate to capture operation diagnostics. + /// + /// + /// This delegate captures the from the operations performed on the Cosmos DB service. + /// Once set, it will be called for all executed operations and can be used for conditionally capturing diagnostics. + /// + public DiagnosticsDelegate DiagnosticsHandler { get; set; } + /// /// Gets the current options values. /// diff --git a/tests/emulator/CosmosCacheEmulatorTests.cs b/tests/emulator/CosmosCacheEmulatorTests.cs index 5871d23..52de51d 100644 --- a/tests/emulator/CosmosCacheEmulatorTests.cs +++ b/tests/emulator/CosmosCacheEmulatorTests.cs @@ -5,6 +5,7 @@ namespace Microsoft.Extensions.Caching.Cosmos.EmulatorTests { using System; + using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -34,6 +35,8 @@ public void Dispose() [Fact] public async Task InitializeContainerIfNotExists() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + const string sessionId = "sessionId"; const int ttl = 1400; const int throughput = 2000; @@ -46,7 +49,8 @@ public async Task InitializeContainerIfNotExists() ContainerThroughput = throughput, CreateIfNotExists = true, ClientBuilder = builder, - DefaultTimeToLiveInMs = ttl + DefaultTimeToLiveInMs = ttl, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics }); CosmosCache cache = new CosmosCache(options); @@ -64,6 +68,12 @@ public async Task InitializeContainerIfNotExists() int? throughputContainer = await this.testClient.GetContainer(CosmosCacheEmulatorTests.databaseName, "session").ReadThroughputAsync(); Assert.Equal(throughput, throughputContainer); + + Assert.Equal(4, diagnosticsSink.CapturedDiagnostics.Count); + foreach (CosmosDiagnostics diagnostics in diagnosticsSink.CapturedDiagnostics) + { + Assert.NotNull(diagnostics?.ToString()); + } } [Fact] @@ -173,6 +183,8 @@ public async Task StoreSessionData_CustomPartitionKey() [Fact] public async Task GetSessionData() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + const string sessionId = "sessionId"; const int ttl = 1400; const int throughput = 2000; @@ -185,7 +197,8 @@ public async Task GetSessionData() DatabaseName = CosmosCacheEmulatorTests.databaseName, ContainerThroughput = throughput, CreateIfNotExists = true, - ClientBuilder = builder + ClientBuilder = builder, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics }); CosmosCache cache = new CosmosCache(options); @@ -194,11 +207,19 @@ public async Task GetSessionData() await cache.SetAsync(sessionId, data, cacheOptions); Assert.Equal(data, await cache.GetAsync(sessionId)); + + Assert.Equal(6, diagnosticsSink.CapturedDiagnostics.Count); + foreach (CosmosDiagnostics diagnostics in diagnosticsSink.CapturedDiagnostics) + { + Assert.NotNull(diagnostics?.ToString()); + } } [Fact] public async Task RemoveSessionData() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + const string sessionId = "sessionId"; const int ttl = 1400; const int throughput = 2000; @@ -211,7 +232,8 @@ public async Task RemoveSessionData() DatabaseName = CosmosCacheEmulatorTests.databaseName, ContainerThroughput = throughput, CreateIfNotExists = true, - ClientBuilder = builder + ClientBuilder = builder, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics }); CosmosCache cache = new CosmosCache(options); @@ -223,6 +245,12 @@ public async Task RemoveSessionData() CosmosException exception = await Assert.ThrowsAsync(() => this.testClient.GetContainer(CosmosCacheEmulatorTests.databaseName, "session").ReadItemAsync(sessionId, new PartitionKey(sessionId))); Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + + Assert.Equal(5, diagnosticsSink.CapturedDiagnostics.Count); + foreach (CosmosDiagnostics diagnostics in diagnosticsSink.CapturedDiagnostics) + { + Assert.NotNull(diagnostics?.ToString()); + } } [Fact] @@ -287,6 +315,8 @@ public async Task RemoveSessionData_CustomPartitionKey() [Fact] public async Task RemoveSessionData_WhenNotExists() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + const string sessionId = "sessionId"; const int ttl = 1400; const int throughput = 2000; @@ -298,18 +328,27 @@ public async Task RemoveSessionData_WhenNotExists() DatabaseName = CosmosCacheEmulatorTests.databaseName, ContainerThroughput = throughput, CreateIfNotExists = true, - ClientBuilder = builder + ClientBuilder = builder, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics }); CosmosCache cache = new CosmosCache(options); DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions(); cacheOptions.SlidingExpiration = TimeSpan.FromSeconds(ttl); await cache.RemoveAsync(sessionId); + + Assert.Equal(4, diagnosticsSink.CapturedDiagnostics.Count); + foreach (CosmosDiagnostics diagnostics in diagnosticsSink.CapturedDiagnostics) + { + Assert.NotNull(diagnostics?.ToString()); + } } [Fact] public async Task GetSessionData_WhenNotExists() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + const string sessionId = "sessionId"; const int ttl = 1400; const int throughput = 2000; @@ -321,13 +360,20 @@ public async Task GetSessionData_WhenNotExists() DatabaseName = CosmosCacheEmulatorTests.databaseName, ContainerThroughput = throughput, CreateIfNotExists = true, - ClientBuilder = builder + ClientBuilder = builder, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics }); CosmosCache cache = new CosmosCache(options); DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions(); cacheOptions.SlidingExpiration = TimeSpan.FromSeconds(ttl); Assert.Null(await cache.GetAsync(sessionId)); + + Assert.Equal(4, diagnosticsSink.CapturedDiagnostics.Count); + foreach (CosmosDiagnostics diagnostics in diagnosticsSink.CapturedDiagnostics) + { + Assert.NotNull(diagnostics?.ToString()); + } } [Fact] @@ -431,5 +477,17 @@ private class CosmosCacheSession [JsonProperty("ttl")] public long? TimeToLive { get; set; } } + + private class DiagnosticsSink + { + private List capturedDiagnostics = new List(); + + public IReadOnlyList CapturedDiagnostics => this.capturedDiagnostics.AsReadOnly(); + + public void CaptureDiagnostics(CosmosDiagnostics diagnostics) + { + this.capturedDiagnostics.Add(diagnostics); + } + } } } \ No newline at end of file diff --git a/tests/unit/CosmosCacheTests.cs b/tests/unit/CosmosCacheTests.cs index 206710c..3e2efe4 100644 --- a/tests/unit/CosmosCacheTests.cs +++ b/tests/unit/CosmosCacheTests.cs @@ -5,6 +5,7 @@ namespace Microsoft.Extensions.Caching.Cosmos.Tests { using System; + using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -14,7 +15,6 @@ namespace Microsoft.Extensions.Caching.Cosmos.Tests using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; using Moq; - using Newtonsoft.Json; using Xunit; public class CosmosCacheTests @@ -52,47 +52,64 @@ public void RequiredParameters() [Fact] public async Task ConnectAsyncThrowsIfContainerDoesNotExist() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); - mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ThrowsAsync(new CosmosException("test", HttpStatusCode.NotFound, 0, "", 0)); + CosmosException exception = new CosmosException("test", HttpStatusCode.NotFound, 0, "", 0); + mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ThrowsAsync(exception); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); mockedClient.Setup(x => x.Endpoint).Returns(new Uri("http://localhost")); CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", - CosmosClient = mockedClient.Object + CosmosClient = mockedClient.Object, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); await Assert.ThrowsAsync(() => cache.GetAsync("key")); + Assert.Equal(1, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Equal(exception.Diagnostics.ToString(), diagnosticsSink.CapturedDiagnostics[0].ToString()); } [Fact] public async Task GetObtainsSessionAndUpdatesCacheForSlidingExpiration() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + string etag = "etag"; CosmosCacheSession existingSession = new CosmosCacheSession(); existingSession.SessionKey = "key"; existingSession.Content = new byte[0]; existingSession.IsSlidingExpiration = true; Mock> mockedItemResponse = new Mock>(); + Mock mockedItemDiagnostics = new Mock(); Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); Mock mockedDatabase = new Mock(); Mock mockedResponse = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); + Mock mockedDatabaseResponse = new Mock(); + Mock mockedDatabaseDiagnostics = new Mock(); mockedItemResponse.Setup(c => c.Resource).Returns(existingSession); - mockedItemResponse.Setup(c => c.ETag).Returns(etag); + mockedItemResponse.Setup(c => c.ETag).Returns(etag); + mockedItemResponse.Setup(c => c.Diagnostics).Returns(mockedItemDiagnostics.Object); mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); mockedContainer.Setup(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); mockedContainer.Setup(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); + mockedDatabaseResponse.Setup(c => c.Diagnostics).Returns(mockedDatabaseDiagnostics.Object); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); mockedClient.Setup(c => c.GetDatabase(It.IsAny())).Returns(mockedDatabase.Object); mockedClient.Setup(x => x.Endpoint).Returns(new Uri("http://localhost")); + mockedClient.Setup(x => x.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedDatabaseResponse.Object); CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", CreateIfNotExists = true, - CosmosClient = mockedClient.Object + CosmosClient = mockedClient.Object, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); Assert.Same(existingSession.Content, await cache.GetAsync("key")); @@ -100,6 +117,12 @@ public async Task GetObtainsSessionAndUpdatesCacheForSlidingExpiration() mockedClient.Verify(c => c.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + Assert.Equal(4, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedDatabaseDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[1]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[2]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[3]); } [Fact] @@ -185,31 +208,42 @@ public async Task GetObtainsSessionAndUpdatesCacheForSlidingExpirationWithAbsolu [Fact] public async Task GetObtainsSessionAndDoesNotUpdatesCacheForAbsoluteExpiration() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + string etag = "etag"; CosmosCacheSession existingSession = new CosmosCacheSession(); existingSession.SessionKey = "key"; existingSession.Content = new byte[0]; existingSession.IsSlidingExpiration = false; Mock> mockedItemResponse = new Mock>(); + Mock mockedItemDiagnostics = new Mock(); Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); Mock mockedDatabase = new Mock(); Mock mockedResponse = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); + Mock mockedDatabaseResponse = new Mock(); + Mock mockedDatabaseDiagnostics = new Mock(); mockedItemResponse.Setup(c => c.Resource).Returns(existingSession); mockedItemResponse.Setup(c => c.ETag).Returns(etag); + mockedItemResponse.Setup(c => c.Diagnostics).Returns(mockedItemDiagnostics.Object); mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); mockedContainer.Setup(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); mockedContainer.Setup(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); + mockedDatabaseResponse.Setup(c => c.Diagnostics).Returns(mockedDatabaseDiagnostics.Object); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); mockedClient.Setup(c => c.GetDatabase(It.IsAny())).Returns(mockedDatabase.Object); mockedClient.Setup(x => x.Endpoint).Returns(new Uri("http://localhost")); + mockedClient.Setup(x => x.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedDatabaseResponse.Object); CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions() { DatabaseName = "something", ContainerName = "something", CreateIfNotExists = true, - CosmosClient = mockedClient.Object + CosmosClient = mockedClient.Object, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); Assert.Same(existingSession.Content, await cache.GetAsync("key")); @@ -217,36 +251,53 @@ public async Task GetObtainsSessionAndDoesNotUpdatesCacheForAbsoluteExpiration() mockedClient.Verify(c => c.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + Assert.Equal(3, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedDatabaseDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[1]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[2]); } [Fact] public async Task GetDoesNotRetryUpdateIfRetrySlidingExpirationUpdatesIsFalse() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + string etag = "etag"; CosmosCacheSession existingSession = new CosmosCacheSession(); existingSession.SessionKey = "key"; existingSession.Content = new byte[0]; existingSession.IsSlidingExpiration = true; Mock> mockedItemResponse = new Mock>(); + Mock mockedItemDiagnostics = new Mock(); Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); Mock mockedDatabase = new Mock(); Mock mockedResponse = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); + Mock mockedDatabaseResponse = new Mock(); + Mock mockedDatabaseDiagnostics = new Mock(); mockedItemResponse.Setup(c => c.Resource).Returns(existingSession); mockedItemResponse.Setup(c => c.ETag).Returns(etag); + mockedItemResponse.Setup(c => c.Diagnostics).Returns(mockedItemDiagnostics.Object); mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); mockedContainer.Setup(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); - mockedContainer.Setup(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(new CosmosException("test", HttpStatusCode.PreconditionFailed, 0, "", 0));; + CosmosException preconditionFailedException = new CosmosException("test", HttpStatusCode.PreconditionFailed, 0, "", 0); + mockedContainer.Setup(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(preconditionFailedException); + mockedDatabaseResponse.Setup(c => c.Diagnostics).Returns(mockedDatabaseDiagnostics.Object); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); mockedClient.Setup(c => c.GetDatabase(It.IsAny())).Returns(mockedDatabase.Object); mockedClient.Setup(x => x.Endpoint).Returns(new Uri("http://localhost")); + mockedClient.Setup(x => x.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedDatabaseResponse.Object); CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", CreateIfNotExists = true, CosmosClient = mockedClient.Object, - RetrySlidingExpirationUpdates = false + RetrySlidingExpirationUpdates = false, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); Assert.Same(existingSession.Content, await cache.GetAsync("key")); @@ -254,38 +305,56 @@ public async Task GetDoesNotRetryUpdateIfRetrySlidingExpirationUpdatesIsFalse() mockedClient.Verify(c => c.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + Assert.Equal(4, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedDatabaseDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[1]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[2]); + Assert.Equal(preconditionFailedException.Diagnostics.ToString(), diagnosticsSink.CapturedDiagnostics[3].ToString()); } [Fact] public async Task GetDoesRetryUpdateIfRetrySlidingExpirationUpdatesIsTrue() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + string etag = "etag"; CosmosCacheSession existingSession = new CosmosCacheSession(); existingSession.SessionKey = "key"; existingSession.Content = new byte[0]; existingSession.IsSlidingExpiration = true; Mock> mockedItemResponse = new Mock>(); + Mock mockedItemDiagnostics = new Mock(); Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); Mock mockedDatabase = new Mock(); Mock mockedResponse = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); + Mock mockedDatabaseResponse = new Mock(); + Mock mockedDatabaseDiagnostics = new Mock(); mockedItemResponse.Setup(c => c.Resource).Returns(existingSession); mockedItemResponse.Setup(c => c.ETag).Returns(etag); + mockedItemResponse.Setup(c => c.Diagnostics).Returns(mockedItemDiagnostics.Object); mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); mockedContainer.Setup(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); + CosmosException preconditionException = new CosmosException("test", HttpStatusCode.PreconditionFailed, 0, "", 0); mockedContainer.SetupSequence(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new CosmosException("test", HttpStatusCode.PreconditionFailed, 0, "", 0)) + .ThrowsAsync(preconditionException) .ReturnsAsync(mockedItemResponse.Object); + mockedDatabaseResponse.Setup(c => c.Diagnostics).Returns(mockedDatabaseDiagnostics.Object); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); mockedClient.Setup(c => c.GetDatabase(It.IsAny())).Returns(mockedDatabase.Object); mockedClient.Setup(x => x.Endpoint).Returns(new Uri("http://localhost")); + mockedClient.Setup(x => x.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedDatabaseResponse.Object); CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", CreateIfNotExists = true, CosmosClient = mockedClient.Object, - RetrySlidingExpirationUpdates = true + RetrySlidingExpirationUpdates = true, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); Assert.Same(existingSession.Content, await cache.GetAsync("key")); @@ -293,18 +362,31 @@ public async Task GetDoesRetryUpdateIfRetrySlidingExpirationUpdatesIsTrue() mockedClient.Verify(c => c.CreateDatabaseIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); mockedContainer.Verify(c => c.ReplaceItemAsync(It.Is(item => item == existingSession), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + + Assert.Equal(6, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedDatabaseDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[1]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[2]); + Assert.Equal(preconditionException.Diagnostics.ToString(), diagnosticsSink.CapturedDiagnostics[3].ToString()); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[4]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[5]); } [Fact] public async Task GetReturnsNullIfKeyDoesNotExist() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); Mock mockedDatabase = new Mock(); Mock mockedResponse = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); - mockedContainer.Setup(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(new CosmosException("test", HttpStatusCode.NotFound, 0, "", 0)); + CosmosException notFoundException = new CosmosException("test", HttpStatusCode.NotFound, 0, "", 0); + mockedContainer.Setup(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(notFoundException); mockedContainer.Setup(c => c.ReplaceItemAsync(It.IsAny(), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(new CosmosException("test", HttpStatusCode.NotFound, 0, "", 0)); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); mockedClient.Setup(c => c.GetDatabase(It.IsAny())).Returns(mockedDatabase.Object); @@ -312,23 +394,34 @@ public async Task GetReturnsNullIfKeyDoesNotExist() CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", - CosmosClient = mockedClient.Object + CosmosClient = mockedClient.Object, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); Assert.Null(await cache.GetAsync("key")); mockedContainer.Verify(c => c.ReadItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedContainer.Verify(c => c.ReplaceItemAsync(It.IsAny(), It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + Assert.Equal(2, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Equal(notFoundException.Diagnostics.ToString(), diagnosticsSink.CapturedDiagnostics[1].ToString()); } [Fact] public async Task RemoveAsyncDeletesItem() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + Mock> mockedItemResponse = new Mock>(); + Mock mockedItemDiagnostics = new Mock(); Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); Mock mockedDatabase = new Mock(); Mock mockedResponse = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); + mockedItemResponse.Setup(c => c.Diagnostics).Returns(mockedItemDiagnostics.Object); mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); mockedContainer.Setup(c => c.DeleteItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); @@ -337,37 +430,57 @@ public async Task RemoveAsyncDeletesItem() CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", - CosmosClient = mockedClient.Object + CosmosClient = mockedClient.Object, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); await cache.RemoveAsync("key"); mockedContainer.Verify(c => c.DeleteItemAsync(It.Is(id => id == "key"), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + Assert.Equal(2, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[1]); } [Fact] public async Task RemoveAsyncNotExistItem() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + var mockedClient = new Mock(); var mockedContainer = new Mock(); + Mock mockedResponse = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); + mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); + mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); + CosmosException notExistException = new CosmosException("test remove not exist", HttpStatusCode.NotFound, 0, "", 0); mockedContainer.Setup(c => c.DeleteItemAsync("not-exist-key", It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new CosmosException("test remove not exist", HttpStatusCode.NotFound, 0, "", 0)) + .ThrowsAsync(notExistException) .Verifiable(); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object).Verifiable(); CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", - CosmosClient = mockedClient.Object + CosmosClient = mockedClient.Object, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); - await cache.RemoveAsync("not-exist-key"); - mockedContainer.Verify(c => c.DeleteItemAsync("not-exist-key", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + await cache.RemoveAsync("not-exist-key"); + mockedContainer.Verify(c => c.DeleteItemAsync("not-exist-key", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); mockedClient.VerifyAll(); mockedContainer.VerifyAll(); + + Assert.Equal(2, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Equal(notExistException.Diagnostics.ToString(), diagnosticsSink.CapturedDiagnostics[1].ToString()); } [Fact] public async Task SetAsyncCallsUpsert() { + DiagnosticsSink diagnosticsSink = new DiagnosticsSink(); + int ttl = 10; DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions(); cacheOptions.SlidingExpiration = TimeSpan.FromSeconds(ttl); @@ -376,11 +489,15 @@ public async Task SetAsyncCallsUpsert() existingSession.SessionKey = "key"; existingSession.Content = new byte[0]; Mock> mockedItemResponse = new Mock>(); + Mock mockedItemDiagnostics = new Mock(); Mock mockedClient = new Mock(); Mock mockedContainer = new Mock(); + Mock mockedContainerDiagnostics = new Mock(); Mock mockedDatabase = new Mock(); Mock mockedResponse = new Mock(); mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK); + mockedResponse.Setup(c => c.Diagnostics).Returns(mockedContainerDiagnostics.Object); + mockedItemResponse.Setup(c => c.Diagnostics).Returns(mockedItemDiagnostics.Object); mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny(), It.IsAny())).ReturnsAsync(mockedResponse.Object); mockedContainer.Setup(c => c.UpsertItemAsync(It.Is(item => item.SessionKey == existingSession.SessionKey && item.TimeToLive == ttl), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(mockedItemResponse.Object); mockedClient.Setup(c => c.GetContainer(It.IsAny(), It.IsAny())).Returns(mockedContainer.Object); @@ -389,11 +506,16 @@ public async Task SetAsyncCallsUpsert() CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions(){ DatabaseName = "something", ContainerName = "something", - CosmosClient = mockedClient.Object + CosmosClient = mockedClient.Object, + DiagnosticsHandler = diagnosticsSink.CaptureDiagnostics })); await cache.SetAsync(existingSession.SessionKey, existingSession.Content, cacheOptions); mockedContainer.Verify(c => c.UpsertItemAsync(It.Is(item => item.SessionKey == existingSession.SessionKey && item.TimeToLive == ttl), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + Assert.Equal(2, diagnosticsSink.CapturedDiagnostics.Count); + Assert.Same(mockedContainerDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[0]); + Assert.Same(mockedItemDiagnostics.Object, diagnosticsSink.CapturedDiagnostics[1]); } [Fact] @@ -453,5 +575,17 @@ public async Task ValidatesNoExpirationUsesNullTtl() await cache.SetAsync(existingSession.SessionKey, existingSession.Content, cacheOptions); mockedContainer.Verify(c => c.UpsertItemAsync(It.Is(item => item.SessionKey == existingSession.SessionKey && item.TimeToLive == ttl), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } + + private class DiagnosticsSink + { + private List capturedDiagnostics = new List(); + + public IReadOnlyList CapturedDiagnostics => this.capturedDiagnostics.AsReadOnly(); + + public void CaptureDiagnostics(CosmosDiagnostics diagnostics) + { + this.capturedDiagnostics.Add(diagnostics); + } + } } } \ No newline at end of file