From 0d87f8b1ddba214fce0c60120e9dc93931a8131a Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 21 Jul 2021 20:52:18 -0700 Subject: [PATCH] Store options in ServiceProviderCache Fixes #19152 --- .../Internal/CosmosDbOptionExtension.cs | 54 +++-- .../Internal/CosmosSingletonOptions.cs | 2 +- .../Internal/InMemoryOptionsExtension.cs | 10 +- .../Internal/ProxiesOptionsExtension.cs | 8 +- .../RelationalOptionsExtension.cs | 14 +- ...lServerNetTopologySuiteOptionsExtension.cs | 5 +- .../SqliteNetTopologySuiteOptionsExtension.cs | 5 +- src/EFCore/DbContextOptions.cs | 54 ++++- src/EFCore/DbContextOptions`.cs | 18 +- .../Diagnostics/WarningsConfiguration.cs | 56 +++-- .../Infrastructure/CoreOptionsExtension.cs | 39 +++- .../DbContextOptionsExtensionInfo.cs | 13 +- src/EFCore/Internal/ServiceProviderCache.cs | 24 +- .../CosmosDbContextOptionsExtensionsTests.cs | 217 ++++-------------- .../RelationalConnectionTest.cs | 47 +++- .../LoggingTestBase.cs | 4 +- test/EFCore.Tests/DbContextOptionsTest.cs | 10 +- test/EFCore.Tests/ServiceProviderCacheTest.cs | 10 +- 18 files changed, 321 insertions(+), 269 deletions(-) diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs index ba69d143c4c..4e74a248d84 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs @@ -519,7 +519,7 @@ public virtual void Validate(IDbContextOptions options) private sealed class ExtensionInfo : DbContextOptionsExtensionInfo { private string? _logFragment; - private long? _serviceProviderHash; + private int? _serviceProviderHash; public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension) @@ -532,37 +532,57 @@ public ExtensionInfo(IDbContextOptionsExtension extension) public override bool IsDatabaseProvider => true; - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() { if (_serviceProviderHash == null) { - long hashCode; + var hashCode = new HashCode(); + if (!string.IsNullOrEmpty(Extension._connectionString)) { - hashCode = Extension._connectionString.GetHashCode(); + hashCode.Add(Extension._connectionString); } else { - hashCode = (Extension._accountEndpoint?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Extension._accountKey?.GetHashCode() ?? 0); + hashCode.Add(Extension._accountEndpoint); + hashCode.Add(Extension._accountKey); } - hashCode = (hashCode * 397) ^ (Extension._region?.GetHashCode() ?? 0); - hashCode = (hashCode * 3) ^ (Extension._connectionMode?.GetHashCode() ?? 0); - hashCode = (hashCode * 3) ^ (Extension._limitToEndpoint?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Extension._webProxy?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Extension._requestTimeout?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Extension._openTcpConnectionTimeout?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Extension._idleTcpConnectionTimeout?.GetHashCode() ?? 0); - hashCode = (hashCode * 131) ^ (Extension._gatewayModeMaxConnectionLimit?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Extension._maxTcpConnectionsPerEndpoint?.GetHashCode() ?? 0); - hashCode = (hashCode * 131) ^ (Extension._maxRequestsPerTcpConnection?.GetHashCode() ?? 0); - _serviceProviderHash = hashCode; + hashCode.Add(Extension._region); + hashCode.Add(Extension._connectionMode); + hashCode.Add(Extension._limitToEndpoint); + hashCode.Add(Extension._enableContentResponseOnWrite); + hashCode.Add(Extension._webProxy); + hashCode.Add(Extension._requestTimeout); + hashCode.Add(Extension._openTcpConnectionTimeout); + hashCode.Add(Extension._idleTcpConnectionTimeout); + hashCode.Add(Extension._gatewayModeMaxConnectionLimit); + hashCode.Add(Extension._maxTcpConnectionsPerEndpoint); + hashCode.Add(Extension._maxRequestsPerTcpConnection); + + _serviceProviderHash = hashCode.ToHashCode(); } return _serviceProviderHash.Value; } + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => other is ExtensionInfo otherInfo + && Extension._connectionString == otherInfo.Extension._connectionString + && Extension._accountEndpoint == otherInfo.Extension._accountEndpoint + && Extension._accountKey == otherInfo.Extension._accountKey + && Extension._region == otherInfo.Extension._region + && Extension._connectionMode == otherInfo.Extension._connectionMode + && Extension._limitToEndpoint == otherInfo.Extension._limitToEndpoint + && Extension._enableContentResponseOnWrite == otherInfo.Extension._enableContentResponseOnWrite + && Extension._webProxy == otherInfo.Extension._webProxy + && Extension._requestTimeout == otherInfo.Extension._requestTimeout + && Extension._openTcpConnectionTimeout == otherInfo.Extension._openTcpConnectionTimeout + && Extension._idleTcpConnectionTimeout == otherInfo.Extension._idleTcpConnectionTimeout + && Extension._gatewayModeMaxConnectionLimit == otherInfo.Extension._gatewayModeMaxConnectionLimit + && Extension._maxTcpConnectionsPerEndpoint == otherInfo.Extension._maxTcpConnectionsPerEndpoint + && Extension._maxRequestsPerTcpConnection == otherInfo.Extension._maxRequestsPerTcpConnection; + public override void PopulateDebugInfo(IDictionary debugInfo) { Check.NotNull(debugInfo, nameof(debugInfo)); diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs index 20013f11e7f..1bd874266f2 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs @@ -153,6 +153,7 @@ public virtual void Initialize(IDbContextOptions options) ConnectionString = cosmosOptions.ConnectionString; Region = cosmosOptions.Region; LimitToEndpoint = cosmosOptions.LimitToEndpoint; + EnableContentResponseOnWrite = cosmosOptions.EnableContentResponseOnWrite; ConnectionMode = cosmosOptions.ConnectionMode; WebProxy = cosmosOptions.WebProxy; RequestTimeout = cosmosOptions.RequestTimeout; @@ -161,7 +162,6 @@ public virtual void Initialize(IDbContextOptions options) GatewayModeMaxConnectionLimit = cosmosOptions.GatewayModeMaxConnectionLimit; MaxTcpConnectionsPerEndpoint = cosmosOptions.MaxTcpConnectionsPerEndpoint; MaxRequestsPerTcpConnection = cosmosOptions.MaxRequestsPerTcpConnection; - EnableContentResponseOnWrite = cosmosOptions.EnableContentResponseOnWrite; } } diff --git a/src/EFCore.InMemory/Infrastructure/Internal/InMemoryOptionsExtension.cs b/src/EFCore.InMemory/Infrastructure/Internal/InMemoryOptionsExtension.cs index 4e54eded2c1..e02e98f994e 100644 --- a/src/EFCore.InMemory/Infrastructure/Internal/InMemoryOptionsExtension.cs +++ b/src/EFCore.InMemory/Infrastructure/Internal/InMemoryOptionsExtension.cs @@ -186,12 +186,16 @@ public override string LogFragment } } - public override long GetServiceProviderHashCode() - => Extension._databaseRoot?.GetHashCode() ?? 0L; + public override int GetServiceProviderHashCode() + => Extension._databaseRoot?.GetHashCode() ?? 0; + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => other is ExtensionInfo otherInfo + && Extension._databaseRoot == otherInfo.Extension._databaseRoot; public override void PopulateDebugInfo(IDictionary debugInfo) => debugInfo["InMemoryDatabase:DatabaseRoot"] - = (Extension._databaseRoot?.GetHashCode() ?? 0L).ToString(CultureInfo.InvariantCulture); + = (Extension._databaseRoot?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs b/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs index 905cbcfc72f..4e8a9021f9b 100644 --- a/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs +++ b/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs @@ -189,8 +189,12 @@ public override string LogFragment ? "using change tracking proxies " : ""; - public override long GetServiceProviderHashCode() - => Extension.UseProxies ? 541 : 0; + public override int GetServiceProviderHashCode() + => Extension.UseProxies.GetHashCode(); + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => other is ExtensionInfo otherInfo + && Extension.UseProxies == otherInfo.Extension.UseProxies; public override void PopulateDebugInfo(IDictionary debugInfo) { diff --git a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs index 2c2c33c43b3..6d1f9c52156 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs @@ -428,12 +428,22 @@ public override bool IsDatabaseProvider /// /// Returns a hash code created from any options that would cause a new - /// to be needed. Most extensions do not have any such options and should return zero. + /// to be needed. For example, if the options affect a singleton service. However most extensions do not + /// have any such options and should return zero. /// /// A hash over options that require a new service provider when changed. - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() => 0; + /// + /// Returns a value indicating whether all of the options used in + /// are the same as in the given extension. + /// + /// The other extension. + /// A value indicating whether all of the options that require a new service provider are the same. + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + /// /// A message fragment for logging typically containing information about /// any useful non-default options that have been configured. diff --git a/src/EFCore.SqlServer.NTS/Infrastructure/Internal/SqlServerNetTopologySuiteOptionsExtension.cs b/src/EFCore.SqlServer.NTS/Infrastructure/Internal/SqlServerNetTopologySuiteOptionsExtension.cs index b9b7b965c0d..d459d8ed4b2 100644 --- a/src/EFCore.SqlServer.NTS/Infrastructure/Internal/SqlServerNetTopologySuiteOptionsExtension.cs +++ b/src/EFCore.SqlServer.NTS/Infrastructure/Internal/SqlServerNetTopologySuiteOptionsExtension.cs @@ -73,9 +73,12 @@ public ExtensionInfo(IDbContextOptionsExtension extension) public override bool IsDatabaseProvider => false; - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() => 0; + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + public override void PopulateDebugInfo(IDictionary debugInfo) => debugInfo["SqlServer:" + nameof(SqlServerNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite)] = "1"; diff --git a/src/EFCore.Sqlite.NTS/Infrastructure/Internal/SqliteNetTopologySuiteOptionsExtension.cs b/src/EFCore.Sqlite.NTS/Infrastructure/Internal/SqliteNetTopologySuiteOptionsExtension.cs index dd31d21eaaa..42cdff42d69 100644 --- a/src/EFCore.Sqlite.NTS/Infrastructure/Internal/SqliteNetTopologySuiteOptionsExtension.cs +++ b/src/EFCore.Sqlite.NTS/Infrastructure/Internal/SqliteNetTopologySuiteOptionsExtension.cs @@ -76,9 +76,12 @@ public override bool IsDatabaseProvider public override string LogFragment => "using NetTopologySuite "; - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() => 0; + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + public override void PopulateDebugInfo(IDictionary debugInfo) => debugInfo["NetTopologySuite"] = "1"; } diff --git a/src/EFCore/DbContextOptions.cs b/src/EFCore/DbContextOptions.cs index d28ea1daa4d..9e5bd6dec85 100644 --- a/src/EFCore/DbContextOptions.cs +++ b/src/EFCore/DbContextOptions.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore @@ -27,23 +30,25 @@ protected DbContextOptions( { Check.NotNull(extensions, nameof(extensions)); - _extensions = extensions; + _extensionsMap = extensions as ImmutableSortedDictionary + ?? ImmutableSortedDictionary.Create(TypeFullNameComparer.Instance) + .AddRange(extensions); } /// /// Gets the extensions that store the configured options. /// public virtual IEnumerable Extensions - => _extensions.Values; + => ExtensionsMap.Values; /// - /// Gets the extension of the specified type. Returns null if no extension of the specified type is configured. + /// Gets the extension of the specified type. Returns if no extension of the specified type is configured. /// /// The type of the extension to get. - /// The extension, or null if none was found. + /// The extension, or if none was found. public virtual TExtension? FindExtension() where TExtension : class, IDbContextOptionsExtension - => _extensions.TryGetValue(typeof(TExtension), out var extension) ? (TExtension)extension : null; + => ExtensionsMap.TryGetValue(typeof(TExtension), out var extension) ? (TExtension)extension : null; /// /// Gets the extension of the specified type. Throws if no extension of the specified type is configured. @@ -72,7 +77,13 @@ public virtual TExtension GetExtension() public abstract DbContextOptions WithExtension(TExtension extension) where TExtension : class, IDbContextOptionsExtension; - private readonly IReadOnlyDictionary _extensions; + private readonly ImmutableSortedDictionary _extensionsMap; + + /// + /// Gets the extensions that store the configured options. + /// + protected virtual IImmutableDictionary ExtensionsMap + => _extensionsMap; /// /// The type of context that these options are for. Will return if the @@ -91,5 +102,36 @@ public virtual void Freeze() /// configured with . /// public virtual bool IsFrozen { get; private set; } + + /// + public override bool Equals(object? obj) + => ReferenceEquals(this, obj) + || (obj is DbContextOptions otherOptions && Equals(otherOptions)); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// + /// if the specified object is equal to the current object; otherwise, . + /// + 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)); + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var dbContextOptionsExtension in _extensionsMap) + { + hashCode.Add(dbContextOptionsExtension.Key); + hashCode.Add(dbContextOptionsExtension.Value.Info.GetServiceProviderHashCode()); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/EFCore/DbContextOptions`.cs b/src/EFCore/DbContextOptions`.cs index 26062e357aa..270c0d8780d 100644 --- a/src/EFCore/DbContextOptions`.cs +++ b/src/EFCore/DbContextOptions`.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Collections.Immutable; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore @@ -24,7 +25,7 @@ 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() - : base(new Dictionary()) + : this(ImmutableSortedDictionary.Create(TypeFullNameComparer.Instance)) { } @@ -40,21 +41,12 @@ public DbContextOptions( { } - /// - /// Adds the given extension to the underlying options and creates a new - /// with the extension added. - /// - /// The type of extension to be added. - /// The extension to be added. - /// The new options instance with the given extension added. + /// public override DbContextOptions WithExtension(TExtension extension) { Check.NotNull(extension, nameof(extension)); - var extensions = Extensions.ToDictionary(p => p.GetType(), p => p); - extensions[typeof(TExtension)] = extension; - - return new DbContextOptions(extensions); + return new DbContextOptions(ExtensionsMap.SetItem(extension.GetType(), extension)); } /// diff --git a/src/EFCore/Diagnostics/WarningsConfiguration.cs b/src/EFCore/Diagnostics/WarningsConfiguration.cs index bbda125990d..3536f06385d 100644 --- a/src/EFCore/Diagnostics/WarningsConfiguration.cs +++ b/src/EFCore/Diagnostics/WarningsConfiguration.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.Extensions.Logging; @@ -20,11 +21,12 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// public class WarningsConfiguration { - private Dictionary? _explicitBehaviors = new(); + private ImmutableSortedDictionary _explicitBehaviors + = ImmutableSortedDictionary.Empty; private WarningBehavior _defaultBehavior = WarningBehavior.Log; - private long? _serviceProviderHash; + private int? _serviceProviderHash; /// /// Creates a new, empty configuration, with all options set to their defaults. @@ -85,11 +87,11 @@ public virtual WarningsConfiguration WithExplicit( { var clone = Clone(); - clone._explicitBehaviors = _explicitBehaviors is null ? new() : new(_explicitBehaviors); - + var builder = ImmutableSortedDictionary.CreateBuilder(); + builder.AddRange(clone._explicitBehaviors); foreach (var eventId in eventIds) { - if (clone._explicitBehaviors.TryGetValue(eventId.Id, out var pair)) + if (_explicitBehaviors.TryGetValue(eventId.Id, out var pair)) { pair = (warningBehavior, pair.Level); } @@ -98,9 +100,11 @@ public virtual WarningsConfiguration WithExplicit( pair = (warningBehavior, null); } - clone._explicitBehaviors[eventId.Id] = pair; + builder[eventId.Id] = pair; } + clone._explicitBehaviors = builder.ToImmutable(); + return clone; } @@ -115,13 +119,16 @@ public virtual WarningsConfiguration WithExplicit( { var clone = Clone(); - clone._explicitBehaviors = _explicitBehaviors is null ? new() : new(_explicitBehaviors); + var builder = ImmutableSortedDictionary.CreateBuilder(); + builder.AddRange(clone._explicitBehaviors); foreach (var (id, level) in eventsAndLevels) { - clone._explicitBehaviors[id.Id] = (WarningBehavior.Log, level); + builder[id.Id] = (WarningBehavior.Log, level); } + clone._explicitBehaviors = builder.ToImmutable(); + return clone; } @@ -130,7 +137,7 @@ public virtual WarningsConfiguration WithExplicit( /// if no explicit behavior has been set. /// public virtual WarningBehavior? GetBehavior(EventId eventId) - => _explicitBehaviors is not null && _explicitBehaviors.TryGetValue(eventId.Id, out var warningBehavior) + => _explicitBehaviors.TryGetValue(eventId.Id, out var warningBehavior) ? warningBehavior.Behavior : null; @@ -140,7 +147,7 @@ public virtual WarningsConfiguration WithExplicit( /// /// The set for the given event ID. public virtual LogLevel? GetLevel(EventId eventId) - => _explicitBehaviors is not null && _explicitBehaviors.TryGetValue(eventId.Id, out var warningBehavior) + => _explicitBehaviors.TryGetValue(eventId.Id, out var warningBehavior) ? warningBehavior.Level : null; @@ -153,31 +160,40 @@ public virtual WarningsConfiguration WithExplicit( /// The behavior to set. /// A new instance with the behavior set, or this instance if a behavior was already set. public virtual WarningsConfiguration TryWithExplicit(EventId eventId, WarningBehavior warningBehavior) - => _explicitBehaviors is not null && _explicitBehaviors.ContainsKey(eventId.Id) + => _explicitBehaviors.ContainsKey(eventId.Id) ? this : WithExplicit(new[] { eventId }, warningBehavior); + /// + /// Returns a value indicating whether all of the options used in + /// are the same as in the given extension. + /// + /// The other configuration object. + /// A value indicating whether all of the options that require a new service provider are the same. + public virtual bool ShouldUseSameServiceProvider(WarningsConfiguration other) + => _defaultBehavior == other._defaultBehavior + && _explicitBehaviors.Count == other._explicitBehaviors.Count + && _explicitBehaviors.SequenceEqual(other._explicitBehaviors); + /// /// Returns a hash code created from any options that would cause a new /// to be needed. /// /// A hash over options that require a new service provider when changed. - public virtual long GetServiceProviderHashCode() + public virtual int GetServiceProviderHashCode() { if (_serviceProviderHash == null) { - var hashCode = (long)_defaultBehavior.GetHashCode(); + var hashCode = new HashCode(); + hashCode.Add(_defaultBehavior); - if (_explicitBehaviors != null) + foreach (var explicitBehavior in _explicitBehaviors) { - hashCode = _explicitBehaviors - .OrderBy(b => b.Key) - .Aggregate( - hashCode, - (t, e) => (t * 397) ^ (((long)e.Value.GetHashCode() * 3163) ^ (long)e.Key.GetHashCode())); + hashCode.Add(explicitBehavior.Key); + hashCode.Add(explicitBehavior.Value); } - _serviceProviderHash = hashCode; + _serviceProviderHash = hashCode.ToHashCode(); } return _serviceProviderHash.Value; diff --git a/src/EFCore/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index 6eed3272bd4..ad859925502 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -37,7 +37,7 @@ public class CoreOptionsExtension : IDbContextOptionsExtension private bool _detailedErrorsEnabled; private bool _threadSafetyChecksEnabled = true; private QueryTrackingBehavior _queryTrackingBehavior = QueryTrackingBehavior.TrackAll; - private IDictionary<(Type, Type?), Type>? _replacedServices; + private Dictionary<(Type, Type?), Type>? _replacedServices; private int? _maxPoolSize; private TimeSpan _loggingCacheTime = DefaultLoggingCacheTime; private bool _serviceProviderCachingEnabled = true; @@ -436,7 +436,7 @@ public virtual bool ServiceProviderCachingEnabled /// The options set from the method. /// public virtual IReadOnlyDictionary<(Type, Type?), Type>? ReplacedServices - => (IReadOnlyDictionary<(Type, Type?), Type>?)_replacedServices; + => _replacedServices; /// /// The option set from the @@ -525,7 +525,7 @@ public virtual void Validate(IDbContextOptions options) private sealed class ExtensionInfo : DbContextOptionsExtensionInfo { - private long? _serviceProviderHash; + private int? _serviceProviderHash; private string? _logFragment; public ExtensionInfo(CoreOptionsExtension extension) @@ -610,26 +610,43 @@ public override void PopulateDebugInfo(IDictionary debugInfo) } } - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() { if (_serviceProviderHash == null) { - var hashCode = Extension.GetMemoryCache()?.GetHashCode() ?? 0L; - hashCode = (hashCode * 3) ^ Extension._sensitiveDataLoggingEnabled.GetHashCode(); - hashCode = (hashCode * 3) ^ Extension._detailedErrorsEnabled.GetHashCode(); - hashCode = (hashCode * 3) ^ Extension._threadSafetyChecksEnabled.GetHashCode(); - hashCode = (hashCode * 1073742113) ^ Extension._warningsConfiguration.GetServiceProviderHashCode(); + var hashCode = new HashCode(); + hashCode.Add(Extension.GetMemoryCache()); + hashCode.Add(Extension._sensitiveDataLoggingEnabled); + hashCode.Add(Extension._detailedErrorsEnabled); + hashCode.Add(Extension._threadSafetyChecksEnabled); + hashCode.Add(Extension._warningsConfiguration.GetServiceProviderHashCode()); if (Extension._replacedServices != null) { - hashCode = Extension._replacedServices.Aggregate(hashCode, (t, e) => (t * 397) ^ e.Value.GetHashCode()); + foreach (var replacedService in Extension._replacedServices) + { + hashCode.Add(replacedService.Value); + } } - _serviceProviderHash = hashCode; + _serviceProviderHash = hashCode.ToHashCode(); } return _serviceProviderHash.Value; } + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => other is ExtensionInfo otherInfo + && Extension.GetMemoryCache() == otherInfo.Extension.GetMemoryCache() + && Extension._sensitiveDataLoggingEnabled == otherInfo.Extension._sensitiveDataLoggingEnabled + && Extension._detailedErrorsEnabled == otherInfo.Extension._detailedErrorsEnabled + && Extension._threadSafetyChecksEnabled == otherInfo.Extension._threadSafetyChecksEnabled + && Extension._warningsConfiguration.ShouldUseSameServiceProvider(otherInfo.Extension._warningsConfiguration) + && (Extension._replacedServices == otherInfo.Extension._replacedServices + || (Extension._replacedServices != null + && otherInfo.Extension._replacedServices != null + && Extension._replacedServices.Count == otherInfo.Extension._replacedServices.Count + && Extension._replacedServices.SequenceEqual(otherInfo.Extension._replacedServices))); } } } diff --git a/src/EFCore/Infrastructure/DbContextOptionsExtensionInfo.cs b/src/EFCore/Infrastructure/DbContextOptionsExtensionInfo.cs index 87e5a4ba57b..8ead79277a5 100644 --- a/src/EFCore/Infrastructure/DbContextOptionsExtensionInfo.cs +++ b/src/EFCore/Infrastructure/DbContextOptionsExtensionInfo.cs @@ -42,10 +42,19 @@ protected DbContextOptionsExtensionInfo(IDbContextOptionsExtension extension) /// /// Returns a hash code created from any options that would cause a new - /// to be needed. Most extensions do not have any such options and should return zero. + /// to be needed. For example, if the options affect a singleton service. However most extensions do not + /// have any such options and should return zero. /// /// A hash over options that require a new service provider when changed. - public abstract long GetServiceProviderHashCode(); + public abstract int GetServiceProviderHashCode(); + + /// + /// Returns a value indicating whether all of the options used in + /// are the same as in the given extension. + /// + /// The other extension. + /// A value indicating whether all of the options that require a new service provider are the same. + public abstract bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other); /// /// Populates a dictionary of information that may change between uses of the diff --git a/src/EFCore/Internal/ServiceProviderCache.cs b/src/EFCore/Internal/ServiceProviderCache.cs index 7e50c835af1..bc71c2ec033 100644 --- a/src/EFCore/Internal/ServiceProviderCache.cs +++ b/src/EFCore/Internal/ServiceProviderCache.cs @@ -21,7 +21,7 @@ namespace Microsoft.EntityFrameworkCore.Internal /// public class ServiceProviderCache { - private readonly ConcurrentDictionary DebugInfo)> + private readonly ConcurrentDictionary DebugInfo)> _configurations = new(); /// @@ -62,16 +62,14 @@ public virtual IServiceProvider GetOrAdd(IDbContextOptions options, bool provide if (coreOptionsExtension?.ServiceProviderCachingEnabled == false) { - return BuildServiceProvider().ServiceProvider; + return BuildServiceProvider(options, _configurations).ServiceProvider; } - var key = options.Extensions - .OrderBy(e => e.GetType().Name) - .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.Info.GetServiceProviderHashCode()); + return _configurations.GetOrAdd(options, BuildServiceProvider, _configurations).ServiceProvider; - return _configurations.GetOrAdd(key, k => BuildServiceProvider()).ServiceProvider; - - (IServiceProvider ServiceProvider, IDictionary DebugInfo) BuildServiceProvider() + static (IServiceProvider ServiceProvider, IDictionary DebugInfo) BuildServiceProvider( + IDbContextOptions options, + ConcurrentDictionary DebugInfo)> configurations) { ValidateOptions(options); @@ -86,7 +84,7 @@ public virtual IServiceProvider GetOrAdd(IDbContextOptions options, bool provide var services = new ServiceCollection(); var hasProvider = ApplyServices(options, services); - var replacedServices = coreOptionsExtension?.ReplacedServices; + var replacedServices = options.FindExtension()?.ReplacedServices; if (replacedServices != null) { var updatedServices = new ServiceCollection(); @@ -136,7 +134,7 @@ public virtual IServiceProvider GetOrAdd(IDbContextOptions options, bool provide loggingDefinitions, new NullDbContextLogger()); - if (_configurations.Count == 0) + if (configurations.Count == 0) { logger.ServiceProviderCreated(serviceProvider); } @@ -144,12 +142,12 @@ public virtual IServiceProvider GetOrAdd(IDbContextOptions options, bool provide { logger.ServiceProviderDebugInfo( debugInfo, - _configurations.Values.Select(v => v.DebugInfo).ToList()); + configurations.Values.Select(v => v.DebugInfo).ToList()); - if (_configurations.Count >= 20) + if (configurations.Count >= 20) { logger.ManyServiceProvidersCreatedWarning( - _configurations.Values.Select(e => e.ServiceProvider).ToList()); + configurations.Values.Select(e => e.ServiceProvider).ToList()); } } diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs index d0f3606b7d6..6267fbd4aa9 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs @@ -5,7 +5,9 @@ using System.Net; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Xunit; // ReSharper disable once CheckNamespace @@ -29,198 +31,73 @@ public void Throws_with_multiple_providers_new_when_no_provider() } [ConditionalFact] - public void Can_create_options_with_specified_region() + public void Can_create_options_with_valid_values() { - var regionName = Regions.EastAsia; - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.Region(regionName); }); - - var extension = options - .Options.FindExtension(); - - Assert.Equal(regionName, extension.Region); - } - - [ConditionalFact] - public void Can_create_options_with_wrong_region() - { - var regionName = "FakeRegion"; - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.Region(regionName); }); - - var extension = options - .Options.FindExtension(); - + Test(o => o.Region(Regions.EastAsia), o => Assert.Equal(Regions.EastAsia, o.Region)); // The region will be validated by the Cosmos SDK, because the region list is not constant - Assert.Equal(regionName, extension.Region); - } - - [ConditionalFact] - public void Can_create_options_with_correct_connection_mode() - { - var connectionMode = ConnectionMode.Direct; - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.ConnectionMode(connectionMode); }); - - var extension = options.Options.FindExtension(); - - Assert.Equal(connectionMode, extension.ConnectionMode); - } - - [ConditionalFact] - public void Throws_if_wrong_connection_mode() - { - var connectionMode = (ConnectionMode)958410610; - var options = Assert.Throws( - () => - new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.ConnectionMode(connectionMode); })); - } + Test(o => o.Region("FakeRegion"), o => Assert.Equal("FakeRegion", o.Region)); + Test(o => o.ConnectionMode(ConnectionMode.Direct), o => Assert.Equal(ConnectionMode.Direct, o.ConnectionMode)); + Test(o => o.GatewayModeMaxConnectionLimit(3), o => Assert.Equal(3, o.GatewayModeMaxConnectionLimit)); + Test(o => o.MaxRequestsPerTcpConnection(3), o => Assert.Equal(3, o.MaxRequestsPerTcpConnection)); + Test(o => o.MaxTcpConnectionsPerEndpoint(3), o => Assert.Equal(3, o.MaxTcpConnectionsPerEndpoint)); + Test(o => o.LimitToEndpoint(), o => Assert.True(o.LimitToEndpoint)); + Test(o => o.ContentResponseOnWriteEnabled(), o => Assert.True(o.EnableContentResponseOnWrite)); - [ConditionalFact] - public void Can_create_options_and_limit_to_endpoint() - { - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.LimitToEndpoint(); }); - - var extension = options.Options.FindExtension(); - - Assert.True(extension.LimitToEndpoint); - } - - [ConditionalFact] - public void Can_create_options_with_web_proxy() - { var webProxy = new WebProxy(); - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.WebProxy(webProxy); }); - - var extension = options.Options.FindExtension(); - - Assert.Same(webProxy, extension.WebProxy); - } - - [ConditionalFact] - public void Can_create_options_with_request_timeout() - { - var requestTimeout = TimeSpan.FromMinutes(3); - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.RequestTimeout(requestTimeout); }); - - var extension = options.Options.FindExtension(); - - Assert.Equal(requestTimeout, extension.RequestTimeout); - } - - [ConditionalFact] - public void Can_create_options_with_open_tcp_connection_timeout() - { - var timeout = TimeSpan.FromMinutes(3); - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.OpenTcpConnectionTimeout(timeout); }); - - var extension = options.Options.FindExtension(); - - Assert.Equal(timeout, extension.OpenTcpConnectionTimeout); - } - - [ConditionalFact] - public void Can_create_options_with_idle_tcp_connection_timeout() - { - var timeout = TimeSpan.FromMinutes(3); - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.IdleTcpConnectionTimeout(timeout); }); - - var extension = options.Options.FindExtension(); - - Assert.Equal(timeout, extension.IdleTcpConnectionTimeout); + Test(o => o.WebProxy(webProxy), o => Assert.Same(webProxy, o.WebProxy)); + Test( + o => o.ExecutionStrategy(d => new CosmosExecutionStrategy(d)), + o => Assert.IsType(o.ExecutionStrategyFactory(null))); + Test(o => o.RequestTimeout(TimeSpan.FromMinutes(3)), o => Assert.Equal(TimeSpan.FromMinutes(3), o.RequestTimeout)); + Test( + o => o.OpenTcpConnectionTimeout(TimeSpan.FromMinutes(3)), + o => Assert.Equal(TimeSpan.FromMinutes(3), o.OpenTcpConnectionTimeout)); + Test( + o => o.IdleTcpConnectionTimeout(TimeSpan.FromMinutes(3)), + o => Assert.Equal(TimeSpan.FromMinutes(3), o.IdleTcpConnectionTimeout)); } [ConditionalFact] - public void Can_create_options_with_gateway_mode_max_connection_limit() + public void Throws_for_invalid_values() { - var connectionLimit = 3; - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.GatewayModeMaxConnectionLimit(connectionLimit); }); - - var extension = options.Options.FindExtension(); - - Assert.Equal(connectionLimit, extension.GatewayModeMaxConnectionLimit); + Throws(o => o.ConnectionMode((ConnectionMode)958410610)); } - [ConditionalFact] - public void Can_create_options_with_max_tcp_connections_per_endpoint() + private void Test( + Action cosmosOptionsAction, + Action extensionAssert) { - var connectionLimit = 3; var options = new DbContextOptionsBuilder().UseCosmos( "serviceEndPoint", "authKeyOrResourceToken", "databaseName", - o => { o.MaxTcpConnectionsPerEndpoint(connectionLimit); }); + cosmosOptionsAction); - var extension = options.Options.FindExtension(); + var extension = options + .Options.FindExtension(); - Assert.Equal(connectionLimit, extension.MaxTcpConnectionsPerEndpoint); - } + extensionAssert(extension); - [ConditionalFact] - public void Can_create_options_with_max_requests_per_tcp_connection() - { - var requestLimit = 3; - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.MaxRequestsPerTcpConnection(requestLimit); }); - - var extension = options.Options.FindExtension(); + var clone = new DbContextOptionsBuilder().UseCosmos( + "serviceEndPoint", + "authKeyOrResourceToken", + "databaseName", + cosmosOptionsAction) + .Options.FindExtension(); - Assert.Equal(requestLimit, extension.MaxRequestsPerTcpConnection); + Assert.Equal(extension.Info.GetServiceProviderHashCode(), clone.Info.GetServiceProviderHashCode()); + Assert.True(extension.Info.ShouldUseSameServiceProvider(clone.Info)); } - [ConditionalFact] - public void Can_create_options_with_content_response_on_write_enabled() + private void Throws(Action cosmosOptionsAction) + where T : Exception { - var enabled = true; - var options = new DbContextOptionsBuilder().UseCosmos( - "serviceEndPoint", - "authKeyOrResourceToken", - "databaseName", - o => { o.ContentResponseOnWriteEnabled(enabled); }); - - var extension = options.Options.FindExtension(); - - Assert.Equal(enabled, extension.EnableContentResponseOnWrite); + Assert.Throws( + () => new DbContextOptionsBuilder().UseCosmos( + "serviceEndPoint", + "authKeyOrResourceToken", + "databaseName", + cosmosOptionsAction)); } } } diff --git a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs index 83eaae52ecf..4e6f5901dc8 100644 --- a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs +++ b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Data; using System.Threading; using System.Threading.Tasks; @@ -876,7 +877,51 @@ public void Throws_if_multiple_relational_stores_configured() () => new FakeRelationalConnection( CreateOptions( new FakeRelationalOptionsExtension(), - new FakeRelationalOptionsExtension()))).Message); + new AnotherFakeRelationalOptionsExtension()))).Message); + } + + private class AnotherFakeRelationalOptionsExtension : RelationalOptionsExtension + { + private DbContextOptionsExtensionInfo _info; + + public AnotherFakeRelationalOptionsExtension() + { + } + + protected AnotherFakeRelationalOptionsExtension(AnotherFakeRelationalOptionsExtension copyFrom) + : base(copyFrom) + { + } + + public override DbContextOptionsExtensionInfo Info + => _info ??= new ExtensionInfo(this); + + protected override RelationalOptionsExtension Clone() + => new AnotherFakeRelationalOptionsExtension(this); + + public override void ApplyServices(IServiceCollection services) + => AddEntityFrameworkRelationalDatabase(services); + + public static IServiceCollection AddEntityFrameworkRelationalDatabase(IServiceCollection serviceCollection) + { + var builder = new EntityFrameworkRelationalServicesBuilder(serviceCollection); + + builder.TryAddCoreServices(); + + return serviceCollection; + } + + private sealed class ExtensionInfo : RelationalExtensionInfo + { + public ExtensionInfo(IDbContextOptionsExtension extension) + : base(extension) + { + } + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + } + } } [ConditionalFact] diff --git a/test/EFCore.Specification.Tests/LoggingTestBase.cs b/test/EFCore.Specification.Tests/LoggingTestBase.cs index 11795a680f9..f714b90c75d 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("NoTracking " + DefaultOptions), + ExpectedMessage(DefaultOptions + "NoTracking"), 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("SensitiveDataLoggingEnabled " + DefaultOptions), + ExpectedMessage(DefaultOptions + "SensitiveDataLoggingEnabled"), ActualMessage(s => CreateOptionsBuilder(s).EnableSensitiveDataLogging())); } diff --git a/test/EFCore.Tests/DbContextOptionsTest.cs b/test/EFCore.Tests/DbContextOptionsTest.cs index f81c06e8a73..a4ad520eb3b 100644 --- a/test/EFCore.Tests/DbContextOptionsTest.cs +++ b/test/EFCore.Tests/DbContextOptionsTest.cs @@ -162,9 +162,12 @@ public ExtensionInfo(IDbContextOptionsExtension extension) public override bool IsDatabaseProvider => false; - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() => 0; + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + public override string LogFragment => ""; @@ -200,9 +203,12 @@ public ExtensionInfo(IDbContextOptionsExtension extension) public override bool IsDatabaseProvider => true; - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() => 0; + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + public override string LogFragment => ""; diff --git a/test/EFCore.Tests/ServiceProviderCacheTest.cs b/test/EFCore.Tests/ServiceProviderCacheTest.cs index 66ee55b2978..d318271afdf 100644 --- a/test/EFCore.Tests/ServiceProviderCacheTest.cs +++ b/test/EFCore.Tests/ServiceProviderCacheTest.cs @@ -250,9 +250,12 @@ public ExtensionInfo(IDbContextOptionsExtension extension) public override bool IsDatabaseProvider => false; - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() => 0; + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + public override string LogFragment => ""; @@ -288,9 +291,12 @@ public ExtensionInfo(IDbContextOptionsExtension extension) public override bool IsDatabaseProvider => false; - public override long GetServiceProviderHashCode() + public override int GetServiceProviderHashCode() => 0; + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + public override string LogFragment => "";