Skip to content

Commit

Permalink
Allow lazy-loading to be configured per navigation
Browse files Browse the repository at this point in the history
Part of #10787

Also fixes two issues with service properties and their delegates:

- The delegate is now always created pointing to the instance of the service property stored in the entity. Previously, it could sometimes be a delegate pointing to a new service instance.
- The service property instance is always treated as an IInjectableService when attaching or materializing, so the metadata is always present on the instance.
  • Loading branch information
ajcvickers committed Dec 29, 2022
1 parent 0993a63 commit 5e68b4e
Show file tree
Hide file tree
Showing 40 changed files with 2,508 additions and 1,219 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,12 @@ private void Create(
.Append("eagerLoaded: ").Append(_code.Literal(true));
}

if (!navigation.LazyLoadingEnabled)
{
mainBuilder.AppendLine(",")
.Append("lazyLoadingEnabled: ").Append(_code.Literal(false));
}

mainBuilder
.AppendLine(");")
.AppendLine()
Expand Down Expand Up @@ -1346,6 +1352,12 @@ private void CreateSkipNavigation(
.Append("eagerLoaded: ").Append(_code.Literal(true));
}

if (!navigation.LazyLoadingEnabled)
{
mainBuilder.AppendLine(",")
.Append("lazyLoadingEnabled: ").Append(_code.Literal(false));
}

mainBuilder
.AppendLine(");")
.DecrementIndent();
Expand Down
33 changes: 25 additions & 8 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -472,16 +472,33 @@ private void SetServiceProperties(EntityState oldState, EntityState newState)
{
if (EntityType.HasServiceProperties())
{
if (oldState == EntityState.Detached)
List<IServiceProperty>? dependentServices = null;
foreach (var serviceProperty in EntityType.GetServiceProperties())
{
foreach (var serviceProperty in EntityType.GetServiceProperties())
var service = this[serviceProperty] ?? serviceProperty.ParameterBinding.ServiceDelegate(
new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity);

if (service == null)
{
(dependentServices ??= new List<IServiceProperty>()).Add(serviceProperty);
}
else
{
if (service is IInjectableService injectableService)
{
injectableService.Attaching(Context, EntityType, Entity);
}

this[serviceProperty] = service;
}
}

if (dependentServices != null)
{
foreach (var serviceProperty in dependentServices)
{
this[serviceProperty] = (this[serviceProperty] is IInjectableService injectableService
? injectableService.Attaching(Context, Entity, injectableService)
: null)
?? serviceProperty
.ParameterBinding
.ServiceDelegate(new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity);
this[serviceProperty] = serviceProperty.ParameterBinding.ServiceDelegate(
new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity);
}
}
else if (newState == EntityState.Detached)
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore/Infrastructure/AccessorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ public static TService GetService<TService>(this IInfrastructure<IServiceProvide
where TService : class
=> InfrastructureExtensions.GetService<TService>(accessor);

/// <summary>
/// Resolves a service from the <see cref="IServiceProvider" /> exposed from a type that implements
/// <see cref="IInfrastructure{IServiceProvider}" />.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="IInfrastructure{T}" /> is used to hide properties that are not intended to be used in
/// application code but can be used in extension methods written by database providers etc.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-services">Accessing DbContext services</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="accessor">The object exposing the service provider.</param>
/// <param name="serviceType">The type of service to be resolved.</param>
/// <returns>The requested service.</returns>
[DebuggerStepThrough]
public static object GetService(this IInfrastructure<IServiceProvider> accessor, Type serviceType)
=> InfrastructureExtensions.GetService(serviceType, accessor);

/// <summary>
/// <para>
/// Gets the value from a property that is being hidden using <see cref="IInfrastructure{T}" />.
Expand Down
17 changes: 13 additions & 4 deletions src/EFCore/Infrastructure/Internal/InfrastructureExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,30 @@ public static class InfrastructureExtensions
/// </summary>
public static TService GetService<TService>(IInfrastructure<IServiceProvider> accessor)
where TService : class
=> (TService)GetService(typeof(TService), accessor);

/// <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 static object GetService(Type serviceType, IInfrastructure<IServiceProvider> accessor)
{
var internalServiceProvider = accessor.Instance;

var service = internalServiceProvider.GetService(typeof(TService))
var service = internalServiceProvider.GetService(serviceType)
?? internalServiceProvider.GetService<IDbContextOptions>()
?.Extensions.OfType<CoreOptionsExtension>().FirstOrDefault()
?.ApplicationServiceProvider
?.GetService(typeof(TService));
?.GetService(serviceType);

if (service == null)
{
throw new InvalidOperationException(
CoreStrings.NoProviderConfiguredFailedToResolveService(typeof(TService).DisplayName()));
CoreStrings.NoProviderConfiguredFailedToResolveService(serviceType.DisplayName()));
}

return (TService)service;
return service;
}
}
36 changes: 23 additions & 13 deletions src/EFCore/Infrastructure/Internal/LazyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class LazyLoader : ILazyLoader, IInjectableService
private bool _detached;
private IDictionary<string, bool>? _loadedStates;
private List<(object Entity, string NavigationName)>? _isLoading;
private IEntityType? _entityType;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -41,8 +42,11 @@ public LazyLoader(
/// 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 void ServiceObtained(DbContext context, ParameterBindingInfo bindingInfo)
=> _queryTrackingBehavior = bindingInfo.QueryTrackingBehavior;
public virtual void Injected(DbContext context, object entity, ParameterBindingInfo bindingInfo)
{
_queryTrackingBehavior = bindingInfo.QueryTrackingBehavior;
_entityType = bindingInfo.EntityType;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -217,18 +221,24 @@ private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)
{
if (!_detached && !IsLoaded(entity, navigationName))
{
if (_disposed)
{
Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName);
}
else if (Context!.ChangeTracker.LazyLoadingEnabled) // Check again because the nav may be loaded without the loader knowing
var navigation = _entityType?.FindNavigation(navigationName)
?? (INavigationBase?)_entityType?.FindSkipNavigation(navigationName);

if (navigation?.LazyLoadingEnabled != false)
{
navigationEntry = Context.Entry(entity).Navigation(navigationName); // Will use local-DetectChanges, if enabled.
if (!navigationEntry.IsLoaded)
if (_disposed)
{
Logger.NavigationLazyLoading(Context, entity, navigationName);
Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName);
}
else if (Context!.ChangeTracker.LazyLoadingEnabled)
{
navigationEntry = Context.Entry(entity).Navigation(navigationName); // Will use local-DetectChanges, if enabled.
if (!navigationEntry.IsLoaded) // Check again because the nav may be loaded without the loader knowing
{
Logger.NavigationLazyLoading(Context, entity, navigationName);

return true;
return true;
}
}
}
}
Expand Down Expand Up @@ -268,11 +278,11 @@ public virtual bool Detaching(DbContext context, object entity)
/// 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 IInjectableService? Attaching(DbContext context, object entity, IInjectableService? existingService)
public virtual void Attaching(DbContext context, IEntityType entityType, object entity)
{
_disposed = false;
_detached = false;
_entityType = entityType;
Context = context;
return this;
}
}
10 changes: 3 additions & 7 deletions src/EFCore/Internal/IInjectableService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public interface IInjectableService
/// 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>
void ServiceObtained(DbContext context, ParameterBindingInfo bindingInfo);
void Injected(DbContext context, object entity, ParameterBindingInfo bindingInfo);

/// <summary>
/// <para>
Expand Down Expand Up @@ -52,11 +52,7 @@ public interface IInjectableService
/// </para>
/// </summary>
/// <param name="context">The <see cref="DbContext" /> instance.</param>
/// <param name="entityType">The <see cref="IEntityType"/> of the instance being attached.</param>
/// <param name="entity">The entity instance that is being attached.</param>
/// <param name="existingService">
/// The existing instance of this service being held by the entity instance, or <see langword="null" /> if there
/// is no existing instance.
/// </param>
/// <returns>The service instance to use, or <see langword="null" /> if a new instance should be created.</returns>
IInjectableService? Attaching(DbContext context, object entity, IInjectableService? existingService);
void Attaching(DbContext context, IEntityType entityType, object entity);
}
20 changes: 20 additions & 0 deletions src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ public interface IConventionNavigationBuilder : IConventionPropertyBaseBuilder
/// </returns>
IConventionNavigationBuilder? AutoInclude(bool? autoInclude, bool fromDataAnnotation = false);

/// <summary>
/// Returns a value indicating whether this navigation can be configured to enable lazy-loading
/// from the current configuration source.
/// </summary>
/// <param name="lazyLoadingEnabled">A value indicating whether the navigation should be enabled for lazy-loading.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if automatically included can be set for this navigation.</returns>
bool CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, bool fromDataAnnotation = false);

/// <summary>
/// Configures this navigation to be enabled for lazy-loading.
/// </summary>
/// <param name="lazyLoadingEnabled">A value indicating whether the navigation should be enabled for lazy-loading.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
IConventionNavigationBuilder? EnableLazyLoading(bool? lazyLoadingEnabled, bool fromDataAnnotation = false);

/// <summary>
/// Returns a value indicating whether this navigation requiredness can be configured
/// from the current configuration source.
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,24 @@ public interface IConventionSkipNavigationBuilder : IConventionPropertyBaseBuild
/// <see langword="null" /> otherwise.
/// </returns>
IConventionSkipNavigationBuilder? AutoInclude(bool? autoInclude, bool fromDataAnnotation = false);

/// <summary>
/// Returns a value indicating whether this navigation can be configured to enable lazy-loading
/// from the current configuration source.
/// </summary>
/// <param name="lazyLoadingEnabled">A value indicating whether the navigation should be enabled for lazy-loading.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if automatically included can be set for this navigation.</returns>
bool CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, bool fromDataAnnotation = false);

/// <summary>
/// Configures this navigation to be enabled for lazy-loading.
/// </summary>
/// <param name="lazyLoadingEnabled">A value indicating whether the navigation should be enabled for lazy-loading.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
IConventionSkipNavigationBuilder? EnableLazyLoading(bool? lazyLoadingEnabled, bool fromDataAnnotation = false);
}
25 changes: 25 additions & 0 deletions src/EFCore/Metadata/Builders/NavigationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,31 @@ public virtual NavigationBuilder AutoInclude(bool autoInclude = true)
return this;
}

/// <summary>
/// Configures whether this navigation should be enabled for lazy-loading. Note that a property can only be lazy-loaded
/// if a lazy-loading mechanism such as lazy-loading proxies or <see cref="ILazyLoader"/> injection has been configured.
/// </summary>
/// <remarks>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-lazy-loading">Lazy loading</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="lazyLoadingEnabled">A value indicating if the navigation should be enabled for lazy-loading.</param>
/// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
public virtual NavigationBuilder EnableLazyLoading(bool lazyLoadingEnabled = true)
{
if (InternalNavigationBuilder != null)
{
InternalNavigationBuilder.EnableLazyLoading(lazyLoadingEnabled, ConfigurationSource.Explicit);
}
else
{
InternalSkipNavigationBuilder!.EnableLazyLoading(lazyLoadingEnabled, ConfigurationSource.Explicit);
}

return this;
}

/// <summary>
/// Configures whether this navigation is required.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore/Metadata/Builders/NavigationBuilder`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ public NavigationBuilder(IMutableNavigationBase navigationOrSkipNavigation)
public new virtual NavigationBuilder<TSource, TTarget> AutoInclude(bool autoInclude = true)
=> (NavigationBuilder<TSource, TTarget>)base.AutoInclude(autoInclude);

/// <summary>
/// Configures whether this navigation should be enabled for lazy-loading. Note that a property can only be lazy-loaded
/// if a lazy-loading mechanism such as lazy-loading proxies or <see cref="ILazyLoader"/> injection has been configured.
/// </summary>
/// <remarks>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-lazy-loading">Lazy loading</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="lazyLoadingEnabled">A value indicating if the navigation should be enabled for lazy-loading.</param>
/// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
public new virtual NavigationBuilder<TSource, TTarget> EnableLazyLoading(bool lazyLoadingEnabled = true)
=> (NavigationBuilder<TSource, TTarget>)base.EnableLazyLoading(lazyLoadingEnabled);

/// <summary>
/// Configures whether this navigation is required.
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,8 @@ private static RuntimeNavigation Create(INavigation navigation, RuntimeForeignKe
navigation.PropertyInfo,
navigation.FieldInfo,
navigation.GetPropertyAccessMode(),
navigation.IsEagerLoaded);
navigation.IsEagerLoaded,
navigation.LazyLoadingEnabled);

/// <summary>
/// Updates the navigation annotations that will be set on the read-only object.
Expand Down Expand Up @@ -590,7 +591,8 @@ private RuntimeSkipNavigation Create(ISkipNavigation navigation, RuntimeEntityTy
navigation.PropertyInfo,
navigation.FieldInfo,
navigation.GetPropertyAccessMode(),
navigation.IsEagerLoaded);
navigation.IsEagerLoaded,
navigation.LazyLoadingEnabled);

/// <summary>
/// Gets the corresponding foreign key in the read-optimized model.
Expand Down
Loading

0 comments on commit 5e68b4e

Please sign in to comment.