diff --git a/appveyor.yml b/appveyor.yml index c9e2fdd3..73805dd0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -37,7 +37,6 @@ services: - mssql2019 - mysql - postgresql - - mongodb nuget: disable_publish_on_pr: true diff --git a/docs/Releases.md b/docs/Releases.md index 520668fe..cd85345e 100644 --- a/docs/Releases.md +++ b/docs/Releases.md @@ -6,7 +6,8 @@ layout: "default" This page tracks major changes included in any update starting with version 4.0.0.3 #### Unreleased -No pending unreleased changes. +- **Fixes/Changes**: + - Upgraded MongoDB driver, allowing automatic index creation and profiler expiration ([#613](https://github.com/MiniProfiler/dotnet/pull/613) - thanks [IanKemp](https://github.com/IanKemp)) #### Version 4.3.8 - **New**: diff --git a/src/MiniProfiler.Providers.MongoDB/MiniProfiler.Providers.MongoDB.csproj b/src/MiniProfiler.Providers.MongoDB/MiniProfiler.Providers.MongoDB.csproj index dc9c9808..0d949959 100644 --- a/src/MiniProfiler.Providers.MongoDB/MiniProfiler.Providers.MongoDB.csproj +++ b/src/MiniProfiler.Providers.MongoDB/MiniProfiler.Providers.MongoDB.csproj @@ -3,7 +3,7 @@ MiniProfiler.Providers.MongoDB MiniProfiler.Providers.MongoDB MiniProfiler: Profiler storage for MongoDB - Nick Craver, Roger Calaf + Nick Craver, Roger Calaf, Ian Kemp NoSQL;MongoDB;$(PackageBaseTags) net461;netstandard2.0 ..\..\miniprofiler.snk @@ -11,6 +11,6 @@ - + \ No newline at end of file diff --git a/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs b/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs index 4cc73b03..1b36718e 100644 --- a/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs +++ b/src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; +using MongoDB.Driver.Core.Operations; using StackExchange.Profiling.Storage; namespace StackExchange.Profiling @@ -12,38 +16,69 @@ namespace StackExchange.Profiling /// public class MongoDbStorage : IAsyncStorage { + private readonly MongoDbStorageOptions _options; private readonly MongoClient _client; private readonly IMongoCollection _collection; /// /// Returns a new . MongoDb connection string will default to "mongodb://localhost" + /// and collection name to "profilers". /// /// The MongoDB connection string. public MongoDbStorage(string connectionString) : this(connectionString, "profilers") { } /// - /// Returns a new . MongoDb connection string will default to "mongodb://localhost" + /// Returns a new . MongoDb connection string will default to "mongodb://localhost". /// /// The MongoDB connection string. /// The collection name to use in the database. - public MongoDbStorage(string connectionString, string collectionName) + public MongoDbStorage(string connectionString, string collectionName) : this(new MongoDbStorageOptions + { + ConnectionString = connectionString, + CollectionName = collectionName, + }) { } + + /// + /// Creates a new instance of this class using the provided . + /// + /// Options to use for configuring this instance. + /// If is null or contains only whitespace. + public MongoDbStorage(MongoDbStorageOptions options) { + if (string.IsNullOrWhiteSpace(options.CollectionName)) + { + throw new ArgumentException("Collection name may not be null or contain only whitespace", nameof(options.CollectionName)); + } + + _options = options; + if (!BsonClassMap.IsClassMapRegistered(typeof(MiniProfiler))) { BsonClassMapFields(); } - var url = new MongoUrl(connectionString); + var url = new MongoUrl(options.ConnectionString); var databaseName = url.DatabaseName ?? "MiniProfiler"; _client = new MongoClient(url); _collection = _client .GetDatabase(databaseName) - .GetCollection(collectionName); + .GetCollection(options.CollectionName); + + if (options.AutomaticallyCreateIndexes) + { + WithIndexCreation(options.CacheDuration); + } } - private static void BsonClassMapFields() + private void BsonClassMapFields() { + if (_options.SerializeDecimalFieldsAsNumberDecimal) + { + BsonSerializer.RegisterSerializer(typeof(decimal), new DecimalSerializer(BsonType.Decimal128)); + BsonSerializer.RegisterSerializer(typeof(decimal?), new NullableSerializer(new DecimalSerializer(BsonType.Decimal128))); + } + BsonClassMap.RegisterClassMap( map => { @@ -97,13 +132,59 @@ private static void BsonClassMapFields() /// public MongoDbStorage WithIndexCreation() { - _collection.Indexes.CreateOne(Builders.IndexKeys.Ascending(_ => _.User)); - _collection.Indexes.CreateOne(Builders.IndexKeys.Ascending(_ => _.HasUserViewed)); - _collection.Indexes.CreateOne(Builders.IndexKeys.Ascending(_ => _.Started)); - _collection.Indexes.CreateOne(Builders.IndexKeys.Descending(_ => _.Started)); + _collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(_ => _.User))); + _collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(_ => _.HasUserViewed))); + CreateStartedAscendingIndex(); + _collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Descending(_ => _.Started))); + return this; } + /// + /// Creates indexes on the following fields for faster querying: + /// + /// FieldDirectionNotes + /// UserAscending + /// HasUserViewedAscending + /// StartedAscendingUsed to apply the , if one was specified + /// StartedDescending + /// + /// + /// The time to persist profiles before they expire. + public MongoDbStorage WithIndexCreation(TimeSpan cacheDuration) + { + _options.CacheDuration = cacheDuration; + return WithIndexCreation(); + } + + private void CreateStartedAscendingIndex() + { + var index = Builders.IndexKeys.Ascending(_ => _.Started); + var options = _options.CacheDuration != default + ? new CreateIndexOptions { ExpireAfter = _options.CacheDuration } + : null; + var model = new CreateIndexModel(index, options); + + try + { + _collection.Indexes.CreateOne(model); + } + catch (MongoCommandException ex) when (_options.AutomaticallyRecreateIndexes && ex.Code == 85) + { + // Handling the case we found an conflicting existing index, and were told to re-create if this happens + var indexNames = _collection.Indexes.List().ToList() + .SelectMany(index => index.Elements) + .Where(element => element.Name == "name") + .Select(name => name.Value.ToString()); + var indexName = IndexNameHelper.GetIndexName(model.Keys.Render(_collection.Indexes.DocumentSerializer, _collection.Indexes.Settings.SerializerRegistry)); + if (indexNames.Contains(indexName)) + { + _collection.Indexes.DropOne(indexName); + } + _collection.Indexes.CreateOne(model); + } + } + /// /// Returns a list of s that haven't been seen by . /// @@ -188,7 +269,7 @@ public void Save(MiniProfiler profiler) _collection.ReplaceOne( p => p.Id == profiler.Id, profiler, - new UpdateOptions + new ReplaceOptions { IsUpsert = true }); @@ -203,7 +284,7 @@ public Task SaveAsync(MiniProfiler profiler) return _collection.ReplaceOneAsync( p => p.Id == profiler.Id, profiler, - new UpdateOptions + new ReplaceOptions { IsUpsert = true }); diff --git a/src/MiniProfiler.Providers.MongoDB/MongoDbStorageOptions.cs b/src/MiniProfiler.Providers.MongoDB/MongoDbStorageOptions.cs new file mode 100644 index 00000000..bfcc6f47 --- /dev/null +++ b/src/MiniProfiler.Providers.MongoDB/MongoDbStorageOptions.cs @@ -0,0 +1,55 @@ +using System; + +namespace StackExchange.Profiling +{ + /// + /// Options for configuring . + /// + public class MongoDbStorageOptions + { + /// + /// The connection string to use for connecting to MongoDB. + /// Defaults to mongodb://localhost. + /// + public string? ConnectionString { get; set; } + + /// + /// Name of the collection in which to store sessions in. + /// Defaults to profilers. + /// + public string CollectionName { get; set; } = "profilers"; + + /// + /// If set to , C# decimal fields will be serialized as NumberDecimals in MongoDB. + /// If set to , will serialize these fields as strings (backwards-compatible with older versions of this provider). + /// Defaults to . + /// + public bool SerializeDecimalFieldsAsNumberDecimal { get; set; } = true; + + /// + /// Specifies whether relevant indexes will automatically created when this provider is instantiated. + /// Defaults to . + /// + public bool AutomaticallyCreateIndexes { get; set; } = true; + + /// + /// Specifies whether relevant indexes will automatically recreated if creation fails (e.g. because something with + /// different options was previously created). + /// *THIS DROPS EXISTING DATA* + /// Defaults to . + /// + public bool AutomaticallyRecreateIndexes { get; set; } = false; + + /// + /// Gets or sets how long to cache each for, in absolute terms. + /// Defaults to one hour. + /// + /// + /// You need to either set to true or call + /// for this value to have any effect. + /// Setting this option will drop any (, ascending) index previously + /// defined, including those with custom options. + /// + public TimeSpan CacheDuration { get; set; } = TimeSpan.FromHours(1); + } +} diff --git a/tests/MiniProfiler.Tests/Helpers/TestConfig.cs b/tests/MiniProfiler.Tests/Helpers/TestConfig.cs index 112d184b..67132ce2 100644 --- a/tests/MiniProfiler.Tests/Helpers/TestConfig.cs +++ b/tests/MiniProfiler.Tests/Helpers/TestConfig.cs @@ -30,7 +30,7 @@ static TestConfig() public class Config { public bool RunLongRunning { get; set; } - public bool EnableTestLogging { get; set; } = Environment.GetEnvironmentVariable(nameof(EnableTestLogging)) == "true"; + public bool EnableTestLogging { get; set; } = bool.TryParse(Environment.GetEnvironmentVariable(nameof(EnableTestLogging)), out var enableTestLogging) && enableTestLogging; public string RedisConnectionString { get; set; } = Environment.GetEnvironmentVariable(nameof(RedisConnectionString)) ?? "localhost:6379"; public string SQLServerConnectionString { get; set; } = Environment.GetEnvironmentVariable(nameof(SQLServerConnectionString)) ?? "Server=.;Database=tempdb;Trusted_Connection=True;"; diff --git a/tests/MiniProfiler.Tests/Storage/MongoDbStorageTests.cs b/tests/MiniProfiler.Tests/Storage/MongoDbStorageTests.cs index cb8173c7..56a6746a 100644 --- a/tests/MiniProfiler.Tests/Storage/MongoDbStorageTests.cs +++ b/tests/MiniProfiler.Tests/Storage/MongoDbStorageTests.cs @@ -1,4 +1,5 @@ using System; +using MongoDB.Driver; using Xunit; using Xunit.Abstractions; @@ -9,9 +10,37 @@ public class MongoDbStorageTests : StorageBaseTest, IClassFixture(() => new MongoDbStorage(options)); + Assert.NotNull(ex); + Assert.Equal(85, ex.Code); + + options.AutomaticallyRecreateIndexes = true; + // Succeeds, because drop/re-create is allowed now + var storage4 = new MongoDbStorage(options); + Assert.NotNull(storage4); + } } - public class MongoDbStorageFixture : StorageFixtureBase, IDisposable + public class MongoDbStorageFixture : StorageFixtureBase { public MongoDbStorageFixture() { diff --git a/tests/MiniProfiler.Tests/Storage/RavenDbStoreTests.cs b/tests/MiniProfiler.Tests/Storage/RavenDbStoreTests.cs index 0606ef42..fba7ae8a 100644 --- a/tests/MiniProfiler.Tests/Storage/RavenDbStoreTests.cs +++ b/tests/MiniProfiler.Tests/Storage/RavenDbStoreTests.cs @@ -30,20 +30,21 @@ public RavenDbStoreFixture() { var store = new DocumentStore { - Urls = TestConfig.Current.RavenDbUrls.Split(';'), Database = TestConfig.Current.RavenDatabase + Urls = TestConfig.Current.RavenDbUrls.Split(';'), + Database = TestConfig.Current.RavenDatabase + TestId }; store.Initialize(); try { - store.Maintenance.ForDatabase(TestConfig.Current.RavenDatabase).Send(new GetStatisticsOperation()); + store.Maintenance.ForDatabase(store.Database).Send(new GetStatisticsOperation()); } catch (DatabaseDoesNotExistException) { try { - store.Maintenance.Server.Send(new CreateDatabaseOperation(new DatabaseRecord(TestConfig.Current.RavenDatabase))); + store.Maintenance.Server.Send(new CreateDatabaseOperation(new DatabaseRecord(store.Database))); } catch (ConcurrencyException) { @@ -51,10 +52,11 @@ public RavenDbStoreFixture() } } + var dbName = store.Database; store.Dispose(); store = null; - Storage = new RavenDbStorage(TestConfig.Current.RavenDbUrls.Split(';'), TestConfig.Current.RavenDatabase, waitForIndexes: true); + Storage = new RavenDbStorage(TestConfig.Current.RavenDbUrls.Split(';'), dbName, waitForIndexes: true); Storage.GetUnviewedIds(""); } catch (Exception e)