| File: AspireAzureEFPostgreSqlExtensions.cs | Web Access |
| Project: src\src\Components\Aspire.Azure.Npgsql.EntityFrameworkCore.PostgreSQL\Aspire.Azure.Npgsql.EntityFrameworkCore.PostgreSQL.csproj (Aspire.Azure.Npgsql.EntityFrameworkCore.PostgreSQL) |
// 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; using System.Diagnostics.CodeAnalysis; using Aspire; using Aspire.Azure.Npgsql.EntityFrameworkCore.PostgreSQL; using Aspire.Npgsql.EntityFrameworkCore.PostgreSQL; using Microsoft.EntityFrameworkCore; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; #if NET9_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; #else using Npgsql; #endif namespace Microsoft.Extensions.Hosting; /// <summary> /// Provides extension methods for registering a PostgreSQL database context in an Aspire application. /// </summary> public static partial class AspireAzureEFPostgreSqlExtensions { private const DynamicallyAccessedMemberTypes RequiredByEF = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; /// <summary> /// Registers the given <see cref="DbContext" /> as a service in the services provided by the <paramref name="builder"/>. /// Enables db context pooling, retries, corresponding health check, logging and telemetry. /// </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="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> /// <para> /// Reads the configuration from "Aspire:Npgsql:EntityFrameworkCore:PostgreSQL:{typeof(TContext).Name}" config section, or "Aspire:Npgsql:EntityFrameworkCore:PostgreSQL" if former does not exist. /// </para> /// <para> /// The <see cref="DbContext.OnConfiguring" /> method can then be overridden to configure <see cref="DbContext" /> options. /// </para> /// </remarks> /// <exception cref="ArgumentNullException">Thrown if mandatory <paramref name="builder"/> is null.</exception> /// <exception cref="InvalidOperationException">Thrown when mandatory <see cref="NpgsqlEntityFrameworkCorePostgreSQLSettings.ConnectionString"/> is not provided.</exception> public static void AddAzureNpgsqlDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( this IHostApplicationBuilder builder, string connectionName, Action<AzureNpgsqlEntityFrameworkCorePostgreSQLSettings>? configureSettings = null, Action<DbContextOptionsBuilder>? configureDbContextOptions = null) where TContext : DbContext { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(connectionName); AzureNpgsqlEntityFrameworkCorePostgreSQLSettings? azureSettings = null; builder.AddNpgsqlDbContext<TContext>(connectionName, settings => azureSettings = ConfigureSettings(configureSettings, settings), dbContextOptionsBuilder => { Debug.Assert(azureSettings != null); ConfigureDbContextOptionsBuilder(azureSettings, dbContextOptionsBuilder); configureDbContextOptions?.Invoke(dbContextOptionsBuilder); }); } /// <summary> /// Configures retries, health check, logging and telemetry for the <see cref="DbContext" />. /// </summary> /// <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 EnrichAzureNpgsqlDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( this IHostApplicationBuilder builder, Action<AzureNpgsqlEntityFrameworkCorePostgreSQLSettings>? configureSettings = null) where TContext : DbContext { ArgumentNullException.ThrowIfNull(builder); AzureNpgsqlEntityFrameworkCorePostgreSQLSettings? azureSettings = null; builder.EnrichNpgsqlDbContext<TContext>(settings => azureSettings = ConfigureSettings(configureSettings, settings)); // Enrich should always call ConfigureSettings Debug.Assert(azureSettings != null); #if NET9_0_OR_GREATER builder.Services.ConfigureDbContext<TContext>(dbContextOptionsBuilder => ConfigureDbContextOptionsBuilder(azureSettings, dbContextOptionsBuilder)); #else builder.PatchServiceDescriptor<TContext>(dbContextOptionsBuilder => { ConfigureDbContextOptionsBuilder(azureSettings, dbContextOptionsBuilder); }); #endif } private static AzureNpgsqlEntityFrameworkCorePostgreSQLSettings ConfigureSettings(Action<AzureNpgsqlEntityFrameworkCorePostgreSQLSettings>? userConfigureSettings, NpgsqlEntityFrameworkCorePostgreSQLSettings settings) { var azureSettings = new AzureNpgsqlEntityFrameworkCorePostgreSQLSettings(); // Copy the values updated by Npgsql integration. CopySettings(settings, azureSettings); // Invoke the Aspire configuration. userConfigureSettings?.Invoke(azureSettings); // Copy to the Npgsql integration settings as it needs to get any values set in userConfigureSettings. CopySettings(azureSettings, settings); return azureSettings; } private static void CopySettings(NpgsqlEntityFrameworkCorePostgreSQLSettings source, NpgsqlEntityFrameworkCorePostgreSQLSettings destination) { destination.ConnectionString = source.ConnectionString; destination.DisableHealthChecks = source.DisableHealthChecks; destination.DisableMetrics = source.DisableMetrics; destination.DisableTracing = source.DisableTracing; destination.DisableRetry = source.DisableRetry; destination.CommandTimeout = source.CommandTimeout; } private static void ConfigureDbContextOptionsBuilder(AzureNpgsqlEntityFrameworkCorePostgreSQLSettings settings, DbContextOptionsBuilder dbContextOptionsBuilder) { #pragma warning disable EF1001 // Internal EF Core API usage. // Get the connection string from the Npgsql options extension in case it was set using UseNpgsql(connStr) and Enrich() var connectionString = settings.ConnectionString ?? dbContextOptionsBuilder.Options.GetExtension<NpgsqlOptionsExtension>()?.ConnectionString; #pragma warning restore EF1001 // Internal EF Core API usage. #if NET9_0_OR_GREATER dbContextOptionsBuilder.UseNpgsql(options => { options.ConfigureDataSource(dataSourceBuilder => dataSourceBuilder.ConfigureEntraIdAuthentication(settings.Credential)); }); #else var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); if (dataSourceBuilder.ConfigureEntraIdAuthentication(settings.Credential)) { dbContextOptionsBuilder.UseNpgsql(dataSourceBuilder.Build()); } #endif } }