Skip to content

Commit

Permalink
Add instantiation binding interception and use it for proxies
Browse files Browse the repository at this point in the history
Part of #626
Part of #15911
Fixes #20135
Fixes #14554
Fixes #24902

This is the lowest level of materialization interception--it allows the actual constructor/factory binding to be changed, such that the expression tree for the compiled delegate is altered.

Introduces singleton interceptors, which cannot be changed per context instance without re-building the internal service provider. (Note that we throw by default if this is abused and results in many service providers being created.)

The use of this for proxies has two big advantages:
- Proxy types are created lazily, which vastly improves model building time for big models with proxies. See #20135.
- Proxies can now be used with the compiled model, since the proxy types are not compiled into the model. See
  • Loading branch information
ajcvickers committed May 30, 2022
1 parent bda79d4 commit 18bee0f
Show file tree
Hide file tree
Showing 37 changed files with 975 additions and 497 deletions.
3 changes: 1 addition & 2 deletions src/EFCore.Design/Properties/DesignStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/EFCore.Design/Properties/DesignStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
<value>You cannot add a migration with the name 'Migration'.</value>
</data>
<data name="CompiledModelConstructorBinding" xml:space="preserve">
<value>The entity type '{entityType}' has a custom constructor binding. This is usually caused by using proxies. Compiled model can't be generated, because dynamic proxy types are not supported. If you are not using proxies configure the custom constructor binding in '{customize}' in a partial '{className}' class instead.</value>
<value>The entity type '{entityType}' has a custom constructor binding. Compiled model can't be generated, because custom constructor bindings are not supported. Configure the custom constructor binding in '{customize}' in a partial '{className}' class instead.</value>
</data>
<data name="CompiledModelCustomCacheKeyFactory" xml:space="preserve">
<value>The context is configured to use a custom model cache key factory '{factoryType}', this usually indicates that the produced model can change between context instances. To preserve this behavior manually modify the generated compiled model source code.</value>
Expand Down Expand Up @@ -429,4 +429,4 @@ Change your target project to the migrations project by using the Package Manage
<data name="WritingSnapshot" xml:space="preserve">
<value>Writing model snapshot to '{file}'.</value>
</data>
</root>
</root>
4 changes: 1 addition & 3 deletions src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ object CreateProxy(
/// 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>
Type CreateProxyType(
ProxiesOptionsExtension options,
IReadOnlyEntityType entityType);
Type CreateProxyType(IReadOnlyEntityType entityType);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ public virtual ConventionSet ModifyConventions(ConventionSet conventionSet)

conventionSet.ModelFinalizingConventions.Add(
new ProxyBindingRewriter(
_proxyFactory,
extension,
LazyLoaderParameterBindingFactoryDependencies,
ConventionSetBuilderDependencies));
Expand Down
12 changes: 10 additions & 2 deletions src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,19 @@ public override string LogFragment
: "";

public override int GetServiceProviderHashCode()
=> Extension.UseProxies.GetHashCode();
{
var hashCode = new HashCode();
hashCode.Add(Extension.UseLazyLoadingProxies);
hashCode.Add(Extension.UseChangeTrackingProxies);
hashCode.Add(Extension.CheckEquality);
return hashCode.ToHashCode();
}

public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
=> other is ExtensionInfo otherInfo
&& Extension.UseProxies == otherInfo.Extension.UseProxies;
&& Extension.UseLazyLoadingProxies == otherInfo.Extension.UseLazyLoadingProxies
&& Extension.UseChangeTrackingProxies == otherInfo.Extension.UseChangeTrackingProxies
&& Extension.CheckEquality == otherInfo.Extension.CheckEquality;

public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
{
Expand Down
45 changes: 45 additions & 0 deletions src/EFCore.Proxies/Proxies/Internal/ProxyAnnotationNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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.Proxies.Internal;

/// <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 class ProxyAnnotationNames
{
/// <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 const string Prefix = "Proxies:";

/// <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 const string LazyLoading = Prefix + "LazyLoading";

/// <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 const string ChangeTracking = Prefix + "ChangeTracking";

/// <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 const string CheckEquality = Prefix + "CheckEquality";
}
77 changes: 77 additions & 0 deletions src/EFCore.Proxies/Proxies/Internal/ProxyBindingInterceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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.Proxies.Internal;

/// <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 class ProxyBindingInterceptor : IInstantiationBindingInterceptor
{
private static readonly MethodInfo CreateLazyLoadingProxyMethod
= typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateLazyLoadingProxy))!;

private static readonly MethodInfo CreateProxyMethod
= typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateProxy))!;

private readonly IProxyFactory _proxyFactory;

/// <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 ProxyBindingInterceptor(IProxyFactory proxyFactory)
{
_proxyFactory = proxyFactory;
}

/// <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 InstantiationBinding ModifyBinding(IEntityType entityType, string entityInstanceName, InstantiationBinding binding)
{
var proxyType = _proxyFactory.CreateProxyType(entityType);

if ((bool?)entityType.Model[ProxyAnnotationNames.LazyLoading] == true)
{
var serviceProperty = entityType.GetServiceProperties()
.First(e => e.ClrType == typeof(ILazyLoader));

return new FactoryMethodBinding(
_proxyFactory,
CreateLazyLoadingProxyMethod,
new List<ParameterBinding>
{
new ContextParameterBinding(typeof(DbContext)),
new EntityTypeParameterBinding(),
new DependencyInjectionParameterBinding(typeof(ILazyLoader), typeof(ILazyLoader), serviceProperty),
new ObjectArrayParameterBinding(binding.ParameterBindings)
},
proxyType);
}

if ((bool?)entityType.Model[ProxyAnnotationNames.ChangeTracking] == true)
{
return new FactoryMethodBinding(
_proxyFactory,
CreateProxyMethod,
new List<ParameterBinding>
{
new ContextParameterBinding(typeof(DbContext)),
new EntityTypeParameterBinding(),
new ObjectArrayParameterBinding(binding.ParameterBindings)
},
proxyType);
}

return binding;
}
}
117 changes: 28 additions & 89 deletions src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,9 @@ namespace Microsoft.EntityFrameworkCore.Proxies.Internal;
/// </summary>
public class ProxyBindingRewriter : IModelFinalizingConvention
{
private static readonly MethodInfo CreateLazyLoadingProxyMethod
= typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateLazyLoadingProxy))!;

private static readonly PropertyInfo LazyLoaderProperty
= typeof(IProxyLazyLoader).GetProperty(nameof(IProxyLazyLoader.LazyLoader))!;

private static readonly MethodInfo CreateProxyMethod
= typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateProxy))!;

private readonly ConstructorBindingConvention _directBindingConvention;
private readonly IProxyFactory _proxyFactory;
private readonly ProxiesOptionsExtension? _options;

/// <summary>
Expand All @@ -34,16 +26,13 @@ private static readonly MethodInfo CreateProxyMethod
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public ProxyBindingRewriter(
IProxyFactory proxyFactory,
ProxiesOptionsExtension? options,
LazyLoaderParameterBindingFactoryDependencies lazyLoaderParameterBindingFactoryDependencies,
ProviderConventionSetBuilderDependencies conventionSetBuilderDependencies)
{
_proxyFactory = proxyFactory;
_options = options;
LazyLoaderParameterBindingFactoryDependencies = lazyLoaderParameterBindingFactoryDependencies;
ConventionSetBuilderDependencies = conventionSetBuilderDependencies;
_directBindingConvention = new ConstructorBindingConvention(conventionSetBuilderDependencies);
}

/// <summary>
Expand All @@ -69,6 +58,10 @@ public virtual void ProcessModelFinalizing(
{
if (_options?.UseProxies == true)
{
modelBuilder.HasAnnotation(ProxyAnnotationNames.LazyLoading, _options.UseLazyLoadingProxies);
modelBuilder.HasAnnotation(ProxyAnnotationNames.ChangeTracking, _options.UseChangeTrackingProxies);
modelBuilder.HasAnnotation(ProxyAnnotationNames.CheckEquality, _options.CheckEquality);

foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
var clrType = entityType.ClrType;
Expand All @@ -82,30 +75,6 @@ public virtual void ProcessModelFinalizing(
throw new InvalidOperationException(ProxiesStrings.ItsASeal(entityType.DisplayName()));
}

var proxyType = _proxyFactory.CreateProxyType(_options, entityType);

// WARNING: This code is EF internal; it should not be copied. See #10789 #14554
#pragma warning disable EF1001 // Internal EF Core API usage.
var binding = ((EntityType)entityType).ConstructorBinding;
if (binding == null)
{
_directBindingConvention.ProcessModelFinalizing(modelBuilder, context);
binding = ((EntityType)entityType).ConstructorBinding!;
}

((EntityType)entityType).SetConstructorBinding(
UpdateConstructorBindings(entityType, proxyType, binding),
ConfigurationSource.Convention);

binding = ((EntityType)entityType).ServiceOnlyConstructorBinding;
if (binding != null)
{
((EntityType)entityType).SetServiceOnlyConstructorBinding(
UpdateConstructorBindings(entityType, proxyType, binding),
ConfigurationSource.Convention);
}
#pragma warning restore EF1001 // Internal EF Core API usage.

foreach (var navigationBase in entityType.GetDeclaredNavigations()
.Concat<IConventionNavigationBase>(entityType.GetDeclaredSkipNavigations()))
{
Expand Down Expand Up @@ -136,6 +105,30 @@ public virtual void ProcessModelFinalizing(
}
}

if (_options.UseLazyLoadingProxies)
{
foreach (var conflictingProperty in entityType.GetDerivedTypes()
.SelectMany(e => e.GetDeclaredServiceProperties().Where(p => p.ClrType == typeof(ILazyLoader)))
.ToList())
{
conflictingProperty.DeclaringEntityType.RemoveServiceProperty(conflictingProperty.Name);
}

var serviceProperty = entityType.GetServiceProperties()
.FirstOrDefault(e => e.ClrType == typeof(ILazyLoader));
if (serviceProperty == null)
{
serviceProperty = entityType.AddServiceProperty(LazyLoaderProperty);
serviceProperty.SetParameterBinding(
(ServiceParameterBinding)new LazyLoaderParameterBindingFactory(
LazyLoaderParameterBindingFactoryDependencies)
.Bind(
entityType,
typeof(ILazyLoader),
nameof(IProxyLazyLoader.LazyLoader)));
}
}

if (_options.UseChangeTrackingProxies)
{
var indexerChecked = false;
Expand Down Expand Up @@ -191,58 +184,4 @@ public virtual void ProcessModelFinalizing(
}
}
}

private InstantiationBinding UpdateConstructorBindings(
IConventionEntityType entityType,
Type proxyType,
InstantiationBinding binding)
{
if (_options?.UseLazyLoadingProxies == true)
{
foreach (var conflictingProperty in entityType.GetDerivedTypes()
.SelectMany(e => e.GetDeclaredServiceProperties().Where(p => p.ClrType == typeof(ILazyLoader)))
.ToList())
{
conflictingProperty.DeclaringEntityType.RemoveServiceProperty(conflictingProperty.Name);
}

var serviceProperty = entityType.GetServiceProperties()
.FirstOrDefault(e => e.ClrType == typeof(ILazyLoader));
if (serviceProperty == null)
{
serviceProperty = entityType.AddServiceProperty(LazyLoaderProperty);
serviceProperty.SetParameterBinding(
(ServiceParameterBinding)new LazyLoaderParameterBindingFactory(
LazyLoaderParameterBindingFactoryDependencies)
.Bind(
entityType,
typeof(ILazyLoader),
nameof(IProxyLazyLoader.LazyLoader)));
}

return new FactoryMethodBinding(
_proxyFactory,
CreateLazyLoadingProxyMethod,
new List<ParameterBinding>
{
new ContextParameterBinding(typeof(DbContext)),
new EntityTypeParameterBinding(),
new DependencyInjectionParameterBinding(
typeof(ILazyLoader), typeof(ILazyLoader), (IPropertyBase)serviceProperty),
new ObjectArrayParameterBinding(binding.ParameterBindings)
},
proxyType);
}

return new FactoryMethodBinding(
_proxyFactory,
CreateProxyMethod,
new List<ParameterBinding>
{
new ContextParameterBinding(typeof(DbContext)),
new EntityTypeParameterBinding(),
new ObjectArrayParameterBinding(binding.ParameterBindings)
},
proxyType);
}
}
Loading

0 comments on commit 18bee0f

Please sign in to comment.