Skip to content
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

[release/8.0-preview5] Detect conflicting settings when using EnrichEF #3218

Merged
merged 8 commits into from
Mar 27, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -130,7 +131,28 @@ void UseCosmosBody(CosmosDbContextOptionsBuilder builder)

configureSettings?.Invoke(settings);

builder.PatchServiceDescriptor<TContext>();
if (settings.RequestTimeout.HasValue)
{
builder.PatchServiceDescriptor<TContext>(optionsBuilder =>
{
#pragma warning disable EF1001 // Internal EF Core API usage.
var extension = optionsBuilder.Options.FindExtension<CosmosOptionsExtension>();

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<TContext>();
}

ConfigureInstrumentation<TContext>(builder, settings);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TContext>(optionsBuilder => optionsBuilder.UseSqlServer(options =>
{
var extension = optionsBuilder.Options.FindExtension<SqlServerOptionsExtension>();

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<TContext>(optionsBuilder =>
{
optionsBuilder.UseSqlServer(options => options.EnableRetryOnFailure());
});
#pragma warning restore EF1001 // Internal EF Core API usage.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<TContext>(optionsBuilder => optionsBuilder.UseNpgsql(options =>
{
var extension = optionsBuilder.Options.FindExtension<NpgsqlOptionsExtension>();

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<TContext>(optionsBuilder => optionsBuilder.UseNpgsql(options => options.EnableRetryOnFailure()));
#pragma warning restore EF1001 // Internal EF Core API usage.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<TContext>(optionsBuilder => optionsBuilder.UseOracle(options =>
{
var extension = optionsBuilder.Options.FindExtension<OracleOptionsExtension>();

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<TContext>(optionsBuilder =>
optionsBuilder.UseOracle(options => options.ExecutionStrategy(context => new OracleRetryingExecutionStrategy(context)))
);
#pragma warning restore EF1001 // Internal EF Core API usage.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -155,23 +157,66 @@ void ConfigureDbContext(IServiceProvider serviceProvider, DbContextOptionsBuilde

void ConfigureRetry()
{
if (!settings.Retry)
{
return;
}

builder.PatchServiceDescriptor<TContext>(optionsBuilder =>
{
#pragma warning disable EF1001 // Internal EF Core API usage.
if (optionsBuilder.Options.FindExtension<MySqlOptionsExtension>() is not MySqlOptionsExtension extension
|| extension.ServerVersion is not ServerVersion serverVersion)
if (settings.Retry || settings.CommandTimeout.HasValue)
{
builder.PatchServiceDescriptor<TContext>(optionsBuilder =>
{
throw new InvalidOperationException($"A DbContextOptions<{typeof(TContext).Name}> was not found. Please ensure 'ServerVersion' was configured.");
}
if (optionsBuilder.Options.FindExtension<MySqlOptionsExtension>() 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<MySqlOptionsExtension>();

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());
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,21 @@ public void EnrichSupportCustomOptionsLifetime()
var context = host.Services.GetRequiredService<ITestDbContext>() as TestDbContext;
Assert.NotNull(context);
}

[Fact]
public void EnrichWithConflictingRequestTimeoutThrows()
{
var builder = Host.CreateEmptyApplicationBuilder(null);

builder.Services.AddDbContextPool<ITestDbContext, TestDbContext>(optionsBuilder =>
{
optionsBuilder.UseCosmos(ConnectionString, DatabaseName, builder => builder.RequestTimeout(TimeSpan.FromSeconds(123)));
});

builder.EnrichCosmosDbContext<TestDbContext>(settings => settings.RequestTimeout = TimeSpan.FromSeconds(456));
using var host = builder.Build();

var exception = Assert.Throws<InvalidOperationException>(host.Services.GetRequiredService<TestDbContext>);
Assert.Equal("Conflicting values for 'RequestTimeout' were found in EntityFrameworkCoreCosmosDBSettings and set in DbContextOptions<TestDbContext>.", exception.Message);
}
}
Original file line number Diff line number Diff line change
@@ -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.
Loading