Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/MiniProfiler.Providers.MongoDB/IMongoIndexManagerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Linq;
using System.Threading;
using MongoDB.Driver;
using MongoDB.Driver.Core.Operations;

namespace StackExchange.Profiling
{
/// <summary>
/// Extension methods for <see cref="IMongoIndexManager{TDocument}"/>.
/// </summary>
public static class IMongoIndexManagerExtensions
{
/// <summary>
/// Creates the index specified by <paramref name="model"/>. If an index with the same name already exists, it is first dropped.
/// </summary>
/// <typeparam name="TDocument">Type of the document to be indexed.</typeparam>
/// <param name="indexManager">Manager to create the index in.</param>
/// <param name="model">Model defining the index.</param>
/// <param name="options">Additional index creation options, if required.</param>
/// <returns>Name of the index that was created.</returns>
/// <remarks>The standard <see cref="IMongoIndexManager{TDocument}.CreateOne(CreateIndexModel{TDocument}, CreateOneIndexOptions, CancellationToken)"/>
/// method will throw an exception if attempting to create an index with different options to one that already exists.
/// By dropping that index first, this method ensures an exception will never be thrown, even if different options are used.</remarks>
public static string CreateOneForce<TDocument>(this IMongoIndexManager<TDocument> indexManager, CreateIndexModel<TDocument> model, CreateOneIndexOptions options = null)
{
var indexNames = indexManager
.List().ToList()
.SelectMany(index => index.Elements)
.Where(element => element.Name == "name")
.Select(name => name.Value.ToString());
var indexName = IndexNameHelper.GetIndexName(model.Keys.Render(indexManager.DocumentSerializer, indexManager.Settings.SerializerRegistry));

if (indexNames.Contains(indexName))
{
indexManager.DropOne(indexName);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not expect a method like this (regardless of name) to do a drop. Instead, probably best to have an option for "drop existing" that's very explicitly opt-in. The current overall path (callers to this aren't clear) could unexpectedly lose data for users.


return indexManager.CreateOne(model, options);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
<AssemblyName>MiniProfiler.Providers.MongoDB</AssemblyName>
<Title>MiniProfiler.Providers.MongoDB</Title>
<Description>MiniProfiler: Profiler storage for MongoDB</Description>
<Authors>Nick Craver, Roger Calaf</Authors>
<Authors>Nick Craver, Roger Calaf, Ian Kemp</Authors>
<PackageTags>NoSQL;MongoDB;$(PackageBaseTags)</PackageTags>
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
<AssemblyOriginatorKeyFile>..\..\miniprofiler.snk</AssemblyOriginatorKeyFile>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MiniProfiler.Shared\MiniProfiler.Shared.csproj" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" />
<PackageReference Include="MongoDB.Driver" Version="2.16.1" />
</ItemGroup>
</Project>
90 changes: 76 additions & 14 deletions src/MiniProfiler.Providers.MongoDB/MongoDbStorage.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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 StackExchange.Profiling.Storage;

Expand All @@ -17,33 +20,61 @@ public class MongoDbStorage : IAsyncStorage

/// <summary>
/// Returns a new <see cref="MongoDbStorage"/>. MongoDb connection string will default to "mongodb://localhost"
/// and collection name to "profilers".
/// </summary>
/// <param name="connectionString">The MongoDB connection string.</param>
public MongoDbStorage(string connectionString) : this(connectionString, "profilers") { }

/// <summary>
/// Returns a new <see cref="MongoDbStorage"/>. MongoDb connection string will default to "mongodb://localhost"
/// Returns a new <see cref="MongoDbStorage"/>. MongoDb connection string will default to "mongodb://localhost".
/// </summary>
/// <param name="connectionString">The MongoDB connection string.</param>
/// <param name="collectionName">The collection name to use in the database.</param>
public MongoDbStorage(string connectionString, string collectionName)
public MongoDbStorage(string connectionString, string collectionName) : this(new MongoDbStorageOptions
{
ConnectionString = connectionString,
CollectionName = collectionName,
}) { }

/// <summary>
/// Creates a new instance of this class using the provided <paramref name="options"/>.
/// </summary>
/// <param name="options">Options to use for configuring this instance.</param>
/// <exception cref="ArgumentException">If <see cref="MongoDbStorageOptions.CollectionName"/> is null or contains only whitespace.</exception>
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));
}

if (!BsonClassMap.IsClassMapRegistered(typeof(MiniProfiler)))
{
BsonClassMapFields();
BsonClassMapFields(options.SerializeDecimalFieldsAsNumberDecimal);
}

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<MiniProfiler>(collectionName);
.GetCollection<MiniProfiler>(options.CollectionName);

if (options.AutomaticallyCreateIndexes)
{
WithIndexCreation(options.CacheDuration);
}
}

private static void BsonClassMapFields()
private static void BsonClassMapFields(bool serializeDecimalFieldsAsNumberDecimal = false)
{
if (serializeDecimalFieldsAsNumberDecimal)
{
BsonSerializer.RegisterSerializer(typeof(decimal), new DecimalSerializer(BsonType.Decimal128));
BsonSerializer.RegisterSerializer(typeof(decimal?), new NullableSerializer<decimal>(new DecimalSerializer(BsonType.Decimal128)));
}

BsonClassMap.RegisterClassMap<MiniProfiler>(
map =>
{
Expand Down Expand Up @@ -96,14 +127,45 @@ private static void BsonClassMapFields()
/// Creates indexes for faster querying.
/// </summary>
public MongoDbStorage WithIndexCreation()
=> WithIndexCreation(default);

/// <summary>
/// Creates indexes on the following fields for faster querying:
/// <list type="table">
/// <listheader><term>Field</term><term>Direction</term><term>Notes</term></listheader>
/// <item><term>User</term><term>Ascending</term><term></term></item>
/// <item><term>HasUserViewed</term><term>Ascending</term><term></term></item>
/// <item><term>Started</term><term>Ascending</term><term>Used to apply the <paramref name="cacheDuration"/>, if one was specified</term></item>
/// <item><term>Started</term><term>Descending</term><term></term></item>
/// </list>
/// </summary>
public MongoDbStorage WithIndexCreation(TimeSpan cacheDuration)
{
_collection.Indexes.CreateOne(Builders<MiniProfiler>.IndexKeys.Ascending(_ => _.User));
_collection.Indexes.CreateOne(Builders<MiniProfiler>.IndexKeys.Ascending(_ => _.HasUserViewed));
_collection.Indexes.CreateOne(Builders<MiniProfiler>.IndexKeys.Ascending(_ => _.Started));
_collection.Indexes.CreateOne(Builders<MiniProfiler>.IndexKeys.Descending(_ => _.Started));
_collection.Indexes.CreateOne(new CreateIndexModel<MiniProfiler>(Builders<MiniProfiler>.IndexKeys.Ascending(_ => _.User)));
_collection.Indexes.CreateOne(new CreateIndexModel<MiniProfiler>(Builders<MiniProfiler>.IndexKeys.Ascending(_ => _.HasUserViewed)));
CreateStartedAscendingIndex(cacheDuration);
_collection.Indexes.CreateOne(new CreateIndexModel<MiniProfiler>(Builders<MiniProfiler>.IndexKeys.Descending(_ => _.Started)));

return this;
}

private void CreateStartedAscendingIndex(TimeSpan cacheDuration)
{
var index = Builders<MiniProfiler>.IndexKeys.Ascending(_ => _.Started);
if (cacheDuration != default)
{
_collection.Indexes.CreateOneForce(new CreateIndexModel<MiniProfiler>(index,
new CreateIndexOptions
{
ExpireAfter = cacheDuration,
}));
}
else
{
_collection.Indexes.CreateOne(new CreateIndexModel<MiniProfiler>(index));
}
}

/// <summary>
/// Returns a list of <see cref="MiniProfiler.Id"/>s that haven't been seen by <paramref name="user"/>.
/// </summary>
Expand Down Expand Up @@ -188,7 +250,7 @@ public void Save(MiniProfiler profiler)
_collection.ReplaceOne(
p => p.Id == profiler.Id,
profiler,
new UpdateOptions
new ReplaceOptions
{
IsUpsert = true
});
Expand All @@ -203,14 +265,14 @@ public Task SaveAsync(MiniProfiler profiler)
return _collection.ReplaceOneAsync(
p => p.Id == profiler.Id,
profiler,
new UpdateOptions
new ReplaceOptions
{
IsUpsert = true
});
}

/// <summary>
/// Sets a particular profiler session so it is considered "unviewed"
/// Sets a particular profiler session so it is considered "unviewed"
/// </summary>
/// <param name="user">The user to set this profiler ID as unviewed for.</param>
/// <param name="id">The profiler ID to set unviewed.</param>
Expand All @@ -221,7 +283,7 @@ public void SetUnviewed(string user, Guid id)
}

/// <summary>
/// Asynchronously sets a particular profiler session so it is considered "unviewed"
/// Asynchronously sets a particular profiler session so it is considered "unviewed"
/// </summary>
/// <param name="user">The user to set this profiler ID as unviewed for.</param>
/// <param name="id">The profiler ID to set unviewed.</param>
Expand Down
47 changes: 47 additions & 0 deletions src/MiniProfiler.Providers.MongoDB/MongoDbStorageOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;

namespace StackExchange.Profiling
{
/// <summary>
/// Options for configuring <see cref="MongoDbStorage"/>.
/// </summary>
public class MongoDbStorageOptions
{
/// <summary>
/// The connection string to use for connecting to MongoDB.
/// Defaults to <c>mongodb://localhost</c>.
/// </summary>
public string ConnectionString { get; set; }

/// <summary>
/// Name of the collection in which to store <see cref="MiniProfiler"/> sessions in.
/// Defaults to <c>profilers</c>.
/// </summary>
public string CollectionName { get; set; } = "profilers";

/// <summary>
/// If set to <see langword="true"/>, C# <c>decimal</c> fields will be serialized as <c>NumberDecimal</c>s in MongoDB.
/// If set to <see langword="false"/>, will serialize these fields as strings (backwards-compatible with older versions of this provider).
/// Defaults to <see langword="true"/>.
/// </summary>
public bool SerializeDecimalFieldsAsNumberDecimal { get; set; } = true;

/// <summary>
/// Specifies whether relevant indexes will automatically created when this provider is instantiated.
/// Defaults to <see langword="true" />.
/// </summary>
public bool AutomaticallyCreateIndexes { get; set; } = true;

/// <summary>
/// Gets or sets how long to cache each <see cref="MiniProfiler"/> for, in absolute terms.
/// Defaults to one hour.
/// </summary>
/// <remarks><list type="bullet">
/// <item>You need to either set <see cref="AutomaticallyCreateIndexes"/> to true or call
/// <see cref="MongoDbStorage.WithIndexCreation(TimeSpan)"/> for this value to have any effect.</item>
/// <item>Setting this option will drop any (<see cref="MiniProfiler.Started"/>, ascending) index previously
/// defined, including those with custom options.</item>
/// </list></remarks>
public TimeSpan CacheDuration { get; set; } = TimeSpan.FromHours(1);
}
}
2 changes: 1 addition & 1 deletion tests/MiniProfiler.Tests/Helpers/TestConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;";
Expand Down
15 changes: 9 additions & 6 deletions tests/MiniProfiler.Tests/Storage/MongoDbStorageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ public MongoDbStorageTests(MongoDbStorageFixture fixture, ITestOutputHelper outp
}
}

public class MongoDbStorageFixture : StorageFixtureBase<MongoDbStorage>, IDisposable
public class MongoDbStorageFixture : StorageFixtureBase<MongoDbStorage>
{
public MongoDbStorageFixture()
{
Skip.IfNoConfig(nameof(TestConfig.Current.MongoDbConnectionString), TestConfig.Current.MongoDbConnectionString);

try
{
Storage = new MongoDbStorage(
TestConfig.Current.MongoDbConnectionString,
"MPTest" + TestId);

Storage.WithIndexCreation();
var options = new MongoDbStorageOptions
{
ConnectionString = TestConfig.Current.MongoDbConnectionString,
CollectionName = "MPTest" + TestId,
};

Storage = new MongoDbStorage(options);

Storage.GetUnviewedIds("");
}
catch (Exception e)
Expand Down