diff --git a/src/EFCore/Diagnostics/IMaterializationInterceptor.cs b/src/EFCore/Diagnostics/IMaterializationInterceptor.cs
new file mode 100644
index 00000000000..9fe38f8cd28
--- /dev/null
+++ b/src/EFCore/Diagnostics/IMaterializationInterceptor.cs
@@ -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;
+
+///
+/// A 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.
+///
+///
+/// See EF Core interceptors for more information and examples.
+///
+public interface IMaterializationInterceptor : ISingletonInterceptor
+{
+ ///
+ /// Called immediately before EF is going to create an instance of an entity. That is, before the constructor has been called.
+ ///
+ /// Contextual information about the materialization happening.
+ ///
+ /// Represents the current result if one exists.
+ /// This value will have set to if some previous
+ /// interceptor suppressed execution by calling .
+ /// This value is typically used as the return value for the implementation of this method.
+ ///
+ ///
+ /// If is , then EF will continue as normal.
+ /// If is , then EF will suppress creation of
+ /// the entity instance and use instead.
+ /// An implementation of this method for any interceptor that is not attempting to change the result
+ /// should return the value passed in.
+ ///
+ InterceptionResult CreatingInstance(MaterializationInterceptionData materializationData, InterceptionResult result)
+ => result;
+
+ ///
+ /// 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.
+ ///
+ /// Contextual information about the materialization happening.
+ ///
+ /// The entity instance that has been created.
+ /// This value is typically used as the return value for the implementation of this method.
+ ///
+ ///
+ /// 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 value passed in.
+ ///
+ object CreatedInstance(MaterializationInterceptionData materializationData, object instance)
+ => instance;
+
+ ///
+ /// 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.
+ ///
+ /// Contextual information about the materialization happening.
+ /// The entity instance for which property values will be set.
+ ///
+ /// Represents the current result if one exists.
+ /// This value will have set to if some previous
+ /// interceptor suppressed execution by calling .
+ /// This value is typically used as the return value for the implementation of this method.
+ ///
+ ///
+ /// If is false, the EF will continue as normal.
+ /// If 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 value passed in.
+ ///
+ InterceptionResult InitializingInstance(MaterializationInterceptionData materializationData, object instance, InterceptionResult result)
+ => result;
+
+ ///
+ /// Called immediately after EF has set property values of an entity that has just been created.
+ ///
+ /// Contextual information about the materialization happening.
+ ///
+ /// The entity instance that has been created.
+ /// This value is typically used as the return value for the implementation of this method.
+ ///
+ ///
+ /// 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 value passed in.
+ ///
+ object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
+ => instance;
+}
diff --git a/src/EFCore/Diagnostics/Internal/MaterializationInterceptorAggregator.cs b/src/EFCore/Diagnostics/Internal/MaterializationInterceptorAggregator.cs
new file mode 100644
index 00000000000..c0bd4b19741
--- /dev/null
+++ b/src/EFCore/Diagnostics/Internal/MaterializationInterceptorAggregator.cs
@@ -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;
+
+///
+/// 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 MaterializationInterceptorAggregator : InterceptorAggregator
+{
+ ///
+ /// Must be implemented by the inheriting type to create a single interceptor from the given list.
+ ///
+ /// The interceptors to combine.
+ /// The combined interceptor.
+ protected override IMaterializationInterceptor CreateChain(IEnumerable interceptors)
+ => new CompositeMaterializationInterceptor(interceptors);
+
+ private sealed class CompositeMaterializationInterceptor : IMaterializationInterceptor
+ {
+ private readonly IMaterializationInterceptor[] _interceptors;
+
+ public CompositeMaterializationInterceptor(IEnumerable interceptors)
+ {
+ _interceptors = interceptors.ToArray();
+ }
+
+ public InterceptionResult CreatingInstance(
+ MaterializationInterceptionData materializationData,
+ InterceptionResult 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;
+ }
+ }
+}
diff --git a/src/EFCore/Diagnostics/MaterializationInterceptionData.cs b/src/EFCore/Diagnostics/MaterializationInterceptionData.cs
new file mode 100644
index 00000000000..62596cb207c
--- /dev/null
+++ b/src/EFCore/Diagnostics/MaterializationInterceptionData.cs
@@ -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;
+
+///
+/// A parameter object passed to methods containing data about the instance
+/// being materialized.
+///
+///
+/// See Logging, events, and diagnostics for more information and examples.
+///
+public readonly struct MaterializationInterceptionData
+{
+ private readonly MaterializationContext _materializationContext;
+ private readonly IDictionary Accessor)> _valueAccessor;
+
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ [UsedImplicitly]
+ public MaterializationInterceptionData(
+ MaterializationContext materializationContext,
+ IEntityType entityType,
+ IDictionary Accessor)> valueAccessor)
+ {
+ _materializationContext = materializationContext;
+ _valueAccessor = valueAccessor;
+ EntityType = entityType;
+ }
+
+ ///
+ /// The current instance being used.
+ ///
+ public DbContext Context
+ => _materializationContext.Context;
+
+ ///
+ /// The type of the entity being materialized.
+ ///
+ public IEntityType EntityType { get; }
+
+ ///
+ /// Gets the property value for the property with the given name.
+ ///
+ ///
+ /// This generic overload of this method will not cause a primitive or value-type property value to be boxed into
+ /// a heap-allocated object.
+ ///
+ /// The property name.
+ /// The property value.
+ public T GetPropertyValue(string propertyName)
+ => GetPropertyValue(GetProperty(propertyName));
+
+ ///
+ /// Gets the property value for the property with the given name.
+ ///
+ ///
+ /// 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.
+ ///
+ /// The property name.
+ /// The property value.
+ 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;
+ }
+
+ ///
+ /// Gets the property value for the given property.
+ ///
+ ///
+ /// This generic overload of this method will not cause a primitive or value-type property value to be boxed into
+ /// a heap-allocated object.
+ ///
+ /// The property.
+ /// The property value.
+ public T GetPropertyValue(IPropertyBase property)
+ => ((Func)_valueAccessor[property].TypedAccessor)(_materializationContext);
+
+ ///
+ /// Gets the property value for the given property.
+ ///
+ ///
+ /// 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.
+ ///
+ /// The property.
+ /// The property value.
+ public object? GetPropertyValue(IPropertyBase property)
+ => _valueAccessor[property].Accessor(_materializationContext);
+
+ ///
+ /// Creates a dictionary containing a snapshot of all property values for the instance being materialized.
+ ///
+ ///
+ /// 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
+ /// or can be used.
+ ///
+ /// The values of properties for the entity instance.
+ public IReadOnlyDictionary CreateValuesDictionary()
+ {
+ var dictionary = new Dictionary();
+
+ foreach (var property in EntityType.GetServiceProperties().Cast().Concat(EntityType.GetProperties()))
+ {
+ dictionary[property] = GetPropertyValue(property);
+ }
+
+ return dictionary;
+ }
+}
diff --git a/src/EFCore/Metadata/IModel.cs b/src/EFCore/Metadata/IModel.cs
index e1fa58d4e82..16ea7a50426 100644
--- a/src/EFCore/Metadata/IModel.cs
+++ b/src/EFCore/Metadata/IModel.cs
@@ -65,14 +65,22 @@ public interface IModel : IReadOnlyModel, IAnnotatable
///
/// The type to find the corresponding entity type for.
/// The entity type, or if none is found.
- 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;
}
///
diff --git a/src/EFCore/Query/Internal/EntityMaterializerSource.cs b/src/EFCore/Query/Internal/EntityMaterializerSource.cs
index 69536cd61ee..689979dcbe6 100644
--- a/src/EFCore/Query/Internal/EntityMaterializerSource.cs
+++ b/src/EFCore/Query/Internal/EntityMaterializerSource.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
+using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
namespace Microsoft.EntityFrameworkCore.Query.Internal;
@@ -16,6 +17,7 @@ public class EntityMaterializerSource : IEntityMaterializerSource
private ConcurrentDictionary>? _materializers;
private ConcurrentDictionary>? _emptyMaterializers;
private readonly List _bindingInterceptors;
+ private readonly IMaterializationInterceptor? _materializationInterceptor;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -27,6 +29,10 @@ public EntityMaterializerSource(EntityMaterializerSourceDependencies dependencie
{
Dependencies = dependencies;
_bindingInterceptors = dependencies.SingletonInterceptors.OfType().ToList();
+
+ _materializationInterceptor =
+ (IMaterializationInterceptor?)new MaterializationInterceptorAggregator().AggregateInterceptors(
+ dependencies.SingletonInterceptors.OfType().ToList());
}
///
@@ -72,42 +78,64 @@ public virtual Expression CreateMaterializeExpression(
var constructorExpression = constructorBinding.CreateConstructorExpression(bindingInfo);
- if (properties.Count == 0)
+ if (_materializationInterceptor == null)
{
- return constructorExpression;
- }
+ if (properties.Count == 0)
+ {
+ return constructorExpression;
+ }
- var instanceVariable = Expression.Variable(constructorBinding.RuntimeType, entityInstanceName);
+ var instanceVariable = Expression.Variable(constructorBinding.RuntimeType, entityInstanceName);
- var blockExpressions
- = new List
- {
- Expression.Assign(
- instanceVariable,
- constructorExpression)
- };
+ var blockExpressions
+ = new List
+ {
+ Expression.Assign(
+ instanceVariable,
+ constructorExpression)
+ };
+
+ AddInitializeExpressions(properties, bindingInfo, materializationContextExpression, instanceVariable, blockExpressions);
+
+ blockExpressions.Add(instanceVariable);
+
+ return Expression.Block(new[] { instanceVariable }, blockExpressions);
+ }
+
+ return CreateInterceptionMaterializeExpression(
+ entityType,
+ entityInstanceName,
+ properties,
+ _materializationInterceptor,
+ constructorBinding,
+ bindingInfo,
+ constructorExpression,
+ materializationContextExpression);
+ }
+ private static void AddInitializeExpressions(
+ HashSet properties,
+ ParameterBindingInfo bindingInfo,
+ Expression materializationContextExpression,
+ Expression instanceVariable,
+ List blockExpressions)
+ {
var valueBufferExpression = Expression.Call(materializationContextExpression, MaterializationContext.GetValueBufferMethod);
foreach (var property in properties)
{
var memberInfo = property.GetMemberInfo(forMaterialization: true, forSet: true);
- var readValueExpression
- = property is IServiceProperty serviceProperty
- ? serviceProperty.ParameterBinding.BindToParameter(bindingInfo)
- : valueBufferExpression.CreateValueBufferReadValueExpression(
- memberInfo.GetMemberType(),
- property.GetIndex(),
- property);
-
- blockExpressions.Add(CreateMemberAssignment(instanceVariable, memberInfo, property, readValueExpression));
+ blockExpressions.Add(
+ CreateMemberAssignment(
+ instanceVariable, memberInfo, property, property is IServiceProperty serviceProperty
+ ? serviceProperty.ParameterBinding.BindToParameter(bindingInfo)
+ : valueBufferExpression.CreateValueBufferReadValueExpression(
+ memberInfo.GetMemberType(),
+ property.GetIndex(),
+ property)));
}
- blockExpressions.Add(instanceVariable);
-
- return Expression.Block(new[] { instanceVariable }, blockExpressions);
-
static Expression CreateMemberAssignment(Expression parameter, MemberInfo memberInfo, IPropertyBase property, Expression value)
=> property.IsIndexerProperty()
? Expression.Assign(
@@ -117,6 +145,211 @@ static Expression CreateMemberAssignment(Expression parameter, MemberInfo member
: Expression.MakeMemberAccess(parameter, memberInfo).Assign(value);
}
+ private static readonly ConstructorInfo MaterializationInterceptionDataConstructor
+ = typeof(MaterializationInterceptionData).GetDeclaredConstructor(
+ new[]
+ {
+ typeof(MaterializationContext),
+ typeof(IEntityType),
+ typeof(IDictionary)>)
+ })!;
+
+ private static readonly MethodInfo CreatingInstanceMethod
+ = typeof(IMaterializationInterceptor).GetMethod(nameof(IMaterializationInterceptor.CreatingInstance))!;
+
+ private static readonly MethodInfo CreatedInstanceMethod
+ = typeof(IMaterializationInterceptor).GetMethod(nameof(IMaterializationInterceptor.CreatedInstance))!;
+
+ private static readonly MethodInfo InitializingInstanceMethod
+ = typeof(IMaterializationInterceptor).GetMethod(nameof(IMaterializationInterceptor.InitializingInstance))!;
+
+ private static readonly MethodInfo InitializedInstanceMethod
+ = typeof(IMaterializationInterceptor).GetMethod(nameof(IMaterializationInterceptor.InitializedInstance))!;
+
+ private static readonly PropertyInfo HasResultMethod
+ = typeof(InterceptionResult).GetProperty(nameof(InterceptionResult.HasResult))!;
+
+ private static readonly PropertyInfo ResultProperty
+ = typeof(InterceptionResult).GetProperty(nameof(InterceptionResult.Result))!;
+
+ private static readonly PropertyInfo IsSuppressedProperty
+ = typeof(InterceptionResult).GetProperty(nameof(InterceptionResult.IsSuppressed))!;
+
+ private static readonly MethodInfo DictionaryAddMethod
+ = typeof(Dictionary)>).GetMethod(
+ nameof(Dictionary.Add),
+ new[] { typeof(IPropertyBase), typeof((object, Func)) })!;
+
+ private static readonly ConstructorInfo DictionaryConstructor
+ = typeof(ValueTuple>).GetConstructor(
+ new[] { typeof(object), typeof(Func) })!;
+
+ private static Expression CreateInterceptionMaterializeExpression(
+ IEntityType entityType,
+ string entityInstanceName,
+ HashSet properties,
+ IMaterializationInterceptor materializationInterceptor,
+ InstantiationBinding constructorBinding,
+ ParameterBindingInfo bindingInfo,
+ Expression constructorExpression,
+ Expression materializationContextExpression)
+ {
+ // Something like:
+ // Dictionary)> accessorFactory = CreateAccessors()
+ // var creatingResult = interceptor.CreatingInstance(materializationData, new InterceptionResult());
+ //
+ // var instance = interceptor.CreatedInstance(materializationData,
+ // creatingResult.HasResult ? creatingResult.Result : create(materializationContext));
+ //
+ // if (!interceptor.InitializingInstance(materializationData, instance, default(InterceptionResult)).IsSuppressed)
+ // {
+ // initialize(materializationContext, instance);
+ // }
+ //
+ // instance = interceptor.InitializedInstance(materializationData, instance);
+ //
+ // return instance;
+
+ var instanceVariable = Expression.Variable(constructorBinding.RuntimeType, entityInstanceName);
+ var materializationDataVariable = Expression.Variable(typeof(MaterializationInterceptionData), "materializationData");
+ var creatingResultVariable = Expression.Variable(typeof(InterceptionResult), "creatingResult");
+ var interceptorExpression = Expression.Constant(materializationInterceptor, typeof(IMaterializationInterceptor));
+ var accessorDictionaryVariable = Expression.Variable(
+ typeof(Dictionary)>), "accessorDictionary");
+
+ var blockExpressions = new List
+ {
+ Expression.Assign(
+ accessorDictionaryVariable,
+ CreateAccessorDictionaryExpression()),
+ Expression.Assign(
+ materializationDataVariable,
+ Expression.New(
+ MaterializationInterceptionDataConstructor,
+ materializationContextExpression,
+ Expression.Constant(entityType),
+ accessorDictionaryVariable)),
+ Expression.Assign(
+ creatingResultVariable,
+ Expression.Call(
+ interceptorExpression,
+ CreatingInstanceMethod,
+ materializationDataVariable,
+ Expression.Default(typeof(InterceptionResult)))),
+ Expression.Assign(
+ instanceVariable,
+ Expression.Convert(
+ Expression.Call(
+ interceptorExpression,
+ CreatedInstanceMethod,
+ materializationDataVariable,
+ Expression.Condition(
+ Expression.Property(
+ creatingResultVariable,
+ HasResultMethod),
+ Expression.Convert(
+ Expression.Property(
+ creatingResultVariable,
+ ResultProperty),
+ instanceVariable.Type),
+ constructorExpression)),
+ instanceVariable.Type)),
+ properties.Count == 0
+ ? Expression.Call(
+ interceptorExpression,
+ InitializingInstanceMethod,
+ materializationDataVariable,
+ instanceVariable,
+ Expression.Default(typeof(InterceptionResult)))
+ : Expression.IfThen(
+ Expression.Not(
+ Expression.Property(
+ Expression.Call(
+ interceptorExpression,
+ InitializingInstanceMethod,
+ materializationDataVariable,
+ instanceVariable,
+ Expression.Default(typeof(InterceptionResult))),
+ IsSuppressedProperty)),
+ CreateInitializeExpression()),
+ Expression.Assign(
+ instanceVariable,
+ Expression.Convert(
+ Expression.Call(
+ interceptorExpression,
+ InitializedInstanceMethod,
+ materializationDataVariable,
+ instanceVariable),
+ instanceVariable.Type))
+ };
+
+ blockExpressions.Add(instanceVariable);
+
+ return Expression.Block(
+ new[] { accessorDictionaryVariable, instanceVariable, materializationDataVariable, creatingResultVariable },
+ blockExpressions);
+
+ BlockExpression CreateAccessorDictionaryExpression()
+ {
+ var dictionaryVariable = Expression.Variable(
+ typeof(Dictionary)>), "dictionary");
+ var valueBufferExpression = Expression.Call(materializationContextExpression, MaterializationContext.GetValueBufferMethod);
+ var snapshotBlockExpressions = new List
+ {
+ Expression.Assign(
+ dictionaryVariable,
+ Expression.New(
+ typeof(Dictionary)>)
+ .GetConstructor(Type.EmptyTypes)!))
+ };
+
+ foreach (var property in entityType.GetServiceProperties().Cast().Concat(entityType.GetProperties()))
+ {
+ snapshotBlockExpressions.Add(
+ Expression.Call(
+ dictionaryVariable,
+ DictionaryAddMethod,
+ Expression.Constant(property),
+ Expression.New(
+ DictionaryConstructor,
+ Expression.Lambda(
+ typeof(Func<,>).MakeGenericType(typeof(MaterializationContext), property.ClrType),
+ CreateAccessorReadExpression(),
+ (ParameterExpression)materializationContextExpression),
+ Expression.Lambda>(
+ Expression.Convert(CreateAccessorReadExpression(), typeof(object)),
+ (ParameterExpression)materializationContextExpression))));
+
+ Expression CreateAccessorReadExpression()
+ => property is IServiceProperty serviceProperty
+ ? serviceProperty.ParameterBinding.BindToParameter(bindingInfo)
+ : valueBufferExpression
+ .CreateValueBufferReadValueExpression(
+ property.ClrType,
+ property.GetIndex(),
+ property);
+ }
+
+ snapshotBlockExpressions.Add(dictionaryVariable);
+
+ return Expression.Block(new[] { dictionaryVariable }, snapshotBlockExpressions);
+ }
+
+ BlockExpression CreateInitializeExpression()
+ {
+ var initializeBlockExpressions = new List();
+
+ AddInitializeExpressions(
+ properties,
+ bindingInfo,
+ materializationContextExpression,
+ instanceVariable,
+ initializeBlockExpressions);
+
+ return Expression.Block(initializeBlockExpressions);
+ }
+ }
+
private ConcurrentDictionary> Materializers
=> LazyInitializer.EnsureInitialized(
ref _materializers,
@@ -171,16 +404,28 @@ public virtual Func GetEmptyMaterializer(
throw new InvalidOperationException(CoreStrings.NoParameterlessConstructor(e.DisplayName()));
}
}
-
- binding = self.ModifyBindings(e, "v", binding);
- var contextParam = Expression.Parameter(typeof(MaterializationContext), "mc");
+ binding = self.ModifyBindings(e, "v", binding);
+
+ var materializationContextExpression = Expression.Parameter(typeof(MaterializationContext), "mc");
+ var bindingInfo = new ParameterBindingInfo(e, materializationContextExpression);
+ var constructorExpression = binding.CreateConstructorExpression(bindingInfo);
return Expression.Lambda>(
- binding.CreateConstructorExpression(
- new ParameterBindingInfo(e, contextParam)),
- contextParam)
+ self._materializationInterceptor == null
+ ? constructorExpression
+ : CreateInterceptionMaterializeExpression(
+ e,
+ "instance",
+ new HashSet(),
+ self._materializationInterceptor,
+ binding,
+ bindingInfo,
+ constructorExpression,
+ materializationContextExpression),
+ materializationContextExpression)
.Compile();
+
},
this);
diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
index b3044e91af7..90ac7135e9c 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
@@ -6,29 +6,6 @@
// ReSharper disable UnusedAutoPropertyAccessor.Local
namespace Microsoft.EntityFrameworkCore.Cosmos;
-public class BindingInterceptionCosmosTest : BindingInterceptionTestBase,
- IClassFixture
-{
- public BindingInterceptionCosmosTest(BindingInterceptionCosmosFixture fixture)
- : base(fixture)
- {
- }
-
- public class BindingInterceptionCosmosFixture : SingletonInterceptorsFixtureBase
- {
- protected override string StoreName
- => "BindingInterception";
-
- protected override ITestStoreFactory TestStoreFactory
- => CosmosTestStoreFactory.Instance;
-
- protected override IServiceCollection InjectInterceptors(
- IServiceCollection serviceCollection,
- IEnumerable injectedInterceptors)
- => base.InjectInterceptors(serviceCollection.AddEntityFrameworkCosmos(), injectedInterceptors);
- }
-}
-
public class ConfigPatternsCosmosTest : IClassFixture
{
private const string DatabaseName = "ConfigPatternsCosmos";
diff --git a/test/EFCore.Cosmos.FunctionalTests/MaterializationInterceptionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/MaterializationInterceptionCosmosTest.cs
new file mode 100644
index 00000000000..2328491d5ca
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/MaterializationInterceptionCosmosTest.cs
@@ -0,0 +1,27 @@
+// 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.Cosmos;
+
+public class MaterializationInterceptionCosmosTest : MaterializationInterceptionTestBase,
+ IClassFixture
+{
+ public MaterializationInterceptionCosmosTest(MaterializationInterceptionCosmosFixture fixture)
+ : base(fixture)
+ {
+ }
+
+ public class MaterializationInterceptionCosmosFixture : SingletonInterceptorsFixtureBase
+ {
+ protected override string StoreName
+ => "MaterializationInterception";
+
+ protected override ITestStoreFactory TestStoreFactory
+ => CosmosTestStoreFactory.Instance;
+
+ protected override IServiceCollection InjectInterceptors(
+ IServiceCollection serviceCollection,
+ IEnumerable injectedInterceptors)
+ => base.InjectInterceptors(serviceCollection.AddEntityFrameworkCosmos(), injectedInterceptors);
+ }
+}
diff --git a/test/EFCore.InMemory.FunctionalTests/BindingInterceptionInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/MaterializationInterceptionInMemoryTest.cs
similarity index 67%
rename from test/EFCore.InMemory.FunctionalTests/BindingInterceptionInMemoryTest.cs
rename to test/EFCore.InMemory.FunctionalTests/MaterializationInterceptionInMemoryTest.cs
index a7b6fe517ae..0015ef0211f 100644
--- a/test/EFCore.InMemory.FunctionalTests/BindingInterceptionInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/MaterializationInterceptionInMemoryTest.cs
@@ -5,18 +5,18 @@
namespace Microsoft.EntityFrameworkCore;
-public class BindingInterceptionInMemoryTest : BindingInterceptionTestBase,
- IClassFixture
+public class MaterializationInterceptionInMemoryTest : MaterializationInterceptionTestBase,
+ IClassFixture
{
- public BindingInterceptionInMemoryTest(BindingInterceptionInMemoryFixture fixture)
+ public MaterializationInterceptionInMemoryTest(MaterializationInterceptionInMemoryFixture fixture)
: base(fixture)
{
}
- public class BindingInterceptionInMemoryFixture : SingletonInterceptorsFixtureBase
+ public class MaterializationInterceptionInMemoryFixture : SingletonInterceptorsFixtureBase
{
protected override string StoreName
- => "BindingInterception";
+ => "MaterializationInterception";
protected override ITestStoreFactory TestStoreFactory
=> InMemoryTestStoreFactory.Instance;
diff --git a/test/EFCore.Specification.Tests/BindingInterceptionTestBase.cs b/test/EFCore.Specification.Tests/BindingInterceptionTestBase.cs
deleted file mode 100644
index e7087cb45d6..00000000000
--- a/test/EFCore.Specification.Tests/BindingInterceptionTestBase.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-#nullable enable
-
-namespace Microsoft.EntityFrameworkCore;
-
-public abstract class BindingInterceptionTestBase : SingletonInterceptorsTestBase
-{
- protected BindingInterceptionTestBase(SingletonInterceptorsFixtureBase fixture)
- : base(fixture)
- {
- }
-
- [ConditionalTheory]
- [InlineData(false)]
- [InlineData(true)]
- public virtual void Binding_interceptors_are_used_by_queries(bool inject)
- {
- var interceptors = new[]
- {
- new TestBindingInterceptor("1"),
- new TestBindingInterceptor("2"),
- new TestBindingInterceptor("3"),
- new TestBindingInterceptor("4")
- };
-
- using var context = CreateContext(interceptors, inject);
-
- context.AddRange(
- new Book { Id = inject ? 77 : 87, Title = "Amiga ROM Kernel Reference Manual" },
- new Book { Id = inject ? 78 : 88, Title = "Amiga Hardware Reference Manual" });
-
- context.SaveChanges();
- context.ChangeTracker.Clear();
-
- var results = context.Set().ToList();
- Assert.All(results, e => Assert.Equal("4", e.MaterializedBy));
- Assert.All(interceptors, i => Assert.Equal(1, i.CalledCount));
- }
-
- [ConditionalTheory]
- [InlineData(false)]
- [InlineData(true)]
- public virtual void Binding_interceptors_are_used_when_creating_instances(bool inject)
- {
- var interceptors = new[]
- {
- new TestBindingInterceptor("1"),
- new TestBindingInterceptor("2"),
- new TestBindingInterceptor("3"),
- new TestBindingInterceptor("4")
- };
-
- using var context = CreateContext(interceptors, inject);
-
- var materializer = context.GetService();
- var book = (Book)materializer.GetEmptyMaterializer(context.Model.FindEntityType(typeof(Book))!)(
- new MaterializationContext(ValueBuffer.Empty, context));
-
- Assert.Equal("4", book.MaterializedBy);
- Assert.All(interceptors, i => Assert.Equal(1, i.CalledCount));
- }
-
- protected class TestBindingInterceptor : IInstantiationBindingInterceptor
- {
- private readonly string _id;
-
- public TestBindingInterceptor(string id)
- {
- _id = id;
- }
-
- public int CalledCount { get; private set; }
-
- protected Book BookFactory()
- => new() { MaterializedBy = _id };
-
- public InstantiationBinding ModifyBinding(IEntityType entityType, string entityInstanceName, InstantiationBinding binding)
- {
- CalledCount++;
-
- return new FactoryMethodBinding(
- this,
- typeof(TestBindingInterceptor).GetTypeInfo().GetDeclaredMethod(nameof(BookFactory))!,
- new List(),
- entityType.ClrType);
- }
- }
-}
diff --git a/test/EFCore.Specification.Tests/DatabindingTestBase.cs b/test/EFCore.Specification.Tests/DatabindingTestBase.cs
index aac8046e4ea..48166d5078a 100644
--- a/test/EFCore.Specification.Tests/DatabindingTestBase.cs
+++ b/test/EFCore.Specification.Tests/DatabindingTestBase.cs
@@ -37,6 +37,14 @@ protected void SetupContext(F1Context context)
var drivers = context.Drivers;
drivers.Load();
+ foreach (var driver in drivers.Local)
+ {
+ var proxy = (IF1Proxy)driver;
+ Assert.True(proxy.CreatedCalled);
+ Assert.True(proxy.InitializingCalled);
+ Assert.True(proxy.InitializedCalled);
+ }
+
foreach (var driver in drivers.Local.Where(d => d.TeamId == DeletedTeam).ToList())
{
drivers.Remove(driver);
diff --git a/test/EFCore.Specification.Tests/F1FixtureBase.cs b/test/EFCore.Specification.Tests/F1FixtureBase.cs
index 6d15a5f33fc..565a4ef0171 100644
--- a/test/EFCore.Specification.Tests/F1FixtureBase.cs
+++ b/test/EFCore.Specification.Tests/F1FixtureBase.cs
@@ -1,13 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel;
namespace Microsoft.EntityFrameworkCore;
public abstract class F1FixtureBase : SharedStoreFixtureBase
{
- protected override string StoreName { get; } = "F1Test";
+ protected override string StoreName
+ => "F1Test";
protected override bool UsePooling
=> true;
@@ -18,6 +20,9 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build
.ConfigureWarnings(
w => w.Ignore(CoreEventId.SaveChangesStarting, CoreEventId.SaveChangesCompleted));
+ protected override IServiceCollection AddServices(IServiceCollection serviceCollection)
+ => base.AddServices(serviceCollection.AddSingleton());
+
protected override bool ShouldLogCategory(string logCategory)
=> logCategory == DbLoggerCategory.Update.Name;
@@ -39,7 +44,12 @@ public ModelBuilder CreateModelBuilder()
protected virtual void BuildModelExternal(ModelBuilder modelBuilder)
{
- modelBuilder.Entity(b => b.HasKey(c => c.TeamId));
+ modelBuilder.Entity(
+ b =>
+ {
+ b.HasKey(c => c.TeamId);
+ ConfigureConstructorBinding(b.Metadata, nameof(Chassis.TeamId), nameof(Chassis.Name));
+ });
modelBuilder.Entity(
b =>
@@ -53,11 +63,21 @@ protected virtual void BuildModelExternal(ModelBuilder modelBuilder)
lb.Property(l => l.Latitude).IsConcurrencyToken();
lb.Property(l => l.Longitude).IsConcurrencyToken();
});
+ ConfigureConstructorBinding(b.Metadata, nameof(Engine.Id), nameof(Engine.Name));
});
- modelBuilder.Entity(b => b.HasKey(e => e.Name));
+ modelBuilder.Entity(
+ b =>
+ {
+ b.HasKey(e => e.Name);
+ ConfigureConstructorBinding(b.Metadata, nameof(EngineSupplier.Name));
+ });
- modelBuilder.Entity();
+ modelBuilder.Entity(
+ b =>
+ {
+ ConfigureConstructorBinding(b.Metadata, nameof(Gearbox.Id), nameof(Gearbox.Name));
+ });
modelBuilder.Entity(
b =>
@@ -71,26 +91,85 @@ protected virtual void BuildModelExternal(ModelBuilder modelBuilder)
{
b.HasOne(e => e.Gearbox).WithOne().HasForeignKey(e => e.GearboxId);
b.HasOne(e => e.Chassis).WithOne(e => e.Team).HasForeignKey(e => e.TeamId);
+
+ b.HasMany(t => t.Sponsors)
+ .WithMany(s => s.Teams)
+ .UsingEntity(
+ ts => ts
+ .HasOne(t => t.Sponsor)
+ .WithMany(),
+ ts => ts
+ .HasOne(t => t.Team)
+ .WithMany())
+ .HasKey(ts => new { ts.SponsorId, ts.TeamId });
+
+ ConfigureConstructorBinding(
+ b.Metadata,
+ nameof(Team.Id),
+ nameof(Team.Name),
+ nameof(Team.Constructor),
+ nameof(Team.Tire),
+ nameof(Team.Principal),
+ nameof(Team.ConstructorsChampionships),
+ nameof(Team.DriversChampionships),
+ nameof(Team.Races),
+ nameof(Team.Victories),
+ nameof(Team.Poles),
+ nameof(Team.FastestLaps),
+ nameof(Team.GearboxId)
+ );
});
- modelBuilder.Entity(b => b.Property(e => e.Id).ValueGeneratedNever());
- modelBuilder.Entity();
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property(e => e.Id).ValueGeneratedNever();
+ ConfigureConstructorBinding(
+ b.Metadata,
+ nameof(Driver.Id),
+ nameof(Driver.Name),
+ nameof(Driver.CarNumber),
+ nameof(Driver.Championships),
+ nameof(Driver.Races),
+ nameof(Driver.Wins),
+ nameof(Driver.Podiums),
+ nameof(Driver.Poles),
+ nameof(Driver.FastestLaps),
+ nameof(Driver.TeamId)
+ );
+ });
+
+ modelBuilder.Entity(
+ b =>
+ {
+ ConfigureConstructorBinding(
+ b.Metadata,
+ nameof(Driver.Id),
+ nameof(Driver.Name),
+ nameof(Driver.CarNumber),
+ nameof(Driver.Championships),
+ nameof(Driver.Races),
+ nameof(Driver.Wins),
+ nameof(Driver.Podiums),
+ nameof(Driver.Poles),
+ nameof(Driver.FastestLaps),
+ nameof(Driver.TeamId)
+ );
+ });
+
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property(e => e.Id).ValueGeneratedNever();
+ });
+
+ modelBuilder.Entity(
+ b =>
+ {
+ b.OwnsOne(s => s.Details);
+ ConfigureConstructorBinding(b.Metadata);
+ });
- modelBuilder.Entity(b => b.Property(e => e.Id).ValueGeneratedNever());
- modelBuilder.Entity()
- .OwnsOne(s => s.Details);
-
- modelBuilder.Entity()
- .HasMany(t => t.Sponsors)
- .WithMany(s => s.Teams)
- .UsingEntity(
- ts => ts
- .HasOne(t => t.Sponsor)
- .WithMany(),
- ts => ts
- .HasOne(t => t.Team)
- .WithMany())
- .HasKey(ts => new { ts.SponsorId, ts.TeamId });
modelBuilder.Entity().Property("Version").IsRowVersion();
modelBuilder.Entity().Property("Version").IsRowVersion();
@@ -130,6 +209,37 @@ protected virtual void BuildModelExternal(ModelBuilder modelBuilder)
}
}
+ private static void ConfigureConstructorBinding(IMutableEntityType mutableEntityType, params string[] propertyNames)
+ => ConfigureConstructorBinding(mutableEntityType, propertyNames);
+
+ private static void ConfigureConstructorBinding(
+ IMutableEntityType mutableEntityType, params string[] propertyNames)
+ {
+ var entityType = (EntityType)mutableEntityType;
+ var loaderField = typeof(TLoaderEntity).GetField("_loader", BindingFlags.Instance | BindingFlags.NonPublic);
+ var parameterBindings = new List();
+
+ if (loaderField != null)
+ {
+ var loaderProperty = typeof(TLoaderEntity) == typeof(TEntity)
+ ? entityType.AddServiceProperty(loaderField!, ConfigurationSource.Explicit)
+ : entityType.FindServiceProperty(loaderField.Name)!;
+
+ parameterBindings.Add(new DependencyInjectionParameterBinding(typeof(ILazyLoader), typeof(ILazyLoader), loaderProperty));
+ }
+
+ foreach (var propertyName in propertyNames)
+ {
+ parameterBindings.Add(new PropertyParameterBinding(entityType.FindProperty(propertyName)!));
+ }
+
+ entityType.ConstructorBinding
+ = new ConstructorBinding(
+ typeof(TEntity).GetTypeInfo().DeclaredConstructors.Single(c => c.GetParameters().Length == parameterBindings.Count),
+ parameterBindings
+ );
+ }
+
protected override void Seed(F1Context context)
=> F1Context.Seed(context);
}
diff --git a/test/EFCore.Specification.Tests/F1MaterializationInterceptor.cs b/test/EFCore.Specification.Tests/F1MaterializationInterceptor.cs
new file mode 100644
index 00000000000..b02d15a9b83
--- /dev/null
+++ b/test/EFCore.Specification.Tests/F1MaterializationInterceptor.cs
@@ -0,0 +1,132 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class F1MaterializationInterceptor : IMaterializationInterceptor
+{
+ public InterceptionResult CreatingInstance(
+ MaterializationInterceptionData materializationData,
+ InterceptionResult result)
+ => materializationData.EntityType.ClrType.Name switch
+ {
+ nameof(Chassis) => InterceptionResult.SuppressWithResult(
+ new Chassis.ChassisProxy(
+ materializationData.GetPropertyValue("_loader"),
+ materializationData.GetPropertyValue(nameof(Chassis.TeamId)),
+ materializationData.GetPropertyValue(nameof(Chassis.Name)))),
+ nameof(Driver) => InterceptionResult.SuppressWithResult(
+ new Driver.DriverProxy(
+ materializationData.GetPropertyValue("_loader"),
+ materializationData.GetPropertyValue(nameof(Driver.Id)),
+ materializationData.GetPropertyValue(nameof(Driver.Name)),
+ materializationData.GetPropertyValue(nameof(Driver.CarNumber)),
+ materializationData.GetPropertyValue(nameof(Driver.Championships)),
+ materializationData.GetPropertyValue(nameof(Driver.Races)),
+ materializationData.GetPropertyValue(nameof(Driver.Wins)),
+ materializationData.GetPropertyValue(nameof(Driver.Podiums)),
+ materializationData.GetPropertyValue(nameof(Driver.Poles)),
+ materializationData.GetPropertyValue(nameof(Driver.FastestLaps)),
+ materializationData.GetPropertyValue(nameof(Driver.TeamId)))),
+ nameof(Engine) => InterceptionResult.SuppressWithResult(
+ new Engine.EngineProxy(
+ materializationData.GetPropertyValue("_loader"),
+ materializationData.GetPropertyValue(nameof(Engine.Id)),
+ materializationData.GetPropertyValue(nameof(Engine.Name)))),
+ nameof(EngineSupplier) => InterceptionResult.SuppressWithResult(
+ new EngineSupplier.EngineSupplierProxy(
+ materializationData.GetPropertyValue("_loader"),
+ materializationData.GetPropertyValue(nameof(EngineSupplier.Name)))),
+ nameof(Gearbox) => InterceptionResult.SuppressWithResult(
+ new Gearbox.GearboxProxy(
+ materializationData.GetPropertyValue(nameof(Gearbox.Id)),
+ materializationData.GetPropertyValue(nameof(Gearbox.Name)))),
+ nameof(Location) => InterceptionResult.SuppressWithResult(
+ new Location.LocationProxy(
+ materializationData.GetPropertyValue(nameof(Location.Latitude)),
+ materializationData.GetPropertyValue(nameof(Location.Longitude)))),
+ nameof(Sponsor) => InterceptionResult.SuppressWithResult(new Sponsor.SponsorProxy()),
+ nameof(SponsorDetails) => InterceptionResult.SuppressWithResult(
+ new SponsorDetails.SponsorDetailsProxy(
+ materializationData.GetPropertyValue(nameof(SponsorDetails.Days)),
+ materializationData.GetPropertyValue(nameof(SponsorDetails.Space)))),
+ nameof(Team) => InterceptionResult.SuppressWithResult(
+ new Team.TeamProxy(
+ materializationData.GetPropertyValue("_loader"),
+ materializationData.GetPropertyValue(nameof(Team.Id)),
+ materializationData.GetPropertyValue(nameof(Team.Name)),
+ materializationData.GetPropertyValue(nameof(Team.Constructor)),
+ materializationData.GetPropertyValue(nameof(Team.Tire)),
+ materializationData.GetPropertyValue(nameof(Team.Principal)),
+ materializationData.GetPropertyValue(nameof(Team.ConstructorsChampionships)),
+ materializationData.GetPropertyValue(nameof(Team.DriversChampionships)),
+ materializationData.GetPropertyValue(nameof(Team.Races)),
+ materializationData.GetPropertyValue(nameof(Team.Victories)),
+ materializationData.GetPropertyValue(nameof(Team.Poles)),
+ materializationData.GetPropertyValue(nameof(Team.FastestLaps)),
+ materializationData.GetPropertyValue(nameof(Team.GearboxId)))),
+ nameof(TeamSponsor) => InterceptionResult.SuppressWithResult(new TeamSponsor.TeamSponsorProxy()),
+ nameof(TestDriver) => InterceptionResult.SuppressWithResult(
+ new TestDriver.TestDriverProxy(
+ materializationData.GetPropertyValue("_loader"),
+ materializationData.GetPropertyValue(nameof(Driver.Id)),
+ materializationData.GetPropertyValue(nameof(Driver.Name)),
+ materializationData.GetPropertyValue(nameof(Driver.CarNumber)),
+ materializationData.GetPropertyValue(nameof(Driver.Championships)),
+ materializationData.GetPropertyValue(nameof(Driver.Races)),
+ materializationData.GetPropertyValue(nameof(Driver.Wins)),
+ materializationData.GetPropertyValue(nameof(Driver.Podiums)),
+ materializationData.GetPropertyValue(nameof(Driver.Poles)),
+ materializationData.GetPropertyValue(nameof(Driver.FastestLaps)),
+ materializationData.GetPropertyValue(nameof(Driver.TeamId)))),
+ nameof(TitleSponsor) => InterceptionResult.SuppressWithResult(
+ new TitleSponsor.TitleSponsorProxy(
+ materializationData.GetPropertyValue("_loader"))),
+ _ => result
+ };
+
+ public object CreatedInstance(MaterializationInterceptionData materializationData, object instance)
+ {
+ Assert.True(instance is IF1Proxy);
+
+ ((IF1Proxy)instance).CreatedCalled = true;
+
+ return instance;
+ }
+
+ public InterceptionResult InitializingInstance(
+ MaterializationInterceptionData materializationData,
+ object instance,
+ InterceptionResult result)
+ {
+ Assert.True(instance is IF1Proxy);
+
+ ((IF1Proxy)instance).InitializingCalled = true;
+
+ if (instance is Sponsor sponsor)
+ {
+ sponsor.Id = materializationData.GetPropertyValue(nameof(sponsor.Id));
+ sponsor.Name = "Intercepted: " + materializationData.GetPropertyValue(nameof(sponsor.Name));
+
+ return InterceptionResult.Suppress();
+ }
+
+ return result;
+ }
+
+ public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
+ {
+ Assert.True(instance is IF1Proxy);
+
+ ((IF1Proxy)instance).InitializedCalled = true;
+
+ if (instance is Sponsor.SponsorProxy sponsor)
+ {
+ return new Sponsor.SponsorDoubleProxy(sponsor);
+ }
+
+ return instance;
+ }
+}
diff --git a/test/EFCore.Specification.Tests/MaterializationInterceptionTestBase.cs b/test/EFCore.Specification.Tests/MaterializationInterceptionTestBase.cs
new file mode 100644
index 00000000000..23860cbb2ab
--- /dev/null
+++ b/test/EFCore.Specification.Tests/MaterializationInterceptionTestBase.cs
@@ -0,0 +1,423 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using ISingletonInterceptor = Microsoft.EntityFrameworkCore.Diagnostics.ISingletonInterceptor;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public abstract class MaterializationInterceptionTestBase : SingletonInterceptorsTestBase
+{
+ protected MaterializationInterceptionTestBase(SingletonInterceptorsFixtureBase fixture)
+ : base(fixture)
+ {
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Binding_interceptors_are_used_by_queries(bool inject)
+ {
+ var interceptors = new[]
+ {
+ new TestBindingInterceptor("1"),
+ new TestBindingInterceptor("2"),
+ new TestBindingInterceptor("3"),
+ new TestBindingInterceptor("4")
+ };
+
+ using var context = CreateContext(interceptors, inject);
+
+ context.AddRange(
+ new Book { Title = "Amiga ROM Kernel Reference Manual" },
+ new Book { Title = "Amiga Hardware Reference Manual" });
+
+ context.SaveChanges();
+ context.ChangeTracker.Clear();
+
+ var results = context.Set().ToList();
+ Assert.All(results, e => Assert.Equal("4", e.MaterializedBy));
+ Assert.All(interceptors, i => Assert.Equal(1, i.CalledCount));
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Binding_interceptors_are_used_when_creating_instances(bool inject)
+ {
+ var interceptors = new[]
+ {
+ new TestBindingInterceptor("1"),
+ new TestBindingInterceptor("2"),
+ new TestBindingInterceptor("3"),
+ new TestBindingInterceptor("4")
+ };
+
+ using var context = CreateContext(interceptors, inject);
+
+ var materializer = context.GetService();
+ var book = (Book)materializer.GetEmptyMaterializer(context.Model.FindEntityType(typeof(Book))!)(
+ new MaterializationContext(ValueBuffer.Empty, context));
+
+ Assert.Equal("4", book.MaterializedBy);
+ Assert.All(interceptors, i => Assert.Equal(1, i.CalledCount));
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Intercept_query_materialization_for_empty_constructor(bool inject)
+ {
+ var creatingInstanceCount = 0;
+ var createdInstanceCount = 0;
+ var initializingInstanceCount = 0;
+ var initializedInstanceCount = 0;
+ LibraryContext? context = null;
+ var ids = new HashSet();
+ var titles = new HashSet();
+ var authors = new HashSet();
+
+ var interceptors = new[]
+ {
+ new ValidatingMaterializationInterceptor(
+ (data, instance, method) =>
+ {
+ Assert.Same(context, data.Context);
+ Assert.Same(data.Context.Model.FindEntityType(typeof(Book)), data.EntityType);
+
+ var valuesDictionary = data.CreateValuesDictionary();
+
+ var idProperty = data.EntityType.FindProperty(nameof(Book.Id))!;
+ var id = data.GetPropertyValue(nameof(Book.Id))!;
+ Assert.Equal(id, data.GetPropertyValue(nameof(Book.Id)));
+ Assert.Equal(id, data.GetPropertyValue(idProperty));
+ Assert.Equal(id, data.GetPropertyValue(idProperty));
+ Assert.Equal(id, valuesDictionary[idProperty]);
+ ids.Add(id);
+
+ var titleProperty = data.EntityType.FindProperty(nameof(Book.Title))!;
+ var title = data.GetPropertyValue(nameof(Book.Title));
+ Assert.Equal(title, data.GetPropertyValue(nameof(Book.Title)));
+ Assert.Equal(title, data.GetPropertyValue(titleProperty));
+ Assert.Equal(title, data.GetPropertyValue(titleProperty));
+ Assert.Equal(title, valuesDictionary[titleProperty]);
+ titles.Add(title);
+
+ var authorProperty = data.EntityType.FindProperty("Author")!;
+ var author = data.GetPropertyValue("Author");
+ Assert.Equal(author, data.GetPropertyValue("Author"));
+ Assert.Equal(author, data.GetPropertyValue(authorProperty));
+ Assert.Equal(author, data.GetPropertyValue(authorProperty));
+ Assert.Equal(author, valuesDictionary[authorProperty]);
+ authors.Add(author);
+
+ switch (method)
+ {
+ case nameof(IMaterializationInterceptor.CreatingInstance):
+ creatingInstanceCount++;
+ Assert.Null(instance);
+ break;
+ case nameof(IMaterializationInterceptor.CreatedInstance):
+ createdInstanceCount++;
+ Assert.IsType(instance);
+ Assert.Equal(Guid.Empty, ((Book)instance!).Id);
+ Assert.Null(((Book)instance!).Title);
+ break;
+ case nameof(IMaterializationInterceptor.InitializingInstance):
+ initializingInstanceCount++;
+ Assert.IsType(instance);
+ Assert.Equal(Guid.Empty, ((Book)instance!).Id);
+ Assert.Null(((Book)instance!).Title);
+ break;
+ case nameof(IMaterializationInterceptor.InitializedInstance):
+ initializedInstanceCount++;
+ Assert.IsType(instance);
+ Assert.Equal(id, ((Book)instance!).Id);
+ Assert.Equal(title, ((Book)instance!).Title);
+ break;
+ }
+ })
+ };
+
+ using (context = CreateContext(interceptors, inject))
+ {
+ var books = new[]
+ {
+ new Book { Title = "Amiga ROM Kernel Reference Manual" }, new Book { Title = "Amiga Hardware Reference Manual" }
+ };
+
+ context.AddRange(books);
+
+ context.Entry(books[0]).Property("Author").CurrentValue = "Commodore Business Machines Inc.";
+ context.Entry(books[1]).Property("Author").CurrentValue = "Agnes";
+
+ context.SaveChanges();
+ context.ChangeTracker.Clear();
+
+ var results = context.Set().Where(e => books.Select(e => e.Id).Contains(e.Id)).ToList();
+ Assert.Equal(2, results.Count);
+
+ Assert.Equal(2, creatingInstanceCount);
+ Assert.Equal(2, createdInstanceCount);
+ Assert.Equal(2, initializingInstanceCount);
+ Assert.Equal(2, initializedInstanceCount);
+
+ Assert.Equal(2, ids.Count);
+ Assert.Equal(2, titles.Count);
+ Assert.Equal(2, authors.Count);
+ Assert.Contains(ids, t => t == books[0].Id);
+ Assert.Contains(ids, t => t == books[1].Id);
+ Assert.Contains(titles, t => t == "Amiga ROM Kernel Reference Manual");
+ Assert.Contains(titles, t => t == "Amiga Hardware Reference Manual");
+ Assert.Contains(authors, t => t == "Commodore Business Machines Inc.");
+ Assert.Contains(authors, t => t == "Agnes");
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Intercept_query_materialization_for_full_constructor(bool inject)
+ {
+ var creatingInstanceCount = 0;
+ var createdInstanceCount = 0;
+ var initializingInstanceCount = 0;
+ var initializedInstanceCount = 0;
+ LibraryContext? context = null;
+ var ids = new HashSet();
+ var titles = new HashSet();
+ var authors = new HashSet();
+
+ var interceptors = new[]
+ {
+ new ValidatingMaterializationInterceptor(
+ (data, instance, method) =>
+ {
+ Assert.Same(context, data.Context);
+ Assert.Same(data.Context.Model.FindEntityType(typeof(Pamphlet)), data.EntityType);
+
+ var valuesDictionary = data.CreateValuesDictionary();
+
+ var idProperty = data.EntityType.FindProperty(nameof(Pamphlet.Id))!;
+ var id = data.GetPropertyValue(nameof(Pamphlet.Id))!;
+ Assert.Equal(id, data.GetPropertyValue(nameof(Pamphlet.Id)));
+ Assert.Equal(id, data.GetPropertyValue(idProperty));
+ Assert.Equal(id, data.GetPropertyValue(idProperty));
+ Assert.Equal(id, valuesDictionary[idProperty]);
+ ids.Add(id);
+
+ var titleProperty = data.EntityType.FindProperty(nameof(Pamphlet.Title))!;
+ var title = data.GetPropertyValue(nameof(Pamphlet.Title));
+ Assert.Equal(title, data.GetPropertyValue(nameof(Pamphlet.Title)));
+ Assert.Equal(title, data.GetPropertyValue(titleProperty));
+ Assert.Equal(title, data.GetPropertyValue(titleProperty));
+ Assert.Equal(title, valuesDictionary[titleProperty]);
+ titles.Add(title);
+
+ var authorProperty = data.EntityType.FindProperty("Author")!;
+ var author = data.GetPropertyValue("Author");
+ Assert.Equal(author, data.GetPropertyValue("Author"));
+ Assert.Equal(author, data.GetPropertyValue(authorProperty));
+ Assert.Equal(author, data.GetPropertyValue(authorProperty));
+ Assert.Equal(author, valuesDictionary[authorProperty]);
+ authors.Add(author);
+
+ switch (method)
+ {
+ case nameof(IMaterializationInterceptor.CreatingInstance):
+ creatingInstanceCount++;
+ Assert.Null(instance);
+ break;
+ case nameof(IMaterializationInterceptor.CreatedInstance):
+ createdInstanceCount++;
+ Assert.IsType(instance);
+ Assert.Equal(id, ((Pamphlet)instance!).Id);
+ Assert.Equal(title, ((Pamphlet)instance!).Title);
+ break;
+ case nameof(IMaterializationInterceptor.InitializingInstance):
+ initializingInstanceCount++;
+ Assert.IsType(instance);
+ Assert.Equal(id, ((Pamphlet)instance!).Id);
+ Assert.Equal(title, ((Pamphlet)instance!).Title);
+ break;
+ case nameof(IMaterializationInterceptor.InitializedInstance):
+ initializedInstanceCount++;
+ Assert.IsType(instance);
+ Assert.Equal(id, ((Pamphlet)instance!).Id);
+ Assert.Equal(title, ((Pamphlet)instance!).Title);
+ break;
+ }
+ })
+ };
+
+ using (context = CreateContext(interceptors, inject))
+ {
+ var pamphlets = new[] { new Pamphlet(Guid.Empty, "Rights of Man"), new Pamphlet(Guid.Empty, "Pamphlet des pamphlets") };
+
+ context.AddRange(pamphlets);
+
+ context.Entry(pamphlets[0]).Property("Author").CurrentValue = "Thomas Paine";
+ context.Entry(pamphlets[1]).Property("Author").CurrentValue = "Paul-Louis Courier";
+
+ context.SaveChanges();
+ context.ChangeTracker.Clear();
+
+ var results = context.Set().Where(e => pamphlets.Select(e => e.Id).Contains(e.Id)).ToList();
+ Assert.Equal(2, results.Count);
+
+ Assert.Equal(2, creatingInstanceCount);
+ Assert.Equal(2, createdInstanceCount);
+ Assert.Equal(2, initializingInstanceCount);
+ Assert.Equal(2, initializedInstanceCount);
+
+ Assert.Equal(2, ids.Count);
+ Assert.Equal(2, titles.Count);
+ Assert.Equal(2, authors.Count);
+ Assert.Contains(ids, t => t == pamphlets[0].Id);
+ Assert.Contains(ids, t => t == pamphlets[1].Id);
+ Assert.Contains(titles, t => t == "Rights of Man");
+ Assert.Contains(titles, t => t == "Pamphlet des pamphlets");
+ Assert.Contains(authors, t => t == "Thomas Paine");
+ Assert.Contains(authors, t => t == "Paul-Louis Courier");
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Multiple_materialization_interceptors_can_be_used(bool inject)
+ {
+ var interceptors = new ISingletonInterceptor[]
+ {
+ new CountingMaterializationInterceptor("A"),
+ new TestBindingInterceptor("1"),
+ new CountingMaterializationInterceptor("B"),
+ new TestBindingInterceptor("2"),
+ new TestBindingInterceptor("3"),
+ new TestBindingInterceptor("4"),
+ new CountingMaterializationInterceptor("C")
+ };
+
+ using var context = CreateContext(interceptors, inject);
+
+ context.AddRange(
+ new Book { Title = "Amiga ROM Kernel Reference Manual" },
+ new Book { Title = "Amiga Hardware Reference Manual" });
+
+ context.SaveChanges();
+ context.ChangeTracker.Clear();
+
+ var results = context.Set().ToList();
+ Assert.All(results, e => Assert.Equal("4", e.MaterializedBy));
+ Assert.All(interceptors.OfType(), i => Assert.Equal(1, i.CalledCount));
+
+ Assert.All(results, e => Assert.Equal("ABC", e.CreatedBy));
+ Assert.All(results, e => Assert.Equal("ABC", e.InitializingBy));
+ Assert.All(results, e => Assert.Equal("ABC", e.InitializedBy));
+ }
+
+ protected class TestBindingInterceptor : IInstantiationBindingInterceptor
+ {
+ private readonly string _id;
+
+ public TestBindingInterceptor(string id)
+ {
+ _id = id;
+ }
+
+ public int CalledCount { get; private set; }
+
+ protected Book BookFactory()
+ => new() { MaterializedBy = _id };
+
+ public InstantiationBinding ModifyBinding(IEntityType entityType, string entityInstanceName, InstantiationBinding binding)
+ {
+ CalledCount++;
+
+ return new FactoryMethodBinding(
+ this,
+ typeof(TestBindingInterceptor).GetTypeInfo().GetDeclaredMethod(nameof(BookFactory))!,
+ new List(),
+ entityType.ClrType);
+ }
+ }
+
+ protected class ValidatingMaterializationInterceptor : IMaterializationInterceptor
+ {
+ private readonly Action _validate;
+
+ public ValidatingMaterializationInterceptor(
+ Action validate)
+ {
+ _validate = validate;
+ }
+
+ public InterceptionResult CreatingInstance(
+ MaterializationInterceptionData materializationData, InterceptionResult result)
+ {
+ _validate(materializationData, null, nameof(CreatingInstance));
+
+ return result;
+ }
+
+ public object CreatedInstance(
+ MaterializationInterceptionData materializationData, object instance)
+ {
+ _validate(materializationData, instance, nameof(CreatedInstance));
+
+ return instance;
+ }
+
+ public InterceptionResult InitializingInstance(
+ MaterializationInterceptionData materializationData, object instance, InterceptionResult result)
+ {
+ _validate(materializationData, instance, nameof(InitializingInstance));
+
+ return result;
+ }
+
+ public object InitializedInstance(
+ MaterializationInterceptionData materializationData, object instance)
+ {
+ _validate(materializationData, instance, nameof(InitializedInstance));
+
+ return instance;
+ }
+ }
+
+ protected class CountingMaterializationInterceptor : IMaterializationInterceptor
+ {
+ private readonly string _id;
+
+ public CountingMaterializationInterceptor(string id)
+ {
+ _id = id;
+ }
+
+ public InterceptionResult CreatingInstance(
+ MaterializationInterceptionData materializationData, InterceptionResult result)
+ => result;
+
+ public object CreatedInstance(
+ MaterializationInterceptionData materializationData, object instance)
+ {
+ ((Book)instance).CreatedBy += _id;
+ return instance;
+ }
+
+ public InterceptionResult InitializingInstance(
+ MaterializationInterceptionData materializationData, object instance, InterceptionResult result)
+ {
+ ((Book)instance).InitializingBy += _id;
+ return result;
+ }
+
+ public object InitializedInstance(
+ MaterializationInterceptionData materializationData, object instance)
+ {
+ ((Book)instance).InitializedBy += _id;
+ return instance;
+ }
+ }
+}
diff --git a/test/EFCore.Specification.Tests/OptimisticConcurrencyTestBase.cs b/test/EFCore.Specification.Tests/OptimisticConcurrencyTestBase.cs
index 46d12939877..c7c248ebfaf 100644
--- a/test/EFCore.Specification.Tests/OptimisticConcurrencyTestBase.cs
+++ b/test/EFCore.Specification.Tests/OptimisticConcurrencyTestBase.cs
@@ -42,6 +42,7 @@ public virtual void Nullable_client_side_concurrency_token_can_be_used()
{
using var transaction = BeginTransaction(context.Database);
var sponsor = context.Sponsors.Single(s => s.Id == 1);
+ Assert.IsType(sponsor);
Assert.Null(context.Entry(sponsor).Property(Sponsor.ClientTokenPropertyName).CurrentValue);
originalName = sponsor.Name;
sponsor.Name = "New name";
@@ -51,8 +52,9 @@ public virtual void Nullable_client_side_concurrency_token_can_be_used()
using var innerContext = CreateF1Context();
UseTransaction(innerContext.Database, transaction);
sponsor = innerContext.Sponsors.Single(s => s.Id == 1);
+ Assert.IsType(sponsor);
Assert.Equal(1, innerContext.Entry(sponsor).Property(Sponsor.ClientTokenPropertyName).CurrentValue);
- Assert.Equal(newName, sponsor.Name);
+ Assert.Equal("Intercepted: " + newName, sponsor.Name);
sponsor.Name = originalName;
innerContext.Entry(sponsor).Property(Sponsor.ClientTokenPropertyName).OriginalValue = null;
Assert.Throws(() => innerContext.SaveChanges());
diff --git a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs
index e4756958911..d1374936e5f 100644
--- a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs
+++ b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
// ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local
@@ -61,6 +62,10 @@ private async Task TestPropertyValuesScalars(
Assert.Equal(12, values["Shadow1"]);
Assert.Equal("Pine Walk", values["Shadow2"]);
}
+
+ Assert.True(building.CreatedCalled);
+ Assert.True(building.InitializingCalled);
+ Assert.True(building.InitializedCalled);
}
[ConditionalFact]
@@ -108,6 +113,10 @@ private async Task TestPropertyValuesScalarsIProperty(
Assert.Equal(12, values[entry.Property("Shadow1").Metadata]);
Assert.Equal("Pine Walk", values[entry.Property("Shadow2").Metadata]);
}
+
+ Assert.True(building.CreatedCalled);
+ Assert.True(building.InitializingCalled);
+ Assert.True(building.InitializedCalled);
}
[ConditionalFact]
@@ -157,6 +166,10 @@ private async Task TestPropertyValuesDerivedScalars(
Assert.Equal("Dev", values["Shadow2"]);
Assert.Equal(2222, values["Shadow3"]);
}
+
+ Assert.True(employee.CreatedCalled);
+ Assert.True(employee.InitializingCalled);
+ Assert.True(employee.InitializedCalled);
}
[ConditionalFact]
@@ -456,6 +469,10 @@ private async Task TestPropertyValuesClone(
Assert.Equal("Building One Prime", buildingClone.Name);
Assert.Equal(1500001m, buildingClone.Value);
}
+
+ Assert.True(buildingClone.CreatedCalled);
+ Assert.True(buildingClone.InitializingCalled);
+ Assert.True(buildingClone.InitializedCalled);
}
[ConditionalFact]
@@ -501,6 +518,58 @@ private async Task TestPropertyValuesDerivedClone(
Assert.Equal("Milner", clone.LastName);
Assert.Equal(55m, clone.LeaveBalance);
}
+
+ Assert.True(clone.CreatedCalled);
+ Assert.True(clone.InitializingCalled);
+ Assert.True(clone.InitializedCalled);
+ }
+
+ [ConditionalFact]
+ public virtual Task Current_values_for_join_entity_can_be_copied_into_an_object()
+ => TestPropertyValuesJoinEntityClone(e => Task.FromResult(e.CurrentValues), expectOriginalValues: false);
+
+ [ConditionalFact]
+ public virtual Task Original_values_for_join_entity_can_be_copied_into_an_object()
+ => TestPropertyValuesJoinEntityClone(e => Task.FromResult(e.OriginalValues), expectOriginalValues: true);
+
+ [ConditionalFact]
+ public virtual Task Store_values_for_join_entity_can_be_copied_into_an_object()
+ => TestPropertyValuesJoinEntityClone(e => Task.FromResult(e.GetDatabaseValues()), expectOriginalValues: true);
+
+ [ConditionalFact]
+ public virtual Task Store_values_for_join_entity_can_be_copied_into_an_object_asynchronously()
+ => TestPropertyValuesJoinEntityClone(e => e.GetDatabaseValuesAsync(), expectOriginalValues: true);
+
+ private async Task TestPropertyValuesJoinEntityClone(
+ Func>, Task> getPropertyValues,
+ bool expectOriginalValues)
+ {
+ using var context = CreateContext();
+
+ var employee = context.Set()
+ .OfType()
+ .Include(e => e.VirtualTeams)
+ .Single(b => b.FirstName == "Rowan");
+
+ foreach (var joinEntry in context.ChangeTracker.Entries>())
+ {
+ joinEntry.Property("Payload").CurrentValue = "Payload++";
+
+ var clone = (Dictionary)(await getPropertyValues(joinEntry)).ToObject();
+
+ Assert.True((bool)clone["CreatedCalled"]);
+ Assert.True((bool)clone["InitializingCalled"]);
+ Assert.True((bool)clone["InitializedCalled"]);
+
+ if (expectOriginalValues)
+ {
+ Assert.Equal("Payload", clone["Payload"]);
+ }
+ else
+ {
+ Assert.Equal("Payload++", clone["Payload"]);
+ }
+ }
}
[ConditionalFact]
@@ -543,6 +612,10 @@ private async Task TestNonGenericPropertyValuesClone(
Assert.Equal("Building One Prime", buildingClone.Name);
Assert.Equal(1500001m, buildingClone.Value);
}
+
+ Assert.True(buildingClone.CreatedCalled);
+ Assert.True(buildingClone.InitializingCalled);
+ Assert.True(buildingClone.InitializedCalled);
}
[ConditionalFact]
@@ -1820,6 +1893,10 @@ private async Task NonGeneric_GetDatabaseValues_for_entity_not_in_the_store_retu
context.Set().Attach(building);
Assert.Null(await getPropertyValues(context.Entry((object)building)));
+
+ Assert.True(building.CreatedCalled);
+ Assert.True(building.InitializingCalled);
+ Assert.True(building.InitializedCalled);
}
[ConditionalFact]
@@ -1976,8 +2053,28 @@ public virtual void Setting_store_values_does_not_change_current_or_original_val
var currentValues = (Building)context.Entry(building).CurrentValues.ToObject();
Assert.Equal("Building One", currentValues.Name);
+ Assert.True(currentValues.CreatedCalled);
+ Assert.True(currentValues.InitializingCalled);
+ Assert.True(currentValues.InitializedCalled);
+
var originalValues = (Building)context.Entry(building).OriginalValues.ToObject();
Assert.Equal("Building One", originalValues.Name);
+
+ Assert.True(originalValues.CreatedCalled);
+ Assert.True(originalValues.InitializingCalled);
+ Assert.True(originalValues.InitializedCalled);
+ }
+
+ protected abstract class PropertyValuesBase
+ {
+ [NotMapped]
+ public bool CreatedCalled { get; set; }
+
+ [NotMapped]
+ public bool InitializingCalled { get; set; }
+
+ [NotMapped]
+ public bool InitializedCalled { get; set; }
}
protected abstract class Employee : UnMappedPersonBase
@@ -1985,7 +2082,14 @@ protected abstract class Employee : UnMappedPersonBase
public int EmployeeId { get; set; }
}
- protected class Building
+ protected class VirtualTeam : PropertyValuesBase
+ {
+ public int Id { get; set; }
+ public string TeamName { get; set; }
+ public ICollection Employees { get; set; }
+ }
+
+ protected class Building : PropertyValuesBase
{
private Building()
{
@@ -2057,7 +2161,7 @@ protected class BuildingDtoNoKey
public string Shadow2 { get; set; }
}
- protected class MailRoom
+ protected class MailRoom : PropertyValuesBase
{
#pragma warning disable IDE1006 // Naming Styles
public int id { get; set; }
@@ -2073,20 +2177,20 @@ protected class Office : UnMappedOfficeBase
public IList WhiteBoards { get; } = new List();
}
- protected abstract class UnMappedOfficeBase
+ protected abstract class UnMappedOfficeBase : PropertyValuesBase
{
public string Number { get; set; }
public string Description { get; set; }
}
- protected class BuildingDetail
+ protected class BuildingDetail : PropertyValuesBase
{
public Guid BuildingId { get; set; }
public Building Building { get; set; }
public string Details { get; set; }
}
- protected class WorkOrder
+ protected class WorkOrder : PropertyValuesBase
{
public int WorkOrderId { get; set; }
public int EmployeeId { get; set; }
@@ -2094,7 +2198,7 @@ protected class WorkOrder
public string Details { get; set; }
}
- protected class Whiteboard
+ protected class Whiteboard : PropertyValuesBase
{
#pragma warning disable IDE1006 // Naming Styles
public byte[] iD { get; set; }
@@ -2103,7 +2207,7 @@ protected class Whiteboard
public Office Office { get; set; }
}
- protected class UnMappedPersonBase
+ protected class UnMappedPersonBase : PropertyValuesBase
{
public string FirstName { get; set; }
public string LastName { get; set; }
@@ -2118,6 +2222,7 @@ protected class CurrentEmployee : Employee
public CurrentEmployee Manager { get; set; }
public decimal LeaveBalance { get; set; }
public Office Office { get; set; }
+ public ICollection VirtualTeams { get; set; }
}
protected class PastEmployee : Employee
@@ -2134,7 +2239,11 @@ protected DbContext CreateContext()
public abstract class PropertyValuesFixtureBase : SharedStoreFixtureBase
{
- protected override string StoreName { get; } = "PropertyValues";
+ protected override string StoreName
+ => "PropertyValues";
+
+ protected override IServiceCollection AddServices(IServiceCollection serviceCollection)
+ => base.AddServices(serviceCollection.AddSingleton());
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
@@ -2146,7 +2255,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b.Property("Shadow2");
});
- modelBuilder.Entity(b => b.Property("Shadow3"));
+ modelBuilder.Entity(b =>
+ {
+ b.Property("Shadow3");
+
+ b.HasMany(p => p.VirtualTeams)
+ .WithMany(p => p.Employees)
+ .UsingEntity>(
+ "VirtualTeamEmployee",
+ j => j
+ .HasOne()
+ .WithMany(),
+ j => j
+ .HasOne()
+ .WithMany(),
+ j => j.IndexerProperty("Payload"));
+ });
modelBuilder.Entity(b => b.Property("Shadow4"));
@@ -2213,6 +2337,13 @@ protected override void Seed(PoolableDbContext context)
context.Add(office);
}
+ var teams = new List
+ {
+ new() { TeamName = "Build" },
+ new() { TeamName = "Test" },
+ new() { TeamName = "DevOps" }
+ };
+
var employees = new List
{
new CurrentEmployee
@@ -2221,7 +2352,8 @@ protected override void Seed(PoolableDbContext context)
FirstName = "Rowan",
LastName = "Miller",
LeaveBalance = 45,
- Office = offices[0]
+ Office = offices[0],
+ VirtualTeams = new List{ teams[0], teams[1] }
},
new CurrentEmployee
{
@@ -2229,7 +2361,8 @@ protected override void Seed(PoolableDbContext context)
FirstName = "Arthur",
LastName = "Vickers",
LeaveBalance = 62,
- Office = offices[1]
+ Office = offices[1],
+ VirtualTeams = new List{ teams[1], teams[2] }
},
new PastEmployee
{
@@ -2284,7 +2417,68 @@ protected override void Seed(PoolableDbContext context)
context.Add(whiteboard);
}
+ foreach (var joinEntry in context.ChangeTracker.Entries>())
+ {
+ joinEntry.Property("Payload").CurrentValue = "Payload";
+
+ Assert.True((bool)joinEntry.Entity["CreatedCalled"]);
+ Assert.True((bool)joinEntry.Entity["InitializingCalled"]);
+ Assert.True((bool)joinEntry.Entity["InitializedCalled"]);
+ }
+
context.SaveChanges();
}
}
+
+ public class PropertyValuesMaterializationInterceptor : IMaterializationInterceptor
+ {
+ public InterceptionResult CreatingInstance(
+ MaterializationInterceptionData materializationData, InterceptionResult result)
+ => result;
+
+ public object CreatedInstance(MaterializationInterceptionData materializationData, object instance)
+ {
+ if (instance is IDictionary joinEntity)
+ {
+ joinEntity["CreatedCalled"] = true;
+ }
+ else
+ {
+ ((PropertyValuesBase)instance).CreatedCalled = true;
+ }
+
+ return instance;
+ }
+
+ public InterceptionResult InitializingInstance(
+ MaterializationInterceptionData materializationData,
+ object instance,
+ InterceptionResult result)
+ {
+ if (instance is IDictionary joinEntity)
+ {
+ joinEntity["InitializingCalled"] = true;
+ }
+ else
+ {
+ ((PropertyValuesBase)instance).InitializingCalled = true;
+ }
+
+ return result;
+ }
+
+ public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
+ {
+ if (instance is IDictionary joinEntity)
+ {
+ joinEntity["InitializedCalled"] = true;
+ }
+ else
+ {
+ ((PropertyValuesBase)instance).InitializedCalled = true;
+ }
+
+ return instance;
+ }
+ }
}
diff --git a/test/EFCore.Specification.Tests/SerializationTestBase.cs b/test/EFCore.Specification.Tests/SerializationTestBase.cs
index 1354cc6d755..6d63a5ca4fb 100644
--- a/test/EFCore.Specification.Tests/SerializationTestBase.cs
+++ b/test/EFCore.Specification.Tests/SerializationTestBase.cs
@@ -31,9 +31,7 @@ public virtual void Can_round_trip_through_JSON(bool useNewtonsoft, bool ignoreL
{
using var context = Fixture.CreateContext();
- var teams = context.Teams.Include(e => e.Drivers)
- .Include(e => e.Engine).ThenInclude(e => e.EngineSupplier)
- .ToList();
+ var teams = context.Teams.ToList();
Assert.Equal(12, teams.Count);
Assert.Equal(42, teams.SelectMany(e => e.Drivers).Count());
diff --git a/test/EFCore.Specification.Tests/SingletonInterceptorsTestBase.cs b/test/EFCore.Specification.Tests/SingletonInterceptorsTestBase.cs
index f49ef10b7e4..8bcc673b286 100644
--- a/test/EFCore.Specification.Tests/SingletonInterceptorsTestBase.cs
+++ b/test/EFCore.Specification.Tests/SingletonInterceptorsTestBase.cs
@@ -18,13 +18,32 @@ protected SingletonInterceptorsTestBase(SingletonInterceptorsFixtureBase fixture
protected class Book
{
- [DatabaseGenerated(DatabaseGeneratedOption.None)]
- public int Id { get; set; }
-
+ public Guid Id { get; set; }
public string? Title { get; set; }
[NotMapped]
public string? MaterializedBy { get; set; }
+
+ [NotMapped]
+ public string? CreatedBy { get; set; }
+
+ [NotMapped]
+ public string? InitializingBy { get; set; }
+
+ [NotMapped]
+ public string? InitializedBy { get; set; }
+ }
+
+ protected class Pamphlet
+ {
+ public Pamphlet(Guid id, string? title)
+ {
+ Id = id;
+ Title = title;
+ }
+
+ public Guid Id { get; set; }
+ public string? Title { get; set; }
}
public class LibraryContext : PoolableDbContext
@@ -36,7 +55,17 @@ public LibraryContext(DbContextOptions options)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
- modelBuilder.Entity();
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property("Author");
+ });
+
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property("Author");
+ });
}
}
diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Chassis.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Chassis.cs
index 9530395b5b9..91f48beda33 100644
--- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Chassis.cs
+++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Chassis.cs
@@ -5,6 +5,21 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel;
public class Chassis
{
+ public class ChassisProxy : Chassis, IF1Proxy
+ {
+ public ChassisProxy(
+ ILazyLoader loader,
+ int teamId,
+ string name)
+ : base(loader, teamId, name)
+ {
+ }
+
+ public bool CreatedCalled { get; set; }
+ public bool InitializingCalled { get; set; }
+ public bool InitializedCalled { get; set; }
+ }
+
private readonly ILazyLoader _loader;
private Team _team;
@@ -20,6 +35,8 @@ private Chassis(
_loader = loader;
TeamId = teamId;
Name = name;
+
+ Assert.IsType(this);
}
public int TeamId { get; set; }
diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Driver.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Driver.cs
index 75ae5854694..5f800e5fb21 100644
--- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Driver.cs
+++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Driver.cs
@@ -5,6 +5,29 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel;
public class Driver
{
+ public class DriverProxy : Driver, IF1Proxy
+ {
+ public DriverProxy(
+ ILazyLoader loader,
+ int id,
+ string name,
+ int? carNumber,
+ int championships,
+ int races,
+ int wins,
+ int podiums,
+ int poles,
+ int fastestLaps,
+ int teamId)
+ : base(loader, id, name, carNumber, championships, races, wins, podiums, poles, fastestLaps, teamId)
+ {
+ }
+
+ public bool CreatedCalled { get; set; }
+ public bool InitializingCalled { get; set; }
+ public bool InitializedCalled { get; set; }
+ }
+
private readonly ILazyLoader _loader;
private Team _team;
@@ -37,6 +60,8 @@ protected Driver(
Poles = poles;
FastestLaps = fastestLaps;
TeamId = teamId;
+
+ Assert.True(this is DriverProxy || this is TestDriver);
}
public int Id { get; set; }
diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Engine.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Engine.cs
index d14b12f7c2d..530e7959e48 100644
--- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Engine.cs
+++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Engine.cs
@@ -5,6 +5,21 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel;
public class Engine
{
+ public class EngineProxy : Engine, IF1Proxy
+ {
+ public EngineProxy(
+ ILazyLoader loader,
+ int id,
+ string name)
+ : base(loader, id, name)
+ {
+ }
+
+ public bool CreatedCalled { get; set; }
+ public bool InitializingCalled { get; set; }
+ public bool InitializedCalled { get; set; }
+ }
+
private readonly ILazyLoader _loader;
private EngineSupplier _engineSupplier;
private ICollection