-
Notifications
You must be signed in to change notification settings - Fork 720
Add Enrich EF API #2125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Enrich EF API #2125
Changes from 3 commits
07758f5
a76069d
9deb56f
36005a9
3f8cdf8
5ba7fb0
f7771cb
fa41aa0
d3ffcd9
3a30cc1
7c6f96e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,3 +50,5 @@ uninstrumented | |
| upsert | ||
| uris | ||
| urls | ||
| Npgsql | ||
| Postgre | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,15 +66,108 @@ public static partial class AspireEFPostgreSqlExtensions | |
|
|
||
| configureSettings?.Invoke(settings); | ||
|
|
||
| if (settings.DbContextPooling) | ||
| builder.Services.AddNpgsqlDataSource(settings.ConnectionString ?? string.Empty, builder => | ||
| { | ||
| builder.Services.AddDbContextPool<TContext>(ConfigureDbContext); | ||
| // delay validating the ConnectionString until the DataSource is requested. This ensures an exception doesn't happen until a Logger is established. | ||
| if (string.IsNullOrEmpty(settings.ConnectionString)) | ||
| { | ||
| throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section."); | ||
| } | ||
|
|
||
| builder.UseLoggerFactory(null); // a workaround for https://github.com/npgsql/efcore.pg/issues/2821 | ||
| }); | ||
|
|
||
| builder.Services.AddDbContextPool<TContext>(options => ConfigureDbContext(settings, options, configureDbContextOptions)); | ||
|
|
||
| ConfigureInstrumentation<TContext>(builder, settings); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures 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 EnrichNpgsqlDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( | ||
| this IHostApplicationBuilder builder, | ||
| Action<NpgsqlEntityFrameworkCorePostgreSQLSettings>? configureSettings = null) where TContext : DbContext | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
|
|
||
| NpgsqlEntityFrameworkCorePostgreSQLSettings settings = new(); | ||
| var typeSpecificSectionName = $"{DefaultConfigSectionName}:{typeof(TContext).Name}"; | ||
| var typeSpecificConfigurationSection = builder.Configuration.GetSection(typeSpecificSectionName); | ||
| if (typeSpecificConfigurationSection.Exists()) // https://github.com/dotnet/runtime/issues/91380 | ||
|
||
| { | ||
| typeSpecificConfigurationSection.Bind(settings); | ||
| } | ||
| else | ||
| { | ||
| builder.Services.AddDbContext<TContext>(ConfigureDbContext); | ||
| builder.Configuration.GetSection(DefaultConfigSectionName).Bind(settings); | ||
| } | ||
|
|
||
| configureSettings?.Invoke(settings); | ||
|
|
||
| ConfigureRetry(); | ||
|
|
||
| ConfigureInstrumentation<TContext>(builder, settings); | ||
|
|
||
| void ConfigureRetry() | ||
| { | ||
| if (!settings.Retry) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Resolving DbContext<TContextService> will resolve DbContextOptions<TContextImplementation> | ||
| var olDbContextOptionsDescriptor = builder.Services.FirstOrDefault(sd => sd.ServiceType == typeof(DbContextOptions<TContext>)); | ||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (olDbContextOptionsDescriptor is null) | ||
| { | ||
| throw new InvalidOperationException($"DbContext<{nameof(TContext)}> was not registered"); | ||
| } | ||
|
|
||
| builder.Services.Remove(olDbContextOptionsDescriptor); | ||
|
|
||
| ServiceDescriptor dbContextOptionsDescriptor; | ||
|
|
||
| dbContextOptionsDescriptor = new ServiceDescriptor( | ||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| olDbContextOptionsDescriptor.ServiceType, | ||
| olDbContextOptionsDescriptor.ServiceKey, | ||
| factory: (sp, key) => | ||
| { | ||
| DbContextOptionsBuilder? optionsBuilder = null; | ||
|
|
||
| if (olDbContextOptionsDescriptor.ImplementationFactory != null) | ||
| { | ||
| var instance = olDbContextOptionsDescriptor.ImplementationFactory?.Invoke(sp) as DbContextOptions<TContext>; | ||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (instance == null) | ||
| { | ||
| throw new InvalidOperationException($"DbContextOptions<{nameof(TContext)}> couldn't be resolved"); | ||
| } | ||
|
|
||
| optionsBuilder = new DbContextOptionsBuilder<TContext>(instance); | ||
| } | ||
| else | ||
| { | ||
| optionsBuilder ??= new DbContextOptionsBuilder<TContext>(); | ||
| } | ||
|
|
||
| ConfigureDbContext(settings, optionsBuilder, null); | ||
|
|
||
| optionsBuilder.UseNpgsql(options => options.EnableRetryOnFailure()); | ||
|
|
||
| return optionsBuilder.Options; | ||
| }, | ||
| olDbContextOptionsDescriptor.Lifetime | ||
| ); | ||
|
|
||
| builder.Services.Add(dbContextOptionsDescriptor); | ||
| } | ||
| } | ||
|
|
||
| private static void ConfigureInstrumentation<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TContext>(IHostApplicationBuilder builder, NpgsqlEntityFrameworkCorePostgreSQLSettings settings) where TContext : DbContext | ||
| { | ||
| if (settings.HealthChecks) | ||
| { | ||
| // calling MapHealthChecks is the responsibility of the app, not Component | ||
|
|
@@ -113,34 +206,31 @@ public static partial class AspireEFPostgreSqlExtensions | |
| NpgsqlCommon.AddNpgsqlMetrics(meterProviderBuilder); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder) | ||
| private static void ConfigureDbContext(NpgsqlEntityFrameworkCorePostgreSQLSettings settings, DbContextOptionsBuilder dbContextOptionsBuilder, Action<DbContextOptionsBuilder>? configureDbContextOptions) | ||
| { | ||
| if (!settings.Retry) | ||
| { | ||
| // delay validating the ConnectionString until the DbContext is requested. This ensures an exception doesn't happen until a Logger is established. | ||
| if (string.IsNullOrEmpty(settings.ConnectionString)) | ||
| { | ||
| throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section."); | ||
| } | ||
|
|
||
| // We don't register a logger factory, because there is no need to: https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.useloggerfactory?view=efcore-7.0#remarks | ||
| dbContextOptionsBuilder.UseNpgsql(settings.ConnectionString, builder => | ||
| { | ||
| // Resiliency: | ||
| // 1. Connection resiliency automatically retries failed database commands: https://www.npgsql.org/efcore/misc/other.html#execution-strategy | ||
| if (settings.MaxRetryCount > 0) | ||
| { | ||
| builder.EnableRetryOnFailure(settings.MaxRetryCount); | ||
| } | ||
| // 2. "Scale proportionally: You want to ensure that you don't scale out a resource to a point where it will exhaust other associated resources." | ||
| // The pooling is enabled by default, the min pool size is 0 by default: https://www.npgsql.org/doc/connection-string-parameters.html#pooling | ||
| // There is nothing for us to set here. | ||
| // 3. "Timeout: Places limit on the duration for which a caller can wait for a response." | ||
| // The timeouts have default values, except of Internal Command Timeout, which we should ignore: | ||
| // https://www.npgsql.org/doc/connection-string-parameters.html#timeouts-and-keepalive | ||
| // There is nothing for us to set here. | ||
| }); | ||
|
|
||
| configureDbContextOptions?.Invoke(dbContextOptionsBuilder); | ||
| return; | ||
| } | ||
|
|
||
| // We don't provide the connection string, it's going to use the pre-registered DataSource. | ||
| // We don't register logger factory, because there is no need to: https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.useloggerfactory?view=efcore-7.0#remarks | ||
| dbContextOptionsBuilder.UseNpgsql(builder => | ||
|
||
| { | ||
| // Resiliency: | ||
| // 1. Connection resiliency automatically retries failed database commands: https://www.npgsql.org/efcore/misc/other.html#execution-strategy | ||
| builder.EnableRetryOnFailure(); | ||
| // 2. "Scale proportionally: You want to ensure that you don't scale out a resource to a point where it will exhaust other associated resources." | ||
| // The pooling is enabled by default, the min pool size is 0 by default: https://www.npgsql.org/doc/connection-string-parameters.html#pooling | ||
| // There is nothing for us to set here. | ||
| // 3. "Timeout: Places limit on the duration for which a caller can wait for a response." | ||
| // The timeouts have default values, except of Internal Command Timeout, which we should ignore: | ||
| // https://www.npgsql.org/doc/connection-string-parameters.html#timeouts-and-keepalive | ||
| // There is nothing for us to set here. | ||
| }); | ||
|
|
||
| configureDbContextOptions?.Invoke(dbContextOptionsBuilder); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,25 +58,21 @@ | |
| "type": "string", | ||
| "description": "Gets or sets the connection string of the PostgreSQL database to connect to." | ||
| }, | ||
| "DbContextPooling": { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pooling is enabled by default in |
||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the DbContext will be pooled or explicitly created every time it's requested.", | ||
| "default": true | ||
| }, | ||
| "HealthChecks": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the database health check is enabled or not.", | ||
| "default": true | ||
| }, | ||
| "MaxRetryCount": { | ||
| "type": "integer", | ||
| "description": "Gets or sets the maximum number of retry attempts. Default value is 6, set it to 0 to disable the retry mechanism." | ||
| }, | ||
| "Metrics": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.", | ||
| "default": true | ||
| }, | ||
| "Retry": { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| "type": "boolean", | ||
| "description": "Gets or sets whether retries should be enabled.", | ||
| "default": true | ||
| }, | ||
| "Tracing": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are overwriting my changes in ae11632. Can you revert this so it doesn't add back NpgsqlDataSource into DI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done