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 _teams; @@ -19,6 +34,8 @@ public Engine(ILazyLoader loader, int id, string name) _loader = loader; Id = id; Name = name; + + Assert.IsType(this); } public int Id { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/EngineSupplier.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/EngineSupplier.cs index bbbd8877fe6..f1d3278e131 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/EngineSupplier.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/EngineSupplier.cs @@ -5,6 +5,20 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class EngineSupplier { + public class EngineSupplierProxy : EngineSupplier, IF1Proxy + { + public EngineSupplierProxy( + ILazyLoader loader, + string name) + : base(loader, name) + { + } + + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + private readonly ILazyLoader _loader; private ICollection _engines; @@ -16,6 +30,8 @@ private EngineSupplier(ILazyLoader loader, string name) { _loader = loader; Name = name; + + Assert.IsType(this); } public string Name { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Gearbox.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Gearbox.cs index 95879aa8bc9..9a352896de0 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Gearbox.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Gearbox.cs @@ -5,6 +5,20 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class Gearbox { + public class GearboxProxy : Gearbox, IF1Proxy + { + public GearboxProxy( + int id, + string name) + : base(id, name) + { + } + + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + public Gearbox() { } @@ -13,6 +27,8 @@ private Gearbox(int id, string name) { Id = id; Name = name; + + Assert.IsType(this); } public int Id { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/IF1Proxy.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/IF1Proxy.cs new file mode 100644 index 00000000000..17b50e5e0f9 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/IF1Proxy.cs @@ -0,0 +1,11 @@ +// 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.TestModels.ConcurrencyModel; + +public interface IF1Proxy +{ + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Location.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Location.cs index 3963ca76aa7..71f426d0c38 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Location.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Location.cs @@ -5,6 +5,20 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class Location { + public class LocationProxy : Location, IF1Proxy + { + public LocationProxy( + double latitude, + double longitude) + : base(latitude, longitude) + { + } + + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + public Location() { } @@ -13,6 +27,8 @@ private Location(double latitude, double longitude) { Latitude = latitude; Longitude = longitude; + + Assert.IsType(this); } public double Latitude { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Sponsor.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Sponsor.cs index 2dbf32b5a88..1f196ed6b1c 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Sponsor.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Sponsor.cs @@ -7,6 +7,25 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class Sponsor { + public class SponsorDoubleProxy : SponsorProxy + { + public SponsorDoubleProxy(SponsorProxy copyFrom) + { + Id = copyFrom.Id; + Name = copyFrom.Name; + CreatedCalled = copyFrom.CreatedCalled; + InitializingCalled = copyFrom.InitializingCalled; + InitializedCalled = copyFrom.InitializedCalled; + } + } + + public class SponsorProxy : Sponsor, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + public static readonly string ClientTokenPropertyName = "ClientToken"; private readonly ObservableCollection _teams = new(); diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SponsorDetails.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SponsorDetails.cs index 09cf7c931e0..f0252ced350 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SponsorDetails.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SponsorDetails.cs @@ -5,6 +5,20 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class SponsorDetails { + public class SponsorDetailsProxy : SponsorDetails, IF1Proxy + { + public SponsorDetailsProxy( + int days, + decimal space) + : base(days, space) + { + } + + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + public SponsorDetails() { } @@ -13,6 +27,8 @@ private SponsorDetails(int days, decimal space) { Days = days; Space = space; + + Assert.IsType(this); } public int Days { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Team.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Team.cs index 8193bb876cf..2e527d4bf42 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Team.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Team.cs @@ -7,6 +7,33 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class Team { + public class TeamProxy : Team, IF1Proxy + { + public TeamProxy( + ILazyLoader loader, + int id, + string name, + string constructor, + string tire, + string principal, + int constructorsChampionships, + int driversChampionships, + int races, + int victories, + int poles, + int fastestLaps, + int? gearboxId) + : base( + loader, id, name, constructor, tire, principal, constructorsChampionships, driversChampionships, races, victories, poles, + fastestLaps, gearboxId) + { + } + + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + private readonly ILazyLoader _loader; private readonly ObservableCollection _drivers = new ObservableCollectionListSource(); private readonly ObservableCollection _sponsors = new(); @@ -46,6 +73,8 @@ private Team( Poles = poles; FastestLaps = fastestLaps; GearboxId = gearboxId; + + Assert.IsType(this); } public int Id { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TeamSponsor.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TeamSponsor.cs index dd7d973ca96..a5e07d12aa7 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TeamSponsor.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TeamSponsor.cs @@ -5,6 +5,13 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class TeamSponsor { + public class TeamSponsorProxy : TeamSponsor, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + public int TeamId { get; set; } public int SponsorId { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TestDriver.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TestDriver.cs index 1139e212a9c..ad084ff1284 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TestDriver.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TestDriver.cs @@ -5,6 +5,29 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class TestDriver : Driver { + public class TestDriverProxy : TestDriver, IF1Proxy + { + public TestDriverProxy( + 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; } + } + public TestDriver() { } @@ -23,5 +46,6 @@ private TestDriver( int teamId) : base(loader, id, name, carNumber, championships, races, wins, podiums, poles, fastestLaps, teamId) { + Assert.IsType(this); } } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TitleSponsor.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TitleSponsor.cs index 97270e08be5..bd4efaade8a 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TitleSponsor.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/TitleSponsor.cs @@ -5,6 +5,19 @@ namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; public class TitleSponsor : Sponsor { + public class TitleSponsorProxy : TitleSponsor, IF1Proxy + { + public TitleSponsorProxy( + ILazyLoader loader) + : base(loader) + { + } + + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + private readonly ILazyLoader _loader; private SponsorDetails _details; @@ -15,6 +28,8 @@ public TitleSponsor() private TitleSponsor(ILazyLoader loader) { _loader = loader; + + Assert.IsType(this); } public SponsorDetails Details diff --git a/test/EFCore.SqlServer.FunctionalTests/BindingInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/MaterializationInterceptionSqlServerTest.cs similarity index 70% rename from test/EFCore.SqlServer.FunctionalTests/BindingInterceptionSqlServerTest.cs rename to test/EFCore.SqlServer.FunctionalTests/MaterializationInterceptionSqlServerTest.cs index 12f112a7398..0d2993ce35a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BindingInterceptionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/MaterializationInterceptionSqlServerTest.cs @@ -7,18 +7,18 @@ namespace Microsoft.EntityFrameworkCore; -public class BindingInterceptionSqlServerTest : BindingInterceptionTestBase, - IClassFixture +public class MaterializationInterceptionSqlServerTest : MaterializationInterceptionTestBase, + IClassFixture { - public BindingInterceptionSqlServerTest(BindingInterceptionSqlServerFixture fixture) + public MaterializationInterceptionSqlServerTest(MaterializationInterceptionSqlServerFixture fixture) : base(fixture) { } - public class BindingInterceptionSqlServerFixture : SingletonInterceptorsFixtureBase + public class MaterializationInterceptionSqlServerFixture : SingletonInterceptorsFixtureBase { protected override string StoreName - => "BindingInterception"; + => "MaterializationInterception"; protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; diff --git a/test/EFCore.Sqlite.FunctionalTests/BindingInterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/MaterializationInterceptionSqliteTest.cs similarity index 62% rename from test/EFCore.Sqlite.FunctionalTests/BindingInterceptionSqliteTest.cs rename to test/EFCore.Sqlite.FunctionalTests/MaterializationInterceptionSqliteTest.cs index da8b4b0d9bf..f6f5c1227e8 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BindingInterceptionSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/MaterializationInterceptionSqliteTest.cs @@ -5,18 +5,18 @@ namespace Microsoft.EntityFrameworkCore; -public class BindingInterceptionSqliteTest : BindingInterceptionTestBase, - IClassFixture +public class MaterializationInterceptionSqliteTest : MaterializationInterceptionTestBase, + IClassFixture { - public BindingInterceptionSqliteTest(BindingInterceptionSqliteFixture fixture) + public MaterializationInterceptionSqliteTest(MaterializationInterceptionSqliteFixture fixture) : base(fixture) { } - public class BindingInterceptionSqliteFixture : SingletonInterceptorsFixtureBase + public class MaterializationInterceptionSqliteFixture : SingletonInterceptorsFixtureBase { protected override string StoreName - => "BindingInterception"; + => "MaterializationInterception"; protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance;