| File: AspireMongoDBEntityFrameworkCoreExtensions.cs | Web Access |
| Project: src\src\Components\Aspire.MongoDB.EntityFrameworkCore\Aspire.MongoDB.EntityFrameworkCore.csproj (Aspire.MongoDB.EntityFrameworkCore) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using Aspire; using Aspire.MongoDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; namespace Microsoft.Extensions.Hosting; /// <summary> /// Extension methods for configuring EntityFrameworkCore DbContext to a MongoDB database /// </summary> public static class AspireMongoDBEntityFrameworkCoreExtensions { private const string DefaultConfigSectionName = "Aspire:MongoDB:EntityFrameworkCore"; private const DynamicallyAccessedMemberTypes RequiredByEF = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; private const string ActivityNameSource = "MongoDB.Driver.Core.Extensions.DiagnosticSources"; /// <summary> /// Registers the given <see cref="DbContext" /> as a service in the services provided by the <paramref name="builder"/>. /// Enables db context pooling, retries, health check, logging and telemetry for the <see cref="DbContext" />. /// </summary> /// <typeparam name="TContext">The <see cref="DbContext" /> that needs to be registered.</typeparam> /// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param> /// <param name="connectionName">A name used to retrieve the connection string from the ConnectionStrings configuration section.</param> /// <param name="databaseName">The name of the database. If not provided, it will be extracted from the connection string or settings.</param> /// <param name="configureSettings">An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.</param> /// <param name="configureDbContextOptions">An optional delegate to configure the <see cref="DbContextOptions"/> for the context.</param> /// <remarks>Reads the configuration from "Aspire:MongoDB:EntityFrameworkCore:{typeof(TContext).Name}" config section, or "Aspire:MongoDB:EntityFrameworkCore" if former does not exist.</remarks> /// <exception cref="ArgumentNullException">Thrown if mandatory <paramref name="builder"/> is null.</exception> /// <exception cref="InvalidOperationException">Thrown when mandatory <see cref="MongoDBEntityFrameworkCoreSettings.ConnectionString"/> is not provided.</exception> public static void AddMongoDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( this IHostApplicationBuilder builder, string connectionName, string? databaseName = null, Action<MongoDBEntityFrameworkCoreSettings>? configureSettings = null, Action<DbContextOptionsBuilder>? configureDbContextOptions = null) where TContext : DbContext { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(connectionName); builder.EnsureDbContextNotRegistered<TContext>(); var settings = builder.GetDbContextSettings<TContext, MongoDBEntityFrameworkCoreSettings>( DefaultConfigSectionName, connectionName, (settings, section) => section.Bind(settings) ); if (builder.Configuration.GetConnectionString(connectionName) is { } connectionString) { settings.ConnectionString = connectionString; } // Database name priority: explicit parameter > settings > connection string if (!string.IsNullOrEmpty(databaseName)) { settings.DatabaseName = databaseName; } else if (string.IsNullOrEmpty(settings.DatabaseName) && !string.IsNullOrEmpty(settings.ConnectionString)) { // Try to extract database name from connection string var mongoUrl = MongoUrl.Create(settings.ConnectionString); if (!string.IsNullOrEmpty(mongoUrl.DatabaseName)) { settings.DatabaseName = mongoUrl.DatabaseName; } } configureSettings?.Invoke(settings); builder.Services.AddDbContextPool<TContext>(ConfigureDbContext); ConfigureInstrumentation<TContext>(builder, settings); void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder) { ConnectionStringValidation.ValidateConnectionString(settings.ConnectionString, connectionName, DefaultConfigSectionName, $"{DefaultConfigSectionName}:{typeof(TContext).Name}", isEfDesignTime: EF.IsDesignTime); if (string.IsNullOrEmpty(settings.DatabaseName)) { throw new InvalidOperationException($"A database name is required but was not provided. Specify it via the '{nameof(databaseName)}' parameter, the '{nameof(MongoDBEntityFrameworkCoreSettings.DatabaseName)}' setting, or include it in the connection string."); } dbContextOptionsBuilder.UseMongoDB(settings.ConnectionString!, settings.DatabaseName); configureDbContextOptions?.Invoke(dbContextOptionsBuilder); } } /// <summary> /// Configures logging and telemetry for the <see cref="DbContext" />. /// </summary> /// <typeparam name="TContext">The <see cref="DbContext" /> that needs to be configured.</typeparam> /// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param> /// <param name="configureSettings">An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.</param> /// <remarks>Reads the configuration from "Aspire:MongoDB:EntityFrameworkCore:{typeof(TContext).Name}" config section, or "Aspire:MongoDB:EntityFrameworkCore" if former does not exist.</remarks> /// <exception cref="ArgumentNullException">Thrown if mandatory <paramref name="builder"/> is null.</exception> /// <exception cref="InvalidOperationException">Thrown when mandatory <see cref="DbContext"/> is not registered in DI.</exception> public static void EnrichMongoDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( this IHostApplicationBuilder builder, Action<MongoDBEntityFrameworkCoreSettings>? configureSettings = null) where TContext : DbContext { ArgumentNullException.ThrowIfNull(builder); var settings = builder.GetDbContextSettings<TContext, MongoDBEntityFrameworkCoreSettings>( DefaultConfigSectionName, null, (settings, section) => section.Bind(settings) ); configureSettings?.Invoke(settings); // This call validates that the DbContext is registered in DI. // For net8.0, it also patches the service descriptor if needed. // For net9.0+, it's a no-op when no configuration action is provided. builder.PatchServiceDescriptor<TContext>(); ConfigureInstrumentation<TContext>(builder, settings); } private static void ConfigureInstrumentation<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TContext>(IHostApplicationBuilder builder, MongoDBEntityFrameworkCoreSettings settings) where TContext : DbContext { if (!settings.DisableHealthChecks) { // calling MapHealthChecks is the responsibility of the app, not Component builder.TryAddHealthCheck( name: typeof(TContext).Name, static hcBuilder => hcBuilder.AddDbContextCheck<TContext>()); } if (!settings.DisableTracing) { builder.Services.AddOpenTelemetry() .WithTracing(tracerProviderBuilder => { tracerProviderBuilder.AddSource(ActivityNameSource); }); } } }