diff --git a/Aspire.sln b/Aspire.sln index d09f21713aa..713b364cb5c 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -158,6 +158,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector.Tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.IntegrationServiceA", "tests\testproject\TestProject.IntegrationServiceA\TestProject.IntegrationServiceA.csproj", "{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.MongoDB.Driver", "src\Components\Aspire.MongoDB.Driver\Aspire.MongoDB.Driver.csproj", "{20A5A907-A135-4735-B4BF-E13514F360E3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.MongoDB.Driver.Tests", "tests\Aspire.MongoDB.Driver.Tests\Aspire.MongoDB.Driver.Tests.csproj", "{E592E447-BA3C-44FA-86C1-EBEDC864A644}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -424,6 +428,14 @@ Global {DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU {DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU {DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Release|Any CPU.Build.0 = Release|Any CPU + {20A5A907-A135-4735-B4BF-E13514F360E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20A5A907-A135-4735-B4BF-E13514F360E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20A5A907-A135-4735-B4BF-E13514F360E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20A5A907-A135-4735-B4BF-E13514F360E3}.Release|Any CPU.Build.0 = Release|Any CPU + {E592E447-BA3C-44FA-86C1-EBEDC864A644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E592E447-BA3C-44FA-86C1-EBEDC864A644}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E592E447-BA3C-44FA-86C1-EBEDC864A644}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E592E447-BA3C-44FA-86C1-EBEDC864A644}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -497,6 +509,8 @@ Global {165411FE-755E-4869-A756-F87F455860AC} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {CA283D7F-EB95-4353-B196-C409965D2B42} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {C8079F06-304F-49B1-A0C1-45AA3782A923} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} + {20A5A907-A135-4735-B4BF-E13514F360E3} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} + {E592E447-BA3C-44FA-86C1-EBEDC864A644} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {DCF2D47A-921A-4900-B5B2-CF97B3531CE8} = {975F6F41-B455-451D-A312-098DE4A167B6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/Directory.Packages.props b/Directory.Packages.props index 6827245d7eb..c52d7ad0173 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -71,6 +71,7 @@ + @@ -99,4 +100,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/MongoDB/IMongoDbResource.cs b/src/Aspire.Hosting/MongoDB/IMongoDbResource.cs new file mode 100644 index 00000000000..2c149774a1a --- /dev/null +++ b/src/Aspire.Hosting/MongoDB/IMongoDbResource.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a MongoDb resource that requires a connection string. +/// +public interface IMongoDbResource : IResourceWithConnectionString +{ +} diff --git a/src/Aspire.Hosting/MongoDB/MongoDbBuilderExtensions.cs b/src/Aspire.Hosting/MongoDB/MongoDbBuilderExtensions.cs new file mode 100644 index 00000000000..ae0521d99f9 --- /dev/null +++ b/src/Aspire.Hosting/MongoDB/MongoDbBuilderExtensions.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using System.Text.Json; +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.MongoDb; + +/// +/// Provides extension methods for adding MongoDB resources to an . +/// +public static class MongoDbBuilderExtensions +{ + private const int DefaultContainerPort = 27017; + private const string DefaultPassword = "password"; + private const string PasswordEnvVarName = "MONGO_INITDB_ROOT_PASSWORD"; + + /// + /// Adds a MongoDB container to the application model. The default image is "mongo" and the tag is "latest". + /// + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port for MongoDB. + /// The password for the MongoDB root user. Defaults to a 'password' password. + /// A reference to the . + public static IResourceBuilder AddMongoDbContainer( + this IDistributedApplicationBuilder builder, + string name, + int port = DefaultContainerPort, + string password = DefaultPassword) + { + var mongoDbContainer = new MongoDbContainerResource(name, password); + + return builder + .AddResource(mongoDbContainer) + .WithAnnotation(new ManifestPublishingCallbackAnnotation(WriteMongoDbContainerToManifest)) + .WithAnnotation(new ServiceBindingAnnotation(ProtocolType.Tcp, port: port, containerPort: DefaultContainerPort)) // Internal port is always 27017. + .WithAnnotation(new ContainerImageAnnotation { Image = "mongo", Tag = "latest" }) + .WithEnvironment(PasswordEnvVarName, () => mongoDbContainer.Password); + } + + /// + /// Adds a MongoDB connection to the application model. Connection strings can also be read from the connection string section of the configuration using the name of the resource. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The MongoDB connection string (optional). + /// A reference to the . + public static IResourceBuilder AddMongoDbConnection(this IDistributedApplicationBuilder builder, string name, string? connectionString = null) + { + var mongoDbConnection = new MongoDbConnectionResource(name, connectionString); + + return builder + .AddResource(mongoDbConnection) + .WithAnnotation(new ManifestPublishingCallbackAnnotation((json) => WriteMongoDbConnectionToManifest(json, mongoDbConnection))); + } + + /// + /// Adds a MongoDB database to the application model. + /// + /// The MongoDB server resource builder. + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// A reference to the . + public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name) + { + var mongoDbDatabase = new MongoDbDatabaseResource(name, builder.Resource); + + return builder.ApplicationBuilder + .AddResource(mongoDbDatabase) + .WithAnnotation(new ManifestPublishingCallbackAnnotation( + (json) => WriteMongoDbDatabaseToManifest(json, mongoDbDatabase))); + } + + private static void WriteMongoDbContainerToManifest(Utf8JsonWriter jsonWriter) + { + jsonWriter.WriteString("type", "mongodb.server.v0"); + } + + private static void WriteMongoDbConnectionToManifest(Utf8JsonWriter jsonWriter, MongoDbConnectionResource mongoDbConnection) + { + jsonWriter.WriteString("type", "mongodb.connection.v0"); + jsonWriter.WriteString("connectionString", mongoDbConnection.GetConnectionString()); + } + + private static void WriteMongoDbDatabaseToManifest(Utf8JsonWriter json, MongoDbDatabaseResource mongoDbDatabase) + { + json.WriteString("type", "mongodb.database.v0"); + json.WriteString("parent", mongoDbDatabase.Parent.Name); + } +} diff --git a/src/Aspire.Hosting/MongoDB/MongoDbConnectionResource.cs b/src/Aspire.Hosting/MongoDB/MongoDbConnectionResource.cs new file mode 100644 index 00000000000..f2b15b94205 --- /dev/null +++ b/src/Aspire.Hosting/MongoDB/MongoDbConnectionResource.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a MongoDb connection. +/// +/// The name of the resource. +/// The MongoDb connection string. +public class MongoDbConnectionResource(string name, string? connectionString) : Resource(name), IMySqlResource +{ + private readonly string? _connectionString = connectionString; + + /// + /// Gets the connection string for the MongoDb server. + /// + /// The specified connection string. + public string? GetConnectionString() => _connectionString; +} diff --git a/src/Aspire.Hosting/MongoDB/MongoDbContainerResource.cs b/src/Aspire.Hosting/MongoDB/MongoDbContainerResource.cs new file mode 100644 index 00000000000..322b5517665 --- /dev/null +++ b/src/Aspire.Hosting/MongoDB/MongoDbContainerResource.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a MongoDb container. +/// +/// The name of the resource. +/// The MongoDb root password. +public class MongoDbContainerResource(string name, string password) : ContainerResource(name), IMongoDbResource +{ + public string Password { get; } = password; + + /// + /// Gets the connection string for the MongoDb server. + /// + /// A connection string for the MongoDb server in the form "mongodb://host:port". + public string? GetConnectionString() + { + if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + { + throw new DistributedApplicationException("Expected allocated endpoints!"); + } + + var allocatedEndpoint = allocatedEndpoints.Single(); + + return $"mongodb://root:{Password}@{allocatedEndpoint.Address}:{allocatedEndpoint.Port}"; + } +} diff --git a/src/Aspire.Hosting/MongoDB/MongoDbDatabaseResource.cs b/src/Aspire.Hosting/MongoDB/MongoDbDatabaseResource.cs new file mode 100644 index 00000000000..9a9b0916a5d --- /dev/null +++ b/src/Aspire.Hosting/MongoDB/MongoDbDatabaseResource.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a MongoDb database. This is a child resource of a . +/// +/// The name of the resource. +/// The MongoDb server resource associated with this database. +public class MongoDbDatabaseResource(string name, MongoDbContainerResource mongoDbContainer) + : Resource(name), IMongoDbResource, IResourceWithParent +{ + public MongoDbContainerResource Parent => mongoDbContainer; + + /// + /// Gets the connection string for the MongoDb database. + /// + /// A connection string for the MongoDb database. + public string? GetConnectionString() + { + if (Parent.GetConnectionString() is { } connectionString) + { + return $"{connectionString}/{Name}"; + } + else + { + throw new DistributedApplicationException("Parent resource connection string was null."); + } + } +} diff --git a/src/Components/Aspire.MongoDB.Driver/Aspire.MongoDB.Driver.csproj b/src/Components/Aspire.MongoDB.Driver/Aspire.MongoDB.Driver.csproj new file mode 100644 index 00000000000..c3968ef7c43 --- /dev/null +++ b/src/Components/Aspire.MongoDB.Driver/Aspire.MongoDB.Driver.csproj @@ -0,0 +1,17 @@ + + + + $(NetCurrent) + true + $(ComponentDatabasePackageTags) MongoDb + A generic MongoDb client that integrates with Aspire. + + + + + + + + + + diff --git a/src/Components/Aspire.MongoDB.Driver/AspireMongoDbDriverExtensions.cs b/src/Components/Aspire.MongoDB.Driver/AspireMongoDbDriverExtensions.cs new file mode 100644 index 00000000000..31ca81422e9 --- /dev/null +++ b/src/Components/Aspire.MongoDB.Driver/AspireMongoDbDriverExtensions.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.MongoDB.Driver; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extension methods for connecting MongoDB database with MongoDB.Driver client. +/// +public static class AspireMongoDbDriverExtensions +{ + private const string DefaultConfigSectionName = "Aspire:MongoDB"; + + /// + /// Registers instance for connecting MongoDB database with MongoDB.Driver client. + /// + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration. + /// Reads the configuration from "Aspire:MongoDB" section. + /// Thrown when mandatory is not provided. + public static void AddMongoDbDataSource( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null) + => AddMongoDbDataSource(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null); + + /// + /// Registers instance for connecting MongoDB database with MongoDB.Driver client. + /// + /// The to read config from and add services to. + /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration. + /// Reads the configuration from "Aspire:MongoDB:{name}" section. + /// Thrown if mandatory is null. + /// Thrown when mandatory is not provided. + public static void AddKeyedMongoDbDataSource( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + AddMongoDbDataSource(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name); + } + + private static void AddMongoDbDataSource( + IHostApplicationBuilder builder, + string configurationSectionName, + Action? configureSettings, + string connectionName, + object? serviceKey) + { + ArgumentNullException.ThrowIfNull(builder); + + var settings = builder.GetMongoDbSettings( + configurationSectionName, + configureSettings, + connectionName); + + settings.ValidateSettings(connectionName, configurationSectionName); + + builder.RegisterMongoDbServices(settings, serviceKey); + } + + private static void RegisterMongoDbServices(this IHostApplicationBuilder builder, MongoDbSettings settings, object? serviceKey) + { + if (serviceKey is null) + { + builder.Services + .AddSingleton(_ => new MongoClient(settings.ConnectionString)); + return; + } + + builder.Services.AddKeyedSingleton( + serviceKey, + (_, __) => new MongoClient(settings.ConnectionString)); + } + + private static MongoDbSettings GetMongoDbSettings(this IHostApplicationBuilder builder, string configurationSectionName, Action? configureSettings, string connectionName) + { + MongoDbSettings settings = new(); + + builder.Configuration + .GetSection(configurationSectionName) + .Bind(settings); + + if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + settings.ConnectionString = connectionString; + } + + configureSettings?.Invoke(settings); + + return settings; + } + + private static void ValidateSettings(this MongoDbSettings settings, string connectionName, string configurationSectionName) + { + if (string.IsNullOrEmpty(settings.ConnectionString)) + { + throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{configurationSectionName}' configuration section."); + } + } +} diff --git a/src/Components/Aspire.MongoDB.Driver/ConfigurationSchema.json b/src/Components/Aspire.MongoDB.Driver/ConfigurationSchema.json new file mode 100644 index 00000000000..824c57cfc6c --- /dev/null +++ b/src/Components/Aspire.MongoDB.Driver/ConfigurationSchema.json @@ -0,0 +1,31 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "MongoDB": { + "$ref": "#/definitions/logLevelThreshold" + }, + "MongoDB.Driver": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + }, + "properties": { + "Aspire": { + "type": "object", + "properties": { + "MongoDB": { + "type": "object", + "properties": { + "ConnectionString": { + "type": "string", + "description": "Gets or sets the connection string of the MongoDB database to connect to." + } + } + } + } + } + }, + "type": "object" +} diff --git a/src/Components/Aspire.MongoDB.Driver/MongoDbSettings.cs b/src/Components/Aspire.MongoDB.Driver/MongoDbSettings.cs new file mode 100644 index 00000000000..05b3e6efea7 --- /dev/null +++ b/src/Components/Aspire.MongoDB.Driver/MongoDbSettings.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.MongoDB.Driver; + +/// +/// Provides the client configuration settings for connecting to a MongoDB database using MongoDB driver. +/// +public class MongoDbSettings +{ + /// + /// The connection string of the MongoDB database to connect to. + /// + public string? ConnectionString { get; set; } +} diff --git a/tests/Aspire.MongoDB.Driver.Tests/Aspire.MongoDB.Driver.Tests.csproj b/tests/Aspire.MongoDB.Driver.Tests/Aspire.MongoDB.Driver.Tests.csproj new file mode 100644 index 00000000000..d3129feefc8 --- /dev/null +++ b/tests/Aspire.MongoDB.Driver.Tests/Aspire.MongoDB.Driver.Tests.csproj @@ -0,0 +1,12 @@ + + + + $(NetCurrent) + + + + + + + + diff --git a/tests/Aspire.MongoDB.Driver.Tests/AspireMongoDbDriverExtensionsTests.cs b/tests/Aspire.MongoDB.Driver.Tests/AspireMongoDbDriverExtensionsTests.cs new file mode 100644 index 00000000000..08b7d1480b6 --- /dev/null +++ b/tests/Aspire.MongoDB.Driver.Tests/AspireMongoDbDriverExtensionsTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MongoDB.Driver; +using Xunit; + +namespace Aspire.MongoDB.Driver.Tests; + +public class AspireMongoDbDriverExtensionsTests +{ + private const string ConnectionString = "mongodb://localhost:27017"; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadsFromConnectionStringsCorrectly(bool useKeyed) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:mongodb", ConnectionString) + ]); + + if (useKeyed) + { + builder.AddKeyedMongoDbDataSource("mongodb"); + } + else + { + builder.AddMongoDbDataSource("mongodb"); + } + + var host = builder.Build(); + var mongoClient = useKeyed ? + host.Services.GetRequiredKeyedService("mongodb") : + host.Services.GetRequiredService(); + + var uri = MongoUrl.Create(ConnectionString); + + Assert.Equal(uri.Server.Host, mongoClient.Settings.Server.Host); + Assert.Equal(uri.Server.Port, mongoClient.Settings.Server.Port); + } +}