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

Support replacement of services that allow multiple registrations #19393

Merged
merged 1 commit into from
Dec 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/EFCore/DbContextOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ public virtual DbContextOptionsBuilder ConfigureWarnings(

/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
Expand All @@ -524,6 +524,34 @@ public virtual DbContextOptionsBuilder ReplaceService<TService, TImplementation>
where TImplementation : TService
=> WithOption(e => e.WithReplacedService(typeof(TService), typeof(TImplementation)));

/// <summary>
/// <para>
/// Replaces the internal Entity Framework implementation of a specific implementation of a service contract
/// with a different implementation.
/// </para>
/// <para>
/// This method is useful for replacing a single instance of services that can be legitimately registered
/// multiple times in the EF internal service provider.
/// </para>
/// <para>
/// 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
/// <see cref="UseInternalServiceProvider" />, then replacement services should be configured on
/// that service provider before it is passed to EF.
/// </para>
/// <para>
/// The replacement service gets the same scope as the EF service that it is replacing.
/// </para>
/// </summary>
/// <typeparam name="TService"> The type (usually an interface) that defines the contract of the service to replace. </typeparam>
/// <typeparam name="TCurrentImplementation"> The current implementation type for the service. </typeparam>
/// <typeparam name="TNewImplementation"> The new implementation type for the service. </typeparam>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public virtual DbContextOptionsBuilder ReplaceService<TService, TCurrentImplementation, TNewImplementation>()
where TCurrentImplementation : TService
where TNewImplementation : TService
=> WithOption(e => e.WithReplacedService(typeof(TService), typeof(TNewImplementation), typeof(TCurrentImplementation)));

/// <summary>
/// <para>
/// Adds <see cref="IInterceptor" /> instances to those registered on the context.
Expand Down
82 changes: 81 additions & 1 deletion src/EFCore/DbContextOptionsBuilder`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ public DbContextOptionsBuilder([NotNull] DbContextOptions<TContext> options)

/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
Expand All @@ -404,5 +404,85 @@ public DbContextOptionsBuilder([NotNull] DbContextOptions<TContext> options)
public new virtual DbContextOptionsBuilder<TContext> ReplaceService<TService, TImplementation>()
where TImplementation : TService
=> (DbContextOptionsBuilder<TContext>)base.ReplaceService<TService, TImplementation>();

/// <summary>
/// <para>
/// Replaces the internal Entity Framework implementation of a specific implementation of a service contract
/// with a different implementation.
/// </para>
/// <para>
/// This method is useful for replacing a single instance of services that can be legitimately registered
/// multiple times in the EF internal service provider.
/// </para>
/// <para>
/// 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
/// <see cref="UseInternalServiceProvider" />, then replacement services should be configured on
/// that service provider before it is passed to EF.
/// </para>
/// <para>
/// The replacement service gets the same scope as the EF service that it is replacing.
/// </para>
/// </summary>
/// <typeparam name="TService"> The type (usually an interface) that defines the contract of the service to replace. </typeparam>
/// <typeparam name="TCurrentImplementation"> The current implementation type for the service. </typeparam>
/// <typeparam name="TNewImplementation"> The new implementation type for the service. </typeparam>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public new virtual DbContextOptionsBuilder<TContext> ReplaceService<TService, TCurrentImplementation, TNewImplementation>()
where TCurrentImplementation : TService
where TNewImplementation : TService
=> (DbContextOptionsBuilder<TContext>)base.ReplaceService<TService, TCurrentImplementation, TNewImplementation>();

/// <summary>
/// <para>
/// Adds <see cref="IInterceptor" /> instances to those registered on the context.
/// </para>
/// <para>
/// Interceptors can be used to view, change, or suppress operations taken by Entity Framework.
/// See the specific implementations of <see cref="IInterceptor" /> for details. For example, 'IDbCommandInterceptor'.
/// </para>
/// <para>
/// A single interceptor instance can implement multiple different interceptor interfaces. I will be registered as
/// an interceptor for all interfaces that it implements.
/// </para>
/// <para>
/// Extensions can also register multiple <see cref="IInterceptor" />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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
/// <param name="interceptors"> The interceptors to add. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public new virtual DbContextOptionsBuilder<TContext> AddInterceptors([NotNull] IEnumerable<IInterceptor> interceptors)
=> (DbContextOptionsBuilder<TContext>)base.AddInterceptors(interceptors);

/// <summary>
/// <para>
/// Adds <see cref="IInterceptor" /> instances to those registered on the context.
/// </para>
/// <para>
/// Interceptors can be used to view, change, or suppress operations taken by Entity Framework.
/// See the specific implementations of <see cref="IInterceptor" /> for details. For example, 'IDbCommandInterceptor'.
/// </para>
/// <para>
/// Extensions can also register multiple <see cref="IInterceptor" />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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
/// <param name="interceptors"> The interceptors to add. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public new virtual DbContextOptionsBuilder<TContext> AddInterceptors([NotNull] params IInterceptor[] interceptors)
=> (DbContextOptionsBuilder<TContext>)base.AddInterceptors(interceptors);
}
}
26 changes: 18 additions & 8 deletions src/EFCore/Infrastructure/CoreOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class CoreOptionsExtension : IDbContextOptionsExtension
private bool _sensitiveDataLoggingEnabled;
private bool _detailedErrorsEnabled;
private QueryTrackingBehavior _queryTrackingBehavior = QueryTrackingBehavior.TrackAll;
private IDictionary<Type, Type> _replacedServices;
private IDictionary<(Type, Type), Type> _replacedServices;
private int? _maxPoolSize;
private bool _serviceProviderCachingEnabled = true;
private DbContextOptionsExtensionInfo _info;
Expand Down Expand Up @@ -79,7 +79,7 @@ protected CoreOptionsExtension([NotNull] CoreOptionsExtension copyFrom)

if (copyFrom._replacedServices != null)
{
_replacedServices = new Dictionary<Type, Type>(copyFrom._replacedServices);
_replacedServices = new Dictionary<(Type, Type), Type>(copyFrom._replacedServices);
}
}

Expand Down Expand Up @@ -235,18 +235,22 @@ public virtual CoreOptionsExtension WithQueryTrackingBehavior(QueryTrackingBehav
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="serviceType"> The service contract. </param>
/// <param name="implementationType"> The implementation type to use for the service. </param>
/// <param name="newImplementationType"> The implementation type to use for the service. </param>
/// <param name="currentImplementationType"> The specific existing implementation type to replace. </param>
/// <returns> A new instance with the option changed. </returns>
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<Type, Type>();
clone._replacedServices = new Dictionary<(Type, Type), Type>();
}

clone._replacedServices[serviceType] = implementationType;
clone._replacedServices[(serviceType, currentImplementationType)] = newImplementationType;

return clone;
}
Expand Down Expand Up @@ -373,7 +377,7 @@ public virtual CoreOptionsExtension WithInterceptors([NotNull] IEnumerable<IInte
/// <summary>
/// The options set from the <see cref="DbContextOptionsBuilder.ReplaceService{TService,TImplementation}" /> method.
/// </summary>
public virtual IReadOnlyDictionary<Type, Type> ReplacedServices => (IReadOnlyDictionary<Type, Type>)_replacedServices;
public virtual IReadOnlyDictionary<(Type, Type), Type> ReplacedServices => (IReadOnlyDictionary<(Type, Type), Type>)_replacedServices;

/// <summary>
/// The option set from the
Expand Down Expand Up @@ -508,7 +512,13 @@ public override void PopulateDebugInfo(IDictionary<string, string> 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);
}
}
Expand Down
17 changes: 10 additions & 7 deletions src/EFCore/Internal/ServiceProviderCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ServiceDescriptor>)updatedServices).Add(
new ServiceDescriptor(descriptor.ServiceType, replacementType, descriptor.Lifetime));
}
else if (replacedServices.TryGetValue((descriptor.ServiceType, null), out replacementType))
{
((IList<ServiceDescriptor>)updatedServices).Add(
new ServiceDescriptor(descriptor.ServiceType, replacementType, descriptor.Lifetime));
}
else
{
((IList<ServiceDescriptor>)updatedServices).Add(descriptor);
}
}

ApplyServices(options, updatedServices);
services = updatedServices;
}

Expand Down
2 changes: 1 addition & 1 deletion test/EFCore.Specification.Tests/InterceptionTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public virtual DbContextOptions CreateOptions(
=> AddOptions(
TestStore
.AddProviderOptions(
new DbContextOptionsBuilder()
new DbContextOptionsBuilder<DbContext>()
.AddInterceptors(appInterceptors)
.UseInternalServiceProvider(
InjectInterceptors(new ServiceCollection(), injectedInterceptors)
Expand Down
Loading