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(); + } + } +}