Skip to content

Commit

Permalink
Implement entity materialization interception
Browse files Browse the repository at this point in the history
Part of #626
Fixes #15911

Introduces a new `IMaterializationInterceptor` singleton interceptor that allows:
- Interception before any entity instance has been created, allowing a customized instance to be created (if desired), thereby suppressing of normal EF instance creation.
- Interception after the instance has been created, but before property values have been set. The instance can be replaced with a new instance (if desired), without preventing EF from setting property values.
- Interception before property values have been set, allowing custom setting of the values and/or suppression of setting the values by EF (if desired).
- Interception after property values have been set, allowing the instance to be changed (if desired.)

Access to property values, including shadow and service properties is provided at each point.

If no singleton materialization interceptors are configured, then the materialization delegate is the same as before, meaning any perf impact only applies if interception is used.
  • Loading branch information
ajcvickers committed Jun 20, 2022
1 parent f755ffc commit 44c46ef
Show file tree
Hide file tree
Showing 32 changed files with 1,792 additions and 203 deletions.
88 changes: 88 additions & 0 deletions src/EFCore/Diagnostics/IMaterializationInterceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Diagnostics;

/// <summary>
/// A <see cref="ISingletonInterceptor" /> used to intercept the various parts of object creation and initialization when
/// Entity Framework is creating an object, typically from data returned by a query.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-interceptors">EF Core interceptors</see> for more information and examples.
/// </remarks>
public interface IMaterializationInterceptor : ISingletonInterceptor
{
/// <summary>
/// Called immediately before EF is going to create an instance of an entity. That is, before the constructor has been called.
/// </summary>
/// <param name="materializationData">Contextual information about the materialization happening.</param>
/// <param name="result">
/// Represents the current result if one exists.
/// This value will have <see cref="InterceptionResult{Object}.HasResult" /> set to <see langword="true" /> if some previous
/// interceptor suppressed execution by calling <see cref="InterceptionResult{Object}.SuppressWithResult" />.
/// This value is typically used as the return value for the implementation of this method.
/// </param>
/// <returns>
/// If <see cref="InterceptionResult{Object}.HasResult" /> is <see langword="false" />, then EF will continue as normal.
/// If <see cref="InterceptionResult{Object}.HasResult" /> is <see langword="true" />, then EF will suppress creation of
/// the entity instance and use <see cref="InterceptionResult{Object}.Result" /> instead.
/// An implementation of this method for any interceptor that is not attempting to change the result
/// should return the <paramref name="result" /> value passed in.
/// </returns>
InterceptionResult<object> CreatingInstance(MaterializationInterceptionData materializationData, InterceptionResult<object> result)
=> result;

/// <summary>
/// Called immediately after EF has created an instance of an entity. That is, after the constructor has been called, but before
/// any properties values not set by the constructor have been set.
/// </summary>
/// <param name="materializationData">Contextual information about the materialization happening.</param>
/// <param name="instance">
/// The entity instance that has been created.
/// This value is typically used as the return value for the implementation of this method.
/// </param>
/// <returns>
/// The entity instance that EF will use.
/// An implementation of this method for any interceptor that is not attempting to change the instance used
/// must return the <paramref name="instance" /> value passed in.
/// </returns>
object CreatedInstance(MaterializationInterceptionData materializationData, object instance)
=> instance;

/// <summary>
/// Called immediately before EF is going to set property values of an entity that has just been created. Note that property values
/// set by the constructor will already have been set.
/// </summary>
/// <param name="materializationData">Contextual information about the materialization happening.</param>
/// <param name="instance">The entity instance for which property values will be set.</param>
/// <param name="result">
/// Represents the current result if one exists.
/// This value will have <see cref="InterceptionResult.IsSuppressed" /> set to <see langword="true" /> if some previous
/// interceptor suppressed execution by calling <see cref="InterceptionResult.Suppress" />.
/// This value is typically used as the return value for the implementation of this method.
/// </param>
/// <returns>
/// If <see cref="InterceptionResult.IsSuppressed" /> is false, the EF will continue as normal.
/// If <see cref="InterceptionResult.IsSuppressed" /> is true, then EF will not set any property values.
/// An implementation of this method for any interceptor that is not attempting to suppress
/// setting property values must return the <paramref name="result" /> value passed in.
/// </returns>
InterceptionResult InitializingInstance(MaterializationInterceptionData materializationData, object instance, InterceptionResult result)
=> result;

/// <summary>
/// Called immediately after EF has set property values of an entity that has just been created.
/// </summary>
/// <param name="materializationData">Contextual information about the materialization happening.</param>
/// <param name="instance">
/// The entity instance that has been created.
/// This value is typically used as the return value for the implementation of this method.
/// </param>
/// <returns>
/// The entity instance that EF will use.
/// An implementation of this method for any interceptor that is not attempting to change the instance used
/// must return the <paramref name="instance" /> value passed in.
/// </returns>
object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
=> instance;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Diagnostics.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 MaterializationInterceptorAggregator : InterceptorAggregator<IMaterializationInterceptor>
{
/// <summary>
/// Must be implemented by the inheriting type to create a single interceptor from the given list.
/// </summary>
/// <param name="interceptors">The interceptors to combine.</param>
/// <returns>The combined interceptor.</returns>
protected override IMaterializationInterceptor CreateChain(IEnumerable<IMaterializationInterceptor> interceptors)
=> new CompositeMaterializationInterceptor(interceptors);

private sealed class CompositeMaterializationInterceptor : IMaterializationInterceptor
{
private readonly IMaterializationInterceptor[] _interceptors;

public CompositeMaterializationInterceptor(IEnumerable<IMaterializationInterceptor> interceptors)
{
_interceptors = interceptors.ToArray();
}

public InterceptionResult<object> CreatingInstance(
MaterializationInterceptionData materializationData,
InterceptionResult<object> result)
{
for (var i = 0; i < _interceptors.Length; i++)
{
result = _interceptors[i].CreatingInstance(materializationData, result);
}

return result;
}

public object CreatedInstance(
MaterializationInterceptionData materializationData,
object instance)
{
for (var i = 0; i < _interceptors.Length; i++)
{
instance = _interceptors[i].CreatedInstance(materializationData, instance);
}

return instance;
}

public InterceptionResult InitializingInstance(
MaterializationInterceptionData materializationData,
object instance,
InterceptionResult result)
{
for (var i = 0; i < _interceptors.Length; i++)
{
result = _interceptors[i].InitializingInstance(materializationData, instance, result);
}

return result;
}

public object InitializedInstance(
MaterializationInterceptionData materializationData,
object instance)
{
for (var i = 0; i < _interceptors.Length; i++)
{
instance = _interceptors[i].InitializedInstance(materializationData, instance);
}

return instance;
}
}
}
130 changes: 130 additions & 0 deletions src/EFCore/Diagnostics/MaterializationInterceptionData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Diagnostics;

/// <summary>
/// A parameter object passed to <see cref="IMaterializationInterceptor" /> methods containing data about the instance
/// being materialized.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</see> for more information and examples.
/// </remarks>
public readonly struct MaterializationInterceptionData
{
private readonly MaterializationContext _materializationContext;
private readonly IDictionary<IPropertyBase, (object TypedAccessor, Func<MaterializationContext, object?> Accessor)> _valueAccessor;

/// <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>
[EntityFrameworkInternal]
[UsedImplicitly]
public MaterializationInterceptionData(
MaterializationContext materializationContext,
IEntityType entityType,
IDictionary<IPropertyBase, (object TypedAccessor, Func<MaterializationContext, object?> Accessor)> valueAccessor)
{
_materializationContext = materializationContext;
_valueAccessor = valueAccessor;
EntityType = entityType;
}

/// <summary>
/// The current <see cref="DbContext" /> instance being used.
/// </summary>
public DbContext Context
=> _materializationContext.Context;

/// <summary>
/// The type of the entity being materialized.
/// </summary>
public IEntityType EntityType { get; }

/// <summary>
/// Gets the property value for the property with the given name.
/// </summary>
/// <remarks>
/// This generic overload of this method will not cause a primitive or value-type property value to be boxed into
/// a heap-allocated object.
/// </remarks>
/// <param name="propertyName">The property name.</param>
/// <returns>The property value.</returns>
public T GetPropertyValue<T>(string propertyName)
=> GetPropertyValue<T>(GetProperty(propertyName));

/// <summary>
/// Gets the property value for the property with the given name.
/// </summary>
/// <remarks>
/// This non-generic overload of this method will always cause a primitive or value-type property value to be boxed into
/// a heap-allocated object.
/// </remarks>
/// <param name="propertyName">The property name.</param>
/// <returns>The property value.</returns>
public object? GetPropertyValue(string propertyName)
=> GetPropertyValue(GetProperty(propertyName));

private IPropertyBase GetProperty(string propertyName)
{
var property = (IPropertyBase?)EntityType.FindProperty(propertyName)
?? EntityType.FindServiceProperty(propertyName);

if (property == null)
{
throw new ArgumentException(CoreStrings.PropertyNotFound(propertyName, EntityType.DisplayName()), nameof(propertyName));
}

return property;
}

/// <summary>
/// Gets the property value for the given property.
/// </summary>
/// <remarks>
/// This generic overload of this method will not cause a primitive or value-type property value to be boxed into
/// a heap-allocated object.
/// </remarks>
/// <param name="property">The property.</param>
/// <returns>The property value.</returns>
public T GetPropertyValue<T>(IPropertyBase property)
=> ((Func<MaterializationContext, T>)_valueAccessor[property].TypedAccessor)(_materializationContext);

/// <summary>
/// Gets the property value for the given property.
/// </summary>
/// <remarks>
/// This non-generic overload of this method will always cause a primitive or value-type property value to be boxed into
/// a heap-allocated object.
/// </remarks>
/// <param name="property">The property.</param>
/// <returns>The property value.</returns>
public object? GetPropertyValue(IPropertyBase property)
=> _valueAccessor[property].Accessor(_materializationContext);

/// <summary>
/// Creates a dictionary containing a snapshot of all property values for the instance being materialized.
/// </summary>
/// <remarks>
/// Note that this method causes a new dictionary to be created, and copies all values into that dictionary. This can
/// be less efficient than getting values individually, especially if the non-boxing generic overloads
/// <see cref="GetPropertyValue{T}(string)" /> or <see cref="GetPropertyValue{T}(IPropertyBase)" />can be used.
/// </remarks>
/// <returns>The values of properties for the entity instance.</returns>
public IReadOnlyDictionary<IPropertyBase, object?> CreateValuesDictionary()
{
var dictionary = new Dictionary<IPropertyBase, object?>();

foreach (var property in EntityType.GetServiceProperties().Cast<IPropertyBase>().Concat(EntityType.GetProperties()))
{
dictionary[property] = GetPropertyValue(property);
}

return dictionary;
}
}
18 changes: 13 additions & 5 deletions src/EFCore/Metadata/IModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,22 @@ public interface IModel : IReadOnlyModel, IAnnotatable
/// </remarks>
/// <param name="type">The type to find the corresponding entity type for.</param>
/// <returns>The entity type, or <see langword="null" /> if none is found.</returns>
IEntityType? FindRuntimeEntityType(Type type)
IEntityType? FindRuntimeEntityType(Type? type)
{
Check.NotNull(type, nameof(type));

return FindEntityType(type)
?? (type.BaseType == null
? null
: FindEntityType(type.BaseType));
while (type != null)
{
var entityType = FindEntityType(type);
if (entityType != null)
{
return entityType;
}

type = type.BaseType;
}

return null;
}

/// <summary>
Expand Down
Loading

0 comments on commit 44c46ef

Please sign in to comment.