diff --git a/samples/Misc/RemoteAppendExample.cs b/samples/Misc/RemoteAppendExample.cs index ec53c267f..2ca5f470e 100644 --- a/samples/Misc/RemoteAppendExample.cs +++ b/samples/Misc/RemoteAppendExample.cs @@ -58,6 +58,7 @@ public Task> ListAsync(int maxResults, DateTime? start = null, public Task SaveAsync(MiniProfiler profiler) => Wrapped.SaveAsync(profiler); public Task SetUnviewedAsync(string user, Guid id) => Wrapped.SetUnviewedAsync(user, id); public Task SetViewedAsync(string user, Guid id) => Wrapped.SetViewedAsync(user, id); + public Task SetViewedAsync(string user, IEnumerable ids) => Wrapped.SetViewedAsync(user, ids); public Task> GetUnviewedIdsAsync(string user) => Wrapped.GetUnviewedIdsAsync(user); /// diff --git a/src/MiniProfiler.AspNetCore/Storage/MemoryCacheStorage.cs b/src/MiniProfiler.AspNetCore/Storage/MemoryCacheStorage.cs index edf8c37e3..ed547a512 100644 --- a/src/MiniProfiler.AspNetCore/Storage/MemoryCacheStorage.cs +++ b/src/MiniProfiler.AspNetCore/Storage/MemoryCacheStorage.cs @@ -249,5 +249,18 @@ public Task SetViewedAsync(string user, Guid id) SetViewed(user, id); return Task.CompletedTask; } + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public async Task SetViewedAsync(string user, IEnumerable ids) + { + foreach (var id in ids) + { + await this.SetViewedAsync(user, id).ConfigureAwait(false); + } + } } } diff --git a/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs b/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs index a75647d46..2057d1b48 100644 --- a/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs +++ b/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs @@ -252,6 +252,19 @@ public async Task SetViewedAsync(string user, Guid id) var set = Builders.Update.Set(profiler => profiler.HasUserViewed, true); await _collection.UpdateOneAsync(p => p.Id == id, set).ConfigureAwait(false); } + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public async Task SetViewedAsync(string user, IEnumerable ids) + { + foreach (var id in ids) + { + await this.SetViewedAsync(user, id).ConfigureAwait(false); + } + } /// /// Returns the underlying client. diff --git a/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs b/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs index d3b2bbd1d..330635773 100644 --- a/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs +++ b/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs @@ -268,28 +268,40 @@ public override async Task LoadAsync(Guid id) /// The user to set this profiler ID as viewed for. /// The profiler ID to set viewed. public override Task SetViewedAsync(string user, Guid id) => ToggleViewedAsync(user, id, true); + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public override Task SetViewedAsync(string user, IEnumerable ids) => ToggleViewedAsync(user, ids, true); private string _toggleViewedSql; private string ToggleViewedSql => _toggleViewedSql ??= $@" Update {MiniProfilersTable} - Set HasUserViewed = @hasUserVeiwed - Where Id = @id + Set HasUserViewed = @hasUserViewed + Where Id = ANY(@ids) And ""User"" = @user"; - private void ToggleViewed(string user, Guid id, bool hasUserVeiwed) + private void ToggleViewed(string user, Guid id, bool hasUserViewed) { using (var conn = GetConnection()) { - conn.Execute(ToggleViewedSql, new { id, user, hasUserVeiwed }); + conn.Execute(ToggleViewedSql, new { ids = new [] { id }, user, hasUserViewed }); } } - private async Task ToggleViewedAsync(string user, Guid id, bool hasUserVeiwed) + private Task ToggleViewedAsync(string user, Guid id, bool hasUserViewed) + { + return ToggleViewedAsync(user, new [] { id }, hasUserViewed); + } + + private async Task ToggleViewedAsync(string user, IEnumerable ids, bool hasUserViewed) { using (var conn = GetConnection()) { - await conn.ExecuteAsync(ToggleViewedSql, new { id, user, hasUserVeiwed }).ConfigureAwait(false); + await conn.ExecuteAsync(ToggleViewedSql, new { ids = ids.ToArray(), user, hasUserViewed }).ConfigureAwait(false); } } diff --git a/src/MiniProfiler.Providers.RavenDB/RavenDbStorage.cs b/src/MiniProfiler.Providers.RavenDB/RavenDbStorage.cs index 989862251..3d5dcfaa3 100644 --- a/src/MiniProfiler.Providers.RavenDB/RavenDbStorage.cs +++ b/src/MiniProfiler.Providers.RavenDB/RavenDbStorage.cs @@ -308,6 +308,19 @@ public async Task SetViewedAsync(string user, Guid id) profile.HasUserViewed = true; await session.SaveChangesAsync().ConfigureAwait(false); } + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public async Task SetViewedAsync(string user, IEnumerable ids) + { + foreach (var id in ids) + { + await this.SetViewedAsync(user, id).ConfigureAwait(false); + } + } /// /// Asynchronously returns a list of s that haven't been seen by . diff --git a/src/MiniProfiler.Providers.Redis/RedisStorage.cs b/src/MiniProfiler.Providers.Redis/RedisStorage.cs index 93509fae5..e859744f8 100644 --- a/src/MiniProfiler.Providers.Redis/RedisStorage.cs +++ b/src/MiniProfiler.Providers.Redis/RedisStorage.cs @@ -261,6 +261,19 @@ public Task SetViewedAsync(string user, Guid id) RedisValue value = id.ToString(); return _database.SetRemoveAsync(key, value); } + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public async Task SetViewedAsync(string user, IEnumerable ids) + { + foreach (var id in ids) + { + await this.SetViewedAsync(user, id).ConfigureAwait(false); + } + } /// /// Asynchronously returns a list of s that haven't been seen by . diff --git a/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs b/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs index 1b2dabddd..8c0540333 100644 --- a/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs +++ b/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace StackExchange.Profiling.Internal @@ -23,7 +24,7 @@ public static List ExpireAndGetUnviewed(this MiniProfilerBaseOptions optio { for (var i = 0; i < ids.Count - options.MaxUnviewedProfiles; i++) { - options.Storage.SetViewedAsync(user, ids[i]); + options.Storage.SetViewed(user, ids[i]); } } return ids; @@ -45,10 +46,8 @@ public static async Task> ExpireAndGetUnviewedAsync(this MiniProfiler var ids = await options.Storage.GetUnviewedIdsAsync(user).ConfigureAwait(false); if (ids?.Count > options.MaxUnviewedProfiles) { - for (var i = 0; i < ids.Count - options.MaxUnviewedProfiles; i++) - { - await options.Storage.SetViewedAsync(user, ids[i]).ConfigureAwait(false); - } + var idsToSetViewed = ids.Take(ids.Count - options.MaxUnviewedProfiles); + await options.Storage.SetViewedAsync(user, idsToSetViewed).ConfigureAwait(false); } return ids; } diff --git a/src/MiniProfiler.Shared/Storage/DatabaseStorageBase.cs b/src/MiniProfiler.Shared/Storage/DatabaseStorageBase.cs index 2682d489d..e5d35a9ae 100644 --- a/src/MiniProfiler.Shared/Storage/DatabaseStorageBase.cs +++ b/src/MiniProfiler.Shared/Storage/DatabaseStorageBase.cs @@ -121,6 +121,19 @@ protected DatabaseStorageBase(string connectionString, string profilersTable, st /// The profiler ID to set viewed. public abstract Task SetViewedAsync(string user, Guid id); + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public virtual async Task SetViewedAsync(string user, IEnumerable ids) + { + foreach (var id in ids) + { + await this.SetViewedAsync(user, id).ConfigureAwait(false); + } + } + /// /// Returns a list of s that haven't been seen by . /// diff --git a/src/MiniProfiler.Shared/Storage/IAsyncStorage.cs b/src/MiniProfiler.Shared/Storage/IAsyncStorage.cs index 0b02925d5..594dcbe4b 100644 --- a/src/MiniProfiler.Shared/Storage/IAsyncStorage.cs +++ b/src/MiniProfiler.Shared/Storage/IAsyncStorage.cs @@ -126,6 +126,13 @@ Task> ListAsync( /// The user to set this profiler ID as viewed for. /// The profiler ID to set viewed. Task SetViewedAsync(string user, Guid id); + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + Task SetViewedAsync(string user, IEnumerable ids); /// /// Asynchronously returns a list of s that haven't been seen by . diff --git a/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs b/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs index ebbb4a33a..cd3961706 100644 --- a/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs +++ b/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs @@ -243,6 +243,20 @@ public Task SetViewedAsync(string user, Guid id) return Task.WhenAll(Stores.Select(s => s.SetViewedAsync(user, id))); } + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public Task SetViewedAsync(string user, IEnumerable ids) + { + if (Stores == null) return Task.CompletedTask; + + var idsArray = ids.ToArray(); // Prevent multiple enumerations of ids. + + return Task.WhenAll(Stores.Select(s => s.SetViewedAsync(user, idsArray))); + } /// /// Runs on each object in and returns the Union of results. diff --git a/src/MiniProfiler.Shared/Storage/NullStorage.cs b/src/MiniProfiler.Shared/Storage/NullStorage.cs index 0e6bafccf..f2c600d1d 100644 --- a/src/MiniProfiler.Shared/Storage/NullStorage.cs +++ b/src/MiniProfiler.Shared/Storage/NullStorage.cs @@ -88,6 +88,13 @@ public void SetViewed(string user, Guid id) { /* no-op */ } /// No one cares. public Task SetViewedAsync(string user, Guid id) => Task.CompletedTask; + /// + /// Sets nothing. + /// + /// No one cares. + /// No one cares. + public Task SetViewedAsync(string user, IEnumerable ids) => Task.CompletedTask; + /// /// Gets nothing. /// diff --git a/src/MiniProfiler/Storage/MemoryCacheStorage.cs b/src/MiniProfiler/Storage/MemoryCacheStorage.cs index a572ee191..c7baf12b8 100644 --- a/src/MiniProfiler/Storage/MemoryCacheStorage.cs +++ b/src/MiniProfiler/Storage/MemoryCacheStorage.cs @@ -243,5 +243,18 @@ public Task SetViewedAsync(string user, Guid id) SetViewed(user, id); return Task.CompletedTask; } + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public async Task SetViewedAsync(string user, IEnumerable ids) + { + foreach (var id in ids) + { + await this.SetViewedAsync(user, id).ConfigureAwait(false); + } + } } } diff --git a/tests/MiniProfiler.Tests/InternalErrorTests.cs b/tests/MiniProfiler.Tests/InternalErrorTests.cs index e7a02e1a1..979ae1fd1 100644 --- a/tests/MiniProfiler.Tests/InternalErrorTests.cs +++ b/tests/MiniProfiler.Tests/InternalErrorTests.cs @@ -60,6 +60,7 @@ public class KaboomStorage : IAsyncStorage public Task SetUnviewedAsync(string user, Guid id) => throw new BoomBoom(); public void SetViewed(string user, Guid id) => throw new BoomBoom(); public Task SetViewedAsync(string user, Guid id) => throw new BoomBoom(); + public Task SetViewedAsync(string user, IEnumerable ids) => throw new BoomBoom(); public class BoomBoom : Exception { } } diff --git a/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs b/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs index 758840730..e386f6f21 100644 --- a/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs +++ b/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Dapper; +using StackExchange.Profiling.Internal; using StackExchange.Profiling.Storage; using Xunit; using Xunit.Abstractions; @@ -146,6 +149,35 @@ public async Task SetViewedAsync() var unviewedIds2 = await Storage.GetUnviewedIdsAsync(mp.User).ConfigureAwait(false); Assert.DoesNotContain(mp.Id, unviewedIds2); } + + [Fact] + public async Task ExpireAndGetUnviewedAsync() + { + Options.Storage = Storage; + var user = "TestUser"; + var mps = Enumerable.Range(0, 500) + .Select(i => GetMiniProfiler(user: user)) + .ToList(); + + foreach (var mp in mps) + { + Assert.False(mp.HasUserViewed); + await Storage.SaveAsync(mp).ConfigureAwait(false); + Assert.False(mp.HasUserViewed); + } + + var unviewedIds = await Storage.GetUnviewedIdsAsync(user).ConfigureAwait(false); + Assert.All(mps, mp => Assert.Contains(mp.Id, unviewedIds)); + + var sw = Stopwatch.StartNew(); + await Options.ExpireAndGetUnviewedAsync(user); + sw.Stop(); + Output.WriteLine($"{nameof(MiniProfilerBaseOptionsExtensions.ExpireAndGetUnviewedAsync)} completed in {sw.ElapsedMilliseconds}ms"); + + var unviewedIds2 = await Storage.GetUnviewedIdsAsync(user).ConfigureAwait(false); + Assert.InRange(unviewedIds2.Count, 0, Options.MaxUnviewedProfiles); + Assert.Subset(new HashSet(unviewedIds), new HashSet(unviewedIds2)); + } [Fact] public void SetUnviewed()