diff --git a/src/EFCore/DbContextOptionsBuilder.cs b/src/EFCore/DbContextOptionsBuilder.cs index a58852a5ace..4f69b57366a 100644 --- a/src/EFCore/DbContextOptionsBuilder.cs +++ b/src/EFCore/DbContextOptionsBuilder.cs @@ -504,7 +504,7 @@ public virtual DbContextOptionsBuilder ConfigureWarnings( /// /// - /// Replaces the internal Entity Framework implementation of a service contract with a different + /// Replaces all internal Entity Framework implementations of a service contract with a different /// implementation. /// /// @@ -524,6 +524,34 @@ public virtual DbContextOptionsBuilder ReplaceService where TImplementation : TService => WithOption(e => e.WithReplacedService(typeof(TService), typeof(TImplementation))); + /// + /// + /// Replaces the internal Entity Framework implementation of a specific implementation of a service contract + /// with a different implementation. + /// + /// + /// This method is useful for replacing a single instance of services that can be legitimately registered + /// multiple times in the EF internal service provider. + /// + /// + /// This method can only be used when EF is building and managing its internal service provider. + /// If the service provider is being built externally and passed to + /// , then replacement services should be configured on + /// that service provider before it is passed to EF. + /// + /// + /// The replacement service gets the same scope as the EF service that it is replacing. + /// + /// + /// The type (usually an interface) that defines the contract of the service to replace. + /// The current implementation type for the service. + /// The new implementation type for the service. + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder ReplaceService() + where TCurrentImplementation : TService + where TNewImplementation : TService + => WithOption(e => e.WithReplacedService(typeof(TService), typeof(TNewImplementation), typeof(TCurrentImplementation))); + /// /// /// Adds instances to those registered on the context. diff --git a/src/EFCore/DbContextOptionsBuilder`.cs b/src/EFCore/DbContextOptionsBuilder`.cs index 8c54b18b34f..e306646e5cb 100644 --- a/src/EFCore/DbContextOptionsBuilder`.cs +++ b/src/EFCore/DbContextOptionsBuilder`.cs @@ -385,7 +385,7 @@ public DbContextOptionsBuilder([NotNull] DbContextOptions options) /// /// - /// Replaces the internal Entity Framework implementation of a service contract with a different + /// Replaces all internal Entity Framework implementations of a service contract with a different /// implementation. /// /// @@ -404,5 +404,85 @@ public DbContextOptionsBuilder([NotNull] DbContextOptions options) public new virtual DbContextOptionsBuilder ReplaceService() where TImplementation : TService => (DbContextOptionsBuilder)base.ReplaceService(); + + /// + /// + /// Replaces the internal Entity Framework implementation of a specific implementation of a service contract + /// with a different implementation. + /// + /// + /// This method is useful for replacing a single instance of services that can be legitimately registered + /// multiple times in the EF internal service provider. + /// + /// + /// This method can only be used when EF is building and managing its internal service provider. + /// If the service provider is being built externally and passed to + /// , then replacement services should be configured on + /// that service provider before it is passed to EF. + /// + /// + /// The replacement service gets the same scope as the EF service that it is replacing. + /// + /// + /// The type (usually an interface) that defines the contract of the service to replace. + /// The current implementation type for the service. + /// The new implementation type for the service. + /// The same builder instance so that multiple calls can be chained. + public new virtual DbContextOptionsBuilder ReplaceService() + where TCurrentImplementation : TService + where TNewImplementation : TService + => (DbContextOptionsBuilder)base.ReplaceService(); + + /// + /// + /// Adds instances to those registered on the context. + /// + /// + /// Interceptors can be used to view, change, or suppress operations taken by Entity Framework. + /// See the specific implementations of for details. For example, 'IDbCommandInterceptor'. + /// + /// + /// A single interceptor instance can implement multiple different interceptor interfaces. I will be registered as + /// an interceptor for all interfaces that it implements. + /// + /// + /// Extensions can also register multiple s in the internal service provider. + /// If both injected and application interceptors are found, then the injected interceptors are run in the + /// order that they are resolved from the service provider, and then the application interceptors are run + /// in the order that they were added to the context. + /// + /// + /// Calling this method multiple times will result in all interceptors in every call being added to the context. + /// Interceptors added in a previous call are not overridden by interceptors added in a later call. + /// + /// + /// The interceptors to add. + /// The same builder instance so that multiple calls can be chained. + public new virtual DbContextOptionsBuilder AddInterceptors([NotNull] IEnumerable interceptors) + => (DbContextOptionsBuilder)base.AddInterceptors(interceptors); + + /// + /// + /// Adds instances to those registered on the context. + /// + /// + /// Interceptors can be used to view, change, or suppress operations taken by Entity Framework. + /// See the specific implementations of for details. For example, 'IDbCommandInterceptor'. + /// + /// + /// Extensions can also register multiple s in the internal service provider. + /// If both injected and application interceptors are found, then the injected interceptors are run in the + /// order that they are resolved from the service provider, and then the application interceptors are run + /// in the order that they were added to the context. + /// + /// + /// Calling this method multiple times will result in all interceptors in every call being added to the context. + /// Interceptors added in a previous call are not overridden by interceptors added in a later call. + /// + /// + /// The interceptors to add. + /// The same builder instance so that multiple calls can be chained. + public new virtual DbContextOptionsBuilder AddInterceptors([NotNull] params IInterceptor[] interceptors) + => (DbContextOptionsBuilder)base.AddInterceptors(interceptors); } } diff --git a/src/EFCore/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index 1e509b47df1..770389c8466 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -38,7 +38,7 @@ public class CoreOptionsExtension : IDbContextOptionsExtension private bool _sensitiveDataLoggingEnabled; private bool _detailedErrorsEnabled; private QueryTrackingBehavior _queryTrackingBehavior = QueryTrackingBehavior.TrackAll; - private IDictionary _replacedServices; + private IDictionary<(Type, Type), Type> _replacedServices; private int? _maxPoolSize; private bool _serviceProviderCachingEnabled = true; private DbContextOptionsExtensionInfo _info; @@ -79,7 +79,7 @@ protected CoreOptionsExtension([NotNull] CoreOptionsExtension copyFrom) if (copyFrom._replacedServices != null) { - _replacedServices = new Dictionary(copyFrom._replacedServices); + _replacedServices = new Dictionary<(Type, Type), Type>(copyFrom._replacedServices); } } @@ -235,18 +235,22 @@ public virtual CoreOptionsExtension WithQueryTrackingBehavior(QueryTrackingBehav /// It is unusual to call this method directly. Instead use . /// /// The service contract. - /// The implementation type to use for the service. + /// The implementation type to use for the service. + /// The specific existing implementation type to replace. /// A new instance with the option changed. - public virtual CoreOptionsExtension WithReplacedService([NotNull] Type serviceType, [NotNull] Type implementationType) + public virtual CoreOptionsExtension WithReplacedService( + [NotNull] Type serviceType, + [NotNull] Type newImplementationType, + [CanBeNull] Type currentImplementationType = null) { var clone = Clone(); if (clone._replacedServices == null) { - clone._replacedServices = new Dictionary(); + clone._replacedServices = new Dictionary<(Type, Type), Type>(); } - clone._replacedServices[serviceType] = implementationType; + clone._replacedServices[(serviceType, currentImplementationType)] = newImplementationType; return clone; } @@ -373,7 +377,7 @@ public virtual CoreOptionsExtension WithInterceptors([NotNull] IEnumerable /// The options set from the method. /// - public virtual IReadOnlyDictionary ReplacedServices => (IReadOnlyDictionary)_replacedServices; + public virtual IReadOnlyDictionary<(Type, Type), Type> ReplacedServices => (IReadOnlyDictionary<(Type, Type), Type>)_replacedServices; /// /// The option set from the @@ -508,7 +512,13 @@ public override void PopulateDebugInfo(IDictionary debugInfo) { foreach (var replacedService in Extension._replacedServices) { - debugInfo["Core:" + nameof(DbContextOptionsBuilder.ReplaceService) + ":" + replacedService.Key.DisplayName()] + var (serviceType, implementationType) = replacedService.Key; + + debugInfo["Core:" + + nameof(DbContextOptionsBuilder.ReplaceService) + + ":" + + serviceType.DisplayName() + + (implementationType == null ? "" : ", " + implementationType.DisplayName())] = replacedService.Value.GetHashCode().ToString(CultureInfo.InvariantCulture); } } diff --git a/src/EFCore/Internal/ServiceProviderCache.cs b/src/EFCore/Internal/ServiceProviderCache.cs index 313db0d840b..612d952ffe7 100644 --- a/src/EFCore/Internal/ServiceProviderCache.cs +++ b/src/EFCore/Internal/ServiceProviderCache.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Linq; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -92,21 +91,25 @@ public virtual IServiceProvider GetOrAdd([NotNull] IDbContextOptions options, bo var replacedServices = coreOptionsExtension?.ReplacedServices; if (replacedServices != null) { - // For replaced services we use the service collection to obtain the lifetime of - // the service to replace. The replaced services are added to a new collection, after - // which provider and core services are applied. This ensures that any patching happens - // to the replaced service. var updatedServices = new ServiceCollection(); foreach (var descriptor in services) { - if (replacedServices.TryGetValue(descriptor.ServiceType, out var replacementType)) + if (replacedServices.TryGetValue((descriptor.ServiceType, descriptor.ImplementationType), out var replacementType)) { ((IList)updatedServices).Add( new ServiceDescriptor(descriptor.ServiceType, replacementType, descriptor.Lifetime)); } + else if (replacedServices.TryGetValue((descriptor.ServiceType, null), out replacementType)) + { + ((IList)updatedServices).Add( + new ServiceDescriptor(descriptor.ServiceType, replacementType, descriptor.Lifetime)); + } + else + { + ((IList)updatedServices).Add(descriptor); + } } - ApplyServices(options, updatedServices); services = updatedServices; } diff --git a/test/EFCore.Specification.Tests/InterceptionTestBase.cs b/test/EFCore.Specification.Tests/InterceptionTestBase.cs index 941a4a69b78..3777bc876b9 100644 --- a/test/EFCore.Specification.Tests/InterceptionTestBase.cs +++ b/test/EFCore.Specification.Tests/InterceptionTestBase.cs @@ -180,7 +180,7 @@ public virtual DbContextOptions CreateOptions( => AddOptions( TestStore .AddProviderOptions( - new DbContextOptionsBuilder() + new DbContextOptionsBuilder() .AddInterceptors(appInterceptors) .UseInternalServiceProvider( InjectInterceptors(new ServiceCollection(), injectedInterceptors) diff --git a/test/EFCore.Tests/DbContextServicesTest.cs b/test/EFCore.Tests/DbContextServicesTest.cs index e56351d30fd..dd0ce1a5fbb 100644 --- a/test/EFCore.Tests/DbContextServicesTest.cs +++ b/test/EFCore.Tests/DbContextServicesTest.cs @@ -21,7 +21,6 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Xunit; // ReSharper disable ClassNeverInstantiated.Local @@ -2647,6 +2646,28 @@ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBu .ConfigureWarnings(w => w.Default(WarningBehavior.Throw)); } + private class CustomParameterBindingFactory : IParameterBindingFactory + { + public bool CanBind(Type parameterType, string parameterName) => false; + + public ParameterBinding Bind(IMutableEntityType entityType, Type parameterType, string parameterName) + => throw new NotImplementedException(); + + public ParameterBinding Bind(IConventionEntityType entityType, Type parameterType, string parameterName) + => throw new NotImplementedException(); + } + + private class CustomParameterBindingFactory2 : IParameterBindingFactory + { + public bool CanBind(Type parameterType, string parameterName) => false; + + public ParameterBinding Bind(IMutableEntityType entityType, Type parameterType, string parameterName) + => throw new NotImplementedException(); + + public ParameterBinding Bind(IConventionEntityType entityType, Type parameterType, string parameterName) + => throw new NotImplementedException(); + } + private class CustomModelCustomizer : ModelCustomizer { public CustomModelCustomizer(ModelCustomizerDependencies dependencies) @@ -2692,12 +2713,15 @@ public void Can_replace_services_in_passed_options() { Assert.NotNull(replacedSingleton = context.GetService()); Assert.IsType(replacedSingleton); + Assert.Single(context.GetService>()); Assert.NotNull(replacedScoped = context.GetService()); Assert.IsType(replacedScoped); + Assert.Single(context.GetService>()); Assert.NotNull(replacedProviderService = context.GetService()); Assert.IsType(replacedProviderService); + Assert.Single(context.GetService>()); } using (var context = new ConstructorTestContextWithOC3A(options)) @@ -2733,12 +2757,15 @@ public void Can_replace_services_using_AddDbContext() Assert.NotNull(replacedSingleton = context.GetService()); Assert.IsType(replacedSingleton); + Assert.Single(context.GetService>()); Assert.NotNull(replacedScoped = context.GetService()); Assert.IsType(replacedScoped); + Assert.Single(context.GetService>()); Assert.NotNull(replacedProviderService = context.GetService()); Assert.IsType(replacedProviderService); + Assert.Single(context.GetService>()); } using (var serviceScope = appServiceProvider @@ -2754,6 +2781,84 @@ public void Can_replace_services_using_AddDbContext() } } + [ConditionalFact] + public void Can_replace_all_multiple_registrations() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ReplaceService() + .Options; + + using (var context = new ConstructorTestContextWithOC3A(options)) + { + var replacedServices = context.GetService>().ToList(); + Assert.Equal(3, replacedServices.Count); + + foreach (var replacedService in replacedServices) + { + Assert.IsType(replacedService); + } + } + } + + [ConditionalFact] + public void Can_replace_specific_implementation_of_multiple_registrations() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ReplaceService() + .Options; + + using (var context = new ConstructorTestContextWithOC3A(options)) + { + var replacedServices = context + .GetService>() + .OrderBy(e => e.GetType().Name) + .ToList(); + + Assert.Collection( + replacedServices, + t => Assert.IsType(t), + t => Assert.IsType(t), + t => Assert.IsType(t)); + } + } + + [ConditionalFact] + public void Can_replace_specific_implementation_of_single_registration() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ReplaceService() + .Options; + + using (var context = new ConstructorTestContextWithOC3A(options)) + { + var replacedServices = context.GetService>().ToList(); + Assert.Single(replacedServices); + Assert.IsType(replacedServices.Single()); + } + } + + [ConditionalFact] + public void Can_replace_specific_implementation_and_all_others() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ReplaceService() + .ReplaceService() + .Options; + + using (var context = new ConstructorTestContextWithOC3A(options)) + { + var replacedServices = context.GetService>().ToList(); + Assert.Equal(3, replacedServices.Count); + + Assert.Equal(2, replacedServices.Count(t => t is CustomParameterBindingFactory2)); + Assert.Single(replacedServices.Where(t => t is CustomParameterBindingFactory)); + } + } + [ConditionalFact] public void Throws_replacing_services_in_OnConfiguring_when_UseInternalServiceProvider() {