Skip to content

Commit

Permalink
Support resolving application services from singleton internal services
Browse files Browse the repository at this point in the history
Fixes #13540

Scoped and transient internal services can obtain application services using the `DbContext` as a service locator. Singleton services cannot do this since they do not have access to the `DbContext`. This change allows the root application service provider to be registered as an `ISingletonOption` such that singleton internal services can resolve singleton and transient application services.
  • Loading branch information
ajcvickers committed Dec 31, 2022
1 parent 32552c6 commit 13db9f1
Show file tree
Hide file tree
Showing 8 changed files with 380 additions and 0 deletions.
35 changes: 35 additions & 0 deletions src/EFCore/DbContextOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,41 @@ public virtual DbContextOptionsBuilder UseInternalServiceProvider(IServiceProvid
public virtual DbContextOptionsBuilder UseApplicationServiceProvider(IServiceProvider? serviceProvider)
=> WithOption(e => e.WithApplicationServiceProvider(serviceProvider));

/// <summary>
/// Sets the root <see cref="IServiceProvider" /> from which singleton application services can be obtained from singleton
/// internal services.
/// </summary>
/// <remarks>
/// <para>
/// This is an advanced option that is rarely needed by normal applications. Calling this method will result in a new internal
/// service provider being created for every different root application service provider.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="rootServiceProvider">The service provider to be used.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public virtual DbContextOptionsBuilder UseRootApplicationServiceProvider(IServiceProvider? rootServiceProvider)
=> WithOption(e => e.WithRootApplicationServiceProvider(rootServiceProvider));

/// <summary>
/// Resolves the root <see cref="IServiceProvider" /> from from the scoped application service provider. The root provider can
/// be used to obtain singleton application services from singleton internal services.
/// </summary>
/// <remarks>
/// <para>
/// This is an advanced option that is rarely needed by normal applications. Calling this method will result in a new internal
/// service provider being created for every different root application service provider.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see> for more information and examples.
/// </para>
/// </remarks>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public virtual DbContextOptionsBuilder ResolveRootApplicationServiceProvider()
=> WithOption(e => e.WithAutoResolveRootApplicationServiceProvider(true));

/// <summary>
/// Enables application data to be included in exception messages, logging, etc. This can include the
/// values assigned to properties of your entity instances, parameter values for commands being sent
Expand Down
35 changes: 35 additions & 0 deletions src/EFCore/DbContextOptionsBuilder`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,41 @@ public DbContextOptionsBuilder(DbContextOptions<TContext> options)
public new virtual DbContextOptionsBuilder<TContext> UseApplicationServiceProvider(IServiceProvider? serviceProvider)
=> (DbContextOptionsBuilder<TContext>)base.UseApplicationServiceProvider(serviceProvider);

/// <summary>
/// Sets the root <see cref="IServiceProvider" /> from which singleton application services can be obtained from singleton
/// internal services.
/// </summary>
/// <remarks>
/// <para>
/// This is an advanced option that is rarely needed by normal applications. Calling this method will result in a new internal
/// service provider being created for every different root application service provider.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="rootServiceProvider">The service provider to be used.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public new virtual DbContextOptionsBuilder<TContext> UseRootApplicationServiceProvider(IServiceProvider? rootServiceProvider)
=> (DbContextOptionsBuilder<TContext>)base.UseRootApplicationServiceProvider(rootServiceProvider);

/// <summary>
/// Resolves the root <see cref="IServiceProvider" /> from from the scoped application service provider. The root provider can
/// be used to obtain singleton application services from singleton internal services.
/// </summary>
/// <remarks>
/// <para>
/// This is an advanced option that is rarely needed by normal applications. Calling this method will result in a new internal
/// service provider being created for every different root application service provider.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see> for more information and examples.
/// </para>
/// </remarks>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public new virtual DbContextOptionsBuilder<TContext> ResolveRootApplicationServiceProvider()
=> (DbContextOptionsBuilder<TContext>)base.ResolveRootApplicationServiceProvider();

/// <summary>
/// Enables application data to be included in exception messages, logging, etc. This can include the
/// values assigned to properties of your entity instances, parameter values for commands being sent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,8 @@ private static void AddCoreServices<TContextImplementation>(
ServiceLifetime optionsLifetime)
where TContextImplementation : DbContext
{
serviceCollection.TryAddSingleton<ServiceProviderAccessor>();

serviceCollection.TryAdd(
new ServiceDescriptor(
typeof(DbContextOptions<TContextImplementation>),
Expand Down
51 changes: 51 additions & 0 deletions src/EFCore/Infrastructure/CoreOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class CoreOptionsExtension : IDbContextOptionsExtension
{
private IServiceProvider? _internalServiceProvider;
private IServiceProvider? _applicationServiceProvider;
private IServiceProvider? _rootApplicationServiceProvider;
private bool _autoResolveResolveRootProvider;
private IModel? _model;
private ILoggerFactory? _loggerFactory;
private IDbContextLogger? _contextLogger;
Expand Down Expand Up @@ -66,6 +68,8 @@ protected CoreOptionsExtension(CoreOptionsExtension copyFrom)
{
_internalServiceProvider = copyFrom.InternalServiceProvider;
_applicationServiceProvider = copyFrom.ApplicationServiceProvider;
_rootApplicationServiceProvider = copyFrom.RootApplicationServiceProvider;
_autoResolveResolveRootProvider = copyFrom.AutoResolveRootProvider;
_model = copyFrom.Model;
_loggerFactory = copyFrom.LoggerFactory;
_contextLogger = copyFrom.DbContextLogger;
Expand Down Expand Up @@ -130,6 +134,36 @@ public virtual CoreOptionsExtension WithApplicationServiceProvider(IServiceProvi
return clone;
}

/// <summary>
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="rootApplicationServiceProvider">The option to change.</param>
/// <returns>A new instance with the option changed.</returns>
public virtual CoreOptionsExtension WithRootApplicationServiceProvider(IServiceProvider? rootApplicationServiceProvider)
{
var clone = Clone();

clone._rootApplicationServiceProvider = rootApplicationServiceProvider;

return clone;
}

/// <summary>
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="autoResolve">The option to change.</param>
/// <returns>A new instance with the option changed.</returns>
public virtual CoreOptionsExtension WithAutoResolveRootApplicationServiceProvider(bool autoResolve = true)
{
var clone = Clone();

clone._autoResolveResolveRootProvider = autoResolve;

return clone;
}

/// <summary>
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
Expand Down Expand Up @@ -420,6 +454,21 @@ public virtual IServiceProvider? InternalServiceProvider
public virtual IServiceProvider? ApplicationServiceProvider
=> _applicationServiceProvider;

/// <summary>
/// The option set from the <see cref="DbContextOptionsBuilder.UseRootApplicationServiceProvider" /> method.
/// </summary>
public virtual IServiceProvider? RootApplicationServiceProvider
=> _rootApplicationServiceProvider
?? (_autoResolveResolveRootProvider
? _applicationServiceProvider?.GetService<ServiceProviderAccessor>()?.RootServiceProvider
: null);

/// <summary>
/// The option set from the <see cref="DbContextOptionsBuilder.UseRootApplicationServiceProvider" /> method.
/// </summary>
public virtual bool AutoResolveRootProvider
=> _autoResolveResolveRootProvider;

/// <summary>
/// The options set from the <see cref="DbContextOptionsBuilder.ConfigureWarnings" /> method.
/// </summary>
Expand Down Expand Up @@ -647,6 +696,7 @@ public override int GetServiceProviderHashCode()
hashCode.Add(Extension.GetMemoryCache());
hashCode.Add(Extension._sensitiveDataLoggingEnabled);
hashCode.Add(Extension._detailedErrorsEnabled);
hashCode.Add(Extension.RootApplicationServiceProvider);
hashCode.Add(Extension._threadSafetyChecksEnabled);
hashCode.Add(Extension._warningsConfiguration.GetServiceProviderHashCode());

Expand Down Expand Up @@ -677,6 +727,7 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
&& Extension.GetMemoryCache() == otherInfo.Extension.GetMemoryCache()
&& Extension._sensitiveDataLoggingEnabled == otherInfo.Extension._sensitiveDataLoggingEnabled
&& Extension._detailedErrorsEnabled == otherInfo.Extension._detailedErrorsEnabled
&& Extension.RootApplicationServiceProvider == otherInfo.Extension.RootApplicationServiceProvider
&& Extension._threadSafetyChecksEnabled == otherInfo.Extension._threadSafetyChecksEnabled
&& Extension._warningsConfiguration.ShouldUseSameServiceProvider(otherInfo.Extension._warningsConfiguration)
&& (Extension._replacedServices == otherInfo.Extension._replacedServices
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore/Infrastructure/ICoreSingletonOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ public interface ICoreSingletonOptions : ISingletonOptions
/// Reflects the option set by <see cref="DbContextOptionsBuilder.EnableThreadSafetyChecks" />.
/// </summary>
bool AreThreadSafetyChecksEnabled { get; }

/// <summary>
/// The root service provider for the application, if available. />.
/// </summary>
IServiceProvider? RootApplicationServiceProvider { get; }
}
17 changes: 17 additions & 0 deletions src/EFCore/Infrastructure/Internal/CoreSingletonOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public virtual void Initialize(IDbContextOptions options)

AreDetailedErrorsEnabled = coreOptions.DetailedErrorsEnabled;
AreThreadSafetyChecksEnabled = coreOptions.ThreadSafetyChecksEnabled;
RootApplicationServiceProvider = coreOptions.RootApplicationServiceProvider;
}

/// <summary>
Expand Down Expand Up @@ -54,6 +55,14 @@ public virtual void Validate(IDbContextOptions options)
nameof(DbContextOptionsBuilder.EnableThreadSafetyChecks),
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}

if (RootApplicationServiceProvider != coreOptions.RootApplicationServiceProvider)
{
throw new InvalidOperationException(
CoreStrings.SingletonOptionChanged(
nameof(DbContextOptionsBuilder.UseRootApplicationServiceProvider),
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}
}

/// <summary>
Expand All @@ -71,4 +80,12 @@ public virtual void Validate(IDbContextOptions options)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool AreThreadSafetyChecksEnabled { get; private set; }

/// <summary>
/// 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.
/// </summary>
public virtual IServiceProvider? RootApplicationServiceProvider { get; private set; }
}
25 changes: 25 additions & 0 deletions src/EFCore/Infrastructure/ServiceProviderAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Infrastructure;

/// <summary>
/// This type is added as a singleton service to the application service provider to provide access to the
/// root service provider.
/// </summary>
public class ServiceProviderAccessor
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceProviderAccessor" /> class.
/// </summary>
/// <param name="rootServiceProvider">The injected service provider.</param>
public ServiceProviderAccessor(IServiceProvider rootServiceProvider)
{
RootServiceProvider = rootServiceProvider;
}

/// <summary>
/// The injected service provider.
/// </summary>
public virtual IServiceProvider RootServiceProvider { get; }
}
Loading

0 comments on commit 13db9f1

Please sign in to comment.