diff --git a/EFCore.sln.DotSettings b/EFCore.sln.DotSettings index 1b4974fd608..935bf61c6dd 100644 --- a/EFCore.sln.DotSettings +++ b/EFCore.sln.DotSettings @@ -302,6 +302,7 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True True True True diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 93cd941c2f5..dda85c3052b 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -63,6 +63,7 @@ public static readonly IDictionary RelationalServi { typeof(IQuerySqlGeneratorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IModificationCommandFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ISqlAliasManagerFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IRelationalLiftableConstantFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ICommandBatchPreparer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IModificationCommandBatchFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IRelationalSqlTranslatingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -189,6 +190,9 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(p => p.GetRequiredService()); + TryAdd(); + TryAdd(); ServiceCollectionMap.GetInfrastructure() .AddDependencySingleton() diff --git a/src/EFCore.Relational/Query/IRelationalLiftableConstantFactory.cs b/src/EFCore.Relational/Query/IRelationalLiftableConstantFactory.cs new file mode 100644 index 00000000000..7ba1f1112eb --- /dev/null +++ b/src/EFCore.Relational/Query/IRelationalLiftableConstantFactory.cs @@ -0,0 +1,12 @@ +// 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.Query; + +public interface IRelationalLiftableConstantFactory : ILiftableConstantFactory +{ + LiftableConstantExpression CreateLiftableConstant( + Expression> resolverExpression, + string variableName, + Type type); +} diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs index 2d9ef438560..af71be81d48 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs @@ -653,7 +653,8 @@ private ProjectionBindingExpression AddClientProjection(Expression expression, T return new ProjectionBindingExpression(_selectExpression, existingIndex, type); } - private static T GetParameterValue(QueryContext queryContext, string parameterName) + // Public because can get referenced by precompiled shaper code + public static T GetParameterValue(QueryContext queryContext, string parameterName) #pragma warning restore IDE0052 // Remove unread private members => (T)queryContext.ParameterValues[parameterName]!; diff --git a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs index 53a3c372294..6df78f98407 100644 --- a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs @@ -6,6 +6,28 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; +public static class SingleQueryingEnumerable +{ + public static SingleQueryingEnumerable Create( + RelationalQueryContext relationalQueryContext, + RelationalCommandCache relationalCommandCache, + IReadOnlyList? readerColumns, + Func shaper, + Type contextType, + bool standAloneStateManager, + bool detailedErrorsEnabled, + bool threadSafetyChecksEnabled) + => new( + relationalQueryContext, + relationalCommandCache, + readerColumns, + shaper, + contextType, + standAloneStateManager, + detailedErrorsEnabled, + threadSafetyChecksEnabled); +} + /// /// 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.Relational/Query/RelationalLiftableConstantFactory.cs b/src/EFCore.Relational/Query/RelationalLiftableConstantFactory.cs new file mode 100644 index 00000000000..016f55ce9f5 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalLiftableConstantFactory.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.Query; + +public class RelationalLiftableConstantFactory : LiftableConstantFactory, IRelationalLiftableConstantFactory +{ + public virtual LiftableConstantExpression CreateLiftableConstant( + Expression> resolverExpression, + string variableName, + Type type) + => new(resolverExpression, variableName, type); +} diff --git a/src/EFCore.Relational/Query/RelationalLiftableConstantProcessor.cs b/src/EFCore.Relational/Query/RelationalLiftableConstantProcessor.cs new file mode 100644 index 00000000000..06cb5c6db4c --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalLiftableConstantProcessor.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. + +using Microsoft.EntityFrameworkCore.Query.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +#pragma warning disable EF1001 // LiftableConstantProcessor is internal + +public class RelationalLiftableConstantProcessor : LiftableConstantProcessor +{ + private RelationalMaterializerLiftableConstantContext _relationalMaterializerLiftableConstantContext; + + public RelationalLiftableConstantProcessor( + ShapedQueryCompilingExpressionVisitorDependencies dependencies, + RelationalShapedQueryCompilingExpressionVisitorDependencies relationalDependencies) + : base(dependencies) + => _relationalMaterializerLiftableConstantContext = new(dependencies, relationalDependencies); + + protected override ConstantExpression InlineConstant(LiftableConstantExpression liftableConstant) + { + if (liftableConstant.ResolverExpression is Expression> + resolverExpression) + { + var resolver = resolverExpression.Compile(preferInterpretation: true); + var value = resolver(_relationalMaterializerLiftableConstantContext); + return Expression.Constant(value, liftableConstant.Type); + } + + return base.InlineConstant(liftableConstant); + } +} diff --git a/src/EFCore.Relational/Query/RelationalMaterializerLiftableConstantContext.cs b/src/EFCore.Relational/Query/RelationalMaterializerLiftableConstantContext.cs new file mode 100644 index 00000000000..0326f8e323e --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalMaterializerLiftableConstantContext.cs @@ -0,0 +1,9 @@ +// 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.Query; + +public record RelationalMaterializerLiftableConstantContext( + ShapedQueryCompilingExpressionVisitorDependencies Dependencies, + RelationalShapedQueryCompilingExpressionVisitorDependencies RelationalDependencies) + : MaterializerLiftableConstantContext(Dependencies); diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs index 33b01fd885a..08536bd1156 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs @@ -11,7 +11,14 @@ namespace Microsoft.EntityFrameworkCore.Query; public partial class RelationalShapedQueryCompilingExpressionVisitor { - private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisitor + /// + /// 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] + public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisitor { private static readonly MethodInfo ThrowReadValueExceptionMethod = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ThrowReadValueException))!; @@ -160,7 +167,14 @@ private static void IncludeReference } } - private static void InitializeIncludeCollection( + /// + /// 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] + public static void InitializeIncludeCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, @@ -201,7 +215,14 @@ private static void InitializeIncludeCollection( resultCoordinator.SetSingleQueryCollectionContext(collectionId, collectionMaterializationContext); } - private static void PopulateIncludeCollection( + /// + /// 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] + public static void PopulateIncludeCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, @@ -209,9 +230,12 @@ private static void PopulateIncludeCollection Func parentIdentifier, Func outerIdentifier, Func selfIdentifier, - IReadOnlyList parentIdentifierValueComparers, - IReadOnlyList outerIdentifierValueComparers, - IReadOnlyList selfIdentifierValueComparers, + IReadOnlyList> parentIdentifierValueComparers, + IReadOnlyList> outerIdentifierValueComparers, + IReadOnlyList> selfIdentifierValueComparers, + // IReadOnlyList parentIdentifierValueComparers, + // IReadOnlyList outerIdentifierValueComparers, + // IReadOnlyList selfIdentifierValueComparers, Func innerShaper, INavigationBase? inverseNavigation, Action fixup, @@ -229,14 +253,14 @@ private static void PopulateIncludeCollection return; } - if (!CompareIdentifiers( + if (!CompareIdentifiers2( outerIdentifierValueComparers, outerIdentifier(queryContext, dbDataReader), collectionMaterializationContext.OuterIdentifier)) { // Outer changed so collection has ended. Materialize last element. GenerateCurrentElementIfPending(); // If parent also changed then this row is now pointing to element of next collection - if (!CompareIdentifiers( + if (!CompareIdentifiers2( parentIdentifierValueComparers, parentIdentifier(queryContext, dbDataReader), collectionMaterializationContext.ParentIdentifier)) { @@ -255,7 +279,7 @@ private static void PopulateIncludeCollection if (collectionMaterializationContext.SelfIdentifier != null) { - if (CompareIdentifiers(selfIdentifierValueComparers, innerKey, collectionMaterializationContext.SelfIdentifier)) + if (CompareIdentifiers2(selfIdentifierValueComparers, innerKey, collectionMaterializationContext.SelfIdentifier)) { // repeated row for current element // If it is pending materialization then it may have nested elements @@ -319,7 +343,14 @@ void GenerateCurrentElementIfPending() } } - private static void InitializeSplitIncludeCollection( + /// + /// 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] + public static void InitializeSplitIncludeCollection( int collectionId, QueryContext queryContext, DbDataReader parentDataReader, @@ -358,7 +389,14 @@ private static void InitializeSplitIncludeCollection resultCoordinator.SetSplitQueryCollectionContext(collectionId, splitQueryCollectionContext); } - private static void PopulateSplitIncludeCollection( + /// + /// 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] + public static void PopulateSplitIncludeCollection( int collectionId, RelationalQueryContext queryContext, IExecutionStrategy executionStrategy, @@ -367,7 +405,8 @@ private static void PopulateSplitIncludeCollection childIdentifier, - IReadOnlyList identifierValueComparers, + IReadOnlyList> identifierValueComparers, + // IReadOnlyList identifierValueComparers, Func innerShaper, Action? relatedDataLoaders, INavigationBase? inverseNavigation, @@ -414,7 +453,7 @@ static RelationalDataReader InitializeReader( { while (dataReaderContext.HasNext ?? dbDataReader.Read()) { - if (!CompareIdentifiers( + if (!CompareIdentifiers2( identifierValueComparers, splitQueryCollectionContext.ParentIdentifier, childIdentifier(queryContext, dbDataReader))) { @@ -451,7 +490,8 @@ private static async Task PopulateSplitIncludeCollectionAsync childIdentifier, - IReadOnlyList identifierValueComparers, + IReadOnlyList> identifierValueComparers, + // IReadOnlyList identifierValueComparers, Func innerShaper, Func? relatedDataLoaders, INavigationBase? inverseNavigation, @@ -506,7 +546,7 @@ static async Task InitializeReaderAsync( { while (dataReaderContext.HasNext ?? await dbDataReader.ReadAsync(queryContext.CancellationToken).ConfigureAwait(false)) { - if (!CompareIdentifiers( + if (!CompareIdentifiers2( identifierValueComparers, splitQueryCollectionContext.ParentIdentifier, childIdentifier(queryContext, dbDataReader))) { @@ -538,7 +578,8 @@ static async Task InitializeReaderAsync( } } - private static TCollection InitializeCollection( + [EntityFrameworkInternal] + public static TCollection InitializeCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, @@ -560,7 +601,8 @@ private static TCollection InitializeCollection( return (TCollection)collection; } - private static void PopulateCollection( + [EntityFrameworkInternal] + public static void PopulateCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, @@ -1066,6 +1108,20 @@ private static async Task TaskAwaiter(Func[] taskFactories) } } + private static bool CompareIdentifiers2(IReadOnlyList> valueComparers, object[] left, object[] right) + { + // Ignoring size check on all for perf as they should be same unless bug in code. + for (var i = 0; i < left.Length; i++) + { + if (!valueComparers[i](left[i], right[i])) + { + return false; + } + } + + return true; + } + private static bool CompareIdentifiers(IReadOnlyList valueComparers, object[] left, object[] right) { // Ignoring size check on all for perf as they should be same unless bug in code. diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 01dbeb99eb6..5170e1a3cfa 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -14,7 +14,13 @@ namespace Microsoft.EntityFrameworkCore.Query; public partial class RelationalShapedQueryCompilingExpressionVisitor { - private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisitor + /// + /// 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 sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisitor { /// /// Reading database values @@ -77,6 +83,9 @@ private static readonly MethodInfo Utf8JsonReaderGetStringMethod private static readonly MethodInfo EnumParseMethodInfo = typeof(Enum).GetMethod(nameof(Enum.Parse), [typeof(Type), typeof(string)])!; + private static readonly MethodInfo ReadColumnCreateMethod + = typeof(ReaderColumn).GetMethod(nameof(ReaderColumn.Create))!; + private readonly RelationalShapedQueryCompilingExpressionVisitor _parentVisitor; private readonly ISet? _tags; private readonly bool _isTracking; @@ -249,8 +258,9 @@ private ShaperProcessingExpressionVisitor( public LambdaExpression ProcessRelationalGroupingResult( RelationalGroupByResultExpression relationalGroupByResultExpression, - out RelationalCommandCache relationalCommandCache, - out IReadOnlyList? readerColumns, + out Expression relationalCommandCache, + out Func readerColumns, + // out IReadOnlyList? readerColumns, out LambdaExpression keySelector, out LambdaExpression keyIdentifier, out LambdaExpression? relatedDataLoaders, @@ -279,8 +289,9 @@ public LambdaExpression ProcessRelationalGroupingResult( public LambdaExpression ProcessShaper( Expression shaperExpression, - out RelationalCommandCache? relationalCommandCache, - out IReadOnlyList? readerColumns, + out Expression relationalCommandCache, + out Func readerColumns, + // out IReadOnlyList? readerColumns, out LambdaExpression? relatedDataLoaders, ref int collectionId) { @@ -293,13 +304,8 @@ public LambdaExpression ProcessShaper( _expressions.Add(result); result = Block(_variables, _expressions); - relationalCommandCache = new RelationalCommandCache( - _parentVisitor.Dependencies.MemoryCache, - _parentVisitor.RelationalDependencies.QuerySqlGeneratorFactory, - _parentVisitor.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, - _selectExpression, - _parentVisitor._useRelationalNulls); - readerColumns = _readerColumns; + relationalCommandCache = _parentVisitor.CreateRelationalCommandCacheExpression(_selectExpression); + readerColumns = CreateReaderColumnsExpression(); return Lambda( result, @@ -320,14 +326,9 @@ public LambdaExpression ProcessShaper( result = Block(_variables, _expressions); relationalCommandCache = _generateCommandCache - ? new RelationalCommandCache( - _parentVisitor.Dependencies.MemoryCache, - _parentVisitor.RelationalDependencies.QuerySqlGeneratorFactory, - _parentVisitor.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, - _selectExpression, - _parentVisitor._useRelationalNulls) - : null; - readerColumns = _readerColumns; + ? _parentVisitor.CreateRelationalCommandCacheExpression(_selectExpression) + : Expression.Constant(null, typeof(RelationalCommandCache)); + readerColumns = CreateReaderColumnsExpression(); return Lambda( result, @@ -408,14 +409,9 @@ public LambdaExpression ProcessShaper( } relationalCommandCache = _generateCommandCache - ? new RelationalCommandCache( - _parentVisitor.Dependencies.MemoryCache, - _parentVisitor.RelationalDependencies.QuerySqlGeneratorFactory, - _parentVisitor.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, - _selectExpression, - _parentVisitor._useRelationalNulls) - : null; - readerColumns = _readerColumns; + ? _parentVisitor.CreateRelationalCommandCacheExpression(_selectExpression) + : Expression.Constant(null, typeof(RelationalCommandCache));; + readerColumns = CreateReaderColumnsExpression(); collectionId = _collectionId; @@ -452,8 +448,15 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) ? value : propertyMap.Values.Max() + 1; - var updatedExpression = newExpression.Update( - new[] { Constant(ValueBuffer.Empty), newExpression.Arguments[1] }); + var updatedExpression = newExpression.Update( + new[] + { + _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + _ => ValueBuffer.Empty, + "emptyValueBuffer", + typeof(ValueBuffer)), + newExpression.Arguments[1] + }); return Assign(binaryExpression.Left, updatedExpression); } @@ -597,7 +600,7 @@ protected override Expression VisitExtension(Expression extensionExpression) } else { - var entityParameter = Parameter(shaper.Type); + var entityParameter = Parameter(shaper.Type, "entity"); _variables.Add(entityParameter); if (shaper.StructuralType is IEntityType entityType && entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) @@ -718,7 +721,7 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) var projection = _selectExpression.Projection[projectionIndex]; var nullable = IsNullableProjection(projection); - var valueParameter = Parameter(projectionBindingExpression.Type); + var valueParameter = Parameter(projectionBindingExpression.Type, "value" + (_variables.Count + 1)); _variables.Add(valueParameter); _expressions.Add( @@ -769,13 +772,13 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) _readerColumns) .ProcessShaper(relationalCollectionShaperExpression.InnerShaper, out _, out _, out _, ref _collectionId); - var entityType = entity.Type; + var entityClrType = entity.Type; var navigation = includeExpression.Navigation; - var includingEntityType = navigation.DeclaringEntityType.ClrType; - if (includingEntityType != entityType - && includingEntityType.IsAssignableFrom(entityType)) + var includingEntityClrType = navigation.DeclaringEntityType.ClrType; + if (includingEntityClrType != entityClrType + && includingEntityClrType.IsAssignableFrom(entityClrType)) { - includingEntityType = entityType; + includingEntityClrType = entityClrType; } _inline = true; @@ -799,51 +802,68 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) _includeExpressions.Add( Call( - InitializeIncludeCollectionMethodInfo.MakeGenericMethod(entityType, includingEntityType), + InitializeIncludeCollectionMethodInfo.MakeGenericMethod(entityClrType, includingEntityClrType), collectionIdConstant, QueryCompilationContext.QueryContextParameter, _dataReaderParameter, _resultCoordinatorParameter, entity, - Constant(parentIdentifierLambda.Compile()), - Constant(outerIdentifierLambda.Compile()), - Constant(navigation), - Constant( - navigation.IsShadowProperty() - ? null - : navigation.GetCollectionAccessor(), typeof(IClrCollectionAccessor)), + parentIdentifierLambda, + outerIdentifierLambda, + // Constant(navigation), + _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(navigation.DeclaringEntityType.Name)!.FindNavigation(navigation.Name)!, + navigation.Name + "Navigation", + typeof(INavigationBase)), + // Constant(navigation.IsShadowProperty() + // ? null + // : navigation.GetCollectionAccessor(), typeof(IClrCollectionAccessor)), + navigation.IsShadowProperty() + ? Constant(null, typeof(IClrCollectionAccessor)) + : _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(navigation.DeclaringEntityType.Name)!.FindNavigation(navigation.Name)! + .GetCollectionAccessor()!, + navigation.Name + "NavigationCollectionAccessor", + typeof(IClrCollectionAccessor)), Constant(_isTracking), #pragma warning disable EF1001 // Internal EF Core API usage. Constant(includeExpression.SetLoaded))); #pragma warning restore EF1001 // Internal EF Core API usage. - var relatedEntityType = innerShaper.ReturnType; + var relatedEntityClrType = innerShaper.ReturnType; var inverseNavigation = navigation.Inverse; _collectionPopulatingExpressions!.Add( Call( - PopulateIncludeCollectionMethodInfo.MakeGenericMethod(includingEntityType, relatedEntityType), + PopulateIncludeCollectionMethodInfo.MakeGenericMethod(includingEntityClrType, relatedEntityClrType), collectionIdConstant, QueryCompilationContext.QueryContextParameter, _dataReaderParameter, _resultCoordinatorParameter, - Constant(parentIdentifierLambda.Compile()), - Constant(outerIdentifierLambda.Compile()), - Constant(selfIdentifierLambda.Compile()), - Constant( - relationalCollectionShaperExpression.ParentIdentifierValueComparers, - typeof(IReadOnlyList)), - Constant( - relationalCollectionShaperExpression.OuterIdentifierValueComparers, - typeof(IReadOnlyList)), - Constant( - relationalCollectionShaperExpression.SelfIdentifierValueComparers, - typeof(IReadOnlyList)), - Constant(innerShaper.Compile()), - Constant(inverseNavigation, typeof(INavigationBase)), - Constant( - GenerateFixup( - includingEntityType, relatedEntityType, navigation, inverseNavigation).Compile()), + parentIdentifierLambda, + outerIdentifierLambda, + selfIdentifierLambda, + NewArrayInit(typeof(Func), relationalCollectionShaperExpression.ParentIdentifierValueComparers.Select(vc => vc.ObjectEqualsExpression)), + NewArrayInit(typeof(Func), relationalCollectionShaperExpression.OuterIdentifierValueComparers.Select(vc => vc.ObjectEqualsExpression)), + NewArrayInit(typeof(Func), relationalCollectionShaperExpression.SelfIdentifierValueComparers.Select(vc => vc.ObjectEqualsExpression)), + // Expression.Constant( + // relationalCollectionShaperExpression.ParentIdentifierValueComparers, + // typeof(IReadOnlyList)), + // Expression.Constant( + // relationalCollectionShaperExpression.OuterIdentifierValueComparers, + // typeof(IReadOnlyList)), + // Expression.Constant( + // relationalCollectionShaperExpression.SelfIdentifierValueComparers, + // typeof(IReadOnlyList)), + innerShaper, + // Expression.Constant(inverseNavigation, typeof(INavigationBase)), + inverseNavigation is null + ? Constant(null, typeof(INavigationBase)) + : _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(inverseNavigation.DeclaringEntityType.Name)!.FindNavigation(navigation.Name)!, + navigation.Name + "InverseNavigation", + typeof(INavigationBase)), + GenerateFixup(includingEntityClrType, relatedEntityClrType, navigation, inverseNavigation), Constant(_isTracking))); } else if (includeExpression.NavigationExpression is RelationalSplitCollectionShaperExpression @@ -862,11 +882,11 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) var entityType = entity.Type; var navigation = includeExpression.Navigation; - var includingEntityType = navigation.DeclaringEntityType.ClrType; - if (includingEntityType != entityType - && includingEntityType.IsAssignableFrom(entityType)) + var includingEntityClrType = navigation.DeclaringEntityType.ClrType; + if (includingEntityClrType != entityType + && includingEntityClrType.IsAssignableFrom(entityType)) { - includingEntityType = entityType; + includingEntityClrType = entityType; } _inline = true; @@ -889,48 +909,66 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) _includeExpressions.Add( Call( - InitializeSplitIncludeCollectionMethodInfo.MakeGenericMethod(entityType, includingEntityType), + InitializeSplitIncludeCollectionMethodInfo.MakeGenericMethod(entityType, includingEntityClrType), collectionIdConstant, QueryCompilationContext.QueryContextParameter, _dataReaderParameter, _resultCoordinatorParameter, entity, - Constant(parentIdentifierLambda.Compile()), - Constant(navigation), - Constant(navigation.GetCollectionAccessor()), + parentIdentifierLambda, + // Expression.Constant(navigation), + _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(navigation.DeclaringEntityType.Name)!.FindNavigation(navigation.Name)!, + navigation.Name + "Navigation", + typeof(INavigationBase)), + // Expression.Constant(navigation.GetCollectionAccessor()), + _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(navigation.DeclaringEntityType.Name)!.FindNavigation(navigation.Name)! + .GetCollectionAccessor()!, + navigation.Name + "NavigationCollectionAccessor", + typeof(IClrCollectionAccessor)), Constant(_isTracking), #pragma warning disable EF1001 // Internal EF Core API usage. Constant(includeExpression.SetLoaded))); #pragma warning restore EF1001 // Internal EF Core API usage. - var relatedEntityType = innerShaper.ReturnType; + var relatedEntityClrType = innerShaper.ReturnType; var inverseNavigation = navigation.Inverse; _collectionPopulatingExpressions!.Add( Call( (_isAsync ? PopulateSplitIncludeCollectionAsyncMethodInfo : PopulateSplitIncludeCollectionMethodInfo) - .MakeGenericMethod(includingEntityType, relatedEntityType), + .MakeGenericMethod(includingEntityClrType, relatedEntityClrType), collectionIdConstant, Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), _executionStrategyParameter!, - Constant(relationalCommandCache), - Constant(readerColumns, typeof(IReadOnlyList)), + relationalCommandCache, + readerColumns(), Constant(_detailedErrorsEnabled), _resultCoordinatorParameter, - Constant(childIdentifierLambda.Compile()), - Constant( - relationalSplitCollectionShaperExpression.IdentifierValueComparers, - typeof(IReadOnlyList)), - Constant(innerShaper.Compile()), - Constant( - relatedDataLoaders?.Compile(), + childIdentifierLambda, + // Constant( + // relationalSplitCollectionShaperExpression.IdentifierValueComparers, + // typeof(IReadOnlyList)), + NewArrayInit(typeof(Func), relationalSplitCollectionShaperExpression.IdentifierValueComparers.Select(vc => vc.ObjectEqualsExpression)), + innerShaper, + // Constant( + // relatedDataLoaders?.Compile(), + // _isAsync + // ? typeof(Func) + // : typeof(Action)), + relatedDataLoaders ?? (Expression)Expression.Constant(null, _isAsync ? typeof(Func) : typeof(Action)), - Constant(inverseNavigation, typeof(INavigationBase)), - Constant( - GenerateFixup( - includingEntityType, relatedEntityType, navigation, inverseNavigation).Compile()), + // Constant(inverseNavigation, typeof(INavigationBase)), + inverseNavigation is null + ? Constant(null, typeof(INavigationBase)) + : _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(inverseNavigation.DeclaringEntityType.Name)!.FindNavigation(navigation.Name)!, + navigation.Name + "InverseNavigation", + typeof(INavigationBase)), + GenerateFixup(includingEntityClrType, relatedEntityClrType, navigation, inverseNavigation), Constant(_isTracking))); } else @@ -1170,6 +1208,9 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) case GroupByShaperExpression: throw new InvalidOperationException(RelationalStrings.ClientGroupByNotSupported); + + case LiftableConstantExpression: + return extensionExpression; } return base.VisitExtension(extensionExpression); @@ -2326,7 +2367,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } - private static LambdaExpression GenerateFixup( + private LambdaExpression GenerateFixup( Type entityType, Type relatedEntityType, INavigationBase navigation, @@ -2422,12 +2463,22 @@ private static Expression GetOrCreateCollectionObjectLambda( prm); } - private static Expression AddToCollectionNavigation( + private Expression AddToCollectionNavigation( ParameterExpression entity, ParameterExpression relatedEntity, INavigationBase navigation) => Call( - Constant(navigation.GetCollectionAccessor()), + // Constant(navigation.GetCollectionAccessor()), + + // // TODO: Very temporary hack. This is bad for perf. + // Call(Expression.Constant(navigation), typeof(INavigationBase).GetMethod(nameof(INavigationBase.GetCollectionAccessor), Array.Empty())!), + + _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(navigation.DeclaringEntityType.Name)!.FindNavigation(navigation.Name)! + .GetCollectionAccessor()!, + navigation.Name + "NavigationCollectionAccessor", + typeof(IClrCollectionAccessor)), + CollectionAccessorAddMethodInfo, entity, relatedEntity, @@ -2495,7 +2546,7 @@ Expression valueExpression Lambda( bufferedReaderLambdaExpression, dbDataReader, - _indexMapParameter ?? Parameter(typeof(int[]))).Compile()); + _indexMapParameter ?? Parameter(typeof(int[]), "indexMap"))); } valueExpression = Call( @@ -2629,6 +2680,51 @@ private Expression CreateReadJsonPropertyValueExpression( return resultExpression; } + // TODO: No, this must be a lifted constant, otherwise we re-instantiate on each query + private Func CreateReaderColumnsExpression() + => () => + { + if (_readerColumns is null) + { + return Expression.Constant(null, typeof(ReaderColumn?[])); + } + + var materializerLiftableConstantContextParameter = Expression.Parameter(typeof(MaterializerLiftableConstantContext)); + + return _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + Expression.Lambda>( + Expression.NewArrayInit( + typeof(ReaderColumn), + _readerColumns.Select(rc => + rc is null + ? (Expression)Expression.Constant(null, typeof(ReaderColumn)) + : Expression.New( + ReaderColumn.GetConstructor(rc.Type), + Expression.Constant(rc.IsNullable), + Expression.Constant(rc.Name, typeof(string)), + GetPropertyExpression(rc), + rc.GetFieldValueExpression))), + materializerLiftableConstantContextParameter), + "readerColumns", + typeof(ReaderColumn[])); + + Expression GetPropertyExpression(ReaderColumn readerColumn) + { + if (readerColumn.Property is null) + { + return Expression.Constant(null, typeof(IProperty)); + } + + Expression> wrapper = c + => c.Dependencies.Model + .FindEntityType(readerColumn.Property.DeclaringType.Name)! + .FindProperty(readerColumn.Property.Name)!; + + return ReplacingExpressionVisitor.Replace( + wrapper.Parameters[0], materializerLiftableConstantContextParameter, wrapper.Body); + } + }; + private sealed class CollectionShaperFindingExpressionVisitor : ExpressionVisitor { private bool _containsCollection; diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index ede5805ad30..f22bd84ad3a 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -70,17 +70,19 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression break; } - var relationalCommandCache = new RelationalCommandCache( - Dependencies.MemoryCache, - RelationalDependencies.QuerySqlGeneratorFactory, - RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, - innerExpression, - _useRelationalNulls); + // var relationalCommandCache = new RelationalCommandCache( + // Dependencies.MemoryCache, + // RelationalDependencies.QuerySqlGeneratorFactory, + // RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, + // innerExpression, + // _useRelationalNulls); + + var relationalCommandCache = CreateRelationalCommandCacheExpression(innerExpression); return Call( QueryCompilationContext.IsAsync ? NonQueryAsyncMethodInfo : NonQueryMethodInfo, Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - Constant(relationalCommandCache), + relationalCommandCache, Constant(_contextType), Constant(nonQueryExpression.CommandSource), Constant(_threadSafetyChecksEnabled)); @@ -96,7 +98,14 @@ private static readonly MethodInfo NonQueryAsyncMethodInfo .GetDeclaredMethods(nameof(NonQueryResultAsync)) .Single(mi => mi.GetParameters().Length == 5); - private static int NonQueryResult( + /// + /// 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] + public static int NonQueryResult( RelationalQueryContext relationalQueryContext, RelationalCommandCache relationalCommandCache, Type contextType, @@ -167,7 +176,14 @@ private static int NonQueryResult( } } - private static Task NonQueryResultAsync( + /// + /// 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] + public static Task NonQueryResultAsync( RelationalQueryContext relationalQueryContext, RelationalCommandCache relationalCommandCache, Type contextType, @@ -318,9 +334,10 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery else { var nonComposedFromSql = selectExpression.IsNonComposedFromSql(); - var shaper = new ShaperProcessingExpressionVisitor(this, selectExpression, _tags, splitQuery, nonComposedFromSql).ProcessShaper( - shapedQueryExpression.ShaperExpression, - out var relationalCommandCache, out var readerColumns, out var relatedDataLoaders, ref collectionCount); + var shaper = new ShaperProcessingExpressionVisitor(this, selectExpression, _tags, splitQuery, nonComposedFromSql) + .ProcessShaper( + shapedQueryExpression.ShaperExpression, out var relationalCommandCache, out var readerColumns, + out var relatedDataLoaders, ref collectionCount); if (querySplittingBehavior == null && collectionCount > 1) @@ -334,7 +351,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery typeof(FromSqlQueryingEnumerable<>).MakeGenericType(shaper.ReturnType).GetConstructors()[0], Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), Constant(relationalCommandCache), - Constant(readerColumns, typeof(IReadOnlyList)), + readerColumns(), Constant( selectExpression.Projection.Select(pe => ((ColumnExpression)pe.Expression).Name).ToList(), typeof(IReadOnlyList)), @@ -348,20 +365,30 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery if (splitQuery) { - var relatedDataLoadersParameter = Constant( - QueryCompilationContext.IsAsync ? null : relatedDataLoaders?.Compile(), - typeof(Action)); + var relatedDataLoadersParameter = + QueryCompilationContext.IsAsync || relatedDataLoaders is null + ? Expression.Constant(null, typeof(Action)) + : (Expression)relatedDataLoaders; - var relatedDataLoadersAsyncParameter = Constant( - QueryCompilationContext.IsAsync ? relatedDataLoaders?.Compile() : null, - typeof(Func)); + // var relatedDataLoadersParameter = Constant( + // QueryCompilationContext.IsAsync ? null : relatedDataLoaders?.Compile(), + // typeof(Action)); + + var relatedDataLoadersAsyncParameter = + QueryCompilationContext.IsAsync && relatedDataLoaders is not null + ? (Expression)relatedDataLoaders + : Expression.Constant(null, typeof(Func)); + + // var relatedDataLoadersAsyncParameter = Constant( + // QueryCompilationContext.IsAsync ? relatedDataLoaders?.Compile() : null, + // typeof(Func)); return New( typeof(SplitQueryingEnumerable<>).MakeGenericType(shaper.ReturnType).GetConstructors().Single(), Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - Constant(relationalCommandCache), - Constant(readerColumns, typeof(IReadOnlyList)), - Constant(shaper.Compile()), + relationalCommandCache, + readerColumns(), + shaper, relatedDataLoadersParameter, relatedDataLoadersAsyncParameter, Constant(_contextType), @@ -371,12 +398,15 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Constant(_threadSafetyChecksEnabled)); } - return New( - typeof(SingleQueryingEnumerable<>).MakeGenericType(shaper.ReturnType).GetConstructors()[0], + // TODO: Do the same for the other QueryingEnumerables + return Call( + typeof(SingleQueryingEnumerable).GetMethods() + .Single(m => m.Name == nameof(SingleQueryingEnumerable.Create)) + .MakeGenericMethod(shaper.ReturnType), Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - Constant(relationalCommandCache), - Constant(readerColumns, typeof(IReadOnlyList)), - Constant(shaper.Compile()), + relationalCommandCache, + readerColumns(), + shaper, Constant(_contextType), Constant( QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), @@ -384,4 +414,15 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Constant(_threadSafetyChecksEnabled)); } } + + private LiftableConstantExpression CreateRelationalCommandCacheExpression(Expression queryExpression) + => RelationalDependencies.RelationalLiftableConstantFactory.CreateLiftableConstant( + c => new RelationalCommandCache( + c.Dependencies.MemoryCache, + c.RelationalDependencies.QuerySqlGeneratorFactory, + c.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, + queryExpression, + _useRelationalNulls), + "relationalCommandCache", + typeof(RelationalCommandCache)); } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs index c32ba6d83b5..bca7ecafc93 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs @@ -47,10 +47,12 @@ public sealed record RelationalShapedQueryCompilingExpressionVisitorDependencies [EntityFrameworkInternal] public RelationalShapedQueryCompilingExpressionVisitorDependencies( IQuerySqlGeneratorFactory querySqlGeneratorFactory, - IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory) + IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory, + IRelationalLiftableConstantFactory relationalLiftableConstantFactory) { QuerySqlGeneratorFactory = querySqlGeneratorFactory; RelationalParameterBasedSqlProcessorFactory = relationalParameterBasedSqlProcessorFactory; + RelationalLiftableConstantFactory = relationalLiftableConstantFactory; } /// @@ -62,4 +64,9 @@ public RelationalShapedQueryCompilingExpressionVisitorDependencies( /// The SQL processor based on parameter values. /// public IRelationalParameterBasedSqlProcessorFactory RelationalParameterBasedSqlProcessorFactory { get; init; } + + /// + /// The liftable constant factory. + /// + public IRelationalLiftableConstantFactory RelationalLiftableConstantFactory { get; init; } } diff --git a/src/EFCore.Relational/Storage/ReaderColumn.cs b/src/EFCore.Relational/Storage/ReaderColumn.cs index e6aa7627825..138aa7cc8d4 100644 --- a/src/EFCore.Relational/Storage/ReaderColumn.cs +++ b/src/EFCore.Relational/Storage/ReaderColumn.cs @@ -29,12 +29,14 @@ public abstract class ReaderColumn /// A value indicating if the column is nullable. /// The name of the column. /// The property being read if any, null otherwise. - protected ReaderColumn(Type type, bool nullable, string? name, IPropertyBase? property) + /// A lambda expression to get field value for the column from the reader. + protected ReaderColumn(Type type, bool nullable, string? name, IPropertyBase? property, LambdaExpression getFieldValueExpression) { Type = type; IsNullable = nullable; Name = name; Property = property; + GetFieldValueExpression = getFieldValueExpression; } /// @@ -57,6 +59,11 @@ protected ReaderColumn(Type type, bool nullable, string? name, IPropertyBase? pr /// public virtual IPropertyBase? Property { get; } + /// + /// A lambda expression to get field value for the column from the reader. + /// + public virtual LambdaExpression GetFieldValueExpression { get; } + /// /// Creates an instance of . /// @@ -73,10 +80,17 @@ public static ReaderColumn Create( bool nullable, string? columnName, IPropertyBase? property, - object readFunc) + LambdaExpression readFunc) => (ReaderColumn)GetConstructor(type).Invoke([nullable, columnName, property, readFunc]); - private static ConstructorInfo GetConstructor(Type type) + /// + /// 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] + public static ConstructorInfo GetConstructor(Type type) => Constructors.GetOrAdd( type, t => typeof(ReaderColumn<>).MakeGenericType(t).GetConstructors().First(ci => ci.GetParameters().Length == 4)); } diff --git a/src/EFCore.Relational/Storage/ReaderColumn`.cs b/src/EFCore.Relational/Storage/ReaderColumn`.cs index 9de7877e94f..b3ad9531ac2 100644 --- a/src/EFCore.Relational/Storage/ReaderColumn`.cs +++ b/src/EFCore.Relational/Storage/ReaderColumn`.cs @@ -24,15 +24,15 @@ public class ReaderColumn : ReaderColumn /// A value indicating if the column is nullable. /// The name of the column. /// The property being read if any, null otherwise. - /// A function to get field value for the column from the reader. + /// A lambda expression to get field value for the column from the reader. public ReaderColumn( bool nullable, string? name, IPropertyBase? property, - Func getFieldValue) - : base(typeof(T), nullable, name, property) + Expression> getFieldValueExpression) + : base(typeof(T), nullable, name, property, getFieldValueExpression) { - GetFieldValue = getFieldValue; + GetFieldValue = getFieldValueExpression.Compile(); } /// diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index 345a3910bf5..d1df1053f29 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -353,7 +353,14 @@ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith } } - private static string? ConstructLikePatternParameter( + /// + /// 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] // Can be referenced in shaper code + public static string? ConstructLikePatternParameter( QueryContext queryContext, string baseParameterName, StartsEndsWithContains methodType) @@ -376,10 +383,36 @@ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith _ => throw new UnreachableException() }; - private enum StartsEndsWithContains + /// + /// 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] // Can be referenced in shaper code + public enum StartsEndsWithContains { + /// + /// 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. + /// StartsWith, + + /// + /// 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. + /// EndsWith, + + /// + /// 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. + /// Contains } diff --git a/src/EFCore/ChangeTracking/ValueComparer.cs b/src/EFCore/ChangeTracking/ValueComparer.cs index 2b5e80fd71f..b552272a9d0 100644 --- a/src/EFCore/ChangeTracking/ValueComparer.cs +++ b/src/EFCore/ChangeTracking/ValueComparer.cs @@ -155,6 +155,8 @@ protected ValueComparer( /// public virtual LambdaExpression EqualsExpression { get; } + public abstract LambdaExpression ObjectEqualsExpression { get; } + /// /// The hash code expression. /// diff --git a/src/EFCore/ChangeTracking/ValueComparer`.cs b/src/EFCore/ChangeTracking/ValueComparer`.cs index 84f7632f435..ffed7947783 100644 --- a/src/EFCore/ChangeTracking/ValueComparer`.cs +++ b/src/EFCore/ChangeTracking/ValueComparer`.cs @@ -248,6 +248,29 @@ public override bool Equals(object? left, object? right) return v1Null || v2Null ? v1Null && v2Null : Equals((T?)left, (T?)right); } + public override LambdaExpression ObjectEqualsExpression + { + get + { + // TODO: Cache this + var left = Expression.Parameter(typeof(object), "left"); + var right = Expression.Parameter(typeof(object), "right"); + + return Expression.Lambda>( + Expression.Condition( + Expression.Equal(left, Expression.Constant(null)), + Expression.Equal(right, Expression.Constant(null)), + Expression.AndAlso( + Expression.NotEqual(right, Expression.Constant(null)), + Expression.Invoke( + EqualsExpression, + Expression.Convert(left, typeof(T)), + Expression.Convert(right, typeof(T))))), + left, + right); + } + } + /// /// Returns the hash code for the given instance. /// diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index 60b90236db4..f09a774b481 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -83,6 +83,7 @@ public static readonly IDictionary CoreServices { typeof(IMemoryCache), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IEvaluatableExpressionFilter), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(INavigationExpansionExtensibilityHelper), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(ILiftableConstantFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IExceptionDetector), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IJsonValueReaderWriterSource), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IProviderConventionSetBuilder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -125,6 +126,7 @@ public static readonly IDictionary CoreServices { typeof(IShapedQueryCompilingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IDbContextLogger), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IAdHocMapper), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(ILiftableConstantProcessor), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(ILazyLoader), new ServiceCharacteristics(ServiceLifetime.Transient) }, { typeof(ILazyLoaderFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IParameterBindingFactory), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, @@ -309,6 +311,8 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); + TryAdd(); TryAdd( p => p.GetService()?.FindExtension()?.DbContextLogger @@ -329,7 +333,6 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencySingleton() .AddDependencySingleton() .AddDependencySingleton() - .AddDependencySingleton() .AddDependencySingleton() .AddDependencySingleton() .AddDependencySingleton() @@ -344,6 +347,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() + .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() diff --git a/src/EFCore/Query/ILiftableConstantFactory.cs b/src/EFCore/Query/ILiftableConstantFactory.cs new file mode 100644 index 00000000000..e1bd018015a --- /dev/null +++ b/src/EFCore/Query/ILiftableConstantFactory.cs @@ -0,0 +1,12 @@ +// 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.Query; + +public interface ILiftableConstantFactory +{ + LiftableConstantExpression CreateLiftableConstant( + Expression> resolverExpression, + string variableName, + Type type); +} diff --git a/src/EFCore/Query/ILiftableConstantProcessor.cs b/src/EFCore/Query/ILiftableConstantProcessor.cs new file mode 100644 index 00000000000..606d5eea554 --- /dev/null +++ b/src/EFCore/Query/ILiftableConstantProcessor.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.Query; + +public interface ILiftableConstantProcessor +{ + /// + /// Exposes all constants that have been lifted during the last invocation of . + /// + IReadOnlyList<(ParameterExpression Parameter, Expression Expression)> LiftedConstants { get; } + + /// + /// Inlines all liftable constants as simple nodes in the tree, containing the result of + /// evaluating the liftable constants' resolvers. + /// + /// An expression containing nodes. + /// + /// An expression tree containing nodes instead of nodes. + /// + /// + /// Liftable constant inlining is performed in the regular, non-precompiled query pipeline flow. + /// + Expression InlineConstants(Expression expression); + + /// + /// Lifts all nodes, embedding in their place and + /// exposing the parameter and resolver via . + /// + /// An expression containing nodes. + /// + /// The to be embedded in the liftable constant nodes' resolvers, instead of their lambda + /// parameter. + /// + /// + /// A set of variables already in use, for uniquification. Any generates variables will be added to this set. + /// + /// + /// An expression tree containing nodes instead of nodes. + /// + /// + /// Constant lifting is performed in the precompiled query pipeline flow. + /// + Expression LiftConstants(Expression expression, ParameterExpression contextParameter, HashSet variableNames); +} diff --git a/src/EFCore/Query/Internal/EntityMaterializerSource.cs b/src/EFCore/Query/Internal/EntityMaterializerSource.cs index a4046b3b6e8..2c5b1f6139c 100644 --- a/src/EFCore/Query/Internal/EntityMaterializerSource.cs +++ b/src/EFCore/Query/Internal/EntityMaterializerSource.cs @@ -295,7 +295,8 @@ private Expression CreateMaterializeExpression( if (bindingInfo.StructuralType is IEntityType) { - AddAttachServiceExpressions(bindingInfo, instanceVariable, blockExpressions); + // TODO: bindingInfo is used as a constant, needs to be liftable + //AddAttachServiceExpressions(bindingInfo, instanceVariable, blockExpressions); } blockExpressions.Add(instanceVariable); diff --git a/src/EFCore/Query/Internal/ShaperPublicMethodVerifier.cs b/src/EFCore/Query/Internal/ShaperPublicMethodVerifier.cs new file mode 100644 index 00000000000..0eb364a74a0 --- /dev/null +++ b/src/EFCore/Query/Internal/ShaperPublicMethodVerifier.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace Microsoft.EntityFrameworkCore.Query.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 sealed class ShaperPublicMethodVerifier : ExpressionVisitor +{ + /// + /// 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. + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCall) + { + var method = methodCall.Method; + if (!method.IsPublic) + { + throw new InvalidOperationException($"Method '{method.DeclaringType?.Name}.{method.Name}' isn't public and therefore cannot be invoked in shaper code (incompatible with precompiled queries)"); + } + + var currentType = method.DeclaringType; + while (currentType is not null) + { + if (currentType is { IsPublic: false, IsNestedPublic: false } + // Exclude anonymous types + && currentType.GetCustomAttribute(typeof(CompilerGeneratedAttribute), inherit: false) is null) + { + throw new InvalidOperationException($"Method '{method.DeclaringType?.Name}.{method.Name}' isn't public and therefore cannot be invoked in shaper code (incompatible with precompiled queries)"); + } + + currentType = currentType.DeclaringType; + } + + return base.VisitMethodCall(methodCall); + } + + /// + /// 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. + /// + protected override Expression VisitNew(NewExpression newExpression) + { + var constructor = newExpression.Constructor; + if (constructor is not null) + { + if (!constructor.IsPublic) + { + throw new InvalidOperationException($"The constructor for '{constructor.DeclaringType?.Name}' isn't public and therefore cannot be invoked in shaper code (incompatible with precompiled queries)"); + } + + var currentType = constructor.DeclaringType; + while (currentType is not null) + { + if (currentType is { IsPublic: false, IsNestedPublic: false } + // Exclude anonymous types + && currentType.GetCustomAttribute(typeof(CompilerGeneratedAttribute), inherit: false) is null) + { + throw new InvalidOperationException($"Constructor for '{constructor.DeclaringType?.Name}' isn't public and therefore cannot be invoked in shaper code (incompatible with precompiled queries)"); + } + + currentType = currentType.DeclaringType; + } + } + + return base.VisitNew(newExpression); + } + + // Ignore liftable constant nodes - they contain literals only (not method/constructor invocations) and cause exceptions. + protected override Expression VisitExtension(Expression node) + => node is LiftableConstantExpression ? node : base.VisitExtension(node); +} diff --git a/src/EFCore/Query/LiftableConstantExpression.cs b/src/EFCore/Query/LiftableConstantExpression.cs new file mode 100644 index 00000000000..eb3b6ec14ce --- /dev/null +++ b/src/EFCore/Query/LiftableConstantExpression.cs @@ -0,0 +1,46 @@ +// 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.Query; + +/// +/// A node containing an expression expressing how to obtain a constant value, which may get lifted out of an expression tree. +/// +/// +/// +/// When the expression tree is compiled, the constant value can simply be evaluated beforehand, and a +/// expression can directly reference the result. +/// +/// +/// When the expression tree is translated to source code instead (in query pre-compilation), the expression can be rendered out +/// separately, to be assigned to a variable, and this node is replaced by a reference to that variable. +/// +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class LiftableConstantExpression : Expression, IPrintableExpression +{ + public LiftableConstantExpression( + LambdaExpression resolverExpression, + string variableName, + Type type) + { + ResolverExpression = resolverExpression; + VariableName = char.ToLower(variableName[0]) + variableName[1..]; + Type = type; + } + + public LambdaExpression ResolverExpression { get; } + + public string VariableName { get; } + + public override Type Type { get; } + + public override ExpressionType NodeType + => ExpressionType.Extension; + + // TODO: Complete other expression stuff (equality, etc.) + + /// + public void Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Append($"[Constant: {expressionPrinter.PrintExpression(ResolverExpression)}]"); +} diff --git a/src/EFCore/Query/LiftableConstantFactory.cs b/src/EFCore/Query/LiftableConstantFactory.cs new file mode 100644 index 00000000000..ed645d63384 --- /dev/null +++ b/src/EFCore/Query/LiftableConstantFactory.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.Query; + +public class LiftableConstantFactory : ILiftableConstantFactory +{ + public virtual LiftableConstantExpression CreateLiftableConstant( + Expression> resolverExpression, + string variableName, + Type type) + => new(resolverExpression, variableName, type); +} diff --git a/src/EFCore/Query/LiftableConstantProcessor.cs b/src/EFCore/Query/LiftableConstantProcessor.cs new file mode 100644 index 00000000000..8afa64b6879 --- /dev/null +++ b/src/EFCore/Query/LiftableConstantProcessor.cs @@ -0,0 +1,458 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Microsoft.EntityFrameworkCore.Query.Internal; + +#pragma warning disable CS1591 + +public class LiftableConstantProcessor : ExpressionVisitor, ILiftableConstantProcessor +{ + private bool _inline; + private MaterializerLiftableConstantContext _materializerLiftableConstantContext; + + /// + /// Exposes all constants that have been lifted during the last invocation of . + /// + /// + /// 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 IReadOnlyList<(ParameterExpression Parameter, Expression Expression)> LiftedConstants { get; private set; } + = Array.Empty<(ParameterExpression Parameter, Expression Expression)>(); + + private record LiftedConstant(ParameterExpression Parameter, Expression Expression, ParameterExpression? ReplacingParameter = null); + + private readonly List _liftedConstants = new(); + private readonly LiftedExpressionProcessor _liftedExpressionProcessor = new(); + private readonly LiftedConstantOptimizer _liftedConstantOptimizer = new(); + + private ParameterExpression? _contextParameter; + + public LiftableConstantProcessor(ShapedQueryCompilingExpressionVisitorDependencies dependencies) + { + _materializerLiftableConstantContext = new(dependencies); + + _liftedConstants.Clear(); + } + + /// + /// Inlines all liftable constants as simple nodes in the tree, containing the result of + /// evaluating the liftable constants' resolvers. + /// + /// An expression containing nodes. + /// + /// An expression tree containing nodes instead of nodes. + /// + /// + /// + /// Liftable constant inlining is performed in the regular, non-precompiled query pipeline flow. + /// + /// + /// 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 Expression InlineConstants(Expression expression) + { + _liftedConstants.Clear(); + _inline = true; + + return Visit(expression); + } + + /// + /// Lifts all nodes, embedding in their place and + /// exposing the parameter and resolver via . + /// + /// An expression containing nodes. + /// + /// The to be embedded in the lifted constant nodes' resolvers, instead of their lambda + /// parameter. + /// + /// + /// A set of variables already in use, for uniquification. Any generates variables will be added to this set. + /// + /// + /// An expression tree containing nodes instead of nodes. + /// + public virtual Expression LiftConstants(Expression expression, ParameterExpression contextParameter, HashSet variableNames) + { + _liftedConstants.Clear(); + + _inline = false; + _contextParameter = contextParameter; + + var expressionAfterLifting = Visit(expression); + + // All liftable constant nodes have been lifted out. + // We'll now optimize them, looking for greatest common denominator tree fragments, in cases where e.g. two lifted constants look up + // the same entity type. + _liftedConstantOptimizer.Optimize(_liftedConstants); + + // Uniquify all variable names, taking into account possible remapping done in the optimization phase above + var replacedParameters = new Dictionary(); + // var (originalParameters, newParameters) = (new List(), new List()); + for (var i = 0; i < _liftedConstants.Count; i++) + { + var liftedConstant = _liftedConstants[i]; + + if (liftedConstant.ReplacingParameter is not null) + { + // This lifted constant is being removed, since it's a duplicate of another with the same expression. + // We still need to remap the parameter in the expression, but no uniquification etc. + replacedParameters.Add(liftedConstant.Parameter, + replacedParameters.TryGetValue(liftedConstant.ReplacingParameter, out var replacedReplacingParameter) + ? replacedReplacingParameter + : liftedConstant.ReplacingParameter); + _liftedConstants.RemoveAt(i--); + continue; + } + + var name = liftedConstant.Parameter.Name ?? "unknown"; + var baseName = name; + for (var j = 0; variableNames.Contains(name); j++) + { + name = baseName + j; + } + + variableNames.Add(name); + + if (name != liftedConstant.Parameter.Name) + { + var newParameter = Expression.Parameter(liftedConstant.Parameter.Type, name); + _liftedConstants[i] = liftedConstant with { Parameter = newParameter }; + replacedParameters.Add(liftedConstant.Parameter, newParameter); + } + } + + // Finally, apply all remapping (optimization, uniquification) to both the expression tree and to the lifted constant variable + // themselves. + + // var (originalParametersArray, newParametersArray) = (originalParameters.ToArray(), newParameters.ToArray()); + // var remappedExpression = ReplacingExpressionVisitor.Replace(originalParametersArray, newParametersArray, expressionAfterLifting); + var originalParameters = new Expression[replacedParameters.Count]; + var newParameters = new Expression[replacedParameters.Count]; + var index = 0; + foreach (var (originalParameter, newParameter) in replacedParameters) + { + originalParameters[index] = originalParameter; + newParameters[index] = newParameter; + index++; + } + var remappedExpression = ReplacingExpressionVisitor.Replace(originalParameters, newParameters, expressionAfterLifting); + + for (var i = 0; i < _liftedConstants.Count; i++) + { + var liftedConstant = _liftedConstants[i]; + var remappedLiftedConstantExpression = + ReplacingExpressionVisitor.Replace(originalParameters, newParameters, liftedConstant.Expression); + + if (remappedLiftedConstantExpression != liftedConstant.Expression) + { + _liftedConstants[i] = liftedConstant with { Expression = remappedLiftedConstantExpression }; + } + } + + LiftedConstants = _liftedConstants.Select(c => (c.Parameter, c.Expression)).ToArray(); + return remappedExpression; + } + + protected override Expression VisitExtension(Expression node) + { + if (node is LiftableConstantExpression liftedConstant) + { + return _inline + ? InlineConstant(liftedConstant) + : LiftConstant(liftedConstant); + } + + return base.VisitExtension(node); + } + + protected virtual ConstantExpression InlineConstant(LiftableConstantExpression liftableConstant) + { + if (liftableConstant.ResolverExpression is Expression> + resolverExpression) + { + var resolver = resolverExpression.Compile(preferInterpretation: true); + var value = resolver(_materializerLiftableConstantContext); + return Expression.Constant(value, liftableConstant.Type); + } + + throw new InvalidOperationException( + $"Unknown resolved expression of type {liftableConstant.ResolverExpression.GetType().Name} found on liftable constant expression"); + } + + protected virtual ParameterExpression LiftConstant(LiftableConstantExpression liftableConstant) + { + var resolverLambda = liftableConstant.ResolverExpression; + var parameter = resolverLambda.Parameters[0]; + + // Extract the lambda body, replacing the lambda parameter with our lifted constant context parameter, and also inline any captured + // literals + var body = _liftedExpressionProcessor.Process(resolverLambda.Body, parameter, _contextParameter!); + + // If the lambda returns a value type, a Convert to object node gets needed that we need to unwrap + if (body is UnaryExpression { NodeType: ExpressionType.Convert } convertNode + && convertNode.Type == typeof(object)) + { + body = convertNode.Operand; + } + + // Register the lifted constant; note that the name will be uniquified later + var variableParameter = Expression.Parameter(liftableConstant.Type, liftableConstant.VariableName); + _liftedConstants.Add(new(variableParameter, body)); + + return variableParameter; + } + + private class LiftedConstantOptimizer : ExpressionVisitor + { + private List _liftedConstants = null!; + + private record ExpressionInfo(ExpressionStatus Status, ParameterExpression? Parameter = null, string? PreferredName = null); + private readonly Dictionary _indexedExpressions = new(ExpressionEqualityComparer.Instance); + private LiftedConstant _currentLiftedConstant = null!; + private bool _firstPass; + private int _index; + + public void Optimize(List liftedConstants) + { + _liftedConstants = liftedConstants; + _indexedExpressions.Clear(); + + _firstPass = true; + + // Phase 1: recursively seek out tree fragments which appear more than once across the lifted constants. These will be extracted + // out to separate variables. + foreach (var liftedConstant in liftedConstants) + { + _currentLiftedConstant = liftedConstant; + Visit(liftedConstant.Expression); + } + + // Filter out fragments which don't appear at least once + foreach (var (expression, expressionInfo) in _indexedExpressions) + { + if (expressionInfo.Status == ExpressionStatus.SeenOnce) + { + _indexedExpressions.Remove(expression); + continue; + } + + Check.DebugAssert(expressionInfo.Status == ExpressionStatus.SeenMultipleTimes, + "expressionInfo.Status == ExpressionStatus.SeenMultipleTimes"); + } + + // Second pass: extract common denominator tree fragments to separate variables + _firstPass = false; + for (_index = 0; _index < liftedConstants.Count; _index++) + { + _currentLiftedConstant = _liftedConstants[_index]; + if (_indexedExpressions.TryGetValue(_currentLiftedConstant.Expression, out var expressionInfo) + && expressionInfo.Status == ExpressionStatus.Extracted) + { + // This entire lifted constant has already been extracted before, so we no longer need it as a separate variable. + _liftedConstants[_index] = _currentLiftedConstant with { ReplacingParameter = expressionInfo.Parameter }; + + continue; + } + + var optimizedExpression = Visit(_currentLiftedConstant.Expression); + if (optimizedExpression != _currentLiftedConstant.Expression) + { + _liftedConstants[_index] = _currentLiftedConstant with { Expression = optimizedExpression }; + } + } + } + + [return: NotNullIfNotNull(nameof(node))] + public override Expression? Visit(Expression? node) + { + if (node is null) + { + return null; + } + + if (node is ParameterExpression or ConstantExpression || node.Type.IsAssignableTo(typeof(LambdaExpression))) + { + return node; + } + + if (_firstPass) + { + var preferredName = ReferenceEquals(node, _currentLiftedConstant.Expression) + ? _currentLiftedConstant.Parameter.Name + : null; + + if (!_indexedExpressions.TryGetValue(node, out var expressionInfo)) + { + // Unseen expression, add it to the dictionary with a null value, to indicate it's only a candidate at this point. + _indexedExpressions[node] = new(ExpressionStatus.SeenOnce, PreferredName: preferredName); + return base.Visit(node); + } + + // We've already seen this expression. + if (expressionInfo.Status == ExpressionStatus.SeenOnce + || expressionInfo.PreferredName is null && preferredName is not null) + { + // This is the 2nd time we're seeing the expression - mark it as a common denominator + _indexedExpressions[node] = _indexedExpressions[node] with + { + Status = ExpressionStatus.SeenMultipleTimes, + PreferredName = preferredName + }; + } + + // We've already seen and indexed this expression, no need to do it again + return node; + } + else + { + // 2nd pass + if (_indexedExpressions.TryGetValue(node, out var expressionInfo) && expressionInfo.Status != ExpressionStatus.SeenOnce) + { + // This fragment is common across multiple lifted constants. + if (expressionInfo.Status == ExpressionStatus.SeenMultipleTimes) + { + // This fragment hasn't yet been extracted out to its own variable in the 2nd pass. + + // If this happens to be a top-level node in the lifted constant, no need to extract an additional variable - just + // use that as the "extracted" parameter further down. + if (ReferenceEquals(node, _currentLiftedConstant.Expression)) + { + _indexedExpressions[node] = new(ExpressionStatus.Extracted, _currentLiftedConstant.Parameter); + return base.Visit(node); + } + + // Otherwise, we need to extract a new variable, integrating it just before this one. + var parameter = Expression.Parameter(node.Type, node switch + { + _ when expressionInfo.PreferredName is not null => expressionInfo.PreferredName, + MemberExpression me => char.ToLowerInvariant(me.Member.Name[0]) + me.Member.Name[1..], + MethodCallExpression mce => char.ToLowerInvariant(mce.Method.Name[0]) + mce.Method.Name[1..], + _ => "unknown" + }); + + var visitedNode = base.Visit(node); + _liftedConstants.Insert(_index++, new(parameter, visitedNode)); + + // Mark this node as having been extracted, to prevent it from getting extracted again + expressionInfo = _indexedExpressions[node] = new(ExpressionStatus.Extracted, parameter); + } + + Check.DebugAssert(expressionInfo.Parameter is not null, "expressionInfo.Parameter is not null"); + + return expressionInfo.Parameter; + } + + // This specific fragment only appears once across the lifted constants; keep going down. + return base.Visit(node); + } + } + + private enum ExpressionStatus + { + SeenOnce, + SeenMultipleTimes, + Extracted + } + } + + private class LiftedExpressionProcessor : ExpressionVisitor + { + private ParameterExpression _originalParameter = null!; + private ParameterExpression _replacingParameter = null!; + + public Expression Process(Expression expression, ParameterExpression originalParameter, ParameterExpression replacingParameter) + { + _originalParameter = originalParameter; + _replacingParameter = replacingParameter; + + return Visit(expression); + } + + protected override Expression VisitMember(MemberExpression node) + { + // The expression to be lifted may contain a captured variable; for limited literal scenarios, inline that variable into the + // expression so we can render it out to C#. + + // TODO: For the general case, this needs to be a full blown "evaluatable" identifier (like ParameterExtractingEV), which can + // identify any fragments of the tree which don't depend on the lambda parameter, and evaluate them. + // But for now we're doing a reduced version. + + var visited = base.VisitMember(node); + + if (visited is MemberExpression + { + Expression: ConstantExpression { Value: { } constant }, + Member: var member + }) + { + return member switch + { + FieldInfo fi => Expression.Constant(fi.GetValue(constant), node.Type), + PropertyInfo pi => Expression.Constant(pi.GetValue(constant), node.Type), + _ => visited + }; + } + + return visited; + } + + protected override Expression VisitParameter(ParameterExpression node) + => ReferenceEquals(node, _originalParameter) + ? _replacingParameter + : base.VisitParameter(node); + } + +#if DEBUG + protected override Expression VisitConstant(ConstantExpression node) + { + return IsLiteral(node.Value) + ? node + : throw new InvalidOperationException( + $"Materializer expression contains a non-literal constant of type '{node.Value!.GetType().Name}'. " + + $"Use a {nameof(LiftableConstantExpression)} to reference any non-literal constants."); + + static bool IsLiteral(object? value) + { + return value switch + { + int or long or uint or ulong or short or sbyte or ushort or byte or double or float or decimal + => true, + + string or bool or Type or Enum or null => true, + + ITuple tuple + when tuple.GetType() is { IsGenericType: true } tupleType + && tupleType.Name.StartsWith("ValueTuple`", StringComparison.Ordinal) + && tupleType.Namespace == "System" + => IsTupleLiteral(tuple), + + _ => false + }; + + bool IsTupleLiteral(ITuple tuple) + { + for (var i = 0; i < tuple.Length; i++) + { + if (!IsLiteral(tuple[i])) + { + return false; + } + } + + return true; + } + } + } +#endif +} diff --git a/src/EFCore/Query/MaterializerLiftableConstantContext.cs b/src/EFCore/Query/MaterializerLiftableConstantContext.cs new file mode 100644 index 00000000000..64992d1d19b --- /dev/null +++ b/src/EFCore/Query/MaterializerLiftableConstantContext.cs @@ -0,0 +1,6 @@ +// 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.Query; + +public record MaterializerLiftableConstantContext(ShapedQueryCompilingExpressionVisitorDependencies Dependencies); diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index def674ba898..952cb07c61d 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -1,6 +1,8 @@ // 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.Query.Internal; + namespace Microsoft.EntityFrameworkCore.Query; /// @@ -56,10 +58,14 @@ public class QueryCompilationContext private readonly IQueryTranslationPostprocessorFactory _queryTranslationPostprocessorFactory; private readonly IShapedQueryCompilingExpressionVisitorFactory _shapedQueryCompilingExpressionVisitorFactory; - private readonly ExpressionPrinter _expressionPrinter; + private readonly ExpressionPrinter _expressionPrinter = new(); private Dictionary? _runtimeParameters; +#if DEBUG + private readonly ShaperPublicMethodVerifier _shaperPublicMethodVerifier = new(); +#endif + /// /// Creates a new instance of the class. /// @@ -82,8 +88,6 @@ public QueryCompilationContext( _queryableMethodTranslatingExpressionVisitorFactory = dependencies.QueryableMethodTranslatingExpressionVisitorFactory; _queryTranslationPostprocessorFactory = dependencies.QueryTranslationPostprocessorFactory; _shapedQueryCompilingExpressionVisitorFactory = dependencies.ShapedQueryCompilingExpressionVisitorFactory; - - _expressionPrinter = new ExpressionPrinter(); } /// @@ -155,6 +159,33 @@ public virtual void AddTag(string tag) /// The query to generate executor for. /// Returns which can be invoked to get results of this query. public virtual Func CreateQueryExecutor(Expression query) + { + var queryExecutorExpression = CreateQueryExecutorExpression(query); + + // The materializer expression tree has liftable constant nodes, pointing to various constants that should be the same instances + // across invocations of the query. + // In normal mode, these nodes should simply be evaluated, and a ConstantExpression to those instances embedded directly in the + // tree (for precompiled queries we generate C# code for resolving those instances instead). + var queryExecutorAfterLiftingExpression = + (Expression>)Dependencies.LiftableConstantProcessor.InlineConstants(queryExecutorExpression); + + try + { + return queryExecutorAfterLiftingExpression.Compile(); + } + finally + { + Logger.QueryExecutionPlanned(Dependencies.Context, _expressionPrinter, queryExecutorExpression); + } + } + + /// + /// Creates the query executor func which gives results for this query. + /// + /// The result type of this query. + /// The query to generate executor for. + /// Returns which can be invoked to get results of this query. + public virtual Expression> CreateQueryExecutorExpression(Expression query) { var queryAndEventData = Logger.QueryCompilationStarting(Dependencies.Context, _expressionPrinter, query); query = queryAndEventData.Query; @@ -176,14 +207,14 @@ public virtual Func CreateQueryExecutor(Expressi query, QueryContextParameter); - try - { - return queryExecutorExpression.Compile(); - } - finally - { - Logger.QueryExecutionPlanned(Dependencies.Context, _expressionPrinter, queryExecutorExpression); - } +#if DEBUG + // Verify that the shaper does not call any non-public methods, since such invocations cannot be generated in C# for precompiled + // queries. We have this here (in DEBUG) to check any method calls we integrate somewhere in the query pipeline (e.g. for parameter + // rewriting for string.StartsWith). + _shaperPublicMethodVerifier.Visit(queryExecutorExpression); +#endif + + return queryExecutorExpression; } /// diff --git a/src/EFCore/Query/QueryCompilationContextDependencies.cs b/src/EFCore/Query/QueryCompilationContextDependencies.cs index 85ad3c009b9..48b78572c61 100644 --- a/src/EFCore/Query/QueryCompilationContextDependencies.cs +++ b/src/EFCore/Query/QueryCompilationContextDependencies.cs @@ -53,6 +53,8 @@ public QueryCompilationContextDependencies( IQueryableMethodTranslatingExpressionVisitorFactory queryableMethodTranslatingExpressionVisitorFactory, IQueryTranslationPostprocessorFactory queryTranslationPostprocessorFactory, IShapedQueryCompilingExpressionVisitorFactory shapedQueryCompilingExpressionVisitorFactory, + // ShapedQueryCompilingExpressionVisitorDependencies shapedQueryCompilingExpressionVisitorDependencies, + ILiftableConstantProcessor liftableConstantProcessor, IExecutionStrategy executionStrategy, ICurrentDbContext currentContext, IDbContextOptions contextOptions, @@ -65,6 +67,9 @@ public QueryCompilationContextDependencies( QueryableMethodTranslatingExpressionVisitorFactory = queryableMethodTranslatingExpressionVisitorFactory; QueryTranslationPostprocessorFactory = queryTranslationPostprocessorFactory; ShapedQueryCompilingExpressionVisitorFactory = shapedQueryCompilingExpressionVisitorFactory; + // ShapedQueryCompilingExpressionVisitorDependencies = shapedQueryCompilingExpressionVisitorDependencies; + LiftableConstantProcessor = liftableConstantProcessor; + ShapedQueryCompilingExpressionVisitorDependencies = null!; IsRetryingExecutionStrategy = executionStrategy.RetriesOnFailure; ContextOptions = contextOptions; Logger = logger; @@ -114,6 +119,16 @@ public QueryTrackingBehavior QueryTrackingBehavior /// public IShapedQueryCompilingExpressionVisitorFactory ShapedQueryCompilingExpressionVisitorFactory { get; init; } + /// + /// The shaped query compiling expression visitor dependencies. + /// + public ShapedQueryCompilingExpressionVisitorDependencies ShapedQueryCompilingExpressionVisitorDependencies { get; init; } + + /// + /// The liftable constant processor. + /// + public ILiftableConstantProcessor LiftableConstantProcessor { get; init; } + /// /// Whether the configured execution strategy can retry. /// diff --git a/src/EFCore/Query/ReplacingExpressionVisitor.cs b/src/EFCore/Query/ReplacingExpressionVisitor.cs index 666a63bb096..b0aeeb0e242 100644 --- a/src/EFCore/Query/ReplacingExpressionVisitor.cs +++ b/src/EFCore/Query/ReplacingExpressionVisitor.cs @@ -33,6 +33,16 @@ public class ReplacingExpressionVisitor : ExpressionVisitor public static Expression Replace(Expression original, Expression replacement, Expression tree) => new ReplacingExpressionVisitor(new[] { original }, new[] { replacement }).Visit(tree); + /// + /// Replaces one expression with another in given expression tree. + /// + /// A list of original expressions to replace. + /// A list of expressions to be used as replacements. + /// The expression tree in which replacement is going to be performed. + /// An expression tree with replacements made. + public static Expression Replace(Expression[] originals, Expression[] replacements, Expression tree) + => new ReplacingExpressionVisitor(originals, replacements).Visit(tree); + /// /// Creates a new instance of the class. /// diff --git a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs index 7e346199f23..5cbfc8df1bf 100644 --- a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs @@ -55,6 +55,7 @@ protected ShapedQueryCompilingExpressionVisitor( _entityMaterializerInjectingExpressionVisitor = new EntityMaterializerInjectingExpressionVisitor( dependencies.EntityMaterializerSource, + dependencies.LiftableConstantFactory, queryCompilationContext.QueryTrackingBehavior); _constantVerifyingExpressionVisitor = new ConstantVerifyingExpressionVisitor(dependencies.TypeMappingSource); @@ -128,7 +129,14 @@ private static readonly MethodInfo SingleOrDefaultAsyncMethodInfo .GetDeclaredMethods(nameof(SingleOrDefaultAsync)) .Single(mi => mi.GetParameters().Length == 2); - private static async Task SingleAsync( + /// + /// 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] + public static async Task SingleAsync( IAsyncEnumerable asyncEnumerable, CancellationToken cancellationToken = default) { @@ -150,7 +158,14 @@ private static async Task SingleAsync( return result; } - private static async Task SingleOrDefaultAsync( + /// + /// 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] + public static async Task SingleOrDefaultAsync( IAsyncEnumerable asyncEnumerable, CancellationToken cancellationToken = default) { @@ -293,6 +308,7 @@ private static readonly MethodInfo CreateNullKeyValueInNoTrackingQueryMethod .GetTypeInfo().GetDeclaredMethod(nameof(CreateNullKeyValueInNoTrackingQuery))!; private readonly IEntityMaterializerSource _entityMaterializerSource; + private readonly ILiftableConstantFactory _liftableConstantFactory; private readonly QueryTrackingBehavior _queryTrackingBehavior; private readonly bool _queryStateManager; private readonly ISet _visitedEntityTypes = new HashSet(); @@ -300,9 +316,11 @@ private static readonly MethodInfo CreateNullKeyValueInNoTrackingQueryMethod public EntityMaterializerInjectingExpressionVisitor( IEntityMaterializerSource entityMaterializerSource, + ILiftableConstantFactory liftableConstantFactory, QueryTrackingBehavior queryTrackingBehavior) { _entityMaterializerSource = entityMaterializerSource; + _liftableConstantFactory = liftableConstantFactory; _queryTrackingBehavior = queryTrackingBehavior; _queryStateManager = queryTrackingBehavior is QueryTrackingBehavior.TrackAll or QueryTrackingBehavior.NoTrackingWithIdentityResolution; @@ -384,7 +402,12 @@ private Expression ProcessEntityShaper(StructuralTypeShaperExpression shaper) Call( QueryCompilationContext.QueryContextParameter, TryGetEntryMethodInfo, - Constant(primaryKey), + _liftableConstantFactory.CreateLiftableConstant( + // TODO: Owned, STET + c => c.Dependencies.Model.FindEntityType(typeBase.Name)!.FindPrimaryKey()!, + typeBase.Name + "Key", + typeof(IKey)), + // Constant(primaryKey), NewArrayInit( typeof(object), primaryKey.Properties @@ -491,18 +514,32 @@ private Expression MaterializeEntity( expressions.Add( Assign( shadowValuesVariable, - Constant(Snapshot.Empty))); + // Expression.Constant(Snapshot.Empty), + _liftableConstantFactory.CreateLiftableConstant( + _ => Snapshot.Empty, + "emptySnapshot", + typeof(Snapshot)))); var returnType = typeBase.ClrType; var valueBufferExpression = Call(materializationContextVariable, MaterializationContext.GetValueBufferMethod); + + var materializationConditionBody = ReplacingExpressionVisitor.Replace( + shaper.MaterializationCondition.Parameters[0], + valueBufferExpression, + shaper.MaterializationCondition.Body); + + // TODO: We probably need to recurse, looking for constant references to entity types. + if (materializationConditionBody is ConstantExpression { Value: IEntityType materializedEntityType }) + { + materializationConditionBody = _liftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(materializedEntityType.Name)!, + materializedEntityType.Name + "EntityType", + typeof(IEntityType)); + } + var expressionContext = (returnType, materializationContextVariable, concreteEntityTypeVariable, shadowValuesVariable); expressions.Add( - Assign( - concreteEntityTypeVariable, - ReplacingExpressionVisitor.Replace( - shaper.MaterializationCondition.Parameters[0], - valueBufferExpression, - shaper.MaterializationCondition.Body))); + Expression.Assign(concreteEntityTypeVariable, materializationConditionBody)); var (primaryKey, concreteEntityTypes) = typeBase is IEntityType entityType ? (entityType.FindPrimaryKey(), entityType.GetConcreteDerivedTypesInclusive().Cast().ToArray()) @@ -511,9 +548,15 @@ private Expression MaterializeEntity( var switchCases = new SwitchCase[concreteEntityTypes.Length]; for (var i = 0; i < concreteEntityTypes.Length; i++) { + var concreteEntityType = concreteEntityTypes[i]; + switchCases[i] = SwitchCase( CreateFullMaterializeExpression(concreteEntityTypes[i], expressionContext), - Constant(concreteEntityTypes[i], typeBase is IEntityType ? typeof(IEntityType) : typeof(IComplexType))); + // Constant(concreteEntityTypes[i], typeof(IEntityType)) + _liftableConstantFactory.CreateLiftableConstant( + c => c.Dependencies.Model.FindEntityType(concreteEntityType.Name)!, + concreteEntityType.Name + "EntityType", + typeof(IEntityType))); } var materializationExpression = Switch( diff --git a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs index cc025f1e48d..daef86d8705 100644 --- a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs +++ b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs @@ -51,12 +51,16 @@ public ShapedQueryCompilingExpressionVisitorDependencies( IEntityMaterializerSource entityMaterializerSource, ITypeMappingSource typeMappingSource, IMemoryCache memoryCache, - ICoreSingletonOptions coreSingletonOptions) + ICoreSingletonOptions coreSingletonOptions, + IModel model, + ILiftableConstantFactory liftableConstantFactory) { EntityMaterializerSource = entityMaterializerSource; TypeMappingSource = typeMappingSource; MemoryCache = memoryCache; CoreSingletonOptions = coreSingletonOptions; + Model = model; + LiftableConstantFactory = liftableConstantFactory; } /// @@ -78,4 +82,14 @@ public ShapedQueryCompilingExpressionVisitorDependencies( /// Core singleton options. /// public ICoreSingletonOptions CoreSingletonOptions { get; init; } + + /// + /// The model. + /// + public IModel Model { get; init; } + + /// + /// The liftable constant factory. + /// + public ILiftableConstantFactory LiftableConstantFactory { get; init; } }