Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy Change Detection #19437

Merged
merged 10 commits into from
Jan 14, 2020
22 changes: 11 additions & 11 deletions src/EFCore.Proxies/Properties/ProxiesStrings.Designer.cs

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

14 changes: 7 additions & 7 deletions src/EFCore.Proxies/Properties/ProxiesStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,18 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ProxyServicesMissing" xml:space="preserve">
<value>UseLazyLoadingProxies requires AddEntityFrameworkProxies to be called on the internal service provider used.</value>
<value>UseChangeDetectionProxies and UseLazyLoadingProxies each require AddEntityFrameworkProxies to be called on the internal service provider used.</value>
</data>
<data name="ItsASeal" xml:space="preserve">
<value>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.</value>
<value>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.</value>
</data>
<data name="NonVirtualNavigation" xml:space="preserve">
<value>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.</value>
<data name="NonVirtualProperty" xml:space="preserve">
<value>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.</value>
</data>
<data name="FieldNavigation" xml:space="preserve">
<value>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.</value>
<data name="FieldProperty" xml:space="preserve">
<value>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.</value>
</data>
<data name="ProxiesNotEnabled" xml:space="preserve">
<value>Unable to create proxy for '{entityType}' because proxies are not enabled. Call 'DbContextOptionsBuilder.UseLazyLoadingProxies' to enable lazy-loading proxies.</value>
<value>Unable to create proxy for '{entityType}' because proxies are not enabled. Call 'DbContextOptionsBuilder.UseChangeDetectionProxies' or 'DbContextOptionsBuilder.UseLazyLoadingProxies' to enable proxies.</value>
</data>
</root>
16 changes: 15 additions & 1 deletion src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public interface IProxyFactory
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
object CreateLazyLoadingProxy(
[NotNull] IDbContextOptions dbContextOptions,
[NotNull] IEntityType entityType,
[NotNull] ILazyLoader loader,
[NotNull] object[] constructorArguments);
Expand All @@ -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.
/// </summary>
Type CreateLazyLoadingProxyType([NotNull] IEntityType entityType);
object CreateProxy(
[NotNull] IDbContextOptions dbContextOptions,
[NotNull] IEntityType entityType,
[NotNull] object[] constructorArguments);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
Type CreateProxyType(
[NotNull] ProxiesOptionsExtension options,
[NotNull] IEntityType entityType);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
134 changes: 134 additions & 0 deletions src/EFCore.Proxies/Proxies/Internal/PropertyChangedInterceptor.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class PropertyChangedInterceptor : IInterceptor
{
private static readonly Type _notifyChangedInterface = typeof(INotifyPropertyChanged);

private readonly IEntityType _entityType;
private readonly bool _checkEquality;
private PropertyChangedEventHandler _handler;
private Type _proxyType;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public PropertyChangedInterceptor(
[NotNull] IEntityType entityType,
bool checkEquality)
{
_entityType = entityType;
_checkEquality = checkEquality;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual 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);
}
}
}
128 changes: 128 additions & 0 deletions src/EFCore.Proxies/Proxies/Internal/PropertyChangingInterceptor.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class PropertyChangingInterceptor : IInterceptor
{
private static readonly Type _notifyChangingInterface = typeof(INotifyPropertyChanging);

private readonly IEntityType _entityType;
private readonly bool _checkEquality;
private PropertyChangingEventHandler _handler;
private Type _proxyType;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public PropertyChangingInterceptor(
[NotNull] IEntityType entityType,
bool checkEquality)
{
_entityType = entityType;
_checkEquality = checkEquality;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual 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);
}
}
}
Loading