diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs index e591bc7e6f..9bcff66734 100644 --- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs +++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs @@ -8,6 +8,7 @@ using Azure.Identity; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -130,7 +131,28 @@ void UseCosmosBody(CosmosDbContextOptionsBuilder builder) configureSettings?.Invoke(settings); - builder.PatchServiceDescriptor(); + if (settings.RequestTimeout.HasValue) + { + builder.PatchServiceDescriptor(optionsBuilder => + { +#pragma warning disable EF1001 // Internal EF Core API usage. + var extension = optionsBuilder.Options.FindExtension(); + + if (extension != null && + extension.RequestTimeout.HasValue && + extension.RequestTimeout != settings.RequestTimeout) + { + throw new InvalidOperationException($"Conflicting values for 'RequestTimeout' were found in {nameof(EntityFrameworkCoreCosmosDBSettings)} and set in DbContextOptions<{typeof(TContext).Name}>."); + } + + extension?.WithRequestTimeout(settings.RequestTimeout); +#pragma warning restore EF1001 // Internal EF Core API usage. + }); + } + else + { + builder.PatchServiceDescriptor(); + } ConfigureInstrumentation(builder, settings); } diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs b/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs index b1766dec5d..091a7b0896 100644 --- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs +++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs @@ -5,6 +5,9 @@ using Aspire; using Aspire.Microsoft.EntityFrameworkCore.SqlServer; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Trace; @@ -107,15 +110,56 @@ void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder) void ConfigureRetry() { - if (!settings.Retry) +#pragma warning disable EF1001 // Internal EF Core API usage. + if (settings.Retry || settings.CommandTimeout.HasValue) { - return; + builder.PatchServiceDescriptor(optionsBuilder => optionsBuilder.UseSqlServer(options => + { + var extension = optionsBuilder.Options.FindExtension(); + + if (settings.Retry) + { + var executionStrategy = extension?.ExecutionStrategyFactory?.Invoke(new ExecutionStrategyDependencies(null!, optionsBuilder.Options, null!)); + + if (executionStrategy != null) + { + if (executionStrategy is SqlServerRetryingExecutionStrategy) + { + // Keep custom Retry strategy. + // Any sub-class of SqlServerRetryingExecutionStrategy is a valid retry strategy + // which shouldn't be replaced even with Retry == true + } + else if (executionStrategy.GetType() != typeof(SqlServerExecutionStrategy)) + { + // Check SqlServerExecutionStrategy specifically (no 'is'), any sub-class is treated as a custom strategy. + + throw new InvalidOperationException($"{nameof(MicrosoftEntityFrameworkCoreSqlServerSettings)}.Retry can't be set when a custom Execution Strategy is configured."); + } + else + { + options.EnableRetryOnFailure(); + } + } + else + { + options.EnableRetryOnFailure(); + } + } + + if (settings.CommandTimeout.HasValue) + { + if (extension != null && + extension.CommandTimeout.HasValue && + extension.CommandTimeout != settings.CommandTimeout) + { + throw new InvalidOperationException($"Conflicting values for 'CommandTimeout' were found in {nameof(MicrosoftEntityFrameworkCoreSqlServerSettings)} and set in DbContextOptions<{typeof(TContext).Name}>."); + } + + options.CommandTimeout(settings.CommandTimeout); + } + })); } - - builder.PatchServiceDescriptor(optionsBuilder => - { - optionsBuilder.UseSqlServer(options => options.EnableRetryOnFailure()); - }); +#pragma warning restore EF1001 // Internal EF Core API usage. } } diff --git a/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs b/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs index d736f26f50..db2210f1d1 100644 --- a/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs +++ b/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs @@ -5,9 +5,13 @@ using Aspire; using Aspire.Npgsql.EntityFrameworkCore.PostgreSQL; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Npgsql; +using Npgsql.EntityFrameworkCore.PostgreSQL; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; namespace Microsoft.Extensions.Hosting; @@ -117,12 +121,56 @@ void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder) void ConfigureRetry() { - if (!settings.Retry) +#pragma warning disable EF1001 // Internal EF Core API usage. + if (settings.Retry || settings.CommandTimeout.HasValue) { - return; + builder.PatchServiceDescriptor(optionsBuilder => optionsBuilder.UseNpgsql(options => + { + var extension = optionsBuilder.Options.FindExtension(); + + if (settings.Retry) + { + var executionStrategy = extension?.ExecutionStrategyFactory?.Invoke(new ExecutionStrategyDependencies(null!, optionsBuilder.Options, null!)); + + if (executionStrategy != null) + { + if (executionStrategy is NpgsqlRetryingExecutionStrategy) + { + // Keep custom Retry strategy. + // Any sub-class of NpgsqlRetryingExecutionStrategy is a valid retry strategy + // which shouldn't be replaced even with Retry == true + } + else if (executionStrategy.GetType() != typeof(NpgsqlExecutionStrategy)) + { + // Check NpgsqlExecutionStrategy specifically (no 'is'), any sub-class is treated as a custom strategy. + + throw new InvalidOperationException($"{nameof(NpgsqlEntityFrameworkCorePostgreSQLSettings)}.Retry can't be set when a custom Execution Strategy is configured."); + } + else + { + options.EnableRetryOnFailure(); + } + } + else + { + options.EnableRetryOnFailure(); + } + } + + if (settings.CommandTimeout.HasValue) + { + if (extension != null && + extension.CommandTimeout.HasValue && + extension.CommandTimeout != settings.CommandTimeout) + { + throw new InvalidOperationException($"Conflicting values for 'CommandTimeout' were found in {nameof(NpgsqlEntityFrameworkCorePostgreSQLSettings)} and set in DbContextOptions<{typeof(TContext).Name}>."); + } + + options.CommandTimeout(settings.CommandTimeout); + } + })); } - - builder.PatchServiceDescriptor(optionsBuilder => optionsBuilder.UseNpgsql(options => options.EnableRetryOnFailure())); +#pragma warning restore EF1001 // Internal EF Core API usage. } } diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs index 08fd2ce3ec..c5f9499519 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs @@ -5,9 +5,12 @@ using Aspire; using Aspire.Oracle.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Oracle.EntityFrameworkCore; +using Oracle.EntityFrameworkCore.Infrastructure.Internal; +using Oracle.EntityFrameworkCore.Storage.Internal; namespace Microsoft.Extensions.Hosting; @@ -105,14 +108,56 @@ void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder) void ConfigureRetry() { - if (!settings.Retry) +#pragma warning disable EF1001 // Internal EF Core API usage. + if (settings.Retry || settings.CommandTimeout.HasValue) { - return; + builder.PatchServiceDescriptor(optionsBuilder => optionsBuilder.UseOracle(options => + { + var extension = optionsBuilder.Options.FindExtension(); + + if (settings.Retry) + { + var executionStrategy = extension?.ExecutionStrategyFactory?.Invoke(new ExecutionStrategyDependencies(null!, optionsBuilder.Options, null!)); + + if (executionStrategy != null) + { + if (executionStrategy is OracleRetryingExecutionStrategy) + { + // Keep custom Retry strategy. + // Any sub-class of OracleRetryingExecutionStrategy is a valid retry strategy + // which shouldn't be replaced even with Retry == true + } + else if (executionStrategy.GetType() != typeof(OracleExecutionStrategy)) + { + // Check OracleExecutionStrategy specifically (no 'is'), any sub-class is treated as a custom strategy. + + throw new InvalidOperationException($"{nameof(OracleEntityFrameworkCoreSettings)}.Retry can't be set when a custom Execution Strategy is configured."); + } + else + { + options.ExecutionStrategy(context => new OracleRetryingExecutionStrategy(context)); + } + } + else + { + options.ExecutionStrategy(context => new OracleRetryingExecutionStrategy(context)); + } + } + + if (settings.CommandTimeout.HasValue) + { + if (extension != null && + extension.CommandTimeout.HasValue && + extension.CommandTimeout != settings.CommandTimeout) + { + throw new InvalidOperationException($"Conflicting values for 'CommandTimeout' were found in {nameof(OracleEntityFrameworkCoreSettings)} and set in DbContextOptions<{typeof(TContext).Name}>."); + } + + options.CommandTimeout(settings.CommandTimeout); + } + })); } - - builder.PatchServiceDescriptor(optionsBuilder => - optionsBuilder.UseOracle(options => options.ExecutionStrategy(context => new OracleRetryingExecutionStrategy(context))) - ); +#pragma warning restore EF1001 // Internal EF Core API usage. } } diff --git a/src/Components/Aspire.Pomelo.EntityFrameworkCore.MySql/AspireEFMySqlExtensions.cs b/src/Components/Aspire.Pomelo.EntityFrameworkCore.MySql/AspireEFMySqlExtensions.cs index 3cdfe54c92..9d264ed15e 100644 --- a/src/Components/Aspire.Pomelo.EntityFrameworkCore.MySql/AspireEFMySqlExtensions.cs +++ b/src/Components/Aspire.Pomelo.EntityFrameworkCore.MySql/AspireEFMySqlExtensions.cs @@ -5,6 +5,7 @@ using Aspire; using Aspire.Pomelo.EntityFrameworkCore.MySql; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -14,6 +15,7 @@ using Polly.Registry; using Polly.Retry; using Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal; +using Pomelo.EntityFrameworkCore.MySql.Storage.Internal; namespace Microsoft.Extensions.Hosting; @@ -155,23 +157,66 @@ void ConfigureDbContext(IServiceProvider serviceProvider, DbContextOptionsBuilde void ConfigureRetry() { - if (!settings.Retry) - { - return; - } - - builder.PatchServiceDescriptor(optionsBuilder => - { #pragma warning disable EF1001 // Internal EF Core API usage. - if (optionsBuilder.Options.FindExtension() is not MySqlOptionsExtension extension - || extension.ServerVersion is not ServerVersion serverVersion) + if (settings.Retry || settings.CommandTimeout.HasValue) + { + builder.PatchServiceDescriptor(optionsBuilder => { - throw new InvalidOperationException($"A DbContextOptions<{typeof(TContext).Name}> was not found. Please ensure 'ServerVersion' was configured."); - } + if (optionsBuilder.Options.FindExtension() is not MySqlOptionsExtension extension || + extension.ServerVersion is not ServerVersion serverVersion) + { + throw new InvalidOperationException($"A DbContextOptions<{typeof(TContext).Name}> was not found. Please ensure 'ServerVersion' was configured."); + } + + optionsBuilder.UseMySql(serverVersion, options => + { + var extension = optionsBuilder.Options.FindExtension(); + + if (settings.Retry) + { + var executionStrategy = extension?.ExecutionStrategyFactory?.Invoke(new ExecutionStrategyDependencies(null!, optionsBuilder.Options, null!)); + + if (executionStrategy != null) + { + if (executionStrategy is MySqlRetryingExecutionStrategy) + { + // Keep custom Retry strategy. + // Any sub-class of MySqlRetryingExecutionStrategy is a valid retry strategy + // which shouldn't be replaced even with Retry == true + } + else if (executionStrategy.GetType() != typeof(MySqlExecutionStrategy)) + { + // Check MySqlExecutionStrategy specifically (no 'is'), any sub-class is treated as a custom strategy. + + throw new InvalidOperationException($"{nameof(PomeloEntityFrameworkCoreMySqlSettings)}.Retry can't be set when a custom Execution Strategy is configured."); + } + else + { + options.EnableRetryOnFailure(); + } + } + else + { + options.EnableRetryOnFailure(); + } + } + + if (settings.CommandTimeout.HasValue) + { + if (extension != null && + extension.CommandTimeout.HasValue && + extension.CommandTimeout != settings.CommandTimeout) + { + throw new InvalidOperationException($"Conflicting values for 'CommandTimeout' were found in {nameof(PomeloEntityFrameworkCoreMySqlSettings)} and set in DbContextOptions<{typeof(TContext).Name}>."); + } + + options.CommandTimeout(settings.CommandTimeout); + } + + }); + }); + } #pragma warning restore EF1001 // Internal EF Core API usage. - - optionsBuilder.UseMySql(serverVersion, options => options.EnableRetryOnFailure()); - }); } } diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/EnrichCosmosDbTests.cs b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/EnrichCosmosDbTests.cs index d9cc7dc6a5..aef1943366 100644 --- a/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/EnrichCosmosDbTests.cs +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/EnrichCosmosDbTests.cs @@ -116,4 +116,21 @@ public void EnrichSupportCustomOptionsLifetime() var context = host.Services.GetRequiredService() as TestDbContext; Assert.NotNull(context); } + + [Fact] + public void EnrichWithConflictingRequestTimeoutThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseCosmos(ConnectionString, DatabaseName, builder => builder.RequestTimeout(TimeSpan.FromSeconds(123))); + }); + + builder.EnrichCosmosDbContext(settings => settings.RequestTimeout = TimeSpan.FromSeconds(456)); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("Conflicting values for 'RequestTimeout' were found in EntityFrameworkCoreCosmosDBSettings and set in DbContextOptions.", exception.Message); + } } diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/CustomExecutionStrategy.cs b/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/CustomExecutionStrategy.cs new file mode 100644 index 0000000000..f901cd4ae1 --- /dev/null +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/CustomExecutionStrategy.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomExecutionStrategy : SqlServerExecutionStrategy +{ + public CustomExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/CustomRetryExecutionStrategy.cs b/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/CustomRetryExecutionStrategy.cs new file mode 100644 index 0000000000..c6535b0e68 --- /dev/null +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/CustomRetryExecutionStrategy.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. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomRetryExecutionStrategy : SqlServerRetryingExecutionStrategy +{ + public const int DefaultRetryCount = 123; + + public CustomRetryExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } + + public int RetryCount => DefaultRetryCount; +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/EnrichSqlServerTests.cs b/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/EnrichSqlServerTests.cs index 00518cdc23..ca5cba086e 100644 --- a/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/EnrichSqlServerTests.cs +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.SqlServer.Tests/EnrichSqlServerTests.cs @@ -84,6 +84,26 @@ public void EnrichCanConfigureDbContextOptions() #pragma warning restore EF1001 // Internal EF Core API usage. } + [Fact] + public void EnrichWithConflictingCommandTimeoutThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseSqlServer(ConnectionString, builder => + { + builder.CommandTimeout(123); + }); + }); + + builder.EnrichSqlServerDbContext(settings => settings.CommandTimeout = 456); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("Conflicting values for 'CommandTimeout' were found in MicrosoftEntityFrameworkCoreSqlServerSettings and set in DbContextOptions.", exception.Message); + } + [Fact] public void EnrichEnablesRetryByDefault() { @@ -160,7 +180,7 @@ public void EnrichPreservesDefaultWhenMaxRetryCountNotSet() } [Fact] - public void EnrichOverridesCustomRetryIfNotDisabled() + public void EnrichDoesntOverridesCustomRetry() { var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ @@ -192,8 +212,7 @@ public void EnrichOverridesCustomRetryIfNotDisabled() Assert.NotNull(extension.ExecutionStrategyFactory); var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); var retryStrategy = Assert.IsType(executionStrategy); - Assert.Equal(new WorkaroundToReadProtectedField(context).MaxRetryCount, retryStrategy.MaxRetryCount); - + Assert.Equal(456, retryStrategy.MaxRetryCount); #pragma warning restore EF1001 // Internal EF Core API usage. } @@ -234,4 +253,77 @@ public void EnrichSupportCustomOptionsLifetime() var context = host.Services.GetRequiredService() as TestDbContext; Assert.NotNull(context); } + + [Fact] + public void EnrichWithoutRetryPreservesCustomExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseSqlServer(ConnectionString, builder => builder.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichSqlServerDbContext(settings => settings.Retry = false); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + [Fact] + public void EnrichWithRetryAndCustomExecutionStrategyThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseSqlServer(ConnectionString, builder => builder.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichSqlServerDbContext(settings => settings.Retry = true); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("MicrosoftEntityFrameworkCoreSqlServerSettings.Retry can't be set when a custom Execution Strategy is configured.", exception.Message); + } + + [Fact] + public void EnrichWithRetryAndCustomRetryExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseSqlServer(ConnectionString, builder => builder.ExecutionStrategy(c => new CustomRetryExecutionStrategy(c))); + }); + + builder.EnrichSqlServerDbContext(settings => settings.Retry = true); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } } diff --git a/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/CustomExecutionStrategy.cs b/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/CustomExecutionStrategy.cs new file mode 100644 index 0000000000..1d680ea028 --- /dev/null +++ b/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/CustomExecutionStrategy.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +namespace Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomExecutionStrategy : NpgsqlExecutionStrategy +{ + public CustomExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/CustomRetryExecutionStrategy.cs b/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/CustomRetryExecutionStrategy.cs new file mode 100644 index 0000000000..6bc497687d --- /dev/null +++ b/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/CustomRetryExecutionStrategy.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. + +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql.EntityFrameworkCore.PostgreSQL; + +namespace Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomRetryExecutionStrategy : NpgsqlRetryingExecutionStrategy +{ + public const int DefaultRetryCount = 123; + + public CustomRetryExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } + + public int RetryCount => DefaultRetryCount; +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/EnrichNpgsqlTests.cs b/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/EnrichNpgsqlTests.cs index 281607f1ad..da0d24db93 100644 --- a/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/EnrichNpgsqlTests.cs +++ b/tests/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.Tests/EnrichNpgsqlTests.cs @@ -91,6 +91,27 @@ public void EnrichCanConfigureDbContextOptions() #pragma warning restore EF1001 // Internal EF Core API usage. } + [Fact] + public void EnrichWithConflictingCommandTimeoutThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + AspireEFPostgreSqlExtensionsTests.ConfigureDbContextOptionsBuilderForTesting(optionsBuilder); + optionsBuilder.UseNpgsql(ConnectionString, npgsqlBuilder => + { + npgsqlBuilder.CommandTimeout(123); + }); + }); + + builder.EnrichNpgsqlDbContext(settings => settings.CommandTimeout = 456); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("Conflicting values for 'CommandTimeout' were found in NpgsqlEntityFrameworkCorePostgreSQLSettings and set in DbContextOptions.", exception.Message); + } + [Fact] public void EnrichEnablesRetryByDefault() { @@ -169,7 +190,7 @@ public void EnrichPreservesDefaultWhenMaxRetryCountNotSet() } [Fact] - public void EnrichOverridesCustomRetryIfNotDisabled() + public void EnrichDoesntOverridesCustomRetry() { var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ @@ -202,7 +223,7 @@ public void EnrichOverridesCustomRetryIfNotDisabled() Assert.NotNull(extension.ExecutionStrategyFactory); var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); var retryStrategy = Assert.IsType(executionStrategy); - Assert.Equal(new WorkaroundToReadProtectedField(context).MaxRetryCount, retryStrategy.MaxRetryCount); + Assert.Equal(456, retryStrategy.MaxRetryCount); #pragma warning restore EF1001 // Internal EF Core API usage. } @@ -246,4 +267,80 @@ public void EnrichSupportCustomOptionsLifetime() var context = host.Services.GetRequiredService() as TestDbContext; Assert.NotNull(context); } + + [Fact] + public void EnrichWithoutRetryPreservesCustomExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + AspireEFPostgreSqlExtensionsTests.ConfigureDbContextOptionsBuilderForTesting(optionsBuilder); + optionsBuilder.UseNpgsql(ConnectionString, npgsql => npgsql.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichNpgsqlDbContext(settings => settings.Retry = false); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + [Fact] + public void EnrichWithRetryAndCustomExecutionStrategyThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + AspireEFPostgreSqlExtensionsTests.ConfigureDbContextOptionsBuilderForTesting(optionsBuilder); + optionsBuilder.UseNpgsql(ConnectionString, npgsql => npgsql.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichNpgsqlDbContext(settings => settings.Retry = true); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("NpgsqlEntityFrameworkCorePostgreSQLSettings.Retry can't be set when a custom Execution Strategy is configured.", exception.Message); + } + + [Fact] + public void EnrichWithRetryAndCustomRetryExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + AspireEFPostgreSqlExtensionsTests.ConfigureDbContextOptionsBuilderForTesting(optionsBuilder); + optionsBuilder.UseNpgsql(ConnectionString, npgsql => npgsql.ExecutionStrategy(c => new CustomRetryExecutionStrategy(c))); + }); + + builder.EnrichNpgsqlDbContext(settings => settings.Retry = true); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } } diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/CustomExecutionStrategy.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/CustomExecutionStrategy.cs new file mode 100644 index 0000000000..1867b48c82 --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/CustomExecutionStrategy.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Storage; +using Oracle.EntityFrameworkCore.Storage.Internal; + +namespace Aspire.Oracle.EntityFrameworkCore.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomExecutionStrategy : OracleExecutionStrategy +{ + public CustomExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/CustomRetryExecutionStrategy.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/CustomRetryExecutionStrategy.cs new file mode 100644 index 0000000000..da218060bc --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/CustomRetryExecutionStrategy.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. + +using Microsoft.EntityFrameworkCore.Storage; +using Oracle.EntityFrameworkCore; + +namespace Aspire.Oracle.EntityFrameworkCore.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomRetryExecutionStrategy : OracleRetryingExecutionStrategy +{ + public const int DefaultRetryCount = 123; + + public CustomRetryExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } + + public int RetryCount => DefaultRetryCount; +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/EnrichOracleDatabaseTests.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/EnrichOracleDatabaseTests.cs index 630776d8a7..c10dc961f0 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/EnrichOracleDatabaseTests.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/EnrichOracleDatabaseTests.cs @@ -85,6 +85,26 @@ public void EnrichCanConfigureDbContextOptions() #pragma warning restore EF1001 // Internal EF Core API usage. } + [Fact] + public void EnrichWithConflictingCommandTimeoutThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseOracle(ConnectionString, builder => + { + builder.CommandTimeout(123); + }); + }); + + builder.EnrichOracleDatabaseDbContext(settings => settings.CommandTimeout = 456); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("Conflicting values for 'CommandTimeout' were found in OracleEntityFrameworkCoreSettings and set in DbContextOptions.", exception.Message); + } + [Fact] public void EnrichEnablesRetryByDefault() { @@ -161,7 +181,7 @@ public void EnrichPreservesDefaultWhenMaxRetryCountNotSet() } [Fact] - public void EnrichOverridesCustomRetryIfNotDisabled() + public void EnrichDoesntOverridesCustomRetry() { var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ @@ -193,8 +213,7 @@ public void EnrichOverridesCustomRetryIfNotDisabled() Assert.NotNull(extension.ExecutionStrategyFactory); var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); var retryStrategy = Assert.IsType(executionStrategy); - Assert.Equal(new WorkaroundToReadProtectedField(context).MaxRetryCount, retryStrategy.MaxRetryCount); - + Assert.Equal(456, retryStrategy.MaxRetryCount); #pragma warning restore EF1001 // Internal EF Core API usage. } @@ -235,4 +254,77 @@ public void EnrichSupportCustomOptionsLifetime() var context = host.Services.GetRequiredService() as TestDbContext; Assert.NotNull(context); } + + [Fact] + public void EnrichWithoutRetryPreservesCustomExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseOracle(ConnectionString, builder => builder.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichOracleDatabaseDbContext(settings => settings.Retry = false); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + [Fact] + public void EnrichWithRetryAndCustomExecutionStrategyThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseOracle(ConnectionString, builder => builder.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichOracleDatabaseDbContext(settings => settings.Retry = true); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("OracleEntityFrameworkCoreSettings.Retry can't be set when a custom Execution Strategy is configured.", exception.Message); + } + + [Fact] + public void EnrichWithRetryAndCustomRetryExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseOracle(ConnectionString, builder => builder.ExecutionStrategy(c => new CustomRetryExecutionStrategy(c))); + }); + + builder.EnrichOracleDatabaseDbContext(settings => settings.Retry = true); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } } diff --git a/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/CustomExecutionStrategy.cs b/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/CustomExecutionStrategy.cs new file mode 100644 index 0000000000..6bd24492a8 --- /dev/null +++ b/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/CustomExecutionStrategy.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Storage; +using Pomelo.EntityFrameworkCore.MySql.Storage.Internal; + +namespace Aspire.Pomelo.EntityFrameworkCore.MySql.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomExecutionStrategy : MySqlExecutionStrategy +{ + public CustomExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/CustomRetryExecutionStrategy.cs b/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/CustomRetryExecutionStrategy.cs new file mode 100644 index 0000000000..c7c4bfd84d --- /dev/null +++ b/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/CustomRetryExecutionStrategy.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. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Aspire.Pomelo.EntityFrameworkCore.MySql.Tests; + +#pragma warning disable EF1001 // Internal EF Core API usage. +public class CustomRetryExecutionStrategy : MySqlRetryingExecutionStrategy +{ + public const int DefaultRetryCount = 123; + + public CustomRetryExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies) + { + } + + public int RetryCount => DefaultRetryCount; +} +#pragma warning restore EF1001 // Internal EF Core API usage. diff --git a/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/EnrichMySqlTests.cs b/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/EnrichMySqlTests.cs index e6d034b1c9..98ba864824 100644 --- a/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/EnrichMySqlTests.cs +++ b/tests/Aspire.Pomelo.EntityFrameworkCore.MySql.Tests/EnrichMySqlTests.cs @@ -103,6 +103,26 @@ public void EnrichCanConfigureDbContextOptions() #pragma warning restore EF1001 // Internal EF Core API usage. } + [Fact] + public void EnrichWithConflictingCommandTimeoutThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseMySql(ConnectionString, DefaultVersion, builder => + { + builder.CommandTimeout(123); + }); + }); + + builder.EnrichMySqlDbContext(settings => settings.CommandTimeout = 456); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("Conflicting values for 'CommandTimeout' were found in PomeloEntityFrameworkCoreMySqlSettings and set in DbContextOptions.", exception.Message); + } + [Fact] public void EnrichEnablesRetryByDefault() { @@ -179,7 +199,7 @@ public void EnrichPreservesDefaultWhenMaxRetryCountNotSet() } [Fact] - public void EnrichOverridesCustomRetryIfNotDisabled() + public void EnrichDoesntOverridesCustomRetry() { var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ @@ -211,8 +231,7 @@ public void EnrichOverridesCustomRetryIfNotDisabled() Assert.NotNull(extension.ExecutionStrategyFactory); var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); var retryStrategy = Assert.IsType(executionStrategy); - Assert.Equal(new WorkaroundToReadProtectedField(context).MaxRetryCount, retryStrategy.MaxRetryCount); - + Assert.Equal(456, retryStrategy.MaxRetryCount); #pragma warning restore EF1001 // Internal EF Core API usage. } @@ -253,4 +272,77 @@ public void EnrichSupportCustomOptionsLifetime() var context = host.Services.GetRequiredService() as TestDbContext; Assert.NotNull(context); } + + [Fact] + public void EnrichWithoutRetryPreservesCustomExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseMySql(ConnectionString, DefaultVersion, builder => builder.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichMySqlDbContext(settings => settings.Retry = false); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + [Fact] + public void EnrichWithRetryAndCustomExecutionStrategyThrows() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseMySql(ConnectionString, DefaultVersion, builder => builder.ExecutionStrategy(c => new CustomExecutionStrategy(c))); + }); + + builder.EnrichMySqlDbContext(settings => settings.Retry = true); + using var host = builder.Build(); + + var exception = Assert.Throws(host.Services.GetRequiredService); + Assert.Equal("PomeloEntityFrameworkCoreMySqlSettings.Retry can't be set when a custom Execution Strategy is configured.", exception.Message); + } + + [Fact] + public void EnrichWithRetryAndCustomRetryExecutionStrategy() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Services.AddDbContextPool(optionsBuilder => + { + optionsBuilder.UseMySql(ConnectionString, DefaultVersion, builder => builder.ExecutionStrategy(c => new CustomRetryExecutionStrategy(c))); + }); + + builder.EnrichMySqlDbContext(settings => settings.Retry = true); + + using var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the retry strategy is enabled and set to its default value + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + Assert.IsType(executionStrategy); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } }