-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement entity materialization interception
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
1 parent
35a96c9
commit 6560b2a
Showing
32 changed files
with
1,792 additions
and
203 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
80 changes: 80 additions & 0 deletions
80
src/EFCore/Diagnostics/Internal/MaterializationInterceptorAggregator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
130
src/EFCore/Diagnostics/MaterializationInterceptionData.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.