diff --git a/src/EFCore/DbContextOptions.cs b/src/EFCore/DbContextOptions.cs index 9a6be25581d..d61afcdfba9 100644 --- a/src/EFCore/DbContextOptions.cs +++ b/src/EFCore/DbContextOptions.cs @@ -22,27 +22,56 @@ namespace Microsoft.EntityFrameworkCore /// public abstract class DbContextOptions : IDbContextOptions { + private readonly ImmutableSortedDictionary _extensionsMap; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + protected DbContextOptions() + { + _extensionsMap = ImmutableSortedDictionary.Create(TypeFullNameComparer.Instance); + } + /// - /// Initializes a new instance of the class. You normally override - /// or use a - /// to create instances of this class and it is not designed to be directly constructed in your application code. + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - /// The extensions that store the configured options. + [EntityFrameworkInternal] protected DbContextOptions( IReadOnlyDictionary extensions) { Check.NotNull(extensions, nameof(extensions)); - _extensionsMap = extensions as ImmutableSortedDictionary - ?? ImmutableSortedDictionary.Create(TypeFullNameComparer.Instance) - .AddRange(extensions); + _extensionsMap = ImmutableSortedDictionary.Create(TypeFullNameComparer.Instance) + .AddRange(extensions.Select((p, i) => new KeyValuePair(p.Key, (p.Value, i)))); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + protected DbContextOptions( + ImmutableSortedDictionary extensions) + { + Check.NotNull(extensions, nameof(extensions)); + + _extensionsMap = extensions; } /// /// Gets the extensions that store the configured options. /// public virtual IEnumerable Extensions - => ExtensionsMap.Values; + => _extensionsMap.Values.OrderBy(v => v.Ordinal).Select(v => v.Extension); /// /// Gets the extension of the specified type. Returns if no extension of the specified type is configured. @@ -51,7 +80,7 @@ public virtual IEnumerable Extensions /// The extension, or if none was found. public virtual TExtension? FindExtension() where TExtension : class, IDbContextOptionsExtension - => ExtensionsMap.TryGetValue(typeof(TExtension), out var extension) ? (TExtension)extension : null; + => _extensionsMap.TryGetValue(typeof(TExtension), out var value) ? (TExtension)value.Extension : null; /// /// Gets the extension of the specified type. Throws if no extension of the specified type is configured. @@ -80,12 +109,14 @@ public virtual TExtension GetExtension() public abstract DbContextOptions WithExtension(TExtension extension) where TExtension : class, IDbContextOptionsExtension; - private readonly ImmutableSortedDictionary _extensionsMap; - /// - /// Gets the extensions that store the configured options. + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected virtual IImmutableDictionary ExtensionsMap + [EntityFrameworkInternal] + protected virtual ImmutableSortedDictionary ExtensionsMap => _extensionsMap; /// @@ -121,7 +152,8 @@ public override bool Equals(object? obj) protected virtual bool Equals(DbContextOptions other) => _extensionsMap.Count == other._extensionsMap.Count && _extensionsMap.Zip(other._extensionsMap) - .All(p => p.First.Value.Info.ShouldUseSameServiceProvider(p.Second.Value.Info)); + .All(p => p.First.Value.Ordinal == p.Second.Value.Ordinal + && p.First.Value.Extension.Info.ShouldUseSameServiceProvider(p.Second.Value.Extension.Info)); /// public override int GetHashCode() @@ -131,7 +163,7 @@ public override int GetHashCode() foreach (var dbContextOptionsExtension in _extensionsMap) { hashCode.Add(dbContextOptionsExtension.Key); - hashCode.Add(dbContextOptionsExtension.Value.Info.GetServiceProviderHashCode()); + hashCode.Add(dbContextOptionsExtension.Value.Extension.Info.GetServiceProviderHashCode()); } return hashCode.ToHashCode(); diff --git a/src/EFCore/DbContextOptions`.cs b/src/EFCore/DbContextOptions`.cs index 07759d3bf9a..e0053a0bc9e 100644 --- a/src/EFCore/DbContextOptions`.cs +++ b/src/EFCore/DbContextOptions`.cs @@ -28,7 +28,6 @@ public class DbContextOptions : DbContextOptions /// to create instances of this class and it is not designed to be directly constructed in your application code. /// public DbContextOptions() - : this(ImmutableSortedDictionary.Create(TypeFullNameComparer.Instance)) { } @@ -44,12 +43,25 @@ public DbContextOptions( { } + private DbContextOptions( + ImmutableSortedDictionary extensions) + : base(extensions) + { + } + /// public override DbContextOptions WithExtension(TExtension extension) { Check.NotNull(extension, nameof(extension)); - return new DbContextOptions(ExtensionsMap.SetItem(extension.GetType(), extension)); + var type = extension.GetType(); + var ordinal = ExtensionsMap.Count; + if (ExtensionsMap.TryGetValue(type, out var existingValue)) + { + ordinal = existingValue.Ordinal; + } + + return new DbContextOptions(ExtensionsMap.SetItem(type, (extension, ordinal))); } /// diff --git a/test/EFCore.Specification.Tests/LoggingTestBase.cs b/test/EFCore.Specification.Tests/LoggingTestBase.cs index 11915e94154..dba0543d0b1 100644 --- a/test/EFCore.Specification.Tests/LoggingTestBase.cs +++ b/test/EFCore.Specification.Tests/LoggingTestBase.cs @@ -26,7 +26,7 @@ public void Logs_context_initialization_default_options() public void Logs_context_initialization_no_tracking() { Assert.Equal( - ExpectedMessage(DefaultOptions + "NoTracking"), + ExpectedMessage("NoTracking " + DefaultOptions), ActualMessage(s => CreateOptionsBuilder(s).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking))); } @@ -34,7 +34,7 @@ public void Logs_context_initialization_no_tracking() public void Logs_context_initialization_sensitive_data_logging() { Assert.Equal( - ExpectedMessage(DefaultOptions + "SensitiveDataLoggingEnabled"), + ExpectedMessage("SensitiveDataLoggingEnabled " + DefaultOptions), ActualMessage(s => CreateOptionsBuilder(s).EnableSensitiveDataLogging())); } diff --git a/test/EFCore.Tests/ServiceProviderCacheTest.cs b/test/EFCore.Tests/ServiceProviderCacheTest.cs index 2d7c1bfe56c..c1e7e6a290f 100644 --- a/test/EFCore.Tests/ServiceProviderCacheTest.cs +++ b/test/EFCore.Tests/ServiceProviderCacheTest.cs @@ -65,6 +65,39 @@ public void Returns_different_provider_for_different_type_of_configured_extensio loggerFactory.Log[1].Message); } + [ConditionalFact] + public void Returns_different_provider_for_extensions_configured_in_different_order() + { + var loggerFactory = new ListLoggerFactory(); + + var config1Log = new List(); + var config1Builder = new DbContextOptionsBuilder(); + ((IDbContextOptionsBuilderInfrastructure)config1Builder) + .AddOrUpdateExtension(new FakeDbContextOptionsExtension1(config1Log)); + ((IDbContextOptionsBuilderInfrastructure)config1Builder) + .AddOrUpdateExtension(new FakeDbContextOptionsExtension2(config1Log)); + config1Builder.UseLoggerFactory(loggerFactory); + config1Builder.UseInMemoryDatabase(Guid.NewGuid().ToString()); + + var config2Log = new List(); + var config2Builder = new DbContextOptionsBuilder(); + ((IDbContextOptionsBuilderInfrastructure)config2Builder) + .AddOrUpdateExtension(new FakeDbContextOptionsExtension2(config2Log)); + ((IDbContextOptionsBuilderInfrastructure)config2Builder) + .AddOrUpdateExtension(new FakeDbContextOptionsExtension1(config2Log)); + config2Builder.UseLoggerFactory(loggerFactory); + config2Builder.UseInMemoryDatabase(Guid.NewGuid().ToString()); + + var cache = new ServiceProviderCache(); + + Assert.NotSame(cache.GetOrAdd(config1Builder.Options, true), cache.GetOrAdd(config2Builder.Options, true)); + + Assert.Equal(2, loggerFactory.Log.Count); + + Assert.Equal(new[] { nameof(FakeDbContextOptionsExtension1), nameof(FakeDbContextOptionsExtension2) }, config1Log); + Assert.Equal(new[] { nameof(FakeDbContextOptionsExtension2), nameof(FakeDbContextOptionsExtension1) }, config2Log); + } + [ConditionalFact] public void Returns_same_provider_for_same_type_of_configured_extensions_and_replaced_service_types() { @@ -226,14 +259,26 @@ private static DbContextOptions CreateOptions(ILoggerFactory loggerF private class FakeDbContextOptionsExtension1 : IDbContextOptionsExtension { private DbContextOptionsExtensionInfo _info; + private readonly List _log; public string Something { get; set; } public DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); + public FakeDbContextOptionsExtension1() + : this(new List()) + { + } + + public FakeDbContextOptionsExtension1(List log) + { + _log = log; + } + public virtual void ApplyServices(IServiceCollection services) { + _log.Add(GetType().ShortDisplayName()); } public virtual void Validate(IDbContextOptions options) @@ -269,12 +314,24 @@ public override void PopulateDebugInfo(IDictionary debugInfo) private class FakeDbContextOptionsExtension2 : IDbContextOptionsExtension { private DbContextOptionsExtensionInfo _info; + private readonly List _log; public DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); + public FakeDbContextOptionsExtension2() + : this(new List()) + { + } + + public FakeDbContextOptionsExtension2(List log) + { + _log = log; + } + public virtual void ApplyServices(IServiceCollection services) { + _log.Add(GetType().ShortDisplayName()); } public virtual void Validate(IDbContextOptions options)