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)