diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs index e2efe686d93..77208054a74 100644 --- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs +++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs @@ -72,7 +72,7 @@ public static string CannotGenerateTypeQualifiedMethodCall => GetString("CannotGenerateTypeQualifiedMethodCall"); /// - /// The entity type '{entityType}' has a custom constructor binding. This is usually caused by using proxies. Compiled model can't be generated, because dynamic proxy types are not supported. If you are not using proxies configure the custom constructor binding in '{customize}' in a partial '{className}' class instead. + /// The entity type '{entityType}' has a custom constructor binding. Compiled model can't be generated, because custom constructor bindings are not supported. Configure the custom constructor binding in '{customize}' in a partial '{className}' class instead. /// public static string CompiledModelConstructorBinding(object? entityType, object? customize, object? className) => string.Format( @@ -801,4 +801,3 @@ private static string GetString(string name, params string[] formatterNames) } } } - diff --git a/src/EFCore.Design/Properties/DesignStrings.resx b/src/EFCore.Design/Properties/DesignStrings.resx index 8ce97bf63c8..ad362d22c46 100644 --- a/src/EFCore.Design/Properties/DesignStrings.resx +++ b/src/EFCore.Design/Properties/DesignStrings.resx @@ -142,7 +142,7 @@ You cannot add a migration with the name 'Migration'. - The entity type '{entityType}' has a custom constructor binding. This is usually caused by using proxies. Compiled model can't be generated, because dynamic proxy types are not supported. If you are not using proxies configure the custom constructor binding in '{customize}' in a partial '{className}' class instead. + The entity type '{entityType}' has a custom constructor binding. Compiled model can't be generated, because custom constructor bindings are not supported. Configure the custom constructor binding in '{customize}' in a partial '{className}' class instead. The context is configured to use a custom model cache key factory '{factoryType}', this usually indicates that the produced model can change between context instances. To preserve this behavior manually modify the generated compiled model source code. @@ -429,4 +429,4 @@ Change your target project to the migrations project by using the Package Manage Writing model snapshot to '{file}'. - \ No newline at end of file + diff --git a/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs b/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs index fe19178e195..6710b7983e0 100644 --- a/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs +++ b/src/EFCore.Proxies/Proxies/Internal/IProxyFactory.cs @@ -40,9 +40,7 @@ object CreateProxy( /// 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. /// - Type CreateProxyType( - ProxiesOptionsExtension options, - IReadOnlyEntityType entityType); + Type CreateProxyType(IReadOnlyEntityType entityType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs b/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs index 109ad6cbc3d..034d1f0777a 100644 --- a/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs +++ b/src/EFCore.Proxies/Proxies/Internal/ProxiesConventionSetPlugin.cs @@ -65,7 +65,6 @@ public virtual ConventionSet ModifyConventions(ConventionSet conventionSet) conventionSet.ModelFinalizingConventions.Add( new ProxyBindingRewriter( - _proxyFactory, extension, LazyLoaderParameterBindingFactoryDependencies, ConventionSetBuilderDependencies)); diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs b/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs index db546c19831..495fd2f9d2b 100644 --- a/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs +++ b/src/EFCore.Proxies/Proxies/Internal/ProxiesOptionsExtension.cs @@ -184,11 +184,19 @@ public override string LogFragment : ""; public override int GetServiceProviderHashCode() - => Extension.UseProxies.GetHashCode(); + { + var hashCode = new HashCode(); + hashCode.Add(Extension.UseLazyLoadingProxies); + hashCode.Add(Extension.UseChangeTrackingProxies); + hashCode.Add(Extension.CheckEquality); + return hashCode.ToHashCode(); + } public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => other is ExtensionInfo otherInfo - && Extension.UseProxies == otherInfo.Extension.UseProxies; + && Extension.UseLazyLoadingProxies == otherInfo.Extension.UseLazyLoadingProxies + && Extension.UseChangeTrackingProxies == otherInfo.Extension.UseChangeTrackingProxies + && Extension.CheckEquality == otherInfo.Extension.CheckEquality; public override void PopulateDebugInfo(IDictionary debugInfo) { diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyAnnotationNames.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyAnnotationNames.cs new file mode 100644 index 00000000000..ee4e0995f7b --- /dev/null +++ b/src/EFCore.Proxies/Proxies/Internal/ProxyAnnotationNames.cs @@ -0,0 +1,45 @@ +// 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.Proxies.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 static class ProxyAnnotationNames +{ + /// + /// 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 const string Prefix = "Proxies:"; + + /// + /// 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 const string LazyLoading = Prefix + "LazyLoading"; + + /// + /// 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 const string ChangeTracking = Prefix + "ChangeTracking"; + + /// + /// 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 const string CheckEquality = Prefix + "CheckEquality"; +} diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyBindingInterceptor.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingInterceptor.cs new file mode 100644 index 00000000000..e7f18acbe38 --- /dev/null +++ b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingInterceptor.cs @@ -0,0 +1,77 @@ +// 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.Proxies.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 ProxyBindingInterceptor : IInstantiationBindingInterceptor +{ + private static readonly MethodInfo CreateLazyLoadingProxyMethod + = typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateLazyLoadingProxy))!; + + private static readonly MethodInfo CreateProxyMethod + = typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateProxy))!; + + private readonly IProxyFactory _proxyFactory; + + /// + /// 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 ProxyBindingInterceptor(IProxyFactory proxyFactory) + { + _proxyFactory = proxyFactory; + } + + /// + /// 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 virtual InstantiationBinding ModifyBinding(IEntityType entityType, string entityInstanceName, InstantiationBinding binding) + { + var proxyType = _proxyFactory.CreateProxyType(entityType); + + if ((bool?)entityType.Model[ProxyAnnotationNames.LazyLoading] == true) + { + var serviceProperty = entityType.GetServiceProperties() + .First(e => e.ClrType == typeof(ILazyLoader)); + + return new FactoryMethodBinding( + _proxyFactory, + CreateLazyLoadingProxyMethod, + new List + { + new ContextParameterBinding(typeof(DbContext)), + new EntityTypeParameterBinding(), + new DependencyInjectionParameterBinding(typeof(ILazyLoader), typeof(ILazyLoader), serviceProperty), + new ObjectArrayParameterBinding(binding.ParameterBindings) + }, + proxyType); + } + + if ((bool?)entityType.Model[ProxyAnnotationNames.ChangeTracking] == true) + { + return new FactoryMethodBinding( + _proxyFactory, + CreateProxyMethod, + new List + { + new ContextParameterBinding(typeof(DbContext)), + new EntityTypeParameterBinding(), + new ObjectArrayParameterBinding(binding.ParameterBindings) + }, + proxyType); + } + + return binding; + } +} diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs index ce840edf007..69d02e6b24f 100644 --- a/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs +++ b/src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs @@ -14,17 +14,9 @@ namespace Microsoft.EntityFrameworkCore.Proxies.Internal; /// public class ProxyBindingRewriter : IModelFinalizingConvention { - private static readonly MethodInfo CreateLazyLoadingProxyMethod - = typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateLazyLoadingProxy))!; - private static readonly PropertyInfo LazyLoaderProperty = typeof(IProxyLazyLoader).GetProperty(nameof(IProxyLazyLoader.LazyLoader))!; - private static readonly MethodInfo CreateProxyMethod - = typeof(IProxyFactory).GetTypeInfo().GetDeclaredMethod(nameof(IProxyFactory.CreateProxy))!; - - private readonly ConstructorBindingConvention _directBindingConvention; - private readonly IProxyFactory _proxyFactory; private readonly ProxiesOptionsExtension? _options; /// @@ -34,16 +26,13 @@ private static readonly MethodInfo CreateProxyMethod /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public ProxyBindingRewriter( - IProxyFactory proxyFactory, ProxiesOptionsExtension? options, LazyLoaderParameterBindingFactoryDependencies lazyLoaderParameterBindingFactoryDependencies, ProviderConventionSetBuilderDependencies conventionSetBuilderDependencies) { - _proxyFactory = proxyFactory; _options = options; LazyLoaderParameterBindingFactoryDependencies = lazyLoaderParameterBindingFactoryDependencies; ConventionSetBuilderDependencies = conventionSetBuilderDependencies; - _directBindingConvention = new ConstructorBindingConvention(conventionSetBuilderDependencies); } /// @@ -69,6 +58,10 @@ public virtual void ProcessModelFinalizing( { if (_options?.UseProxies == true) { + modelBuilder.HasAnnotation(ProxyAnnotationNames.LazyLoading, _options.UseLazyLoadingProxies); + modelBuilder.HasAnnotation(ProxyAnnotationNames.ChangeTracking, _options.UseChangeTrackingProxies); + modelBuilder.HasAnnotation(ProxyAnnotationNames.CheckEquality, _options.CheckEquality); + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) { var clrType = entityType.ClrType; @@ -82,30 +75,6 @@ public virtual void ProcessModelFinalizing( throw new InvalidOperationException(ProxiesStrings.ItsASeal(entityType.DisplayName())); } - var proxyType = _proxyFactory.CreateProxyType(_options, entityType); - - // WARNING: This code is EF internal; it should not be copied. See #10789 #14554 -#pragma warning disable EF1001 // Internal EF Core API usage. - var binding = ((EntityType)entityType).ConstructorBinding; - if (binding == null) - { - _directBindingConvention.ProcessModelFinalizing(modelBuilder, context); - binding = ((EntityType)entityType).ConstructorBinding!; - } - - ((EntityType)entityType).SetConstructorBinding( - UpdateConstructorBindings(entityType, proxyType, binding), - ConfigurationSource.Convention); - - binding = ((EntityType)entityType).ServiceOnlyConstructorBinding; - if (binding != null) - { - ((EntityType)entityType).SetServiceOnlyConstructorBinding( - UpdateConstructorBindings(entityType, proxyType, binding), - ConfigurationSource.Convention); - } -#pragma warning restore EF1001 // Internal EF Core API usage. - foreach (var navigationBase in entityType.GetDeclaredNavigations() .Concat(entityType.GetDeclaredSkipNavigations())) { @@ -136,6 +105,30 @@ public virtual void ProcessModelFinalizing( } } + if (_options.UseLazyLoadingProxies) + { + foreach (var conflictingProperty in entityType.GetDerivedTypes() + .SelectMany(e => e.GetDeclaredServiceProperties().Where(p => p.ClrType == typeof(ILazyLoader))) + .ToList()) + { + conflictingProperty.DeclaringEntityType.RemoveServiceProperty(conflictingProperty.Name); + } + + var serviceProperty = entityType.GetServiceProperties() + .FirstOrDefault(e => e.ClrType == typeof(ILazyLoader)); + if (serviceProperty == null) + { + serviceProperty = entityType.AddServiceProperty(LazyLoaderProperty); + serviceProperty.SetParameterBinding( + (ServiceParameterBinding)new LazyLoaderParameterBindingFactory( + LazyLoaderParameterBindingFactoryDependencies) + .Bind( + entityType, + typeof(ILazyLoader), + nameof(IProxyLazyLoader.LazyLoader))); + } + } + if (_options.UseChangeTrackingProxies) { var indexerChecked = false; @@ -191,58 +184,4 @@ public virtual void ProcessModelFinalizing( } } } - - private InstantiationBinding UpdateConstructorBindings( - IConventionEntityType entityType, - Type proxyType, - InstantiationBinding binding) - { - if (_options?.UseLazyLoadingProxies == true) - { - foreach (var conflictingProperty in entityType.GetDerivedTypes() - .SelectMany(e => e.GetDeclaredServiceProperties().Where(p => p.ClrType == typeof(ILazyLoader))) - .ToList()) - { - conflictingProperty.DeclaringEntityType.RemoveServiceProperty(conflictingProperty.Name); - } - - var serviceProperty = entityType.GetServiceProperties() - .FirstOrDefault(e => e.ClrType == typeof(ILazyLoader)); - if (serviceProperty == null) - { - serviceProperty = entityType.AddServiceProperty(LazyLoaderProperty); - serviceProperty.SetParameterBinding( - (ServiceParameterBinding)new LazyLoaderParameterBindingFactory( - LazyLoaderParameterBindingFactoryDependencies) - .Bind( - entityType, - typeof(ILazyLoader), - nameof(IProxyLazyLoader.LazyLoader))); - } - - return new FactoryMethodBinding( - _proxyFactory, - CreateLazyLoadingProxyMethod, - new List - { - new ContextParameterBinding(typeof(DbContext)), - new EntityTypeParameterBinding(), - new DependencyInjectionParameterBinding( - typeof(ILazyLoader), typeof(ILazyLoader), (IPropertyBase)serviceProperty), - new ObjectArrayParameterBinding(binding.ParameterBindings) - }, - proxyType); - } - - return new FactoryMethodBinding( - _proxyFactory, - CreateProxyMethod, - new List - { - new ContextParameterBinding(typeof(DbContext)), - new EntityTypeParameterBinding(), - new ObjectArrayParameterBinding(binding.ParameterBindings) - }, - proxyType); - } } diff --git a/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs b/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs index f8d1fecf9e5..f2036276684 100644 --- a/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs +++ b/src/EFCore.Proxies/Proxies/Internal/ProxyFactory.cs @@ -55,12 +55,11 @@ public virtual object Create( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual Type CreateProxyType( - ProxiesOptionsExtension options, IReadOnlyEntityType entityType) => _generator.ProxyBuilder.CreateClassProxyType( entityType.ClrType, - GetInterfacesToProxy(options, entityType.ClrType), - GenerationOptions); + GetInterfacesToProxy(entityType), + GenerationOptions); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -81,23 +80,21 @@ public virtual object CreateLazyLoadingProxy( } return CreateLazyLoadingProxy( - options, entityType, context.GetService(), constructorArguments); } private object CreateLazyLoadingProxy( - ProxiesOptionsExtension options, IEntityType entityType, ILazyLoader loader, object[] constructorArguments) => _generator.CreateClassProxy( entityType.ClrType, - GetInterfacesToProxy(options, entityType.ClrType), - GenerationOptions, + GetInterfacesToProxy(entityType), + GenerationOptions, constructorArguments, - GetNotifyChangeInterceptors(options, entityType, new LazyLoadingInterceptor(entityType, loader))); + GetNotifyChangeInterceptors(entityType, new LazyLoadingInterceptor(entityType, loader))); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -116,51 +113,46 @@ public virtual object CreateProxy( throw new InvalidOperationException(ProxiesStrings.ProxyServicesMissing); } - if (options.UseLazyLoadingProxies) + if ((bool?)entityType.Model[ProxyAnnotationNames.LazyLoading] == true) { return CreateLazyLoadingProxy( - options, entityType, context.GetService(), constructorArguments); } return CreateProxy( - options, entityType, constructorArguments); } private object CreateProxy( - ProxiesOptionsExtension options, IEntityType entityType, object[] constructorArguments) => _generator.CreateClassProxy( entityType.ClrType, - GetInterfacesToProxy(options, entityType.ClrType), - GenerationOptions, + GetInterfacesToProxy(entityType), + GenerationOptions, constructorArguments, - GetNotifyChangeInterceptors(options, entityType)); + GetNotifyChangeInterceptors(entityType)); - private static Type[] GetInterfacesToProxy( - ProxiesOptionsExtension options, - Type type) + private static Type[] GetInterfacesToProxy(IReadOnlyEntityType entityType) { var interfacesToProxy = new List(); - if (options.UseLazyLoadingProxies) + if ((bool?)entityType.Model[ProxyAnnotationNames.LazyLoading] == true) { interfacesToProxy.Add(ProxyLazyLoaderInterface); } - if (options.UseChangeTrackingProxies) + if ((bool?)entityType.Model[ProxyAnnotationNames.ChangeTracking] == true) { - if (!NotifyPropertyChangedInterface.IsAssignableFrom(type)) + if (!NotifyPropertyChangedInterface.IsAssignableFrom(entityType.ClrType)) { interfacesToProxy.Add(NotifyPropertyChangedInterface); } - if (!NotifyPropertyChangingInterface.IsAssignableFrom(type)) + if (!NotifyPropertyChangingInterface.IsAssignableFrom(entityType.ClrType)) { interfacesToProxy.Add(NotifyPropertyChangingInterface); } @@ -170,7 +162,6 @@ private static Type[] GetInterfacesToProxy( } private static IInterceptor[] GetNotifyChangeInterceptors( - ProxiesOptionsExtension options, IEntityType entityType, LazyLoadingInterceptor? lazyLoadingInterceptor = null) { @@ -181,16 +172,18 @@ private static IInterceptor[] GetNotifyChangeInterceptors( interceptors.Add(lazyLoadingInterceptor); } - if (options.UseChangeTrackingProxies) + if ((bool?)entityType.Model[ProxyAnnotationNames.ChangeTracking] == true) { + var checkEquality = (bool?)entityType.Model[ProxyAnnotationNames.CheckEquality] == true; + if (!NotifyPropertyChangedInterface.IsAssignableFrom(entityType.ClrType)) { - interceptors.Add(new PropertyChangedInterceptor(entityType, options.CheckEquality)); + interceptors.Add(new PropertyChangedInterceptor(entityType, checkEquality)); } if (!NotifyPropertyChangingInterface.IsAssignableFrom(entityType.ClrType)) { - interceptors.Add(new PropertyChangingInterceptor(entityType, options.CheckEquality)); + interceptors.Add(new PropertyChangingInterceptor(entityType, checkEquality)); } } diff --git a/src/EFCore.Proxies/ProxiesServiceCollectionExtensions.cs b/src/EFCore.Proxies/ProxiesServiceCollectionExtensions.cs index 6698da70149..3b3a0707893 100644 --- a/src/EFCore.Proxies/ProxiesServiceCollectionExtensions.cs +++ b/src/EFCore.Proxies/ProxiesServiceCollectionExtensions.cs @@ -30,6 +30,7 @@ public static IServiceCollection AddEntityFrameworkProxies( { new EntityFrameworkServicesBuilder(serviceCollection) .TryAdd() + .TryAdd() .TryAddProviderSpecificServices( b => b.TryAddSingleton()); diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 0b5ebb1ef2c..4d4da6d7479 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -22,6 +22,7 @@ public class NavigationFixer : INavigationFixer bool SetModified)>? _danglingJoinEntities; private readonly IEntityGraphAttacher _attacher; + private readonly IEntityMaterializerSource _entityMaterializerSource; private bool _inFixup; private bool _inAttachGraph; @@ -31,9 +32,12 @@ public class NavigationFixer : INavigationFixer /// 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 NavigationFixer(IEntityGraphAttacher attacher) + public NavigationFixer( + IEntityGraphAttacher attacher, + IEntityMaterializerSource entityMaterializerSource) { _attacher = attacher; + _entityMaterializerSource = entityMaterializerSource; } /// @@ -1002,8 +1006,8 @@ private void FindOrCreateJoinEntry( else if (!_inAttachGraph) { var joinEntityType = arguments.SkipNavigation.JoinEntityType; - var joinEntity = joinEntityType.GetInstanceFactory()( - new MaterializationContext(ValueBuffer.Empty, arguments.Entry.StateManager.Context)); + var joinEntity = _entityMaterializerSource.GetEmptyMaterializer(joinEntityType) + (new MaterializationContext(ValueBuffer.Empty, arguments.Entry.StateManager.Context)); joinEntry = arguments.Entry.StateManager.GetOrCreateEntry(joinEntity, joinEntityType); diff --git a/src/EFCore/DbContextOptionsBuilder.cs b/src/EFCore/DbContextOptionsBuilder.cs index a7ac734643f..ca7cd7c7a16 100644 --- a/src/EFCore/DbContextOptionsBuilder.cs +++ b/src/EFCore/DbContextOptionsBuilder.cs @@ -547,7 +547,18 @@ public virtual DbContextOptionsBuilder ReplaceServiceThe interceptors to add. /// The same builder instance so that multiple calls can be chained. public virtual DbContextOptionsBuilder AddInterceptors(IEnumerable interceptors) - => WithOption(e => e.WithInterceptors(Check.NotNull(interceptors, nameof(interceptors)))); + { + Check.NotNull(interceptors, nameof(interceptors)); + + var singletonInterceptors = interceptors.OfType().ToList(); + var builder = this; + if (singletonInterceptors.Count > 0) + { + builder = WithOption(e => e.WithSingletonInterceptors(singletonInterceptors)); + } + + return builder.WithOption(e => e.WithInterceptors(interceptors)); + } /// /// Adds instances to those registered on the context. diff --git a/src/EFCore/Diagnostics/IInstantiationBindingInterceptor.cs b/src/EFCore/Diagnostics/IInstantiationBindingInterceptor.cs new file mode 100644 index 00000000000..53f7ee70e28 --- /dev/null +++ b/src/EFCore/Diagnostics/IInstantiationBindingInterceptor.cs @@ -0,0 +1,25 @@ +// 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 modify the used when creating +/// entity instances. +/// +/// +/// instances define how to create an entity instance through the binding of EF model properties +/// to, for example, constructor parameters or parameters of a factory method. This is then built into the expression tree which is +/// compiled into a delegate used to materialize entity instances. +/// +public interface IInstantiationBindingInterceptor : ISingletonInterceptor +{ + /// + /// Returns a new for the given entity type, potentially modified from the given binding. + /// + /// The entity type for which the binding is being used. + /// The name of the instance being materialized. + /// The current binding. + /// A new binding. + InstantiationBinding ModifyBinding(IEntityType entityType, string entityInstanceName, InstantiationBinding binding); +} diff --git a/src/EFCore/Diagnostics/ISingletonInterceptor.cs b/src/EFCore/Diagnostics/ISingletonInterceptor.cs new file mode 100644 index 00000000000..0523c7cd327 --- /dev/null +++ b/src/EFCore/Diagnostics/ISingletonInterceptor.cs @@ -0,0 +1,13 @@ +// 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; + +/// +/// The base interface for all Entity Framework interceptors that are registered as +/// services. This means a single instance is used by many instances. +/// The implementation must be thread-safe. +/// +public interface ISingletonInterceptor : IInterceptor +{ +} diff --git a/src/EFCore/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index d6adb65d3b9..dc493a885cd 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -39,6 +39,7 @@ public class CoreOptionsExtension : IDbContextOptionsExtension private bool _serviceProviderCachingEnabled = true; private DbContextOptionsExtensionInfo? _info; private IEnumerable? _interceptors; + private IEnumerable? _singletonInterceptors; private static readonly TimeSpan DefaultLoggingCacheTime = TimeSpan.FromSeconds(1); @@ -78,6 +79,7 @@ protected CoreOptionsExtension(CoreOptionsExtension copyFrom) _loggingCacheTime = copyFrom.LoggingCacheTime; _serviceProviderCachingEnabled = copyFrom.ServiceProviderCachingEnabled; _interceptors = copyFrom.Interceptors?.ToList(); + _singletonInterceptors = copyFrom.SingletonInterceptors?.ToList(); if (copyFrom._replacedServices != null) { @@ -347,6 +349,23 @@ public virtual CoreOptionsExtension WithInterceptors(IEnumerable i return clone; } + /// + /// Creates a new instance with all options the same as for this instance, but with the given option changed. + /// It is unusual to call this method directly. Instead use . + /// + /// The option to change. + /// A new instance with the option changed. + public virtual CoreOptionsExtension WithSingletonInterceptors(IEnumerable interceptors) + { + var clone = Clone(); + + clone._singletonInterceptors = _singletonInterceptors == null + ? interceptors + : _singletonInterceptors.Concat(interceptors); + + return clone; + } + /// /// The option set from the method. /// @@ -444,11 +463,19 @@ public virtual TimeSpan LoggingCacheTime => _loggingCacheTime; /// - /// The options set from the method. + /// The options set from the method + /// for scoped interceptors. /// public virtual IEnumerable? Interceptors => _interceptors; + /// + /// The options set from the method + /// for singleton interceptors. + /// + public virtual IEnumerable? SingletonInterceptors + => _singletonInterceptors; + /// /// Adds the services required to make the selected options work. This is used when there /// is no external and EF is maintaining its own service @@ -463,6 +490,14 @@ public virtual void ApplyServices(IServiceCollection services) { services.AddSingleton(memoryCache); } + + if (_singletonInterceptors != null) + { + foreach (var interceptor in _singletonInterceptors) + { + services.AddSingleton(interceptor); + } + } } private IMemoryCache? GetMemoryCache() @@ -507,6 +542,15 @@ public virtual void Validate(IDbContextOptions options) nameof(DbContextOptionsBuilder.UseInternalServiceProvider), nameof(IMemoryCache))); } + + if (SingletonInterceptors != null && SingletonInterceptors.Any()) + { + throw new InvalidOperationException( + CoreStrings.InvalidUseService( + nameof(DbContextOptionsBuilder.AddInterceptors), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider), + nameof(ISingletonInterceptor))); + } } } @@ -614,6 +658,14 @@ public override int GetServiceProviderHashCode() } } + if (Extension._singletonInterceptors != null) + { + foreach (var interceptor in Extension._singletonInterceptors) + { + hashCode.Add(interceptor); + } + } + _serviceProviderHash = hashCode.ToHashCode(); } @@ -631,6 +683,10 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo || (Extension._replacedServices != null && otherInfo.Extension._replacedServices != null && Extension._replacedServices.Count == otherInfo.Extension._replacedServices.Count - && Extension._replacedServices.SequenceEqual(otherInfo.Extension._replacedServices))); + && Extension._replacedServices.SequenceEqual(otherInfo.Extension._replacedServices))) + && (Extension._singletonInterceptors == otherInfo.Extension._singletonInterceptors + || (Extension._singletonInterceptors != null + && otherInfo.Extension._singletonInterceptors != null + && Extension._singletonInterceptors.SequenceEqual(otherInfo.Extension._singletonInterceptors))); } } diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index a162e414b28..f2670cc30d5 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -131,6 +131,7 @@ public static readonly IDictionary CoreServices }, { typeof(ISingletonOptions), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, { typeof(IConventionSetPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, + { typeof(ISingletonInterceptor), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, { typeof(IResettableService), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, { typeof(IInterceptor), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, { typeof(IInterceptorAggregator), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) } diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index 23a38ca8a8a..bd8574d7e8a 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -22,6 +22,12 @@ public interface IEntityType : IReadOnlyEntityType, ITypeBase /// InstantiationBinding? ConstructorBinding { get; } + /// + /// Gets the for the preferred constructor when creating instances with only service + /// properties initialized. + /// + InstantiationBinding? ServiceOnlyConstructorBinding { get; } + /// /// Returns the that will be used for storing a discriminator value. /// diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index d3687903017..f6e04423f88 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -69,7 +69,6 @@ private readonly SortedDictionary _serviceProperties private Func? _storeGeneratedValuesFactory; private Func? _shadowValuesFactory; private Func? _emptyShadowValuesFactory; - private Func? _instanceFactory; private IProperty[]? _foreignKeyProperties; private IProperty[]? _valueGeneratingProperties; @@ -2737,39 +2736,6 @@ public virtual Func EmptyShadowValuesFactory return new EmptyShadowValuesFactoryFactory().CreateEmpty(entityType); }); - /// - /// 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 virtual Func InstanceFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _instanceFactory, this, - static entityType => - { - entityType.EnsureReadOnly(); - - var binding = entityType.ServiceOnlyConstructorBinding; - if (binding == null) - { - var _ = entityType.ConstructorBinding; - binding = entityType.ServiceOnlyConstructorBinding; - if (binding == null) - { - throw new InvalidOperationException(CoreStrings.NoParameterlessConstructor(entityType.DisplayName())); - } - } - - var contextParam = Expression.Parameter(typeof(MaterializationContext), "mc"); - - return Expression.Lambda>( - binding.CreateConstructorExpression( - new ParameterBindingInfo(entityType, contextParam)), - contextParam) - .Compile(); - }); - /// /// 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 diff --git a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs index eec43e4408a..8b26227b643 100644 --- a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs @@ -319,15 +319,6 @@ public static PropertyCounts CalculateCounts(this IRuntimeEntityType entityType) storeGenerationIndex); } - /// - /// 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 static Func GetInstanceFactory(this IEntityType entityType) - => ((IRuntimeEntityType)entityType).InstanceFactory; - /// /// 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 diff --git a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs index 108322bd39b..1eb92d77e9d 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs @@ -69,14 +69,6 @@ public interface IRuntimeEntityType : IEntityType /// Func EmptyShadowValuesFactory { get; } - /// - /// 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. - /// - Func InstanceFactory { get; } - /// /// 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 @@ -85,14 +77,6 @@ public interface IRuntimeEntityType : IEntityType /// ConfigurationSource? GetConstructorBindingConfigurationSource(); - /// - /// 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. - /// - InstantiationBinding? ServiceOnlyConstructorBinding { get; } - /// /// 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 diff --git a/src/EFCore/Metadata/Internal/PropertyBaseExtensions.cs b/src/EFCore/Metadata/Internal/PropertyBaseExtensions.cs index 1f83077aee1..dc712687fe7 100644 --- a/src/EFCore/Metadata/Internal/PropertyBaseExtensions.cs +++ b/src/EFCore/Metadata/Internal/PropertyBaseExtensions.cs @@ -339,15 +339,14 @@ public static bool TryGetMemberInfo( } private static string GetNoFieldErrorMessage(IPropertyBase propertyBase) - { - var constructorBinding = ((EntityType)propertyBase.DeclaringType).ConstructorBinding; - return constructorBinding?.ParameterBindings - .OfType() - .Any(b => b.ServiceType == typeof(ILazyLoader)) + => ((EntityType)propertyBase.DeclaringType).GetServiceProperties() + .Any(p => typeof(ILazyLoader).IsAssignableFrom(p.ClrType)) + || ((EntityType)propertyBase.DeclaringType).ConstructorBinding?.ParameterBindings + .OfType() + .Any(b => b.ServiceType == typeof(ILazyLoader)) == true ? CoreStrings.NoBackingFieldLazyLoading( propertyBase.Name, propertyBase.DeclaringType.DisplayName()) : CoreStrings.NoBackingField( propertyBase.Name, propertyBase.DeclaringType.DisplayName(), nameof(PropertyAccessMode)); - } } diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index ada954cb6bd..e584e27c3cb 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -58,7 +58,6 @@ private readonly SortedDictionary _serviceProper private Func? _storeGeneratedValuesFactory; private Func? _shadowValuesFactory; private Func? _emptyShadowValuesFactory; - private Func? _instanceFactory; private IProperty[]? _foreignKeyProperties; private IProperty[]? _valueGeneratingProperties; @@ -1274,34 +1273,6 @@ Func IRuntimeEntityType.EmptyShadowValuesFactory ref _emptyShadowValuesFactory, this, static entityType => new EmptyShadowValuesFactoryFactory().CreateEmpty(entityType)); - /// - Func IRuntimeEntityType.InstanceFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _instanceFactory, this, - static entityType => - { - var binding = entityType._serviceOnlyConstructorBinding; - if (binding == null) - { - var _ = ((IEntityType)entityType).ConstructorBinding; - binding = entityType._serviceOnlyConstructorBinding; - if (binding == null) - { - throw new InvalidOperationException( - CoreStrings.NoParameterlessConstructor( - ((IReadOnlyEntityType)entityType).DisplayName())); - } - } - - var contextParam = Expression.Parameter(typeof(MaterializationContext), "mc"); - - return Expression.Lambda>( - binding.CreateConstructorExpression( - new ParameterBindingInfo(entityType, contextParam)), - contextParam) - .Compile(); - }); - /// [DebuggerStepThrough] IEnumerable IEntityType.GetForeignKeyProperties() diff --git a/src/EFCore/Query/IEntityMaterializerSource.cs b/src/EFCore/Query/IEntityMaterializerSource.cs index 438f4b618d1..54008a56748 100644 --- a/src/EFCore/Query/IEntityMaterializerSource.cs +++ b/src/EFCore/Query/IEntityMaterializerSource.cs @@ -56,4 +56,17 @@ Expression CreateMaterializeExpression( /// The entity type being materialized. /// A delegate to create instances. Func GetMaterializer(IEntityType entityType); + + /// + /// + /// Returns a cached delegate that creates empty instances of the given entity type. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The entity type being materialized. + /// A delegate to create instances. + Func GetEmptyMaterializer(IEntityType entityType); } diff --git a/src/EFCore/Query/Internal/EntityMaterializerSource.cs b/src/EFCore/Query/Internal/EntityMaterializerSource.cs index 630d36c3d63..071f09dce5c 100644 --- a/src/EFCore/Query/Internal/EntityMaterializerSource.cs +++ b/src/EFCore/Query/Internal/EntityMaterializerSource.cs @@ -14,6 +14,8 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; public class EntityMaterializerSource : IEntityMaterializerSource { private ConcurrentDictionary>? _materializers; + private ConcurrentDictionary>? _emptyMaterializers; + private readonly List _bindingInterceptors; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -24,6 +26,7 @@ public class EntityMaterializerSource : IEntityMaterializerSource public EntityMaterializerSource(EntityMaterializerSourceDependencies dependencies) { Dependencies = dependencies; + _bindingInterceptors = dependencies.SingletonInterceptors.OfType().ToList(); } /// @@ -47,7 +50,7 @@ public virtual Expression CreateMaterializeExpression( throw new InvalidOperationException(CoreStrings.CannotMaterializeAbstractType(entityType.DisplayName())); } - var constructorBinding = entityType.ConstructorBinding!; + var constructorBinding = ModifyBindings(entityType, entityInstanceName, entityType.ConstructorBinding!); var bindingInfo = new ParameterBindingInfo( entityType, @@ -125,7 +128,8 @@ private ConcurrentDictionary> /// 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 virtual Func GetMaterializer(IEntityType entityType) + public virtual Func GetMaterializer( + IEntityType entityType) => Materializers.GetOrAdd( entityType, static (e, self) => @@ -139,4 +143,54 @@ var materializationContextParameter .Compile(); }, this); + + private ConcurrentDictionary> EmptyMaterializers + => LazyInitializer.EnsureInitialized( + ref _emptyMaterializers, + () => new ConcurrentDictionary>()); + + /// + /// 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 virtual Func GetEmptyMaterializer( + IEntityType entityType) + => EmptyMaterializers.GetOrAdd( + entityType, + static (e, self) => + { + var binding = e.ServiceOnlyConstructorBinding; + if (binding == null) + { + var _ = e.ConstructorBinding; + binding = e.ServiceOnlyConstructorBinding; + if (binding == null) + { + throw new InvalidOperationException(CoreStrings.NoParameterlessConstructor(e.DisplayName())); + } + } + + binding = self.ModifyBindings(e, "v", binding); + + var contextParam = Expression.Parameter(typeof(MaterializationContext), "mc"); + + return Expression.Lambda>( + binding.CreateConstructorExpression( + new ParameterBindingInfo(e, contextParam)), + contextParam) + .Compile(); + }, + this); + + private InstantiationBinding ModifyBindings(IEntityType entityType, string entityInstanceName, InstantiationBinding binding) + { + foreach (var bindingInterceptor in _bindingInterceptors) + { + binding = bindingInterceptor.ModifyBinding(entityType, entityInstanceName, binding); + } + + return binding; + } } diff --git a/src/EFCore/Query/Internal/EntityMaterializerSourceDependencies.cs b/src/EFCore/Query/Internal/EntityMaterializerSourceDependencies.cs index a0d368ae9bf..dbc61c0a52a 100644 --- a/src/EFCore/Query/Internal/EntityMaterializerSourceDependencies.cs +++ b/src/EFCore/Query/Internal/EntityMaterializerSourceDependencies.cs @@ -44,7 +44,13 @@ public sealed record EntityMaterializerSourceDependencies /// the constructor at any point in this process. /// [EntityFrameworkInternal] - public EntityMaterializerSourceDependencies() + public EntityMaterializerSourceDependencies(IEnumerable singletonInterceptors) { + SingletonInterceptors = singletonInterceptors; } + + /// + /// Registered singleton interceptors. + /// + public IEnumerable SingletonInterceptors { get; init; } } diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs index b6c981749a6..b3044e91af7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs @@ -6,6 +6,29 @@ // 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"; @@ -81,7 +104,7 @@ public async Task Should_throw_if_specified_region_is_wrong() context.SaveChanges(); }); - + Assert.Equal("ApplicationRegion configuration 'FakeRegion' is not a valid Azure region or the current SDK version does not recognize it. If the value represents a valid region, make sure you are using the latest SDK version.", exception.Message); } diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index abb2f30f9f6..5aff17b6cc7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -401,6 +401,9 @@ public bool IsPropertyBag public InstantiationBinding ConstructorBinding => throw new NotImplementedException(); + public InstantiationBinding ServiceOnlyConstructorBinding + => throw new NotImplementedException(); + IReadOnlyEntityType IReadOnlyEntityType.BaseType => throw new NotImplementedException(); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index f8d6bec37a2..128850130ce 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -143,11 +143,11 @@ public void Global_namespace_works() [ConditionalFact] public void Throws_for_constructor_binding() => Test( - new LazyLoadingProxiesContext(), + new ConstructorBindingContext(), new CompiledModelCodeGenerationOptions(), expectedExceptionMessage: DesignStrings.CompiledModelConstructorBinding("Lazy", "Customize()", "LazyEntityType")); - public class LazyLoadingProxiesContext : ContextBase + public class ConstructorBindingContext : ContextBase { protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -158,6 +158,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { e.Property("Id"); e.HasKey("Id"); + ((EntityType)e.Metadata).ConstructorBinding = new ConstructorBinding( + typeof(object).GetConstructor(Type.EmptyTypes)!, + Array.Empty()); }); } @@ -349,7 +352,7 @@ public override int GetHashCode(object instance) public override object Snapshot(object instance) => throw new NotImplementedException(); } - + [ConditionalFact] public void Throws_for_provider_value_comparer() => Test( diff --git a/test/EFCore.InMemory.FunctionalTests/BindingInterceptionInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/BindingInterceptionInMemoryTest.cs new file mode 100644 index 00000000000..a7b6fe517ae --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/BindingInterceptionInMemoryTest.cs @@ -0,0 +1,32 @@ +// 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 class BindingInterceptionInMemoryTest : BindingInterceptionTestBase, + IClassFixture +{ + public BindingInterceptionInMemoryTest(BindingInterceptionInMemoryFixture fixture) + : base(fixture) + { + } + + public class BindingInterceptionInMemoryFixture : SingletonInterceptorsFixtureBase + { + protected override string StoreName + => "BindingInterception"; + + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkInMemoryDatabase(), injectedInterceptors); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(c => c.Ignore(InMemoryEventId.TransactionIgnoredWarning)); + } +} diff --git a/test/EFCore.Specification.Tests/BindingInterceptionTestBase.cs b/test/EFCore.Specification.Tests/BindingInterceptionTestBase.cs new file mode 100644 index 00000000000..e7087cb45d6 --- /dev/null +++ b/test/EFCore.Specification.Tests/BindingInterceptionTestBase.cs @@ -0,0 +1,90 @@ +// 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/SingletonInterceptorsTestBase.cs b/test/EFCore.Specification.Tests/SingletonInterceptorsTestBase.cs new file mode 100644 index 00000000000..f49ef10b7e4 --- /dev/null +++ b/test/EFCore.Specification.Tests/SingletonInterceptorsTestBase.cs @@ -0,0 +1,71 @@ +// 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 System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class SingletonInterceptorsTestBase +{ + protected SingletonInterceptorsTestBase(SingletonInterceptorsFixtureBase fixture) + { + Fixture = fixture; + } + + protected SingletonInterceptorsFixtureBase Fixture { get; } + + protected class Book + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public string? Title { get; set; } + + [NotMapped] + public string? MaterializedBy { get; set; } + } + + public class LibraryContext : PoolableDbContext + { + public LibraryContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + } + } + + public LibraryContext CreateContext(IEnumerable interceptors, bool inject) + => new(Fixture.CreateOptions(interceptors, inject)); + + public abstract class SingletonInterceptorsFixtureBase : SharedStoreFixtureBase + { + public virtual DbContextOptions CreateOptions(IEnumerable interceptors, bool inject) + { + var optionsBuilder = inject + ? new DbContextOptionsBuilder().UseInternalServiceProvider( + InjectInterceptors(new ServiceCollection(), interceptors) + .BuildServiceProvider(validateScopes: true)) + : new DbContextOptionsBuilder().AddInterceptors(interceptors); + + return AddOptions(TestStore.AddProviderOptions(optionsBuilder)).EnableDetailedErrors().Options; + } + + protected virtual IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + { + foreach (var interceptor in injectedInterceptors) + { + serviceCollection.AddSingleton(interceptor); + } + + return serviceCollection; + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/BindingInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BindingInterceptionSqlServerTest.cs new file mode 100644 index 00000000000..12f112a7398 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/BindingInterceptionSqlServerTest.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public class BindingInterceptionSqlServerTest : BindingInterceptionTestBase, + IClassFixture +{ + public BindingInterceptionSqlServerTest(BindingInterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class BindingInterceptionSqlServerFixture : SingletonInterceptorsFixtureBase + { + protected override string StoreName + => "BindingInterception"; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlServer(), injectedInterceptors); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new SqlServerDbContextOptionsBuilder(base.AddOptions(builder)) + .ExecutionStrategy(d => new SqlServerExecutionStrategy(d)); + return builder; + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/BindingInterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BindingInterceptionSqliteTest.cs new file mode 100644 index 00000000000..da8b4b0d9bf --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/BindingInterceptionSqliteTest.cs @@ -0,0 +1,29 @@ +// 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 class BindingInterceptionSqliteTest : BindingInterceptionTestBase, + IClassFixture +{ + public BindingInterceptionSqliteTest(BindingInterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class BindingInterceptionSqliteFixture : SingletonInterceptorsFixtureBase + { + protected override string StoreName + => "BindingInterception"; + + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlite(), injectedInterceptors); + } +} diff --git a/test/EFCore.Tests/ChangeTracking/InstanceFactoryTest.cs b/test/EFCore.Tests/ChangeTracking/InstanceFactoryTest.cs deleted file mode 100644 index a2ace0fe681..00000000000 --- a/test/EFCore.Tests/ChangeTracking/InstanceFactoryTest.cs +++ /dev/null @@ -1,255 +0,0 @@ -// 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; - -namespace Microsoft.EntityFrameworkCore.ChangeTracking; - -public class InstanceFactoryTest -{ - [ConditionalFact] - public void Create_instance_with_parameterless_constructor() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(Parameterless)); - var factory = entityType.GetInstanceFactory(); - var instance1 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - var instance2 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - - Assert.IsType(instance1); - Assert.IsType(instance2); - Assert.NotSame(instance1, instance2); - } - - [ConditionalFact] - public void Create_instance_with_lazy_loader() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(WithLazyLoader)); - var factory = entityType.GetInstanceFactory(); - var instance1 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - var instance2 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - - Assert.IsType(instance1); - Assert.NotNull(((WithLazyLoader)instance1).LazyLoader); - Assert.IsType(instance2); - Assert.NotSame(instance1, instance2); - Assert.NotSame(((WithLazyLoader)instance1).LazyLoader, ((WithLazyLoader)instance2).LazyLoader); - } - - [ConditionalFact] - public void Create_instance_with_lazy_loading_delegate() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(WithLazyLoaderDelegate)); - var factory = entityType.GetInstanceFactory(); - var instance1 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - var instance2 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - - Assert.IsType(instance1); - Assert.NotNull(((WithLazyLoaderDelegate)instance1).LazyLoader); - Assert.IsType(instance2); - Assert.NotSame(instance1, instance2); - Assert.NotSame(((WithLazyLoaderDelegate)instance1).LazyLoader, ((WithLazyLoaderDelegate)instance2).LazyLoader); - } - - [ConditionalFact] - public void Create_instance_with_entity_type() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(WithEntityType)); - var factory = entityType.GetInstanceFactory(); - var instance1 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - var instance2 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - - Assert.IsType(instance1); - Assert.NotNull(((WithEntityType)instance1).EntityType); - Assert.IsType(instance2); - Assert.NotSame(instance1, instance2); - Assert.Same(((WithEntityType)instance1).EntityType, ((WithEntityType)instance2).EntityType); - } - - [ConditionalFact] - public void Create_instance_with_context() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(WithContext)); - var factory = entityType.GetInstanceFactory(); - var instance1 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - var instance2 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - - Assert.IsType(instance1); - Assert.Same(context, ((WithContext)instance1).Context); - Assert.IsType(instance2); - Assert.NotSame(instance1, instance2); - Assert.Same(context, ((WithContext)instance2).Context); - } - - [ConditionalFact] - public void Create_instance_with_service_and_with_properties() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(WithServiceAndWithProperties)); - var factory = entityType.GetInstanceFactory(); - var instance1 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - var instance2 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - - Assert.IsType(instance1); - Assert.NotNull(((WithServiceAndWithProperties)instance1).LazyLoader); - Assert.IsType(instance2); - Assert.NotSame(instance1, instance2); - Assert.NotSame(((WithServiceAndWithProperties)instance1).LazyLoader, ((WithServiceAndWithProperties)instance2).LazyLoader); - } - - [ConditionalFact] - public void Create_instance_with_parameterless_and_with_properties() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(ParameterlessAndWithProperties)); - var factory = entityType.GetInstanceFactory(); - var instance1 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - var instance2 = factory(new MaterializationContext(ValueBuffer.Empty, context)); - - Assert.IsType(instance1); - Assert.IsType(instance2); - Assert.NotSame(instance1, instance2); - } - - [ConditionalFact] - public void Throws_for_constructor_with_properties() - { - using var context = new FactoryContext(); - - var entityType = context.Model.FindEntityType(typeof(WithProperties)); - - Assert.Equal( - CoreStrings.NoParameterlessConstructor(nameof(WithProperties)), - Assert.Throws( - () => entityType.GetInstanceFactory()).Message); - } - - private class FactoryContext : DbContext - { - protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder - .UseInMemoryDatabase(nameof(FactoryContext)) - .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider); - - protected internal override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - } - } - - private class Parameterless - { - private Parameterless() - { - } - - public int Id { get; set; } - } - - private class WithProperties - { - public WithProperties(int id) - { - Id = id; - } - - // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local - public int Id { get; set; } - } - - private class ParameterlessAndWithProperties - { - public ParameterlessAndWithProperties() - { - } - - public ParameterlessAndWithProperties(int id) - { - Id = id; - } - - // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local - public int Id { get; set; } - } - - private class WithLazyLoader - { - public WithLazyLoader(ILazyLoader lazyLoader) - { - LazyLoader = lazyLoader; - } - - public int Id { get; set; } - public ILazyLoader LazyLoader { get; } - } - - private class WithLazyLoaderDelegate - { - public WithLazyLoaderDelegate(Action lazyLoader) - { - LazyLoader = lazyLoader; - } - - public int Id { get; set; } - public Action LazyLoader { get; } - } - - private class WithEntityType - { - public WithEntityType(IEntityType entityType) - { - EntityType = entityType; - } - - public int Id { get; set; } - public IEntityType EntityType { get; } - } - - private class WithContext - { - public WithContext(DbContext context) - { - Context = context; - } - - public int Id { get; set; } - public DbContext Context { get; } - } - - private class WithServiceAndWithProperties - { - public WithServiceAndWithProperties(ILazyLoader lazyLoader) - { - LazyLoader = lazyLoader; - } - - public WithServiceAndWithProperties(ILazyLoader lazyLoader, int id) - : this(lazyLoader) - { - Id = id; - } - - public ILazyLoader LazyLoader { get; } - - // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local - public int Id { get; set; } - } -} diff --git a/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs index e6a393325dc..f5bfb2a17c8 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query.Internal; // ReSharper disable UnusedMember.Local // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -2128,7 +2129,8 @@ public override void AttachGraph( private class TestRelationshipListener : NavigationFixer { public TestRelationshipListener(IEntityGraphAttacher attacher) - : base(attacher) + : base(attacher, new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Enumerable.Empty()))) { } diff --git a/test/EFCore.Tests/DbContextServicesTest.cs b/test/EFCore.Tests/DbContextServicesTest.cs index aee6e22e5c8..04342becc19 100644 --- a/test/EFCore.Tests/DbContextServicesTest.cs +++ b/test/EFCore.Tests/DbContextServicesTest.cs @@ -2994,6 +2994,34 @@ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBu .UseInMemoryDatabase(Guid.NewGuid().ToString()); } + [ConditionalFact] + public void Throws_adding_singleton_interceptors_in_OnConfiguring_when_UseInternalServiceProvider() + { + using var context = new SingletonInterceptorFactoryContext(); + Assert.Equal( + CoreStrings.InvalidUseService( + nameof(DbContextOptionsBuilder.AddInterceptors), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider), + nameof(ISingletonInterceptor)), + Assert.Throws(() => context.Model).Message); + } + + private class SingletonInterceptorFactoryContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .AddInterceptors(new DummyInterceptor()) + .UseInternalServiceProvider( + new ServiceCollection() + .AddEntityFrameworkInMemoryDatabase() + .BuildServiceProvider(validateScopes: true)) + .UseInMemoryDatabase(Guid.NewGuid().ToString()); + } + + private class DummyInterceptor : ISingletonInterceptor + { + } + [ConditionalFact] public void Throws_setting_LoggerFactory_in_options_when_UseInternalServiceProvider() { diff --git a/test/EFCore.Tests/Metadata/Internal/EntityMaterializerSourceTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityMaterializerSourceTest.cs index cf7dff33bfa..17b5b816d83 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityMaterializerSourceTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityMaterializerSourceTest.cs @@ -20,7 +20,7 @@ public class EntityMaterializerSourceTest public void Throws_for_abstract_types() { var entityType = CreateConventionalModelBuilder().Model.AddEntityType(typeof(SomeAbstractEntity)); - var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies()); + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); Assert.Equal( CoreStrings.CannotMaterializeAbstractType(nameof(SomeAbstractEntity)), @@ -45,7 +45,8 @@ public void Can_create_materializer_for_entity_with_constructor_properties() entityType.Model.FinalizeModel(); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var gu = Guid.NewGuid(); var entity = (SomeEntity)factory( @@ -82,7 +83,8 @@ public void Can_create_materializer_for_entity_with_factory_method() entityType.Model.FinalizeModel(); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var gu = Guid.NewGuid(); var entity = (SomeEntity)factory( @@ -123,7 +125,8 @@ public void Can_create_materializer_for_entity_with_factory_method_with_object_a entityType.Model.FinalizeModel(); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var gu = Guid.NewGuid(); var entity = (SomeEntity)factory( @@ -157,7 +160,8 @@ public void Can_create_materializer_for_entity_with_instance_factory_method() entityType.Model.FinalizeModel(); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var gu = Guid.NewGuid(); var entity = (SomeEntity)factory( @@ -194,7 +198,8 @@ public void Can_create_materializer_for_entity_with_auto_properties() var entityType = CreateEntityType(); entityType.Model.FinalizeModel(); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var gu = Guid.NewGuid(); var entity = (SomeEntity)factory( @@ -227,7 +232,8 @@ public void Can_create_materializer_for_entity_with_fields() var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(SomeEntityWithFields)); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var gu = Guid.NewGuid(); var entity = (SomeEntityWithFields)factory( @@ -255,7 +261,8 @@ public void Can_read_nulls() var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(SomeEntity)); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var entity = (SomeEntity)factory( new MaterializationContext( @@ -287,7 +294,8 @@ public void Can_create_materializer_for_entity_ignoring_shadow_fields() var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(SomeEntity)); - var factory = GetMaterializer(new EntityMaterializerSource(new EntityMaterializerSourceDependencies()), entityType); + var factory = GetMaterializer(new EntityMaterializerSource( + new EntityMaterializerSourceDependencies(Array.Empty())), entityType); var gu = Guid.NewGuid(); var entity = (SomeEntity)factory( @@ -316,6 +324,253 @@ public void Throws_if_parameterless_constructor_is_not_defined_on_entity_type() Assert.Throws(() => modelBuilder.FinalizeModel()).Message); } + [ConditionalFact] + public void GetEmptyMaterializer_Create_instance_with_parameterless_constructor() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(Parameterless))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + var instance1 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + var instance2 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + + Assert.IsType(instance1); + Assert.IsType(instance2); + Assert.NotSame(instance1, instance2); + } + + [ConditionalFact] + public void GetEmptyMaterializer_Create_instance_with_lazy_loader() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(WithLazyLoader))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + var instance1 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + var instance2 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + + Assert.IsType(instance1); + Assert.NotNull(((WithLazyLoader)instance1).LazyLoader); + Assert.IsType(instance2); + Assert.NotSame(instance1, instance2); + Assert.NotSame(((WithLazyLoader)instance1).LazyLoader, ((WithLazyLoader)instance2).LazyLoader); + } + + [ConditionalFact] + public void GetEmptyMaterializer_Create_instance_with_lazy_loading_delegate() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(WithLazyLoaderDelegate))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + var instance1 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + var instance2 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + + Assert.IsType(instance1); + Assert.NotNull(((WithLazyLoaderDelegate)instance1).LazyLoader); + Assert.IsType(instance2); + Assert.NotSame(instance1, instance2); + Assert.NotSame(((WithLazyLoaderDelegate)instance1).LazyLoader, ((WithLazyLoaderDelegate)instance2).LazyLoader); + } + + [ConditionalFact] + public void GetEmptyMaterializer_Create_instance_with_entity_type() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(WithEntityType))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + var instance1 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + var instance2 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + + Assert.IsType(instance1); + Assert.NotNull(((WithEntityType)instance1).EntityType); + Assert.IsType(instance2); + Assert.NotSame(instance1, instance2); + Assert.Same(((WithEntityType)instance1).EntityType, ((WithEntityType)instance2).EntityType); + } + + [ConditionalFact] + public void GetEmptyMaterializer_Create_instance_with_context() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(WithContext))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + var instance1 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + var instance2 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + + Assert.IsType(instance1); + Assert.Same(context, ((WithContext)instance1).Context); + Assert.IsType(instance2); + Assert.NotSame(instance1, instance2); + Assert.Same(context, ((WithContext)instance2).Context); + } + + [ConditionalFact] + public void GetEmptyMaterializer_Create_instance_with_service_and_with_properties() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(WithServiceAndWithProperties))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + var instance1 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + var instance2 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + + Assert.IsType(instance1); + Assert.NotNull(((WithServiceAndWithProperties)instance1).LazyLoader); + Assert.IsType(instance2); + Assert.NotSame(instance1, instance2); + Assert.NotSame(((WithServiceAndWithProperties)instance1).LazyLoader, ((WithServiceAndWithProperties)instance2).LazyLoader); + } + + [ConditionalFact] + public void GetEmptyMaterializer_Create_instance_with_parameterless_and_with_properties() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(ParameterlessAndWithProperties))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + var instance1 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + var instance2 = source.GetEmptyMaterializer(entityType)(new MaterializationContext(ValueBuffer.Empty, context)); + + Assert.IsType(instance1); + Assert.IsType(instance2); + Assert.NotSame(instance1, instance2); + } + + [ConditionalFact] + public void GetEmptyMaterializer_Throws_for_constructor_with_properties() + { + using var context = new FactoryContext(); + + var entityType = context.Model.FindEntityType(typeof(WithProperties))!; + var source = new EntityMaterializerSource(new EntityMaterializerSourceDependencies(Array.Empty())); + + Assert.Equal( + CoreStrings.NoParameterlessConstructor(nameof(WithProperties)), + Assert.Throws( + () => source.GetEmptyMaterializer(entityType)).Message); + } + + private class FactoryContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInMemoryDatabase(nameof(FactoryContext)) + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider); + + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + private class Parameterless + { + private Parameterless() + { + } + + public int Id { get; set; } + } + + private class WithProperties + { + public WithProperties(int id) + { + Id = id; + } + + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local + public int Id { get; set; } + } + + private class ParameterlessAndWithProperties + { + public ParameterlessAndWithProperties() + { + } + + public ParameterlessAndWithProperties(int id) + { + Id = id; + } + + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local + public int Id { get; set; } + } + + private class WithLazyLoader + { + public WithLazyLoader(ILazyLoader lazyLoader) + { + LazyLoader = lazyLoader; + } + + public int Id { get; set; } + public ILazyLoader LazyLoader { get; } + } + + private class WithLazyLoaderDelegate + { + public WithLazyLoaderDelegate(Action lazyLoader) + { + LazyLoader = lazyLoader; + } + + public int Id { get; set; } + public Action LazyLoader { get; } + } + + private class WithEntityType + { + public WithEntityType(IEntityType entityType) + { + EntityType = entityType; + } + + public int Id { get; set; } + public IEntityType EntityType { get; } + } + + private class WithContext + { + public WithContext(DbContext context) + { + Context = context; + } + + public int Id { get; set; } + public DbContext Context { get; } + } + + private class WithServiceAndWithProperties + { + public WithServiceAndWithProperties(ILazyLoader lazyLoader) + { + LazyLoader = lazyLoader; + } + + public WithServiceAndWithProperties(ILazyLoader lazyLoader, int id) + : this(lazyLoader) + { + Id = id; + } + + public ILazyLoader LazyLoader { get; } + + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local + public int Id { get; set; } + } + protected virtual ModelBuilder CreateConventionalModelBuilder(bool sensitiveDataLoggingEnabled = false) => InMemoryTestHelpers.Instance.CreateConventionBuilder();