diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs
index 22899343..314c3d01 100644
--- a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs
+++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs
@@ -135,6 +135,15 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.AuthorizationsCollectionName = name);
}
+ ///
+ /// Sets the maximal duration given to the MongoDB client to initialize
+ /// the database and register the indexes used by the OpenIddict entities.
+ ///
+ /// The timeout.
+ /// The .
+ public OpenIddictMongoDbBuilder SetInitializationTimeout(TimeSpan timeout)
+ => Configure(options => options.InitializationTimeout = timeout);
+
///
/// Replaces the default scopes collection name (by default, openiddict.scopes).
///
diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs
index be326df5..6288a79d 100644
--- a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs
+++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs
@@ -23,6 +23,7 @@ namespace OpenIddict.MongoDb
{
private readonly IOptions _options;
private readonly IServiceProvider _provider;
+ private readonly SemaphoreSlim _semaphore;
private IMongoDatabase _database;
public OpenIddictMongoDbContext(
@@ -31,6 +32,7 @@ namespace OpenIddict.MongoDb
{
_options = options;
_provider = provider;
+ _semaphore = new SemaphoreSlim(1);
}
///
@@ -55,56 +57,73 @@ namespace OpenIddict.MongoDb
throw new InvalidOperationException("The OpenIddict MongoDB options cannot be retrieved.");
}
- var database = options.Database;
- if (database == null)
- {
- database = _provider.GetService();
- }
-
- if (database == null)
+ if (!await _semaphore.WaitAsync(options.InitializationTimeout, cancellationToken))
{
throw new InvalidOperationException(new StringBuilder()
- .AppendLine("No suitable MongoDB database service can be found.")
- .Append("To configure the OpenIddict MongoDB stores to use a specific database, use ")
- .Append("'services.AddOpenIddict().AddCore().UseMongoDb().UseDatabase()' or register an ")
- .Append("'IMongoDatabase' in the dependency injection container in 'ConfigureServices()'.")
+ .AppendLine("The MongoDB database couldn't be initialized within a reasonable timeframe.")
+ .Append("Make sure that the MongoDB server is ready and accepts connections from this machine ")
+ .Append("or use 'options.UseMongoDb().SetInitializationTimeout()' to manually adjust the timeout.")
.ToString());
}
- // Note: the cancellation token passed as a parameter is deliberately not used here to ensure
- // the cancellation of a single store operation doesn't prevent the indexes from being created.
- var applications = database.GetCollection(options.ApplicationsCollectionName);
- await applications.Indexes.CreateOneAsync(
- Builders.IndexKeys.Ascending(application => application.ClientId),
- new CreateIndexOptions
+ try
+ {
+ var database = options.Database;
+ if (database == null)
{
- Unique = true
- });
+ database = _provider.GetService();
+ }
- await applications.Indexes.CreateOneAsync(
- Builders.IndexKeys.Ascending(application => application.PostLogoutRedirectUris));
+ if (database == null)
+ {
+ throw new InvalidOperationException(new StringBuilder()
+ .AppendLine("No suitable MongoDB database service can be found.")
+ .Append("To configure the OpenIddict MongoDB stores to use a specific database, use ")
+ .Append("'services.AddOpenIddict().AddCore().UseMongoDb().UseDatabase()' or register an ")
+ .Append("'IMongoDatabase' in the dependency injection container in 'ConfigureServices()'.")
+ .ToString());
+ }
- await applications.Indexes.CreateOneAsync(
- Builders.IndexKeys.Ascending(application => application.RedirectUris));
+ // Note: the cancellation token passed as a parameter is deliberately not used here to ensure
+ // the cancellation of a single store operation doesn't prevent the indexes from being created.
+ var applications = database.GetCollection(options.ApplicationsCollectionName);
+ await applications.Indexes.CreateOneAsync(
+ Builders.IndexKeys.Ascending(application => application.ClientId),
+ new CreateIndexOptions
+ {
+ Unique = true
+ });
- var scopes = database.GetCollection(options.ScopesCollectionName);
- await scopes.Indexes.CreateOneAsync(
- Builders.IndexKeys.Ascending(scope => scope.Name),
- new CreateIndexOptions
- {
- Unique = true
- });
+ await applications.Indexes.CreateOneAsync(
+ Builders.IndexKeys.Ascending(application => application.PostLogoutRedirectUris));
- var tokens = database.GetCollection(options.TokensCollectionName);
- await tokens.Indexes.CreateOneAsync(
- Builders.IndexKeys.Ascending(token => token.ReferenceId),
- new CreateIndexOptions
- {
- PartialFilterExpression = Builders.Filter.Exists(token => token.ReferenceId),
- Unique = true
- });
+ await applications.Indexes.CreateOneAsync(
+ Builders.IndexKeys.Ascending(application => application.RedirectUris));
- return _database = database;
+ var scopes = database.GetCollection(options.ScopesCollectionName);
+ await scopes.Indexes.CreateOneAsync(
+ Builders.IndexKeys.Ascending(scope => scope.Name),
+ new CreateIndexOptions
+ {
+ Unique = true
+ });
+
+ var tokens = database.GetCollection(options.TokensCollectionName);
+ await tokens.Indexes.CreateOneAsync(
+ Builders.IndexKeys.Ascending(token => token.ReferenceId),
+ new CreateIndexOptions
+ {
+ PartialFilterExpression = Builders.Filter.Exists(token => token.ReferenceId),
+ Unique = true
+ });
+
+ return _database = database;
+ }
+
+ finally
+ {
+ _semaphore.Release();
+ }
}
return new ValueTask(ExecuteAsync());
diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs
index d8abbec3..326eaa09 100644
--- a/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs
+++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs
@@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
+using System;
using MongoDB.Driver;
namespace OpenIddict.MongoDb
@@ -29,6 +30,12 @@ namespace OpenIddict.MongoDb
///
public IMongoDatabase Database { get; set; }
+ ///
+ /// Gets or sets the maximal duration given to the MongoDB client to initialize
+ /// the database and register the indexes used by the OpenIddict entities.
+ ///
+ public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(30);
+
///
/// Gets or sets the name of the scopes collection (by default, openiddict.scopes).
///
diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs
index 17413e8f..cd1051a2 100644
--- a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs
+++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs
@@ -196,6 +196,23 @@ namespace OpenIddict.MongoDb.Tests
Assert.Equal("custom_collection", options.ScopesCollectionName);
}
+ [Fact]
+ public void SetInitializationTimeout_TimeoutIsCorrectlySet()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act
+ builder.SetInitializationTimeout(TimeSpan.FromDays(42));
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+ var options = provider.GetRequiredService>().Value;
+
+ Assert.Equal(TimeSpan.FromDays(42), options.InitializationTimeout);
+ }
+
[Theory]
[InlineData(null)]
[InlineData("")]
diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs
new file mode 100644
index 00000000..7d971548
--- /dev/null
+++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs
@@ -0,0 +1,255 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using MongoDB.Driver;
+using Moq;
+using OpenIddict.MongoDb.Models;
+using Xunit;
+
+namespace OpenIddict.MongoDb.Tests
+{
+ public class OpenIddictMongoDbContextTests
+ {
+ [Fact]
+ public async Task GetDatabaseAsync_ThrowsAnExceptionForNullOptions()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = services.BuildServiceProvider();
+
+ var database = GetDatabase();
+ var options = Mock.Of>();
+
+ var context = new OpenIddictMongoDbContext(options, provider);
+
+ // Act and assert
+ var exception = await Assert.ThrowsAsync(async delegate
+ {
+ await context.GetDatabaseAsync(CancellationToken.None);
+ });
+
+ Assert.Equal("The OpenIddict MongoDB options cannot be retrieved.", exception.Message);
+ }
+
+ [Fact]
+ public async Task GetDatabaseAsync_ThrowsAnExceptionForConcurrentCallsWhenInitializationTimesOut()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = services.BuildServiceProvider();
+
+ var manager = new Mock>();
+ manager.Setup(mock => mock.CreateOneAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Returns(async delegate
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(1000));
+ return nameof(OpenIddictMongoDbContextTests);
+ });
+
+ var collection = new Mock>();
+ collection.SetupGet(mock => mock.Indexes)
+ .Returns(manager.Object);
+
+ var database = GetDatabase();
+ database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny()))
+ .Returns(collection.Object);
+
+ var options = Options.Create(new OpenIddictMongoDbOptions
+ {
+ Database = database.Object,
+ InitializationTimeout = TimeSpan.FromMilliseconds(50)
+ });
+
+ var context = new OpenIddictMongoDbContext(options, provider);
+
+ // Act and assert
+ var exception = await Assert.ThrowsAsync(async delegate
+ {
+ await Task.WhenAll(
+ context.GetDatabaseAsync(CancellationToken.None).AsTask(),
+ context.GetDatabaseAsync(CancellationToken.None).AsTask(),
+ context.GetDatabaseAsync(CancellationToken.None).AsTask(),
+ context.GetDatabaseAsync(CancellationToken.None).AsTask());
+ });
+
+ Assert.Equal(new StringBuilder()
+ .AppendLine("The MongoDB database couldn't be initialized within a reasonable timeframe.")
+ .Append("Make sure that the MongoDB server is ready and accepts connections from this machine ")
+ .Append("or use 'options.UseMongoDb().SetInitializationTimeout()' to manually adjust the timeout.")
+ .ToString(), exception.Message);
+ }
+
+ [Fact]
+ public async Task GetDatabaseAsync_PrefersDatabaseRegisteredInOptionsToDatabaseRegisteredInDependencyInjectionContainer()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddSingleton(Mock.Of());
+
+ var provider = services.BuildServiceProvider();
+
+ var database = GetDatabase();
+ var options = Options.Create(new OpenIddictMongoDbOptions
+ {
+ Database = database.Object
+ });
+
+ var context = new OpenIddictMongoDbContext(options, provider);
+
+ // Act and assert
+ Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task GetDatabaseAsync_ThrowsAnExceptionWhenDatabaseCannotBeFound()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = services.BuildServiceProvider();
+
+ var database = GetDatabase();
+ var options = Options.Create(new OpenIddictMongoDbOptions
+ {
+ Database = null
+ });
+
+ var context = new OpenIddictMongoDbContext(options, provider);
+
+ // Act and assert
+ var exception = await Assert.ThrowsAsync(async delegate
+ {
+ await context.GetDatabaseAsync(CancellationToken.None);
+ });
+
+ Assert.Equal(new StringBuilder()
+ .AppendLine("No suitable MongoDB database service can be found.")
+ .Append("To configure the OpenIddict MongoDB stores to use a specific database, use ")
+ .Append("'services.AddOpenIddict().AddCore().UseMongoDb().UseDatabase()' or register an ")
+ .Append("'IMongoDatabase' in the dependency injection container in 'ConfigureServices()'.")
+ .ToString(), exception.Message);
+ }
+
+ [Fact]
+ public async Task GetDatabaseAsync_UsesDatabaseRegisteredInDependencyInjectionContainer()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddSingleton(Mock.Of());
+
+ var database = GetDatabase();
+ services.AddSingleton(database.Object);
+
+ var provider = services.BuildServiceProvider();
+
+ var options = Options.Create(new OpenIddictMongoDbOptions
+ {
+ Database = null
+ });
+
+ var context = new OpenIddictMongoDbContext(options, provider);
+
+ // Act and assert
+ Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task GetDatabaseAsync_ReturnsCachedDatabase()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = services.BuildServiceProvider();
+
+ var database = GetDatabase();
+ var options = Options.Create(new OpenIddictMongoDbOptions
+ {
+ Database = database.Object
+ });
+
+ var context = new OpenIddictMongoDbContext(options, provider);
+
+ // Act and assert
+ Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None));
+ Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None));
+
+ database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once());
+ database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once());
+ database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task GetDatabaseAsync_FailedInvocationDoesNotPreventFutureInvocations()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = services.BuildServiceProvider();
+
+ var count = 0;
+
+ var collection = new Mock>();
+ collection.SetupGet(mock => mock.Indexes)
+ .Returns(Mock.Of>());
+
+ var database = GetDatabase();
+ database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny()))
+ .Callback(() => count++)
+ .Returns(delegate
+ {
+ if (count == 1)
+ {
+ throw new Exception();
+ }
+
+ return collection.Object;
+ });
+
+ var options = Options.Create(new OpenIddictMongoDbOptions
+ {
+ Database = database.Object
+ });
+
+ var context = new OpenIddictMongoDbContext(options, provider);
+
+ // Act and assert
+ await Assert.ThrowsAsync(async () => await context.GetDatabaseAsync(CancellationToken.None));
+ Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None));
+
+ database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Exactly(2));
+ database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once());
+ database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once());
+ }
+
+ private static Mock GetDatabase()
+ {
+ var applications = new Mock>();
+ applications.SetupGet(mock => mock.Indexes)
+ .Returns(Mock.Of>());
+
+ var scopes = new Mock>();
+ scopes.SetupGet(mock => mock.Indexes)
+ .Returns(Mock.Of>());
+
+ var tokens = new Mock>();
+ tokens.SetupGet(mock => mock.Indexes)
+ .Returns(Mock.Of>());
+
+ var database = new Mock();
+ database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny()))
+ .Returns(applications.Object);
+ database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny()))
+ .Returns(scopes.Object);
+ database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny()))
+ .Returns(tokens.Object);
+
+ return database;
+ }
+ }
+}