diff --git a/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs b/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs
index 3b2bbfda277..f57694436c9 100644
--- a/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs
+++ b/src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs
@@ -19,13 +19,13 @@ private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.EntityFrameworkCore.Properties.ProxiesStrings", typeof(ProxiesStrings).Assembly);
///
- /// UseLazyLoadingProxies requires AddEntityFrameworkProxies to be called on the internal service provider used.
+ /// UseChangeDetectionProxies and UseLazyLoadingProxies each require AddEntityFrameworkProxies to be called on the internal service provider used.
///
public static string ProxyServicesMissing
=> GetString("ProxyServicesMissing");
///
- /// Entity type '{entityType}' is sealed. UseLazyLoadingProxies requires all entity types to be public, unsealed, have virtual navigation properties, and have a public or protected constructor.
+ /// Entity type '{entityType}' is sealed. UseChangeDetectionProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual.
///
public static string ItsASeal([CanBeNull] object entityType)
=> string.Format(
@@ -33,23 +33,23 @@ public static string ItsASeal([CanBeNull] object entityType)
entityType);
///
- /// Navigation property '{navigation}' on entity type '{entityType}' is not virtual. UseLazyLoadingProxies requires all entity types to be public, unsealed, have virtual navigation properties, and have a public or protected constructor.
+ /// Property '{property}' on entity type '{entityType}' is not virtual. UseChangeDetectionProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual.
///
- public static string NonVirtualNavigation([CanBeNull] object navigation, [CanBeNull] object entityType)
+ public static string NonVirtualProperty([CanBeNull] object property, [CanBeNull] object entityType)
=> string.Format(
- GetString("NonVirtualNavigation", nameof(navigation), nameof(entityType)),
- navigation, entityType);
+ GetString("NonVirtualProperty", nameof(property), nameof(entityType)),
+ property, entityType);
///
- /// Navigation property '{navigation}' on entity type '{entityType}' is mapped without a CLR property. UseLazyLoadingProxies requires all entity types to be public, unsealed, have virtual navigation properties, and have a public or protected constructor.
+ /// Property '{property}' on entity type '{entityType}' is mapped without a CLR property. UseChangeDetectionProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual.
///
- public static string FieldNavigation([CanBeNull] object navigation, [CanBeNull] object entityType)
+ public static string FieldProperty([CanBeNull] object property, [CanBeNull] object entityType)
=> string.Format(
- GetString("FieldNavigation", nameof(navigation), nameof(entityType)),
- navigation, entityType);
+ GetString("FieldProperty", nameof(property), nameof(entityType)),
+ property, entityType);
///
- /// Unable to create proxy for '{entityType}' because proxies are not enabled. Call 'DbContextOptionsBuilder.UseLazyLoadingProxies' to enable lazy-loading proxies.
+ /// Unable to create proxy for '{entityType}' because proxies are not enabled. Call 'DbContextOptionsBuilder.UseChangeDetectionProxies' or 'DbContextOptionsBuilder.UseLazyLoadingProxies' to enable proxies.
///
public static string ProxiesNotEnabled([CanBeNull] object entityType)
=> string.Format(
diff --git a/src/EFCore.Proxies/Properties/ProxiesStrings.resx b/src/EFCore.Proxies/Properties/ProxiesStrings.resx
index 985ac375cf9..0b752b450a6 100644
--- a/src/EFCore.Proxies/Properties/ProxiesStrings.resx
+++ b/src/EFCore.Proxies/Properties/ProxiesStrings.resx
@@ -118,18 +118,18 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- UseLazyLoadingProxies requires AddEntityFrameworkProxies to be called on the internal service provider used.
+ UseChangeDetectionProxies and UseLazyLoadingProxies each require AddEntityFrameworkProxies to be called on the internal service provider used.
- Entity type '{entityType}' is sealed. UseLazyLoadingProxies requires all entity types to be public, unsealed, have virtual navigation properties, and have a public or protected constructor.
+ Entity type '{entityType}' is sealed. UseChangeDetectionProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual.
-
- Navigation property '{navigation}' on entity type '{entityType}' is not virtual. UseLazyLoadingProxies requires all entity types to be public, unsealed, have virtual navigation properties, and have a public or protected constructor.
+
+ Property '{property}' on entity type '{entityType}' is not virtual. UseChangeDetectionProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual.
-
- Navigation property '{navigation}' on entity type '{entityType}' is mapped without a CLR property. UseLazyLoadingProxies requires all entity types to be public, unsealed, have virtual navigation properties, and have a public or protected constructor.
+
+ Property '{property}' on entity type '{entityType}' is mapped without a CLR property. UseChangeDetectionProxies requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. UseLazyLoadingProxies requires only the navigation properties be virtual.
- Unable to create proxy for '{entityType}' because proxies are not enabled. Call 'DbContextOptionsBuilder.UseLazyLoadingProxies' to enable lazy-loading proxies.
+ Unable to create proxy for '{entityType}' because proxies are not enabled. Call 'DbContextOptionsBuilder.UseChangeDetectionProxies' or 'DbContextOptionsBuilder.UseLazyLoadingProxies' to enable proxies.
\ No newline at end of file
diff --git a/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs b/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs
index dbbfb6599e9..5c89f68476d 100644
--- a/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs
+++ b/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs
@@ -23,6 +23,7 @@ public interface IProxyFactory
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
object CreateLazyLoadingProxy(
+ [NotNull] IDbContextOptions dbContextOptions,
[NotNull] IEntityType entityType,
[NotNull] ILazyLoader loader,
[NotNull] object[] constructorArguments);
@@ -33,7 +34,20 @@ object CreateLazyLoadingProxy(
/// 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.
///
- Type CreateLazyLoadingProxyType([NotNull] IEntityType entityType);
+ object CreateProxy(
+ [NotNull] IDbContextOptions dbContextOptions,
+ [NotNull] IEntityType entityType,
+ [NotNull] object[] constructorArguments);
+
+ ///
+ /// 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.
+ ///
+ Type CreateProxyType(
+ [NotNull] ProxiesOptionsExtension options,
+ [NotNull] IEntityType entityType);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Proxies/Proxies/Internal/PropertyChangedInterceptor.cs b/src/EFCore.Proxies/Proxies/Internal/PropertyChangedInterceptor.cs
new file mode 100644
index 00000000000..28b29de9845
--- /dev/null
+++ b/src/EFCore.Proxies/Proxies/Internal/PropertyChangedInterceptor.cs
@@ -0,0 +1,134 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.ComponentModel;
+using Castle.DynamicProxy;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+namespace Microsoft.EntityFrameworkCore.Proxies.Internal
+{
+ ///
+ /// 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.
+ ///
+ public class PropertyChangedInterceptor : IInterceptor
+ {
+ private static readonly Type _notifyChangedInterface = typeof(INotifyPropertyChanged);
+
+ private readonly IEntityType _entityType;
+ private readonly bool _checkEquality;
+ private PropertyChangedEventHandler _handler;
+ private Type _proxyType;
+
+ ///
+ /// 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.
+ ///
+ public PropertyChangedInterceptor(
+ [NotNull] IEntityType entityType,
+ bool checkEquality)
+ {
+ _entityType = entityType;
+ _checkEquality = checkEquality;
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual void Intercept(IInvocation invocation)
+ {
+ var methodName = invocation.Method.Name;
+
+ if (invocation.Method.DeclaringType.Equals(_notifyChangedInterface))
+ {
+ if (methodName == $"add_{nameof(INotifyPropertyChanged.PropertyChanged)}")
+ {
+ _handler = (PropertyChangedEventHandler)Delegate.Combine(
+ _handler, (Delegate)invocation.Arguments[0]);
+ }
+ else if (methodName == $"remove_{nameof(INotifyPropertyChanged.PropertyChanged)}")
+ {
+ _handler = (PropertyChangedEventHandler)Delegate.Remove(
+ _handler, (Delegate)invocation.Arguments[0]);
+ }
+ }
+ else if (methodName.StartsWith("set_", StringComparison.Ordinal))
+ {
+ var propertyName = methodName.Substring(4);
+
+ var property = _entityType.FindProperty(propertyName);
+ if (property != null)
+ {
+ HandleChanged(invocation, propertyName);
+ }
+ else
+ {
+ var navigation = _entityType.FindNavigation(propertyName);
+ if (navigation != null)
+ {
+ HandleChanged(invocation, propertyName);
+ }
+ else
+ {
+ invocation.Proceed();
+ }
+ }
+ }
+ else
+ {
+ invocation.Proceed();
+ }
+ }
+
+ private void HandleChanged(IInvocation invocation, string propertyName)
+ {
+ var newValue = invocation.Arguments[^1];
+
+ if (_checkEquality)
+ {
+ if (_proxyType == null)
+ {
+ _proxyType = invocation.Proxy.GetType();
+ }
+
+ var property = _proxyType.GetProperty(propertyName);
+ if (property != null)
+ {
+ var oldValue = property.GetValue(invocation.Proxy);
+
+ invocation.Proceed();
+
+ if ((oldValue is null ^ newValue is null)
+ || oldValue?.Equals(newValue) == false)
+ {
+ NotifyPropertyChanged(propertyName, invocation.Proxy);
+ }
+ }
+ else
+ {
+ invocation.Proceed();
+ }
+ }
+ else
+ {
+ invocation.Proceed();
+ NotifyPropertyChanged(propertyName, invocation.Proxy);
+ }
+ }
+
+ private void NotifyPropertyChanged(string propertyName, object proxy)
+ {
+ var args = new PropertyChangedEventArgs(propertyName);
+ _handler?.Invoke(proxy, args);
+ }
+ }
+}
diff --git a/src/EFCore.Proxies/Proxies/Internal/PropertyChangingInterceptor.cs b/src/EFCore.Proxies/Proxies/Internal/PropertyChangingInterceptor.cs
new file mode 100644
index 00000000000..b730c07fc5e
--- /dev/null
+++ b/src/EFCore.Proxies/Proxies/Internal/PropertyChangingInterceptor.cs
@@ -0,0 +1,128 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.ComponentModel;
+using Castle.DynamicProxy;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+namespace Microsoft.EntityFrameworkCore.Proxies.Internal
+{
+ ///
+ /// 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.
+ ///
+ public class PropertyChangingInterceptor : IInterceptor
+ {
+ private static readonly Type _notifyChangingInterface = typeof(INotifyPropertyChanging);
+
+ private readonly IEntityType _entityType;
+ private readonly bool _checkEquality;
+ private PropertyChangingEventHandler _handler;
+ private Type _proxyType;
+
+ ///
+ /// 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.
+ ///
+ public PropertyChangingInterceptor(
+ [NotNull] IEntityType entityType,
+ bool checkEquality)
+ {
+ _entityType = entityType;
+ _checkEquality = checkEquality;
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual void Intercept(IInvocation invocation)
+ {
+ var methodName = invocation.Method.Name;
+
+ if (invocation.Method.DeclaringType.Equals(_notifyChangingInterface))
+ {
+ if (methodName == $"add_{nameof(INotifyPropertyChanging.PropertyChanging)}")
+ {
+ _handler = (PropertyChangingEventHandler)Delegate.Combine(
+ _handler, (Delegate)invocation.Arguments[0]);
+ }
+ else if (methodName == $"remove_{nameof(INotifyPropertyChanging.PropertyChanging)}")
+ {
+ _handler = (PropertyChangingEventHandler)Delegate.Remove(
+ _handler, (Delegate)invocation.Arguments[0]);
+ }
+ }
+ else if (methodName.StartsWith("set_", StringComparison.Ordinal))
+ {
+ var propertyName = methodName.Substring(4);
+
+ var property = _entityType.FindProperty(propertyName);
+ if (property != null)
+ {
+ HandleChanging(invocation, propertyName);
+ }
+ else
+ {
+ var navigation = _entityType.FindNavigation(propertyName);
+ if (navigation != null)
+ {
+ HandleChanging(invocation, propertyName);
+ }
+ else
+ {
+ invocation.Proceed();
+ }
+ }
+ }
+ else
+ {
+ invocation.Proceed();
+ }
+ }
+
+ private void HandleChanging(IInvocation invocation, string propertyName)
+ {
+ if (_checkEquality)
+ {
+ if (_proxyType == null)
+ {
+ _proxyType = invocation.Proxy.GetType();
+ }
+
+ var property = _proxyType.GetProperty(propertyName);
+ if (property != null)
+ {
+ var oldValue = property.GetValue(invocation.Proxy);
+ var newValue = invocation.Arguments[^1];
+
+ if ((oldValue is null ^ newValue is null)
+ || oldValue?.Equals(newValue) == false)
+ {
+ NotifyPropertyChanging(propertyName, invocation.Proxy);
+ }
+ }
+ }
+ else
+ {
+ NotifyPropertyChanging(propertyName, invocation.Proxy);
+ }
+
+ invocation.Proceed();
+ }
+
+ private void NotifyPropertyChanging(string propertyName, object proxy)
+ {
+ var args = new PropertyChangingEventArgs(propertyName);
+ _handler?.Invoke(proxy, args);
+ }
+ }
+}
diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs b/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs
index 20a0a1c1af4..31e2d67bd6c 100644
--- a/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs
+++ b/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs
@@ -58,11 +58,18 @@ public ProxiesConventionSetPlugin(
///
public virtual ConventionSet ModifyConventions(ConventionSet conventionSet)
{
+ var extension = _options.FindExtension();
+
+ ConventionSet.AddAfter(
+ conventionSet.ModelInitializedConventions,
+ new ProxyChangeTrackingConvention(extension),
+ typeof(DbSetFindingConvention));
+
ConventionSet.AddBefore(
conventionSet.ModelFinalizedConventions,
new ProxyBindingRewriter(
_proxyFactory,
- _options.FindExtension(),
+ extension,
_lazyLoaderParameterBindingFactoryDependencies,
_conventionSetBuilderDependencies),
typeof(ValidatingConvention));
diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs b/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs
index 684d02cb9f2..3767537fbd5 100644
--- a/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs
+++ b/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs
@@ -23,6 +23,8 @@ public class ProxiesOptionsExtension : IDbContextOptionsExtension
{
private DbContextOptionsExtensionInfo _info;
private bool _useLazyLoadingProxies;
+ private bool _useChangeDetectionProxies;
+ private bool _checkEquality;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -43,6 +45,8 @@ public ProxiesOptionsExtension()
protected ProxiesOptionsExtension([NotNull] ProxiesOptionsExtension copyFrom)
{
_useLazyLoadingProxies = copyFrom._useLazyLoadingProxies;
+ _useChangeDetectionProxies = copyFrom._useChangeDetectionProxies;
+ _checkEquality = copyFrom._checkEquality;
}
///
@@ -70,6 +74,30 @@ public virtual DbContextOptionsExtensionInfo Info
///
public virtual bool UseLazyLoadingProxies => _useLazyLoadingProxies;
+ ///
+ /// 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.
+ ///
+ public virtual bool UseChangeDetectionProxies => _useChangeDetectionProxies;
+
+ ///
+ /// 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.
+ ///
+ public virtual bool CheckEquality => _checkEquality;
+
+ ///
+ /// 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.
+ ///
+ public virtual bool UseProxies => UseLazyLoadingProxies || UseChangeDetectionProxies;
+
///
/// 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
@@ -85,6 +113,22 @@ public virtual ProxiesOptionsExtension WithLazyLoading(bool useLazyLoadingProxie
return clone;
}
+ ///
+ /// 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.
+ ///
+ public virtual ProxiesOptionsExtension WithChangeDetection(bool useChangeDetectionProxies = true, bool checkEquality = true)
+ {
+ var clone = Clone();
+
+ clone._useChangeDetectionProxies = useChangeDetectionProxies;
+ clone._checkEquality = checkEquality;
+
+ return clone;
+ }
+
///
/// 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
@@ -93,7 +137,7 @@ public virtual ProxiesOptionsExtension WithLazyLoading(bool useLazyLoadingProxie
///
public virtual void Validate(IDbContextOptions options)
{
- if (_useLazyLoadingProxies)
+ if (UseProxies)
{
var internalServiceProvider = options.FindExtension()?.InternalServiceProvider;
if (internalServiceProvider != null)
@@ -132,15 +176,24 @@ public ExtensionInfo(IDbContextOptionsExtension extension)
public override bool IsDatabaseProvider => false;
public override string LogFragment
- => _logFragment ??= Extension._useLazyLoadingProxies
+ => _logFragment ??= Extension.UseLazyLoadingProxies && Extension.UseChangeDetectionProxies
+ ? "using lazy-loading and change detection proxies "
+ : Extension.UseLazyLoadingProxies
? "using lazy-loading proxies "
+ : Extension.UseChangeDetectionProxies
+ ? "using change detection proxies "
: "";
- public override long GetServiceProviderHashCode() => Extension._useLazyLoadingProxies ? 541 : 0;
+ public override long GetServiceProviderHashCode() => Extension.UseProxies ? 541 : 0;
public override void PopulateDebugInfo(IDictionary debugInfo)
- => debugInfo["Proxies:" + nameof(ProxiesExtensions.UseLazyLoadingProxies)]
+ {
+ debugInfo["Proxies:" + nameof(ProxiesExtensions.UseLazyLoadingProxies)]
= (Extension._useLazyLoadingProxies ? 541 : 0).ToString(CultureInfo.InvariantCulture);
+
+ debugInfo["Proxies:" + nameof(ProxiesExtensions.UseChangeDetectionProxies)]
+ = (Extension._useChangeDetectionProxies ? 541 : 0).ToString(CultureInfo.InvariantCulture);
+ }
}
}
}
diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs
index 87afd943839..ec088b8b10d 100644
--- a/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs
+++ b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
@@ -30,6 +31,9 @@ private static readonly MethodInfo _createLazyLoadingProxyMethod
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 LazyLoaderParameterBindingFactoryDependencies _lazyLoaderParameterBindingFactoryDependencies;
private readonly IProxyFactory _proxyFactory;
@@ -60,7 +64,7 @@ public ProxyBindingRewriter(
/// Additional information associated with convention execution.
public virtual void ProcessModelFinalized(IConventionModelBuilder modelBuilder, IConventionContext context)
{
- if (_options?.UseLazyLoadingProxies == true)
+ if (_options?.UseProxies == true)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
@@ -71,27 +75,7 @@ public virtual void ProcessModelFinalized(IConventionModelBuilder modelBuilder,
throw new InvalidOperationException(ProxiesStrings.ItsASeal(entityType.DisplayName()));
}
- var proxyType = _proxyFactory.CreateLazyLoadingProxyType(entityType);
-
- 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)));
- }
+ var proxyType = _proxyFactory.CreateProxyType(_options, entityType);
// WARNING: This code is EF internal; it should not be copied. See #10789 #14554
var binding = (InstantiationBinding)entityType[CoreAnnotationNames.ConstructorBinding];
@@ -103,35 +87,100 @@ public virtual void ProcessModelFinalized(IConventionModelBuilder modelBuilder,
// WARNING: This code is EF internal; it should not be copied. See #10789 #14554
binding = (InstantiationBinding)entityType[CoreAnnotationNames.ConstructorBinding];
- entityType.SetAnnotation(
- // WARNING: This code is EF internal; it should not be copied. See #10789 #14554
- CoreAnnotationNames.ConstructorBinding,
- new FactoryMethodBinding(
- _proxyFactory,
- _createLazyLoadingProxyMethod,
- new List
+ 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)));
+ }
+
+ entityType.SetAnnotation(
+ // WARNING: This code is EF internal; it should not be copied. See #10789 #14554
+ CoreAnnotationNames.ConstructorBinding,
+ new FactoryMethodBinding(
+ _proxyFactory,
+ _createLazyLoadingProxyMethod,
+ new List
+ {
+ new DependencyInjectionParameterBinding(typeof(IDbContextOptions), typeof(IDbContextOptions)),
+ new EntityTypeParameterBinding(),
+ new DependencyInjectionParameterBinding(typeof(ILazyLoader), typeof(ILazyLoader), serviceProperty),
+ new ObjectArrayParameterBinding(binding.ParameterBindings)
+ },
+ proxyType));
+ }
+ else
+ {
+ entityType.SetAnnotation(
+ // WARNING: This code is EF internal; it should not be copied. See #10789 #14554
+ CoreAnnotationNames.ConstructorBinding,
+ new FactoryMethodBinding(
+ _proxyFactory,
+ _createProxyMethod,
+ new List
+ {
+ new DependencyInjectionParameterBinding(typeof(IDbContextOptions), typeof(IDbContextOptions)),
+ new EntityTypeParameterBinding(),
+ new ObjectArrayParameterBinding(binding.ParameterBindings)
+ },
+ proxyType));
+
+ foreach (var prop in entityType.GetProperties().Where(p => !p.IsShadowProperty()))
+ {
+ if (prop.PropertyInfo == null)
{
- new EntityTypeParameterBinding(),
- new DependencyInjectionParameterBinding(typeof(ILazyLoader), typeof(ILazyLoader), serviceProperty),
- new ObjectArrayParameterBinding(binding.ParameterBindings)
- },
- proxyType));
+ throw new InvalidOperationException(
+ ProxiesStrings.FieldProperty(prop.Name, entityType.DisplayName()));
+ }
+
+ if (prop.PropertyInfo.SetMethod?.IsVirtual == false)
+ {
+ throw new InvalidOperationException(
+ ProxiesStrings.NonVirtualProperty(prop.Name, entityType.DisplayName()));
+ }
+ }
+ }
foreach (var navigation in entityType.GetNavigations())
{
if (navigation.PropertyInfo == null)
{
throw new InvalidOperationException(
- ProxiesStrings.FieldNavigation(navigation.Name, entityType.DisplayName()));
+ ProxiesStrings.FieldProperty(navigation.Name, entityType.DisplayName()));
}
- if (!navigation.PropertyInfo.GetMethod.IsVirtual)
+ if (_options.UseChangeDetectionProxies
+ && navigation.PropertyInfo.SetMethod?.IsVirtual == false)
{
throw new InvalidOperationException(
- ProxiesStrings.NonVirtualNavigation(navigation.Name, entityType.DisplayName()));
+ ProxiesStrings.NonVirtualProperty(navigation.Name, entityType.DisplayName()));
}
- navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
+ if (_options.UseLazyLoadingProxies)
+ {
+ if (!navigation.PropertyInfo.GetMethod.IsVirtual)
+ {
+ throw new InvalidOperationException(
+ ProxiesStrings.NonVirtualProperty(navigation.Name, entityType.DisplayName()));
+ }
+
+ navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
+ }
}
}
}
diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyChangeTrackingConvention.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyChangeTrackingConvention.cs
new file mode 100644
index 00000000000..f8154a3fd7a
--- /dev/null
+++ b/src/EFCore.Proxies/Proxies/Internal/ProxyChangeTrackingConvention.cs
@@ -0,0 +1,47 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.Metadata.Conventions;
+
+namespace Microsoft.EntityFrameworkCore.Proxies.Internal
+{
+ ///
+ /// 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.
+ ///
+ public class ProxyChangeTrackingConvention : IModelInitializedConvention
+ {
+ private readonly ProxiesOptionsExtension _options;
+
+ ///
+ /// 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.
+ ///
+ public ProxyChangeTrackingConvention(
+ [CanBeNull] ProxiesOptionsExtension options)
+ {
+ _options = options;
+ }
+
+ ///
+ /// Called after a model is finalized.
+ ///
+ /// The builder for the model.
+ /// Additional information associated with convention execution.
+ public virtual void ProcessModelInitialized(IConventionModelBuilder modelBuilder, IConventionContext context)
+ {
+ if (_options?.UseChangeDetectionProxies == true)
+ {
+ modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
+ modelBuilder.HasAnnotation(ModelValidator.SkipChangeTrackingStrategyValidationAnnotation, "true");
+ }
+ }
+ }
+}
diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs
index e9a207e1f57..7851e679d7f 100644
--- a/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs
+++ b/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs
@@ -2,9 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using Castle.DynamicProxy;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Microsoft.EntityFrameworkCore.Proxies.Internal
@@ -17,8 +20,11 @@ namespace Microsoft.EntityFrameworkCore.Proxies.Internal
///
public class ProxyFactory : IProxyFactory
{
+ private static readonly Type _proxyLazyLoaderInterface = typeof(IProxyLazyLoader);
+ private static readonly Type _notifyPropertyChangedInterface = typeof(INotifyPropertyChanged);
+ private static readonly Type _notifyPropertyChangingInterface = typeof(INotifyPropertyChanging);
+
private readonly ProxyGenerator _generator = new ProxyGenerator();
- private static readonly Type[] _additionalInterfacesToProxy = { typeof(IProxyLazyLoader) };
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -37,7 +43,25 @@ public virtual object Create(
throw new InvalidOperationException(CoreStrings.EntityTypeNotFound(entityClrType.ShortDisplayName()));
}
- return CreateLazyLoadingProxy(entityType, context.GetService(), constructorArguments);
+ var options = context.GetService().FindExtension();
+ if (options == null)
+ {
+ throw new InvalidOperationException(ProxiesStrings.ProxyServicesMissing);
+ }
+
+ if (options.UseLazyLoadingProxies)
+ {
+ return CreateLazyLoadingProxy(
+ options,
+ entityType,
+ context.GetService(),
+ constructorArguments);
+ }
+
+ return CreateProxy(
+ options,
+ entityType,
+ constructorArguments);
}
///
@@ -46,10 +70,12 @@ public virtual object Create(
/// 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.
///
- public virtual Type CreateLazyLoadingProxyType(IEntityType entityType)
+ public virtual Type CreateProxyType(
+ ProxiesOptionsExtension options,
+ IEntityType entityType)
=> _generator.ProxyBuilder.CreateClassProxyType(
entityType.ClrType,
- _additionalInterfacesToProxy,
+ GetInterfacesToProxy(options, entityType),
ProxyGenerationOptions.Default);
///
@@ -59,14 +85,161 @@ public virtual Type CreateLazyLoadingProxyType(IEntityType entityType)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual object CreateLazyLoadingProxy(
+ IDbContextOptions dbContextOptions,
+ IEntityType entityType,
+ ILazyLoader loader,
+ object[] constructorArguments)
+ {
+ var options = dbContextOptions.FindExtension();
+ if (options == null)
+ {
+ throw new InvalidOperationException(ProxiesStrings.ProxyServicesMissing);
+ }
+
+ return CreateLazyLoadingProxy(
+ options,
+ entityType,
+ loader,
+ constructorArguments);
+ }
+
+ private object CreateLazyLoadingProxy(
+ ProxiesOptionsExtension options,
IEntityType entityType,
ILazyLoader loader,
object[] constructorArguments)
=> _generator.CreateClassProxy(
entityType.ClrType,
- _additionalInterfacesToProxy,
+ GetInterfacesToProxy(options, entityType),
ProxyGenerationOptions.Default,
constructorArguments,
- new LazyLoadingInterceptor(entityType, loader));
+ GetNotifyChangeInterceptors(options, entityType, new LazyLoadingInterceptor(entityType, loader)));
+
+ ///
+ /// 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.
+ ///
+ public virtual object CreateProxy(
+ IDbContextOptions dbContextOptions,
+ IEntityType entityType,
+ object[] constructorArguments)
+ {
+ var options = dbContextOptions.FindExtension();
+ if (options == null)
+ {
+ throw new InvalidOperationException(ProxiesStrings.ProxyServicesMissing);
+ }
+
+ return CreateProxy(
+ options,
+ entityType,
+ constructorArguments);
+ }
+
+ private object CreateProxy(
+ ProxiesOptionsExtension options,
+ IEntityType entityType,
+ object[] constructorArguments)
+ => _generator.CreateClassProxy(
+ entityType.ClrType,
+ GetInterfacesToProxy(options, entityType),
+ ProxyGenerationOptions.Default,
+ constructorArguments,
+ GetNotifyChangeInterceptors(options, entityType));
+
+ private Type[] GetInterfacesToProxy(
+ ProxiesOptionsExtension options,
+ IEntityType entityType)
+ {
+ var interfacesToProxy = new List();
+
+ if (options.UseLazyLoadingProxies)
+ {
+ interfacesToProxy.Add(_proxyLazyLoaderInterface);
+ }
+
+ if (options.UseChangeDetectionProxies)
+ {
+ var changeTrackingStrategy = entityType.GetChangeTrackingStrategy();
+ switch (changeTrackingStrategy)
+ {
+ case ChangeTrackingStrategy.ChangedNotifications:
+
+ if (!_notifyPropertyChangedInterface.IsAssignableFrom(entityType.ClrType))
+ {
+ interfacesToProxy.Add(_notifyPropertyChangedInterface);
+ }
+
+ break;
+ case ChangeTrackingStrategy.ChangingAndChangedNotifications:
+ case ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues:
+
+ if (!_notifyPropertyChangedInterface.IsAssignableFrom(entityType.ClrType))
+ {
+ interfacesToProxy.Add(_notifyPropertyChangedInterface);
+ }
+
+ if (!_notifyPropertyChangingInterface.IsAssignableFrom(entityType.ClrType))
+ {
+ interfacesToProxy.Add(_notifyPropertyChangingInterface);
+ }
+
+ break;
+ default:
+ break;
+ }
+ }
+
+ return interfacesToProxy.ToArray();
+ }
+
+ private Castle.DynamicProxy.IInterceptor[] GetNotifyChangeInterceptors(
+ ProxiesOptionsExtension options,
+ IEntityType entityType,
+ LazyLoadingInterceptor lazyLoadingInterceptor = null)
+ {
+ var interceptors = new List();
+
+ if (lazyLoadingInterceptor != null)
+ {
+ interceptors.Add(lazyLoadingInterceptor);
+ }
+
+ if (options.UseChangeDetectionProxies)
+ {
+ var changeTrackingStrategy = entityType.GetChangeTrackingStrategy();
+ switch (changeTrackingStrategy)
+ {
+ case ChangeTrackingStrategy.ChangedNotifications:
+
+ if (!_notifyPropertyChangedInterface.IsAssignableFrom(entityType.ClrType))
+ {
+ interceptors.Add(new PropertyChangedInterceptor(entityType, options.CheckEquality));
+ }
+
+ break;
+ case ChangeTrackingStrategy.ChangingAndChangedNotifications:
+ case ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues:
+
+ if (!_notifyPropertyChangedInterface.IsAssignableFrom(entityType.ClrType))
+ {
+ interceptors.Add(new PropertyChangedInterceptor(entityType, options.CheckEquality));
+ }
+
+ if (!_notifyPropertyChangingInterface.IsAssignableFrom(entityType.ClrType))
+ {
+ interceptors.Add(new PropertyChangingInterceptor(entityType, options.CheckEquality));
+ }
+
+ break;
+ default:
+ break;
+ }
+ }
+
+ return interceptors.ToArray();
+ }
}
}
diff --git a/src/EFCore.Proxies/ProxiesExtensions.cs b/src/EFCore.Proxies/ProxiesExtensions.cs
index 7f062bbf1d3..65d868a503f 100644
--- a/src/EFCore.Proxies/ProxiesExtensions.cs
+++ b/src/EFCore.Proxies/ProxiesExtensions.cs
@@ -16,6 +16,65 @@ namespace Microsoft.EntityFrameworkCore
///
public static class ProxiesExtensions
{
+ ///
+ ///
+ /// Turns on the creation of change detection proxies.
+ ///
+ ///
+ /// Note that this requires appropriate services to be available in the EF internal service provider. Normally this
+ /// will happen automatically, but if the application is controlling the service provider, then a call to
+ /// may be needed.
+ ///
+ ///
+ ///
+ /// The options builder, as passed to
+ /// or exposed AddDbContext.
+ ///
+ /// True to use change detection proxies; false to prevent their use.
+ /// True if proxy change detection should check if the incoming value is equal to the current value before notifying. Defaults to True.
+ /// The same builder to allow method calls to be chained.
+ public static DbContextOptionsBuilder UseChangeDetectionProxies(
+ [NotNull] this DbContextOptionsBuilder optionsBuilder,
+ bool useChangeDetectionProxies = true,
+ bool checkEquality = true)
+ {
+ Check.NotNull(optionsBuilder, nameof(optionsBuilder));
+
+ var extension = optionsBuilder.Options.FindExtension()
+ ?? new ProxiesOptionsExtension();
+
+ extension = extension.WithChangeDetection(useChangeDetectionProxies, checkEquality);
+
+ ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);
+
+ return optionsBuilder;
+ }
+
+ ///
+ ///
+ /// Turns on the creation of change detection proxies.
+ ///
+ ///
+ /// Note that this requires appropriate services to be available in the EF internal service provider. Normally this
+ /// will happen automatically, but if the application is controlling the service provider, then a call to
+ /// may be needed.
+ ///
+ ///
+ /// The type.
+ ///
+ /// The options builder, as passed to
+ /// or exposed AddDbContext.
+ ///
+ /// True to use change detection proxies; false to prevent their use.
+ /// True if proxy change detection should check if the incoming value is equal to the current value before notifying. Defaults to True.
+ /// The same builder to allow method calls to be chained.
+ public static DbContextOptionsBuilder UseChangeDetectionProxies(
+ [NotNull] this DbContextOptionsBuilder optionsBuilder,
+ bool useChangeDetectionProxies = true,
+ bool checkEquality = true)
+ where TContext : DbContext
+ => (DbContextOptionsBuilder)UseChangeDetectionProxies((DbContextOptionsBuilder)optionsBuilder, useChangeDetectionProxies, checkEquality);
+
///
///
/// Turns on the creation of lazy-loading proxies.
@@ -127,7 +186,7 @@ private static object CreateProxy(
{
var options = serviceProvider.GetService().FindExtension();
- if (options?.UseLazyLoadingProxies != true)
+ if (options?.UseProxies != true)
{
throw new InvalidOperationException(ProxiesStrings.ProxiesNotEnabled(entityType.ShortDisplayName()));
}
diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs
index ecc38f320b8..6fdc3c28f33 100644
--- a/src/EFCore/Infrastructure/ModelValidator.cs
+++ b/src/EFCore/Infrastructure/ModelValidator.cs
@@ -29,6 +29,14 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure
///
public class ModelValidator : IModelValidator
{
+ ///
+ /// 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.
+ ///
+ public const string SkipChangeTrackingStrategyValidationAnnotation = "ModelValidator.SkipChangeTrackingStrategyValidation";
+
///
/// Creates a new instance of .
///
@@ -585,12 +593,15 @@ protected virtual void ValidateChangeTrackingStrategy(
{
Check.NotNull(model, nameof(model));
- foreach (var entityType in model.GetEntityTypes())
+ if ((string)model[SkipChangeTrackingStrategyValidationAnnotation] != "true")
{
- var errorMessage = entityType.AsEntityType().CheckChangeTrackingStrategy(entityType.GetChangeTrackingStrategy());
- if (errorMessage != null)
+ foreach (var entityType in model.GetEntityTypes())
{
- throw new InvalidOperationException(errorMessage);
+ var errorMessage = entityType.AsEntityType().CheckChangeTrackingStrategy(entityType.GetChangeTrackingStrategy());
+ if (errorMessage != null)
+ {
+ throw new InvalidOperationException(errorMessage);
+ }
}
}
}
diff --git a/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs b/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs
new file mode 100644
index 00000000000..9fd79c6601b
--- /dev/null
+++ b/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs
@@ -0,0 +1,330 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.ComponentModel;
+using System.Linq;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.TestUtilities;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore
+{
+ public class ChangeDetectionProxyTests
+ {
+ [ConditionalFact]
+ public void Throws_if_sealed_class()
+ {
+ using var context = new ChangeContext();
+ Assert.Equal(
+ ProxiesStrings.ItsASeal(nameof(ChangeSealedEntity)),
+ Assert.Throws(
+ () => context.Model).Message);
+ }
+
+ [ConditionalFact]
+ public void Throws_if_non_virtual_property()
+ {
+ using var context = new ChangeContext();
+ Assert.Equal(
+ ProxiesStrings.NonVirtualProperty(nameof(ChangeNonVirtualPropEntity.Id), nameof(ChangeNonVirtualPropEntity)),
+ Assert.Throws(
+ () => context.Model).Message);
+ }
+
+ [ConditionalFact]
+ public void Throws_if_non_virtual_navigation()
+ {
+ using var context = new ChangeContext();
+ Assert.Equal(
+ ProxiesStrings.NonVirtualProperty(nameof(ChangeNonVirtualNavEntity.SelfRef), nameof(ChangeNonVirtualNavEntity)),
+ Assert.Throws(
+ () => context.Model).Message);
+ }
+
+ [ConditionalFact]
+ public void Sets_default_change_tracking_strategy()
+ {
+ using var context = new ChangeContext();
+ Assert.Equal(
+ ChangeTrackingStrategy.ChangingAndChangedNotifications,
+ context.Model.GetChangeTrackingStrategy());
+ }
+
+ [ConditionalFact]
+ public void Default_change_tracking_strategy_doesnt_overwrite_entity_strategy()
+ {
+ using var context = new ChangeContext(
+ entityBuilderAction: b =>
+ {
+ b.HasChangeTrackingStrategy(ChangeTrackingStrategy.Snapshot);
+ });
+
+ var entityType = context.Model.FindEntityType(typeof(ChangeValueEntity));
+ Assert.Equal(
+ ChangeTrackingStrategy.Snapshot,
+ entityType.GetChangeTrackingStrategy());
+ }
+
+ private static readonly Type changeInterface = typeof(INotifyPropertyChanged);
+ private static readonly Type changingInterface = typeof(INotifyPropertyChanging);
+
+ [ConditionalFact]
+ public void Proxies_correct_interfaces_for_Snapshot()
+ {
+ using var context = new ProxyGenerationContext(ChangeTrackingStrategy.Snapshot);
+ var proxy = context.CreateProxy();
+ var proxyType = proxy.GetType();
+
+ Assert.False(changeInterface.IsAssignableFrom(proxyType));
+ Assert.False(changingInterface.IsAssignableFrom(proxyType));
+ }
+
+ [ConditionalFact]
+ public void Proxies_correct_interfaces_for_ChangedNotifications()
+ {
+ using var context = new ProxyGenerationContext(ChangeTrackingStrategy.ChangedNotifications);
+ var proxy = context.CreateProxy();
+ var proxyType = proxy.GetType();
+
+ Assert.True(changeInterface.IsAssignableFrom(proxyType));
+ Assert.False(changingInterface.IsAssignableFrom(proxyType));
+ }
+
+ [ConditionalFact]
+ public void Proxies_correct_interfaces_for_ChangingAndChangedNotifications()
+ {
+ using var context = new ProxyGenerationContext(ChangeTrackingStrategy.ChangingAndChangedNotifications);
+ var proxy = context.CreateProxy();
+ var proxyType = proxy.GetType();
+
+ Assert.True(changeInterface.IsAssignableFrom(proxyType));
+ Assert.True(changingInterface.IsAssignableFrom(proxyType));
+ }
+
+ [ConditionalFact]
+ public void Proxies_correct_interfaces_for_ChangingAndChangedNotificationsWithOriginalValues()
+ {
+ using var context = new ProxyGenerationContext(ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues);
+ var proxy = context.CreateProxy();
+ var proxyType = proxy.GetType();
+
+ Assert.True(changeInterface.IsAssignableFrom(proxyType));
+ Assert.True(changingInterface.IsAssignableFrom(proxyType));
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Raises_changed_event_when_changed(bool useLazyLoading)
+ {
+ using var context = new ChangeContext(useLazyLoading: useLazyLoading);
+ var proxy = context.CreateProxy();
+ context.Add(proxy);
+ context.SaveChanges();
+
+ var eventRaised = false;
+
+ ((INotifyPropertyChanged)proxy).PropertyChanged += (s, e) =>
+ {
+ eventRaised = true;
+
+ Assert.Equal(proxy, s);
+
+ Assert.Equal(
+ nameof(ChangeValueEntity.Value),
+ e.PropertyName);
+
+ Assert.Equal(
+ 10,
+ ((ChangeValueEntity)s).Value);
+ };
+
+ proxy.Value = 10;
+ Assert.True(eventRaised);
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Raises_changing_event_before_change(bool useLazyLoading)
+ {
+ using var context = new ChangeContext(useLazyLoading: useLazyLoading);
+ var proxy = context.CreateProxy();
+ proxy.Value = 5;
+ context.Add(proxy);
+ context.SaveChanges();
+
+ var eventRaised = false;
+
+ ((INotifyPropertyChanging)proxy).PropertyChanging += (s, e) =>
+ {
+ eventRaised = true;
+
+ Assert.Equal(proxy, s);
+
+ Assert.Equal(
+ nameof(ChangeValueEntity.Value),
+ e.PropertyName);
+
+ Assert.Equal(
+ 5,
+ ((ChangeValueEntity)s).Value);
+ };
+
+ proxy.Value = 10;
+ Assert.True(eventRaised);
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Doesnt_raise_change_event_when_equal_and_check_equality_true(bool useLazyLoading)
+ {
+ using var context = new ChangeContext(useLazyLoading: useLazyLoading, checkEquality: true);
+ var proxy = context.CreateProxy();
+ proxy.Value = 10;
+ context.Add(proxy);
+ context.SaveChanges();
+
+ var eventRaised = false;
+
+ ((INotifyPropertyChanged)proxy).PropertyChanged += (s, e) =>
+ {
+ eventRaised = true;
+ };
+
+ proxy.Value = 10;
+ Assert.False(eventRaised);
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Doesnt_raise_changing_event_when_equal_and_check_equality_true(bool useLazyLoading)
+ {
+ using var context = new ChangeContext(useLazyLoading: useLazyLoading, checkEquality: true);
+ var proxy = context.CreateProxy();
+ proxy.Value = 10;
+ context.Add(proxy);
+ context.SaveChanges();
+
+ var eventRaised = false;
+
+ ((INotifyPropertyChanging)proxy).PropertyChanging += (s, e) =>
+ {
+ eventRaised = true;
+ };
+
+ proxy.Value = 10;
+ Assert.False(eventRaised);
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Raises_change_event_when_equal_and_check_equality_false(bool useLazyLoading)
+ {
+ using var context = new ChangeContext(useLazyLoading: useLazyLoading, checkEquality: false);
+ var proxy = context.CreateProxy();
+ proxy.Value = 10;
+ context.Add(proxy);
+ context.SaveChanges();
+
+ var eventRaised = false;
+
+ ((INotifyPropertyChanged)proxy).PropertyChanged += (s, e) =>
+ {
+ eventRaised = true;
+ };
+
+ proxy.Value = 10;
+ Assert.True(eventRaised);
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Raises_changing_event_when_equal_and_check_equality_false(bool useLazyLoading)
+ {
+ using var context = new ChangeContext(useLazyLoading: useLazyLoading, checkEquality: false);
+ var proxy = context.CreateProxy();
+ proxy.Value = 10;
+ context.Add(proxy);
+ context.SaveChanges();
+
+ var eventRaised = false;
+
+ ((INotifyPropertyChanging)proxy).PropertyChanging += (s, e) =>
+ {
+ eventRaised = true;
+ };
+
+ proxy.Value = 10;
+ Assert.True(eventRaised);
+ }
+
+ private class ChangeContext : TestContext
+ where TEntity : class
+ {
+ private readonly Action> _entityBuilderAction;
+
+ public ChangeContext(bool useLazyLoading = false, bool checkEquality = true, Action> entityBuilderAction = null)
+ : base(dbName: "ChangeDetectionContext", useLazyLoading: useLazyLoading, useChangeDetection: true, checkEquality: checkEquality)
+ {
+ _entityBuilderAction = entityBuilderAction;
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ var builder = modelBuilder.Entity();
+ _entityBuilderAction?.Invoke(builder);
+ }
+ }
+
+ public sealed class ChangeSealedEntity
+ {
+ public int Id { get; set; }
+ }
+
+ public class ChangeNonVirtualPropEntity
+ {
+ public int Id { get; set; }
+
+ public virtual ChangeNonVirtualPropEntity SelfRef { get; set; }
+ }
+
+ public class ChangeNonVirtualNavEntity
+ {
+ public virtual int Id { get; set; }
+
+ public ChangeNonVirtualNavEntity SelfRef { get; set; }
+ }
+
+ public class ChangeValueEntity
+ {
+ public virtual int Id { get; set; }
+
+ public virtual int Value { get; set; }
+ }
+
+ public class ChangeSelfRefEntity
+ {
+ public virtual int Id { get; set; }
+
+ public virtual ChangeSelfRefEntity SelfRef { get; set; }
+ }
+
+ private class ProxyGenerationContext : TestContext
+ {
+ public ProxyGenerationContext(
+ ChangeTrackingStrategy changeTrackingStrategy)
+ : base("ProxyGenerationContext", false, true, true, changeTrackingStrategy)
+ {
+ }
+ }
+ }
+}
diff --git a/test/EFCore.Proxies.Tests/LazyLoadingProxyTests.cs b/test/EFCore.Proxies.Tests/LazyLoadingProxyTests.cs
index 60037500334..b247b59afe2 100644
--- a/test/EFCore.Proxies.Tests/LazyLoadingProxyTests.cs
+++ b/test/EFCore.Proxies.Tests/LazyLoadingProxyTests.cs
@@ -4,12 +4,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Castle.DynamicProxy;
-using Castle.DynamicProxy.Generators;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Internal;
-using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@@ -18,148 +15,12 @@ namespace Microsoft.EntityFrameworkCore
{
public class LazyLoadingProxyTests
{
- [ConditionalFact]
- public void Materialization_uses_parameterless_constructor()
- {
- using (var context = new NeweyContext(nameof(Materialization_uses_parameterless_constructor)))
- {
- context.Add(new March82GGtp());
- context.SaveChanges();
- }
-
- using (var context = new NeweyContext(nameof(Materialization_uses_parameterless_constructor)))
- {
- Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
- }
- }
-
- [ConditionalFact]
- public void Materialization_uses_parameterized_constructor()
- {
- using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor)))
- {
- context.Add(new March881(77, "Leyton House"));
- context.SaveChanges();
- }
-
- using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor)))
- {
- var proxy = context.Set().Single();
-
- Assert.Same(typeof(March881), proxy.GetType().BaseType);
- Assert.Equal(77, proxy.Id);
- Assert.Equal("Leyton House", proxy.Sponsor);
- }
- }
-
- [ConditionalFact]
- public void Materialization_uses_parameterized_constructor_taking_context()
- {
- using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor_taking_context)))
- {
- context.Add(new WilliamsFw14(context, 6, "Canon"));
- context.SaveChanges();
- }
-
- using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor_taking_context)))
- {
- var proxy = context.Set().Single();
-
- Assert.Same(typeof(WilliamsFw14), proxy.GetType().BaseType);
- Assert.Same(context, proxy.Context);
- Assert.Equal(6, proxy.Id);
- Assert.Equal("Canon", proxy.Sponsor);
- }
- }
-
- [ConditionalFact]
- public void CreateProxy_uses_parameterless_constructor()
- {
- using var context = new NeweyContext();
- Assert.Same(typeof(March82GGtp), context.CreateProxy().GetType().BaseType);
- }
-
- [ConditionalFact]
- public void CreateProxy_uses_parameterized_constructor()
- {
- using var context = new NeweyContext();
- var proxy = context.CreateProxy(77, "Leyton House");
-
- Assert.Same(typeof(March881), proxy.GetType().BaseType);
- Assert.Equal(77, proxy.Id);
- Assert.Equal("Leyton House", proxy.Sponsor);
- }
-
- [ConditionalFact]
- public void CreateProxy_uses_parameterized_constructor_taking_context()
- {
- using var context = new NeweyContext();
- var proxy = context.CreateProxy(context, 6, "Canon");
-
- Assert.Same(typeof(WilliamsFw14), proxy.GetType().BaseType);
- Assert.Same(context, proxy.Context);
- Assert.Equal(6, proxy.Id);
- Assert.Equal("Canon", proxy.Sponsor);
- }
-
- [ConditionalFact]
- public void Proxies_only_created_if_Use_called()
- {
- using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called), false))
- {
- context.Add(new March82GGtp());
- context.SaveChanges();
- }
-
- using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called), false))
- {
- Assert.Same(typeof(March82GGtp), context.Set().Single().GetType());
- }
-
- using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called)))
- {
- Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
- }
- }
-
- [ConditionalFact]
- public void Proxy_services_must_be_available()
- {
- var withoutProxies = new ServiceCollection()
- .AddEntityFrameworkInMemoryDatabase()
- .BuildServiceProvider();
-
- using (var context = new NeweyContext(withoutProxies, nameof(Proxy_services_must_be_available), false))
- {
- context.Add(new March82GGtp());
- context.SaveChanges();
- }
-
- using (var context = new NeweyContext(withoutProxies, nameof(Proxy_services_must_be_available), false))
- {
- Assert.Same(typeof(March82GGtp), context.Set().Single().GetType());
- }
-
- using (var context = new NeweyContext(nameof(Proxy_services_must_be_available)))
- {
- Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
- }
-
- using (var context = new NeweyContext(withoutProxies, nameof(Proxy_services_must_be_available)))
- {
- Assert.Equal(
- ProxiesStrings.ProxyServicesMissing,
- Assert.Throws(
- () => context.Model).Message);
- }
- }
-
[ConditionalFact]
public void Throws_if_sealed_class()
{
- using var context = new NeweyContextN1();
+ using var context = new LazyContext();
Assert.Equal(
- ProxiesStrings.ItsASeal(nameof(McLarenMp418)),
+ ProxiesStrings.ItsASeal(nameof(LazySealedEntity)),
Assert.Throws(
() => context.Model).Message);
}
@@ -167,9 +28,9 @@ public void Throws_if_sealed_class()
[ConditionalFact]
public void Throws_if_non_virtual_navigation()
{
- using var context = new NeweyContextN2();
+ using var context = new LazyContext();
Assert.Equal(
- ProxiesStrings.NonVirtualNavigation(nameof(McLarenMp419.SelfRef), nameof(McLarenMp419)),
+ ProxiesStrings.NonVirtualProperty(nameof(LazyNonVirtualNavEntity.SelfRef), nameof(LazyNonVirtualNavEntity)),
Assert.Throws(
() => context.Model).Message);
}
@@ -177,71 +38,13 @@ public void Throws_if_non_virtual_navigation()
[ConditionalFact]
public void Throws_if_no_field_found()
{
- using var context = new NeweyContextN3();
+ using var context = new LazyContext();
Assert.Equal(
- CoreStrings.NoBackingFieldLazyLoading(nameof(MarchCg901.SelfRef), nameof(MarchCg901)),
+ CoreStrings.NoBackingFieldLazyLoading(nameof(LazyHiddenFieldEntity.SelfRef), nameof(LazyHiddenFieldEntity)),
Assert.Throws(
() => context.Model).Message);
}
- [ConditionalFact]
- public void Throws_if_type_not_available_to_Castle()
- {
- using var context = new NeweyContextN4();
- Assert.Throws(() => context.CreateProxy());
- }
-
- [ConditionalFact]
- public void Throws_if_constructor_not_available_to_Castle()
- {
- using var context = new NeweyContextN5();
- Assert.Throws(() => context.CreateProxy());
- }
-
- [ConditionalFact]
- public void CreateProxy_throws_if_constructor_args_do_not_match()
- {
- using var context = new NeweyContext();
- Assert.Throws(() => context.CreateProxy(77, 88));
- }
-
- [ConditionalFact]
- public void CreateProxy_throws_if_wrong_number_of_constructor_args()
- {
- using var context = new NeweyContext();
- Assert.Throws(() => context.CreateProxy(77, 88, 99));
- }
-
- [ConditionalFact]
- public void Throws_if_create_proxy_for_non_mapped_type()
- {
- using var context = new NeweyContextN();
- Assert.Equal(
- CoreStrings.EntityTypeNotFound(nameof(March82GGtp)),
- Assert.Throws(
- () => context.CreateProxy()).Message);
- }
-
- [ConditionalFact]
- public void Throws_if_create_proxy_when_proxies_not_used()
- {
- using var context = new NeweyContextN6();
- Assert.Equal(
- ProxiesStrings.ProxiesNotEnabled(nameof(RedBullRb3)),
- Assert.Throws(
- () => context.CreateProxy()).Message);
- }
-
- [ConditionalFact]
- public void Throws_if_create_proxy_when_proxies_not_enabled()
- {
- using var context = new NeweyContextN7();
- Assert.Equal(
- ProxiesStrings.ProxiesNotEnabled(nameof(RedBullRb3)),
- Assert.Throws(
- () => context.CreateProxy()).Message);
- }
-
[ConditionalFact]
public void Throws_when_context_is_disposed()
{
@@ -279,231 +82,61 @@ public void Throws_when_context_is_disposed()
() => phone.Texts).Message);
}
- private class JammieDodgerContext : DbContext
+ private class LazyContext : TestContext
+ where TEntity : class
{
- public JammieDodgerContext(DbContextOptions options)
- : base(options)
+ public LazyContext()
+ : base(dbName: "LazyLoadingContext", useLazyLoading: true, useChangeDetection: false)
{
}
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
}
- public class Phone
+ public sealed class LazySealedEntity
{
public int Id { get; set; }
- public virtual ICollection Texts { get; set; }
}
- public class Text
+ public class LazyNonVirtualNavEntity
{
public int Id { get; set; }
- }
-
- public class March82GGtp
- {
- public int Id { get; set; }
- }
-
- public class March881
- {
- public March881(int id, string sponsor)
- {
- Id = id;
- Sponsor = sponsor;
- }
-
- public int Id { get; }
- public string Sponsor { get; }
- }
-
- public class WilliamsFw14
- {
- public WilliamsFw14(DbContext context, int id, string sponsor)
- {
- Context = context;
- Id = id;
- Sponsor = sponsor;
- }
- public DbContext Context { get; }
- public int Id { get; }
- public string Sponsor { get; }
+ public LazyNonVirtualNavEntity SelfRef { get; set; }
}
- private class NeweyContext : DbContext
+ public class LazyHiddenFieldEntity
{
- private readonly IServiceProvider _internalServiceProvider;
- private static readonly InMemoryDatabaseRoot _dbRoot = new InMemoryDatabaseRoot();
- private readonly bool _useProxies;
- private readonly string _dbName;
-
- public NeweyContext(string dbName = null, bool useProxies = true)
- {
- _internalServiceProvider
- = new ServiceCollection()
- .AddEntityFrameworkInMemoryDatabase()
- .AddEntityFrameworkProxies()
- .BuildServiceProvider();
-
- _dbName = dbName;
- _useProxies = useProxies;
- }
-
- public NeweyContext(IServiceProvider internalServiceProvider, string dbName = null, bool useProxies = true)
- : this(dbName, useProxies)
- {
- _internalServiceProvider = internalServiceProvider;
- }
-
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- if (_useProxies)
- {
- optionsBuilder.UseLazyLoadingProxies();
- }
-
- if (_internalServiceProvider != null)
- {
- optionsBuilder.UseInternalServiceProvider(_internalServiceProvider);
- }
-
- optionsBuilder.UseInMemoryDatabase(_dbName ?? nameof(NeweyContext), _dbRoot);
- }
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity();
-
- modelBuilder.Entity(
- b =>
- {
- b.Property(e => e.Id);
- b.Property(e => e.Sponsor);
- });
-
- modelBuilder.Entity(
- b =>
- {
- b.Property(e => e.Id);
- b.Property(e => e.Sponsor);
- });
- }
- }
-
- public sealed class McLarenMp418
- {
- public int Id { get; set; }
- }
-
- private class NeweyContextN : DbContext
- {
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- => optionsBuilder
- .UseLazyLoadingProxies()
- .UseInternalServiceProvider(
- new ServiceCollection()
- .AddEntityFrameworkInMemoryDatabase()
- .AddEntityFrameworkProxies()
- .BuildServiceProvider())
- .UseInMemoryDatabase(Guid.NewGuid().ToString());
- }
-
- private class NeweyContextN1 : NeweyContextN
- {
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
- }
-
- public class McLarenMp419
- {
- public int Id { get; set; }
-
- public McLarenMp419 SelfRef { get; set; }
- }
-
- private class NeweyContextN2 : NeweyContextN
- {
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
- }
-
- public class MarchCg901
- {
- private MarchCg901 _hiddenBackingField;
+ private LazyHiddenFieldEntity _hiddenBackingField;
public int Id { get; set; }
// ReSharper disable once ConvertToAutoProperty
- public virtual MarchCg901 SelfRef
+ public virtual LazyHiddenFieldEntity SelfRef
{
get => _hiddenBackingField;
set => _hiddenBackingField = value;
}
}
- private class NeweyContextN3 : NeweyContextN
- {
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
- }
-
- internal class McLarenMp421
- {
- public int Id { get; set; }
- }
-
- private class NeweyContextN4 : NeweyContextN
- {
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
- }
-
- public class RedBullRb3
+ private class JammieDodgerContext : DbContext
{
- internal RedBullRb3()
+ public JammieDodgerContext(DbContextOptions options)
+ : base(options)
{
}
- public int Id { get; set; }
- }
-
- private class NeweyContextN5 : NeweyContextN
- {
protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
+ => modelBuilder.Entity();
}
- private class NeweyContextN6 : DbContext
+ public class Phone
{
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- => optionsBuilder
- .UseInternalServiceProvider(
- new ServiceCollection()
- .AddEntityFrameworkInMemoryDatabase()
- .AddEntityFrameworkProxies()
- .BuildServiceProvider())
- .UseInMemoryDatabase(Guid.NewGuid().ToString());
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
+ public int Id { get; set; }
+ public virtual ICollection Texts { get; set; }
}
- private class NeweyContextN7 : DbContext
+ public class Text
{
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- => optionsBuilder
- .UseLazyLoadingProxies(false)
- .UseInternalServiceProvider(
- new ServiceCollection()
- .AddEntityFrameworkInMemoryDatabase()
- .AddEntityFrameworkProxies()
- .BuildServiceProvider())
- .UseInMemoryDatabase(Guid.NewGuid().ToString());
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- => modelBuilder.Entity();
+ public int Id { get; set; }
}
}
}
diff --git a/test/EFCore.Proxies.Tests/ProxyTests.cs b/test/EFCore.Proxies.Tests/ProxyTests.cs
new file mode 100644
index 00000000000..b9a150543cd
--- /dev/null
+++ b/test/EFCore.Proxies.Tests/ProxyTests.cs
@@ -0,0 +1,393 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using Castle.DynamicProxy;
+using Castle.DynamicProxy.Generators;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.Storage;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore
+{
+ public class ProxyTests
+ {
+ [ConditionalFact]
+ public void Materialization_uses_parameterless_constructor()
+ {
+ using (var context = new NeweyContext(nameof(Materialization_uses_parameterless_constructor)))
+ {
+ context.Add(new March82GGtp());
+ context.SaveChanges();
+ }
+
+ using (var context = new NeweyContext(nameof(Materialization_uses_parameterless_constructor)))
+ {
+ Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
+ }
+ }
+
+ [ConditionalFact]
+ public void Materialization_uses_parameterized_constructor()
+ {
+ using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor)))
+ {
+ context.Add(new March881(77, "Leyton House"));
+ context.SaveChanges();
+ }
+
+ using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor)))
+ {
+ var proxy = context.Set().Single();
+
+ Assert.Same(typeof(March881), proxy.GetType().BaseType);
+ Assert.Equal(77, proxy.Id);
+ Assert.Equal("Leyton House", proxy.Sponsor);
+ }
+ }
+
+ [ConditionalFact]
+ public void Materialization_uses_parameterized_constructor_taking_context()
+ {
+ using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor_taking_context)))
+ {
+ context.Add(new WilliamsFw14(context, 6, "Canon"));
+ context.SaveChanges();
+ }
+
+ using (var context = new NeweyContext(nameof(Materialization_uses_parameterized_constructor_taking_context)))
+ {
+ var proxy = context.Set().Single();
+
+ Assert.Same(typeof(WilliamsFw14), proxy.GetType().BaseType);
+ Assert.Same(context, proxy.Context);
+ Assert.Equal(6, proxy.Id);
+ Assert.Equal("Canon", proxy.Sponsor);
+ }
+ }
+
+ [ConditionalFact]
+ public void CreateProxy_uses_parameterless_constructor()
+ {
+ using var context = new NeweyContext();
+ Assert.Same(typeof(March82GGtp), context.CreateProxy().GetType().BaseType);
+ }
+
+ [ConditionalFact]
+ public void CreateProxy_uses_parameterized_constructor()
+ {
+ using var context = new NeweyContext();
+ var proxy = context.CreateProxy(77, "Leyton House");
+
+ Assert.Same(typeof(March881), proxy.GetType().BaseType);
+ Assert.Equal(77, proxy.Id);
+ Assert.Equal("Leyton House", proxy.Sponsor);
+ }
+
+ [ConditionalFact]
+ public void CreateProxy_uses_parameterized_constructor_taking_context()
+ {
+ using var context = new NeweyContext();
+ var proxy = context.CreateProxy(context, 6, "Canon");
+
+ Assert.Same(typeof(WilliamsFw14), proxy.GetType().BaseType);
+ Assert.Same(context, proxy.Context);
+ Assert.Equal(6, proxy.Id);
+ Assert.Equal("Canon", proxy.Sponsor);
+ }
+
+ [ConditionalFact]
+ public void Proxies_only_created_if_Use_called()
+ {
+ using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called), false, false))
+ {
+ context.Add(new March82GGtp());
+ context.SaveChanges();
+ }
+
+ using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called), false, false))
+ {
+ Assert.Same(typeof(March82GGtp), context.Set().Single().GetType());
+ }
+
+ using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called), true, false))
+ {
+ Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
+ }
+
+ using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called), false, true))
+ {
+ Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
+ }
+
+ using (var context = new NeweyContext(nameof(Proxies_only_created_if_Use_called), true, true))
+ {
+ Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
+ }
+ }
+
+ [ConditionalFact]
+ public void Proxy_services_must_be_available()
+ {
+ var withoutProxies = new ServiceCollection()
+ .AddEntityFrameworkInMemoryDatabase()
+ .BuildServiceProvider();
+
+ using (var context = new NeweyContext(withoutProxies, nameof(Proxy_services_must_be_available), false))
+ {
+ context.Add(new March82GGtp());
+ context.SaveChanges();
+ }
+
+ using (var context = new NeweyContext(withoutProxies, nameof(Proxy_services_must_be_available), false))
+ {
+ Assert.Same(typeof(March82GGtp), context.Set().Single().GetType());
+ }
+
+ using (var context = new NeweyContext(nameof(Proxy_services_must_be_available)))
+ {
+ Assert.Same(typeof(March82GGtp), context.Set().Single().GetType().BaseType);
+ }
+
+ using (var context = new NeweyContext(withoutProxies, nameof(Proxy_services_must_be_available)))
+ {
+ Assert.Equal(
+ ProxiesStrings.ProxyServicesMissing,
+ Assert.Throws(
+ () => context.Model).Message);
+ }
+ }
+
+ [ConditionalFact]
+ public void Throws_if_type_not_available_to_Castle()
+ {
+ using var context = new NeweyContextN4();
+ Assert.Throws(() => context.CreateProxy());
+ }
+
+ [ConditionalFact]
+ public void Throws_if_constructor_not_available_to_Castle()
+ {
+ using var context = new NeweyContextN5();
+ Assert.Throws(() => context.CreateProxy());
+ }
+
+ [ConditionalFact]
+ public void CreateProxy_throws_if_constructor_args_do_not_match()
+ {
+ using var context = new NeweyContext();
+ Assert.Throws(() => context.CreateProxy(77, 88));
+ }
+
+ [ConditionalFact]
+ public void CreateProxy_throws_if_wrong_number_of_constructor_args()
+ {
+ using var context = new NeweyContext();
+ Assert.Throws(() => context.CreateProxy(77, 88, 99));
+ }
+
+ [ConditionalFact]
+ public void Throws_if_create_proxy_for_non_mapped_type()
+ {
+ using var context = new NeweyContextN();
+ Assert.Equal(
+ CoreStrings.EntityTypeNotFound(nameof(RedBullRb3)),
+ Assert.Throws(
+ () => context.CreateProxy()).Message);
+ }
+
+ [ConditionalFact]
+ public void Throws_if_create_proxy_when_proxies_not_used()
+ {
+ using var context = new NeweyContextN6();
+ Assert.Equal(
+ ProxiesStrings.ProxiesNotEnabled(nameof(RedBullRb3)),
+ Assert.Throws(
+ () => context.CreateProxy()).Message);
+ }
+
+ [ConditionalFact]
+ public void Throws_if_create_proxy_when_proxies_not_enabled()
+ {
+ using var context = new NeweyContextN7();
+ Assert.Equal(
+ ProxiesStrings.ProxiesNotEnabled(nameof(RedBullRb3)),
+ Assert.Throws(
+ () => context.CreateProxy()).Message);
+ }
+
+ public class March82GGtp
+ {
+ public virtual int Id { get; set; }
+ }
+
+ public class March881
+ {
+ public March881(int id, string sponsor)
+ {
+ Id = id;
+ Sponsor = sponsor;
+ }
+
+ public virtual int Id { get; set; }
+
+ public virtual string Sponsor { get; set; }
+ }
+
+ public class WilliamsFw14
+ {
+ public WilliamsFw14(DbContext context, int id, string sponsor)
+ {
+ Context = context;
+ Id = id;
+ Sponsor = sponsor;
+ }
+
+ public DbContext Context { get; set; }
+
+ public virtual int Id { get; set; }
+
+ public virtual string Sponsor { get; set; }
+ }
+
+ private class NeweyContext : DbContext
+ {
+ private readonly IServiceProvider _internalServiceProvider;
+ private static readonly InMemoryDatabaseRoot _dbRoot = new InMemoryDatabaseRoot();
+ private readonly bool _useLazyLoadingProxies;
+ private readonly bool _useChangeDetectionProxies;
+ private readonly string _dbName;
+
+ public NeweyContext(string dbName = null, bool useLazyLoading = true, bool useChangeDetection = false)
+ {
+ _internalServiceProvider
+ = new ServiceCollection()
+ .AddEntityFrameworkInMemoryDatabase()
+ .AddEntityFrameworkProxies()
+ .BuildServiceProvider();
+
+ _dbName = dbName;
+ _useLazyLoadingProxies = useLazyLoading;
+ _useChangeDetectionProxies = useChangeDetection;
+ }
+
+ public NeweyContext(IServiceProvider internalServiceProvider, string dbName = null, bool useLazyLoading = true, bool useChangeDetection = false)
+ : this(dbName, useLazyLoading, useChangeDetection)
+ {
+ _internalServiceProvider = internalServiceProvider;
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (_useLazyLoadingProxies)
+ {
+ optionsBuilder.UseLazyLoadingProxies();
+ }
+
+ if (_useChangeDetectionProxies)
+ {
+ optionsBuilder.UseChangeDetectionProxies();
+ }
+
+ if (_internalServiceProvider != null)
+ {
+ optionsBuilder.UseInternalServiceProvider(_internalServiceProvider);
+ }
+
+ optionsBuilder.UseInMemoryDatabase(_dbName ?? nameof(NeweyContext), _dbRoot);
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity();
+
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property(e => e.Id);
+ b.Property(e => e.Sponsor);
+ });
+
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property(e => e.Id);
+ b.Property(e => e.Sponsor);
+ });
+ }
+ }
+
+ private class NeweyContextN : DbContext
+ {
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder
+ .UseLazyLoadingProxies()
+ .UseInternalServiceProvider(
+ new ServiceCollection()
+ .AddEntityFrameworkInMemoryDatabase()
+ .AddEntityFrameworkProxies()
+ .BuildServiceProvider())
+ .UseInMemoryDatabase(Guid.NewGuid().ToString());
+ }
+
+ internal class McLarenMp421
+ {
+ public virtual int Id { get; set; }
+ }
+
+ private class NeweyContextN4 : NeweyContextN
+ {
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity();
+ }
+
+ public class RedBullRb3
+ {
+ internal RedBullRb3()
+ {
+ }
+
+ public virtual int Id { get; set; }
+ }
+
+ private class NeweyContextN5 : NeweyContextN
+ {
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity();
+ }
+
+ private class NeweyContextN6 : DbContext
+ {
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder
+ .UseInternalServiceProvider(
+ new ServiceCollection()
+ .AddEntityFrameworkInMemoryDatabase()
+ .AddEntityFrameworkProxies()
+ .BuildServiceProvider())
+ .UseInMemoryDatabase(Guid.NewGuid().ToString());
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity();
+ }
+
+ private class NeweyContextN7 : DbContext
+ {
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder
+ .UseLazyLoadingProxies(false)
+ .UseInternalServiceProvider(
+ new ServiceCollection()
+ .AddEntityFrameworkInMemoryDatabase()
+ .AddEntityFrameworkProxies()
+ .BuildServiceProvider())
+ .UseInMemoryDatabase(Guid.NewGuid().ToString());
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity();
+ }
+ }
+}
diff --git a/test/EFCore.Proxies.Tests/TestUtilities/TestContext.cs b/test/EFCore.Proxies.Tests/TestUtilities/TestContext.cs
new file mode 100644
index 00000000000..40dd17912f4
--- /dev/null
+++ b/test/EFCore.Proxies.Tests/TestUtilities/TestContext.cs
@@ -0,0 +1,73 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.Storage;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.EntityFrameworkCore.TestUtilities
+{
+ internal abstract class TestContext : DbContext
+ where TEntity : class
+ {
+ private static readonly InMemoryDatabaseRoot _dbRoot = new InMemoryDatabaseRoot();
+
+ private readonly IServiceProvider _internalServiceProvider;
+ private readonly string _dbName;
+ private readonly bool _useLazyLoadingProxies;
+ private readonly bool _useChangeDetectionProxies;
+ private readonly bool _checkEquality;
+ private readonly ChangeTrackingStrategy? _changeTrackingStrategy;
+
+ protected TestContext(
+ string dbName = null,
+ bool useLazyLoading = false,
+ bool useChangeDetection = false,
+ bool checkEquality = true,
+ ChangeTrackingStrategy? changeTrackingStrategy = null)
+ {
+ _internalServiceProvider
+ = new ServiceCollection()
+ .AddEntityFrameworkInMemoryDatabase()
+ .AddEntityFrameworkProxies()
+ .BuildServiceProvider();
+
+ _dbName = dbName;
+ _useLazyLoadingProxies = useLazyLoading;
+ _useChangeDetectionProxies = useChangeDetection;
+ _checkEquality = checkEquality;
+ _changeTrackingStrategy = changeTrackingStrategy;
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (_useLazyLoadingProxies)
+ {
+ optionsBuilder.UseLazyLoadingProxies();
+ }
+
+ if (_useChangeDetectionProxies)
+ {
+ optionsBuilder.UseChangeDetectionProxies(checkEquality: _checkEquality);
+ }
+
+ if (_internalServiceProvider != null)
+ {
+ optionsBuilder.UseInternalServiceProvider(_internalServiceProvider);
+ }
+
+ optionsBuilder.UseInMemoryDatabase(_dbName ?? "TestContext", _dbRoot);
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ if (_changeTrackingStrategy.HasValue)
+ {
+ modelBuilder.HasChangeTrackingStrategy(_changeTrackingStrategy.Value);
+ }
+
+ modelBuilder.Entity();
+ }
+ }
+}