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

Add instantiation binding interception and use it for proxies #28127

Merged
merged 5 commits into from
Jun 1, 2022
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
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(IEntityType 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;
}
}
121 changes: 32 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,34 @@ public virtual void ProcessModelFinalizing(
}
}

if (_options.UseLazyLoadingProxies)
{
foreach (var conflictingProperty in entityType.GetDerivedTypes()
.SelectMany(e => e.GetDeclaredServiceProperties().Where(p => p.ClrType == typeof(ILazyLoader)))
.ToList())
{
if (!ConfigurationSource.Convention.Overrides(conflictingProperty.GetConfigurationSource()))
{
break;
}
conflictingProperty.DeclaringEntityType.RemoveServiceProperty(conflictingProperty.Name);
ajcvickers marked this conversation as resolved.
Show resolved Hide resolved
}

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 +188,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