From d53215a8b1636600b8e6c20ffabbde8e724f9257 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 20 Mar 2024 23:49:29 +0100 Subject: [PATCH] Pregenerate SQL for precompiled queries Closes #29753 --- EFCore.sln.DotSettings | 5 + .../Query/Internal/CSharpToLinqTranslator.cs | 27 +- .../Internal/PrecompiledQueryCodeGenerator.cs | 5 +- ...=> RelationalCommandResolverExtensions.cs} | 6 +- .../Internal/FromSqlQueryingEnumerable.cs | 25 +- .../GroupBySingleQueryingEnumerable.cs | 21 +- .../GroupBySplitQueryingEnumerable.cs | 21 +- .../Internal/RelationalCommandResolver.cs | 12 + ...elationalQueryCompilationContextFactory.cs | 11 +- .../Internal/SingleQueryingEnumerable.cs | 25 +- .../Query/Internal/SplitQueryingEnumerable.cs | 25 +- .../RelationalLiftableConstantProcessor.cs | 5 +- ...onalMaterializerLiftableConstantContext.cs | 3 +- .../RelationalQueryCompilationContext.cs | 23 +- ...ocessingExpressionVisitor.ClientMethods.cs | 40 +-- ...sitor.ShaperProcessingExpressionVisitor.cs | 36 +- ...alShapedQueryCompilingExpressionVisitor.cs | 299 +++++++++++++++-- ...lationalSqlTranslatingExpressionVisitor.cs | 21 +- .../SqlExpressions/SqlParameterExpression.cs | 11 +- .../SqlServerQueryCompilationContext.cs | 26 +- ...SqlServerQueryCompilationContextFactory.cs | 12 +- .../Internal/SqliteQueryCompilationContext.cs | 22 +- .../SqliteQueryCompilationContextFactory.cs | 13 +- ...yableMethodTranslatingExpressionVisitor.cs | 9 +- .../Query/IQueryCompilationContextFactory.cs | 8 +- .../Internal/ExpressionTreeFuncletizer.cs | 54 ++- .../Internal/IParameterNullabilityInfo.cs | 21 ++ src/EFCore/Query/Internal/IParameterValues.cs | 2 + .../NavigationExpandingExpressionVisitor.cs | 12 +- .../QueryCompilationContextFactory.cs | 11 +- src/EFCore/Query/Internal/QueryCompiler.cs | 11 +- src/EFCore/Query/QueryCompilationContext.cs | 15 +- src/EFCore/Query/QueryContext.cs | 1 + src/EFCore/Storage/Database.cs | 9 +- src/EFCore/Storage/IDatabase.cs | 6 +- .../PrecompiledQueryRelationalFixture.cs | 32 +- .../PrecompiledQueryRelationalTestBase.cs | 4 + ...dSqlPregenerationQueryRelationalFixture.cs | 32 ++ ...SqlPregenerationQueryRelationalTestBase.cs | 316 ++++++++++++++++++ .../PrecompiledQueryTestHelpers.cs | 2 +- .../RelationalApiConsistencyTest.cs | 3 + ...piledSqlPregenerationQuerySqlServerTest.cs | 268 +++++++++++++++ ...compiledSqlPregenerationQuerySqliteTest.cs | 22 ++ ...vigationExpandingExpressionVisitorTests.cs | 2 +- 44 files changed, 1343 insertions(+), 191 deletions(-) rename src/EFCore.Relational/Extensions/Internal/{RelationCommandCacheExtensions.cs => RelationalCommandResolverExtensions.cs} (86%) create mode 100644 src/EFCore.Relational/Query/Internal/RelationalCommandResolver.cs create mode 100644 src/EFCore/Query/Internal/IParameterNullabilityInfo.cs create mode 100644 test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalFixture.cs create mode 100644 test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqliteTest.cs diff --git a/EFCore.sln.DotSettings b/EFCore.sln.DotSettings index d9724cb9987..e09a00ba6f5 100644 --- a/EFCore.sln.DotSettings +++ b/EFCore.sln.DotSettings @@ -216,6 +216,7 @@ The .NET Foundation licenses this file to you under the MIT license. <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Test Methods"><ElementKinds><Kind Name="TEST_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="Aa_bb" /></Policy> EF + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -280,6 +281,7 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True True True True @@ -326,6 +328,9 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True + True + True True True True diff --git a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs index 284d611a950..ca903070fdc 100644 --- a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs +++ b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs @@ -229,6 +229,16 @@ public override Expression VisitBinaryExpression(BinaryExpressionSyntax binary) var left = Visit(binary.Left); var right = Visit(binary.Right); + // TODO: This most probably needs to be more general, not just for binary + if (Nullable.GetUnderlyingType(left.Type) == right.Type) + { + right = Convert(right, left.Type); + } + else if (Nullable.GetUnderlyingType(right.Type) == left.Type) + { + left = Convert(left, right.Type); + } + // https://learn.microsoft.com/dotnet/api/Microsoft.CodeAnalysis.CSharp.Syntax.BinaryExpressionSyntax return binary.Kind() switch { @@ -413,7 +423,8 @@ public override Expression VisitIdentifierName(IdentifierNameSyntax identifierNa new FakeFieldInfo( typeof(FakeClosureFrameClass), ResolveType(localSymbol.Type), - localSymbol.Name))); + localSymbol.Name, + localSymbol.NullableAnnotation is NullableAnnotation.NotAnnotated))); } throw new InvalidOperationException( @@ -1131,6 +1142,11 @@ private Type ResolveType(ITypeSymbol typeSymbol, Dictionary? gener Type GetClrType(INamedTypeSymbol symbol) { + if (symbol.SpecialType == SpecialType.System_Nullable_T) + { + return typeof(Nullable<>); + } + var name = symbol.ContainingType is null ? typeSymbol.ToDisplayString(QualifiedTypeNameSymbolDisplayFormat) : typeSymbol.Name; @@ -1215,8 +1231,15 @@ public int GetHashCode(T[] obj) [CompilerGenerated] private sealed class FakeClosureFrameClass; - private sealed class FakeFieldInfo(Type declaringType, Type fieldType, string name) : FieldInfo + private sealed class FakeFieldInfo( + Type declaringType, + Type fieldType, + string name, + bool isNonNullableReferenceType) + : FieldInfo, IParameterNullabilityInfo { + public bool IsNonNullableReferenceType { get; } = isNonNullableReferenceType; + public override object[] GetCustomAttributes(bool inherit) => Array.Empty(); diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index c531aa54e0a..480e4ac3265 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -851,7 +851,10 @@ void ProcessCapturedVariables() .IncrementIndent() .AppendLine("var relationalModel = dbContext.Model.GetRelationalModel();") .AppendLine("var relationalTypeMappingSource = dbContext.GetService();") - .AppendLine("var materializerLiftableConstantContext = new RelationalMaterializerLiftableConstantContext(dbContext.GetService(), dbContext.GetService());"); + .AppendLine("var materializerLiftableConstantContext = new RelationalMaterializerLiftableConstantContext(") + .AppendLine(" dbContext.GetService(),") + .AppendLine(" dbContext.GetService(),") + .AppendLine(" dbContext.GetService());"); HashSet variableNames = ["relationalModel", "relationalTypeMappingSource", "materializerLiftableConstantContext"]; diff --git a/src/EFCore.Relational/Extensions/Internal/RelationCommandCacheExtensions.cs b/src/EFCore.Relational/Extensions/Internal/RelationalCommandResolverExtensions.cs similarity index 86% rename from src/EFCore.Relational/Extensions/Internal/RelationCommandCacheExtensions.cs rename to src/EFCore.Relational/Extensions/Internal/RelationalCommandResolverExtensions.cs index b1c67b0504c..5723ef60360 100644 --- a/src/EFCore.Relational/Extensions/Internal/RelationCommandCacheExtensions.cs +++ b/src/EFCore.Relational/Extensions/Internal/RelationalCommandResolverExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public static class RelationCommandCacheExtensions +public static class RelationalCommandResolverExtensions { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -21,10 +21,10 @@ public static class RelationCommandCacheExtensions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public static IRelationalCommand RentAndPopulateRelationalCommand( - this RelationalCommandCache relationalCommandCache, + this RelationalCommandResolver relationalCommandResolver, RelationalQueryContext queryContext) { - var relationalCommandTemplate = relationalCommandCache.GetRelationalCommandTemplate(queryContext.ParameterValues); + var relationalCommandTemplate = relationalCommandResolver(queryContext.ParameterValues); var relationalCommand = queryContext.Connection.RentCommand(); relationalCommand.PopulateFrom(relationalCommandTemplate); return relationalCommand; diff --git a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs index 4893afefe89..3437db94e04 100644 --- a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs @@ -22,7 +22,7 @@ public static class FromSqlQueryingEnumerable /// public static FromSqlQueryingEnumerable Create( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, IReadOnlyList columnNames, Func shaper, @@ -32,7 +32,7 @@ public static class FromSqlQueryingEnumerable bool threadSafetyChecksEnabled) => new( relationalQueryContext, - relationalCommandCache, + relationalCommandResolver, readerColumns, columnNames, shaper, @@ -51,7 +51,7 @@ public static class FromSqlQueryingEnumerable public class FromSqlQueryingEnumerable : IEnumerable, IAsyncEnumerable, IRelationalQueryingEnumerable { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly IReadOnlyList _columnNames; private readonly Func _shaper; @@ -69,7 +69,7 @@ public class FromSqlQueryingEnumerable : IEnumerable, IAsyncEnumerable, /// public FromSqlQueryingEnumerable( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, IReadOnlyList columnNames, Func shaper, @@ -79,7 +79,7 @@ public class FromSqlQueryingEnumerable : IEnumerable, IAsyncEnumerable, bool threadSafetyChecksEnabled) { _relationalQueryContext = relationalQueryContext; - _relationalCommandCache = relationalCommandCache; + _relationalCommandResolver = relationalCommandResolver; _readerColumns = readerColumns; _columnNames = columnNames; _shaper = shaper; @@ -128,8 +128,7 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandCache - .GetRelationalCommandTemplate(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.ParameterValues) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, @@ -187,7 +186,7 @@ public static int[] BuildIndexMap(IReadOnlyList columnNames, DbDataReade private sealed class Enumerator : IEnumerator { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly IReadOnlyList _columnNames; private readonly Func _shaper; @@ -205,7 +204,7 @@ private sealed class Enumerator : IEnumerator public Enumerator(FromSqlQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _columnNames = queryingEnumerable._columnNames; _shaper = queryingEnumerable._shaper; @@ -272,7 +271,7 @@ private static bool InitializeReader(Enumerator enumerator) EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); enumerator._dataReader = relationalCommand.ExecuteReader( new RelationalCommandParameterObject( @@ -307,7 +306,7 @@ public void Reset() private sealed class AsyncEnumerator : IAsyncEnumerator { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly IReadOnlyList _columnNames; private readonly Func _shaper; @@ -325,7 +324,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator public AsyncEnumerator(FromSqlQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _columnNames = queryingEnumerable._columnNames; _shaper = queryingEnumerable._shaper; @@ -394,7 +393,7 @@ private static async Task InitializeReaderAsync(AsyncEnumerator enumerator EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); enumerator._dataReader = await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( diff --git a/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs index 73d4960150f..d7a63894ee8 100644 --- a/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs @@ -16,7 +16,7 @@ public class GroupBySingleQueryingEnumerable : IEnumerable>, IAsyncEnumerable>, IRelationalQueryingEnumerable { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _keySelector; private readonly Func _keyIdentifier; @@ -36,7 +36,7 @@ public class GroupBySingleQueryingEnumerable /// public GroupBySingleQueryingEnumerable( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, Func keySelector, Func keyIdentifier, @@ -48,7 +48,7 @@ public class GroupBySingleQueryingEnumerable bool threadSafetyChecksEnabled) { _relationalQueryContext = relationalQueryContext; - _relationalCommandCache = relationalCommandCache; + _relationalCommandResolver = relationalCommandResolver; _readerColumns = readerColumns; _keySelector = keySelector; _keyIdentifier = keyIdentifier; @@ -99,8 +99,7 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandCache - .GetRelationalCommandTemplate(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.ParameterValues) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, @@ -156,7 +155,7 @@ private static bool CompareIdentifiers(IReadOnlyList> private sealed class Enumerator : IEnumerator> { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _keySelector; private readonly Func _keyIdentifier; @@ -177,7 +176,7 @@ private sealed class Enumerator : IEnumerator> public Enumerator(GroupBySingleQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _keySelector = queryingEnumerable._keySelector; _keyIdentifier = queryingEnumerable._keyIdentifier; @@ -302,7 +301,7 @@ private static bool InitializeReader(Enumerator enumerator) EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = relationalCommand.ExecuteReader( new RelationalCommandParameterObject( @@ -340,7 +339,7 @@ public void Reset() private sealed class AsyncEnumerator : IAsyncEnumerator> { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _keySelector; private readonly Func _keyIdentifier; @@ -362,7 +361,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _keySelector = queryingEnumerable._keySelector; _keyIdentifier = queryingEnumerable._keyIdentifier; @@ -489,7 +488,7 @@ private static async Task InitializeReaderAsync(AsyncEnumerator enumerator EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( diff --git a/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs index 7c610c47b3a..b902512a627 100644 --- a/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs @@ -16,7 +16,7 @@ public class GroupBySplitQueryingEnumerable : IEnumerable>, IAsyncEnumerable>, IRelationalQueryingEnumerable { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _keySelector; private readonly Func _keyIdentifier; @@ -38,7 +38,7 @@ public class GroupBySplitQueryingEnumerable /// public GroupBySplitQueryingEnumerable( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, Func keySelector, Func keyIdentifier, @@ -52,7 +52,7 @@ public class GroupBySplitQueryingEnumerable bool threadSafetyChecksEnabled) { _relationalQueryContext = relationalQueryContext; - _relationalCommandCache = relationalCommandCache; + _relationalCommandResolver = relationalCommandResolver; _readerColumns = readerColumns; _keySelector = keySelector; _keyIdentifier = keyIdentifier; @@ -105,8 +105,7 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandCache - .GetRelationalCommandTemplate(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.ParameterValues) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, @@ -162,7 +161,7 @@ private static bool CompareIdentifiers(IReadOnlyList> private sealed class Enumerator : IEnumerator> { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _keySelector; private readonly Func _keyIdentifier; @@ -184,7 +183,7 @@ private sealed class Enumerator : IEnumerator> public Enumerator(GroupBySplitQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _keySelector = queryingEnumerable._keySelector; _keyIdentifier = queryingEnumerable._keyIdentifier; @@ -298,7 +297,7 @@ private static bool InitializeReader(Enumerator enumerator) EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = relationalCommand.ExecuteReader( new RelationalCommandParameterObject( @@ -336,7 +335,7 @@ public void Reset() private sealed class AsyncEnumerator : IAsyncEnumerator> { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _keySelector; private readonly Func _keyIdentifier; @@ -359,7 +358,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _keySelector = queryingEnumerable._keySelector; _keyIdentifier = queryingEnumerable._keyIdentifier; @@ -476,7 +475,7 @@ private static async Task InitializeReaderAsync(AsyncEnumerator enumerator EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( diff --git a/src/EFCore.Relational/Query/Internal/RelationalCommandResolver.cs b/src/EFCore.Relational/Query/Internal/RelationalCommandResolver.cs new file mode 100644 index 00000000000..2e73b0413b9 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalCommandResolver.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.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 delegate IRelationalCommandTemplate RelationalCommandResolver(IReadOnlyDictionary parameters); diff --git a/src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.cs b/src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.cs index d7df2bb5eb2..25d329bc8b3 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.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 System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Query.Internal; /// @@ -41,8 +43,8 @@ public class RelationalQueryCompilationContextFactory : IQueryCompilationContext /// 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 QueryCompilationContext Create(bool async, bool precompiling) - => new RelationalQueryCompilationContext(Dependencies, RelationalDependencies, async, precompiling); + public virtual QueryCompilationContext Create(bool async) + => new RelationalQueryCompilationContext(Dependencies, RelationalDependencies, async); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -50,6 +52,7 @@ public virtual QueryCompilationContext Create(bool async, bool precompiling) /// 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 QueryCompilationContext Create(bool async) - => throw new UnreachableException("The overload with `precompiling` should be called"); + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + public virtual QueryCompilationContext CreatePrecompiled(bool async, IReadOnlySet nonNullableReferenceTypeParameters) + => new RelationalQueryCompilationContext(Dependencies, RelationalDependencies, async, precompiling: true, nonNullableReferenceTypeParameters); } diff --git a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs index 560179079fe..5cba383fadf 100644 --- a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs @@ -22,7 +22,7 @@ public static class SingleQueryingEnumerable /// public static SingleQueryingEnumerable Create( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, Func shaper, Type contextType, @@ -31,7 +31,7 @@ public static class SingleQueryingEnumerable bool threadSafetyChecksEnabled) => new( relationalQueryContext, - relationalCommandCache, + relationalCommandResolver, readerColumns, shaper, contextType, @@ -49,7 +49,7 @@ public static class SingleQueryingEnumerable public class SingleQueryingEnumerable : IEnumerable, IAsyncEnumerable, IRelationalQueryingEnumerable { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _shaper; private readonly Type _contextType; @@ -66,7 +66,7 @@ public class SingleQueryingEnumerable : IEnumerable, IAsyncEnumerable, /// public SingleQueryingEnumerable( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, Func shaper, Type contextType, @@ -75,7 +75,7 @@ public class SingleQueryingEnumerable : IEnumerable, IAsyncEnumerable, bool threadSafetyChecksEnabled) { _relationalQueryContext = relationalQueryContext; - _relationalCommandCache = relationalCommandCache; + _relationalCommandResolver = relationalCommandResolver; _readerColumns = readerColumns; _shaper = shaper; _contextType = contextType; @@ -123,8 +123,7 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandCache - .GetRelationalCommandTemplate(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.ParameterValues) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, @@ -150,7 +149,7 @@ public virtual string ToQueryString() private sealed class Enumerator : IEnumerator { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _shaper; private readonly Type _contextType; @@ -168,7 +167,7 @@ private sealed class Enumerator : IEnumerator public Enumerator(SingleQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; @@ -269,7 +268,7 @@ private static bool InitializeReader(Enumerator enumerator) EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = relationalCommand.ExecuteReader( new RelationalCommandParameterObject( @@ -307,7 +306,7 @@ public void Reset() private sealed class AsyncEnumerator : IAsyncEnumerator { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _shaper; private readonly Type _contextType; @@ -326,7 +325,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator public AsyncEnumerator(SingleQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; @@ -430,7 +429,7 @@ private static async Task InitializeReaderAsync(AsyncEnumerator enumerator EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( diff --git a/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs index 1575352ebbd..b8353a7a887 100644 --- a/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs @@ -22,7 +22,7 @@ public static class SplitQueryingEnumerable /// public static SplitQueryingEnumerable Create( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, Func shaper, Action? relatedDataLoaders, @@ -33,7 +33,7 @@ public static class SplitQueryingEnumerable bool threadSafetyChecksEnabled) => new( relationalQueryContext, - relationalCommandCache, + relationalCommandResolver, readerColumns, shaper, relatedDataLoaders, @@ -53,7 +53,7 @@ public static class SplitQueryingEnumerable public class SplitQueryingEnumerable : IEnumerable, IAsyncEnumerable, IRelationalQueryingEnumerable { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _shaper; private readonly Action? _relatedDataLoaders; @@ -72,7 +72,7 @@ public class SplitQueryingEnumerable : IEnumerable, IAsyncEnumerable, I /// public SplitQueryingEnumerable( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, Func shaper, Action? relatedDataLoaders, @@ -83,7 +83,7 @@ public class SplitQueryingEnumerable : IEnumerable, IAsyncEnumerable, I bool threadSafetyChecksEnabled) { _relationalQueryContext = relationalQueryContext; - _relationalCommandCache = relationalCommandCache; + _relationalCommandResolver = relationalCommandResolver; _readerColumns = readerColumns; _shaper = shaper; _relatedDataLoaders = relatedDataLoaders; @@ -133,8 +133,7 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandCache - .GetRelationalCommandTemplate(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.ParameterValues) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, @@ -162,7 +161,7 @@ public virtual string ToQueryString() private sealed class Enumerator : IEnumerator { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _shaper; private readonly Action? _relatedDataLoaders; @@ -181,7 +180,7 @@ private sealed class Enumerator : IEnumerator public Enumerator(SplitQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _shaper = queryingEnumerable._shaper; _relatedDataLoaders = queryingEnumerable._relatedDataLoaders; @@ -263,7 +262,7 @@ private static bool InitializeReader(Enumerator enumerator) EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = relationalCommand.ExecuteReader( new RelationalCommandParameterObject( @@ -313,7 +312,7 @@ public void Reset() private sealed class AsyncEnumerator : IAsyncEnumerator { private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; + private readonly RelationalCommandResolver _relationalCommandResolver; private readonly IReadOnlyList? _readerColumns; private readonly Func _shaper; private readonly Func? _relatedDataLoaders; @@ -333,7 +332,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator public AsyncEnumerator(SplitQueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _relationalCommandResolver = queryingEnumerable._relationalCommandResolver; _readerColumns = queryingEnumerable._readerColumns; _shaper = queryingEnumerable._shaper; _relatedDataLoaders = queryingEnumerable._relatedDataLoadersAsync; @@ -418,7 +417,7 @@ private static async Task InitializeReaderAsync(AsyncEnumerator enumerator EntityFrameworkEventSource.Log.QueryExecuting(); var relationalCommand = enumerator._relationalCommand = - enumerator._relationalCommandCache.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); + enumerator._relationalCommandResolver.RentAndPopulateRelationalCommand(enumerator._relationalQueryContext); var dataReader = enumerator._dataReader = await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( diff --git a/src/EFCore.Relational/Query/RelationalLiftableConstantProcessor.cs b/src/EFCore.Relational/Query/RelationalLiftableConstantProcessor.cs index 1e942372ab0..adbccbefe6d 100644 --- a/src/EFCore.Relational/Query/RelationalLiftableConstantProcessor.cs +++ b/src/EFCore.Relational/Query/RelationalLiftableConstantProcessor.cs @@ -24,9 +24,10 @@ public class RelationalLiftableConstantProcessor : LiftableConstantProcessor /// public RelationalLiftableConstantProcessor( ShapedQueryCompilingExpressionVisitorDependencies dependencies, - RelationalShapedQueryCompilingExpressionVisitorDependencies relationalDependencies) + RelationalShapedQueryCompilingExpressionVisitorDependencies relationalDependencies, + RelationalCommandBuilderDependencies commandBuilderDependencies) : base(dependencies) - => _relationalMaterializerLiftableConstantContext = new(dependencies, relationalDependencies); + => _relationalMaterializerLiftableConstantContext = new(dependencies, relationalDependencies, commandBuilderDependencies); /// protected override ConstantExpression InlineConstant(LiftableConstantExpression liftableConstant) diff --git a/src/EFCore.Relational/Query/RelationalMaterializerLiftableConstantContext.cs b/src/EFCore.Relational/Query/RelationalMaterializerLiftableConstantContext.cs index e54e2b507a2..9b7227cd841 100644 --- a/src/EFCore.Relational/Query/RelationalMaterializerLiftableConstantContext.cs +++ b/src/EFCore.Relational/Query/RelationalMaterializerLiftableConstantContext.cs @@ -14,5 +14,6 @@ namespace Microsoft.EntityFrameworkCore.Query; [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] public record RelationalMaterializerLiftableConstantContext( ShapedQueryCompilingExpressionVisitorDependencies Dependencies, - RelationalShapedQueryCompilingExpressionVisitorDependencies RelationalDependencies) + RelationalShapedQueryCompilingExpressionVisitorDependencies RelationalDependencies, + RelationalCommandBuilderDependencies CommandBuilderDependencies) : MaterializerLiftableConstantContext(Dependencies); diff --git a/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs b/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs index 2f3935a8184..30fb6e152b6 100644 --- a/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs +++ b/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Query.Internal; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.EntityFrameworkCore.Query; @@ -16,6 +16,20 @@ namespace Microsoft.EntityFrameworkCore.Query; /// public class RelationalQueryCompilationContext : QueryCompilationContext { + /// + /// Creates a new instance of the class. + /// + /// Parameter object containing dependencies for this class. + /// Parameter object containing relational dependencies for this class. + /// A bool value indicating whether it is for async query. + public RelationalQueryCompilationContext( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies, + bool async) + : this(dependencies, relationalDependencies, async, precompiling: false, nonNullableReferenceTypeParameters: null) + { + } + /// /// Creates a new instance of the class. /// @@ -23,12 +37,15 @@ public class RelationalQueryCompilationContext : QueryCompilationContext /// Parameter object containing relational dependencies for this class. /// A bool value indicating whether it is for async query. /// Indicates whether the query is being precompiled. + /// Names of parameters which have non-nullable reference types. + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] public RelationalQueryCompilationContext( QueryCompilationContextDependencies dependencies, RelationalQueryCompilationContextDependencies relationalDependencies, bool async, - bool precompiling) - : base(dependencies, async, precompiling) + bool precompiling, + IReadOnlySet? nonNullableReferenceTypeParameters) + : base(dependencies, async, precompiling, nonNullableReferenceTypeParameters) { RelationalDependencies = relationalDependencies; QuerySplittingBehavior = RelationalOptionsExtension.Extract(ContextOptions).QuerySplittingBehavior; diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs index 07df5d5c415..eb13af861a1 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs @@ -412,7 +412,7 @@ void GenerateCurrentElementIfPending() int collectionId, RelationalQueryContext queryContext, IExecutionStrategy executionStrategy, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled, SplitQueryResultCoordinator resultCoordinator, @@ -431,18 +431,18 @@ void GenerateCurrentElementIfPending() { // Execute and fetch data reader var dataReader = executionStrategy.Execute( - (queryContext, relationalCommandCache, readerColumns, detailedErrorsEnabled), - ((RelationalQueryContext, RelationalCommandCache, IReadOnlyList?, bool) tup) + (queryContext, relationalCommandResolver, readerColumns, detailedErrorsEnabled), + ((RelationalQueryContext, RelationalCommandResolver, IReadOnlyList?, bool) tup) => InitializeReader(tup.Item1, tup.Item2, tup.Item3, tup.Item4), verifySucceeded: null); static RelationalDataReader InitializeReader( RelationalQueryContext queryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled) { - var relationalCommand = relationalCommandCache.RentAndPopulateRelationalCommand(queryContext); + var relationalCommand = relationalCommandResolver.RentAndPopulateRelationalCommand(queryContext); return relationalCommand.ExecuteReader( new RelationalCommandParameterObject( @@ -503,7 +503,7 @@ void GenerateCurrentElementIfPending() int collectionId, RelationalQueryContext queryContext, IExecutionStrategy executionStrategy, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled, SplitQueryResultCoordinator resultCoordinator, @@ -522,9 +522,9 @@ void GenerateCurrentElementIfPending() { // Execute and fetch data reader var dataReader = await executionStrategy.ExecuteAsync( - (queryContext, relationalCommandCache, readerColumns, detailedErrorsEnabled), + (queryContext, relationalCommandResolver, readerColumns, detailedErrorsEnabled), ( - (RelationalQueryContext, RelationalCommandCache, IReadOnlyList?, bool) tup, + (RelationalQueryContext, RelationalCommandResolver, IReadOnlyList?, bool) tup, CancellationToken cancellationToken) => InitializeReaderAsync(tup.Item1, tup.Item2, tup.Item3, tup.Item4, cancellationToken), verifySucceeded: null, @@ -533,12 +533,12 @@ void GenerateCurrentElementIfPending() static async Task InitializeReaderAsync( RelationalQueryContext queryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled, CancellationToken cancellationToken) { - var relationalCommand = relationalCommandCache.RentAndPopulateRelationalCommand(queryContext); + var relationalCommand = relationalCommandResolver.RentAndPopulateRelationalCommand(queryContext); return await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( @@ -780,7 +780,7 @@ void GenerateCurrentElementIfPending() int collectionId, RelationalQueryContext queryContext, IExecutionStrategy executionStrategy, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled, SplitQueryResultCoordinator resultCoordinator, @@ -796,18 +796,18 @@ void GenerateCurrentElementIfPending() { // Execute and fetch data reader var dataReader = executionStrategy.Execute( - (queryContext, relationalCommandCache, readerColumns, detailedErrorsEnabled), - ((RelationalQueryContext, RelationalCommandCache, IReadOnlyList?, bool) tup) + (queryContext, relationalCommandResolver, readerColumns, detailedErrorsEnabled), + ((RelationalQueryContext, RelationalCommandResolver, IReadOnlyList?, bool) tup) => InitializeReader(tup.Item1, tup.Item2, tup.Item3, tup.Item4), verifySucceeded: null); static RelationalDataReader InitializeReader( RelationalQueryContext queryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled) { - var relationalCommand = relationalCommandCache.RentAndPopulateRelationalCommand(queryContext); + var relationalCommand = relationalCommandResolver.RentAndPopulateRelationalCommand(queryContext); return relationalCommand.ExecuteReader( new RelationalCommandParameterObject( @@ -866,7 +866,7 @@ void GenerateCurrentElementIfPending() int collectionId, RelationalQueryContext queryContext, IExecutionStrategy executionStrategy, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled, SplitQueryResultCoordinator resultCoordinator, @@ -882,9 +882,9 @@ void GenerateCurrentElementIfPending() { // Execute and fetch data reader var dataReader = await executionStrategy.ExecuteAsync( - (queryContext, relationalCommandCache, readerColumns, detailedErrorsEnabled), + (queryContext, relationalCommandResolver, readerColumns, detailedErrorsEnabled), ( - (RelationalQueryContext, RelationalCommandCache, IReadOnlyList?, bool) tup, + (RelationalQueryContext, RelationalCommandResolver, IReadOnlyList?, bool) tup, CancellationToken cancellationToken) => InitializeReaderAsync(tup.Item1, tup.Item2, tup.Item3, tup.Item4, cancellationToken), verifySucceeded: null, @@ -893,12 +893,12 @@ void GenerateCurrentElementIfPending() static async Task InitializeReaderAsync( RelationalQueryContext queryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, IReadOnlyList? readerColumns, bool detailedErrorsEnabled, CancellationToken cancellationToken) { - var relationalCommand = relationalCommandCache.RentAndPopulateRelationalCommand(queryContext); + var relationalCommand = relationalCommandResolver.RentAndPopulateRelationalCommand(queryContext); return await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index ecc5c893c8d..4842ec1282d 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -105,7 +105,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito private readonly bool _isAsync; private readonly bool _splitQuery; private readonly bool _detailedErrorsEnabled; - private readonly bool _generateCommandCache; + private readonly bool _generateCommandResolver; private readonly ParameterExpression _resultCoordinatorParameter; private readonly ParameterExpression? _executionStrategyParameter; private readonly IDiagnosticsLogger _queryLogger; @@ -210,7 +210,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito _readerColumns = new ReaderColumn?[_selectExpression.Projection.Count]; } - _generateCommandCache = true; + _generateCommandResolver = true; _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; _isTracking = parentVisitor.QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll; _isAsync = parentVisitor.QueryCompilationContext.IsAsync; @@ -236,7 +236,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito _dataReaderParameter = dataReaderParameter; _resultContextParameter = resultContextParameter; _readerColumns = readerColumns; - _generateCommandCache = false; + _generateCommandResolver = false; _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; _isTracking = parentVisitor.QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll; _isAsync = parentVisitor.QueryCompilationContext.IsAsync; @@ -265,7 +265,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito _readerColumns = new ReaderColumn[_selectExpression.Projection.Count]; } - _generateCommandCache = true; + _generateCommandResolver = true; _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; _isTracking = parentVisitor.QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll; _isAsync = parentVisitor.QueryCompilationContext.IsAsync; @@ -282,7 +282,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito /// public LambdaExpression ProcessRelationalGroupingResult( RelationalGroupByResultExpression relationalGroupByResultExpression, - out Expression relationalCommandCache, + out Expression relationalCommandResolver, out IReadOnlyList? readerColumns, out LambdaExpression keySelector, out LambdaExpression keyIdentifier, @@ -304,7 +304,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito return ProcessShaper( relationalGroupByResultExpression.ElementShaper, - out relationalCommandCache!, + out relationalCommandResolver!, out readerColumns, out relatedDataLoaders, ref collectionId); @@ -318,7 +318,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito /// public LambdaExpression ProcessShaper( Expression shaperExpression, - out Expression relationalCommandCache, + out Expression relationalCommandResolver, out IReadOnlyList? readerColumns, out LambdaExpression? relatedDataLoaders, ref int collectionId) @@ -332,7 +332,7 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito _expressions.Add(result); result = Block(_variables, _expressions); - relationalCommandCache = _parentVisitor.CreateRelationalCommandCacheExpression(_selectExpression); + relationalCommandResolver = _parentVisitor.CreateRelationalCommandResolverExpression(_selectExpression); readerColumns = _readerColumns; return Lambda( @@ -353,9 +353,9 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito _expressions.Add(result); result = Block(_variables, _expressions); - relationalCommandCache = _generateCommandCache - ? _parentVisitor.CreateRelationalCommandCacheExpression(_selectExpression) - : Constant(null, typeof(RelationalCommandCache)); + relationalCommandResolver = _generateCommandResolver + ? _parentVisitor.CreateRelationalCommandResolverExpression(_selectExpression) + : Constant(null, typeof(RelationalCommandResolver)); readerColumns = _readerColumns; return Lambda( @@ -436,8 +436,8 @@ public sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisito result = Block(conditionalMaterializationExpressions); } - relationalCommandCache = _generateCommandCache - ? _parentVisitor.CreateRelationalCommandCacheExpression(_selectExpression) + relationalCommandResolver = _generateCommandResolver + ? _parentVisitor.CreateRelationalCommandResolverExpression(_selectExpression) : Constant(null, typeof(RelationalCommandCache));; readerColumns = _readerColumns; @@ -916,7 +916,7 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) _executionStrategyParameter!, relationalSplitCollectionShaperExpression.SelectExpression, _tags!); var innerShaper = innerProcessor.ProcessShaper( relationalSplitCollectionShaperExpression.InnerShaper, - out var relationalCommandCache, + out var relationalCommandResolver, out var readerColumns, out var relatedDataLoaders, ref _collectionId); @@ -982,7 +982,7 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) collectionIdConstant, Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), _executionStrategyParameter!, - relationalCommandCache, + relationalCommandResolver, CreateReaderColumnsExpression(readerColumns, _parentVisitor.Dependencies.LiftableConstantFactory), Constant(_detailedErrorsEnabled), _resultCoordinatorParameter, @@ -1170,7 +1170,7 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) _executionStrategyParameter!, relationalSplitCollectionShaperExpression.SelectExpression, _tags!); var innerShaper = innerProcessor.ProcessShaper( relationalSplitCollectionShaperExpression.InnerShaper, - out var relationalCommandCache, + out var relationalCommandResolver, out var readerColumns, out var relatedDataLoaders, ref _collectionId); @@ -1232,7 +1232,7 @@ when GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) collectionIdConstant, Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), _executionStrategyParameter!, - relationalCommandCache, + relationalCommandResolver, CreateReaderColumnsExpression(readerColumns, _parentVisitor.Dependencies.LiftableConstantFactory), Constant(_detailedErrorsEnabled), _resultCoordinatorParameter, @@ -1972,7 +1972,7 @@ void ProcessFixup(IDictionary fixupMap) var switchCases = new List(); var testsCount = testExpressions.Count; - // generate PropertyName switch-case code + // generate PropertyName switch-case code if (testsCount > 0) { var testExpression = IfThen( diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 19d4ecd9f39..9cf0637e29b 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage.Internal; using static System.Linq.Expressions.Expression; namespace Microsoft.EntityFrameworkCore.Query; @@ -16,6 +19,14 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue private readonly bool _threadSafetyChecksEnabled; private readonly bool _detailedErrorsEnabled; private readonly bool _useRelationalNulls; + private readonly bool _isPrecompiling; + + private readonly RelationalParameterBasedSqlProcessor _relationalParameterBasedSqlProcessor; + private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; + + private static ConstructorInfo? _relationalCommandConstructor; + private static ConstructorInfo? _typeMappedRelationalParameterConstructor; + private static PropertyInfo? _commandBuilderDependenciesProperty; /// /// Creates a new instance of the class. @@ -30,12 +41,16 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue : base(dependencies, queryCompilationContext) { RelationalDependencies = relationalDependencies; + _relationalParameterBasedSqlProcessor = + relationalDependencies.RelationalParameterBasedSqlProcessorFactory.Create(_useRelationalNulls); + _querySqlGeneratorFactory = relationalDependencies.QuerySqlGeneratorFactory; _contextType = queryCompilationContext.ContextType; _tags = queryCompilationContext.Tags; _threadSafetyChecksEnabled = dependencies.CoreSingletonOptions.AreThreadSafetyChecksEnabled; _detailedErrorsEnabled = dependencies.CoreSingletonOptions.AreDetailedErrorsEnabled; _useRelationalNulls = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).UseRelationalNulls; + _isPrecompiling = queryCompilationContext.IsPrecompiling; } /// @@ -43,6 +58,16 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue /// protected virtual RelationalShapedQueryCompilingExpressionVisitorDependencies RelationalDependencies { get; } + /// + /// Determines the maximum number of nullable parameters a query may have for us to pregenerate SQL for it in precompiled queries; + /// each additional nullable parameter doubles the number of SQLs we need to pregenerate. If a query has more nullable parameters + /// than this number, we don't pregenerate SQL, but instead insert the SQL as an expression tree and execute + /// at runtime as usual (slower startup). + /// + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + protected virtual int MaxNullableParametersForPregeneratedSql + => 3; + /// protected override Expression VisitExtension(Expression extensionExpression) => extensionExpression is NonQueryExpression nonQueryExpression @@ -70,12 +95,12 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression break; } - var relationalCommandCache = CreateRelationalCommandCacheExpression(innerExpression); + var relationalCommandResolver = CreateRelationalCommandResolverExpression(innerExpression); return Call( QueryCompilationContext.IsAsync ? NonQueryAsyncMethodInfo : NonQueryMethodInfo, Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - relationalCommandCache, + relationalCommandResolver, Constant(_contextType), Constant(nonQueryExpression.CommandSource), Constant(_threadSafetyChecksEnabled)); @@ -100,7 +125,7 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression [EntityFrameworkInternal] public static int NonQueryResult( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, Type contextType, CommandSource commandSource, bool threadSafetyChecksEnabled) @@ -115,12 +140,12 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression try { return relationalQueryContext.ExecutionStrategy.Execute( - (relationalQueryContext, relationalCommandCache, commandSource), + (relationalQueryContext, relationalCommandResolver, commandSource), static (_, state) => { EntityFrameworkEventSource.Log.QueryExecuting(); - var relationalCommand = state.relationalCommandCache.RentAndPopulateRelationalCommand(state.relationalQueryContext); + var relationalCommand = state.relationalCommandResolver.RentAndPopulateRelationalCommand(state.relationalQueryContext); return relationalCommand.ExecuteNonQuery( new RelationalCommandParameterObject( @@ -178,7 +203,7 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression [EntityFrameworkInternal] public static Task NonQueryResultAsync( RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, + RelationalCommandResolver relationalCommandResolver, Type contextType, CommandSource commandSource, bool threadSafetyChecksEnabled) @@ -193,12 +218,12 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression try { return relationalQueryContext.ExecutionStrategy.ExecuteAsync( - (relationalQueryContext, relationalCommandCache, commandSource), + (relationalQueryContext, relationalCommandResolver, commandSource), static (_, state, cancellationToken) => { EntityFrameworkEventSource.Log.QueryExecuting(); - var relationalCommand = state.relationalCommandCache.RentAndPopulateRelationalCommand(state.relationalQueryContext); + var relationalCommand = state.relationalCommandResolver.RentAndPopulateRelationalCommand(state.relationalQueryContext); return relationalCommand.ExecuteNonQueryAsync( new RelationalCommandParameterObject( @@ -264,7 +289,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery var elementSelector = new ShaperProcessingExpressionVisitor(this, selectExpression, selectExpression.Tags, splitQuery, false) .ProcessRelationalGroupingResult( relationalGroupByResultExpression, - out var relationalCommandCache, + out var relationalCommandResolver, out var readerColumns, out var keySelector, out var keyIdentifier, @@ -293,7 +318,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery keySelector.ReturnType, elementSelector.ReturnType).GetConstructors()[0], Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - relationalCommandCache, + relationalCommandResolver, readerColumnsExpression, keySelector, keyIdentifier, @@ -314,7 +339,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery keySelector.ReturnType, elementSelector.ReturnType).GetConstructors()[0], Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - relationalCommandCache, + relationalCommandResolver, readerColumnsExpression, keySelector, keyIdentifier, @@ -330,9 +355,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 relationalCommandResolver, out var readerColumns, + out var relatedDataLoaders, ref collectionCount); if (querySplittingBehavior == null && collectionCount > 1) @@ -348,7 +374,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery .Single(m => m.Name == nameof(FromSqlQueryingEnumerable.Create)) .MakeGenericMethod(shaper.ReturnType), Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - relationalCommandCache, + relationalCommandResolver, readerColumnsExpression, NewArrayInit( typeof(string), @@ -374,10 +400,10 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery return Call( typeof(SplitQueryingEnumerable).GetMethods() - .Single(m => m.Name == nameof(FromSqlQueryingEnumerable.Create)) + .Single(m => m.Name == nameof(SplitQueryingEnumerable.Create)) .MakeGenericMethod(shaper.ReturnType), Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - relationalCommandCache, + relationalCommandResolver, readerColumnsExpression, shaper, relatedDataLoadersParameter, @@ -393,7 +419,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery .Single(m => m.Name == nameof(SingleQueryingEnumerable.Create)) .MakeGenericMethod(shaper.ReturnType), Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - relationalCommandCache, + relationalCommandResolver, readerColumnsExpression, shaper, Constant(_contextType), @@ -450,8 +476,16 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery return result; } - private Expression CreateRelationalCommandCacheExpression(Expression queryExpression) + private Expression CreateRelationalCommandResolverExpression(Expression queryExpression) { + // In the regular case, we generate code that accesses the RelationalCommandCache (which invokes the 2nd part of the + // query pipeline). This is only skipped in query precompilation with few nullable parameters, where we pregenerate the SQL, + // bypassing the RelationalCommandCache (no more 2nd part of the query pipeline at runtime). + if (_isPrecompiling && TryGeneratePregeneratedCommandResolver(queryExpression, out var relationalCommandResolver)) + { + return relationalCommandResolver; + } + var relationalCommandCache = new RelationalCommandCache( Dependencies.MemoryCache, RelationalDependencies.QuerySqlGeneratorFactory, @@ -459,7 +493,7 @@ private Expression CreateRelationalCommandCacheExpression(Expression queryExpres queryExpression, _useRelationalNulls); - return RelationalDependencies.RelationalLiftableConstantFactory.CreateLiftableConstant( + var commandLiftableConstant = RelationalDependencies.RelationalLiftableConstantFactory.CreateLiftableConstant( relationalCommandCache, c => new RelationalCommandCache( c.Dependencies.MemoryCache, @@ -469,5 +503,230 @@ private Expression CreateRelationalCommandCacheExpression(Expression queryExpres _useRelationalNulls), "relationalCommandCache", typeof(RelationalCommandCache)); + + var parametersParameter = Parameter(typeof(IReadOnlyDictionary), "parameters"); + + return Lambda( + Call( + commandLiftableConstant, + typeof(RelationalCommandCache).GetMethod(nameof(RelationalCommandCache.GetRelationalCommandTemplate))!, + parametersParameter), + parametersParameter); + + bool TryGeneratePregeneratedCommandResolver( + Expression select, + [NotNullWhen(true)] out Expression? resolver) + { + var parameters = new Dictionary(); + var nullableParameterList = new List(); + foreach (var parameter in new SqlParameterLocator().LocateParameters(select)) + { + if (parameter.IsNullable) + { + nullableParameterList.Add(parameter); + parameters[parameter.Name] = null; + } + else + { + parameters[parameter.Name] = GenerateNonNullParameterValue(parameter.Type); + } + } + + var numNullableParameters = nullableParameterList.Count; + + if (numNullableParameters > MaxNullableParametersForPregeneratedSql) + { + resolver = null; + return false; + } + + var parameterDictionaryParameter = Parameter(typeof(IReadOnlyDictionary), "parameters"); + var resultParameter = Parameter(typeof(IRelationalCommandTemplate), "result"); + Expression resolverBody; + bool canCache; + + if (numNullableParameters == 0) + { + resolverBody = GenerateRelationalCommandExpression(parameters, out canCache); + } + else + { + var parameterIndex = 0; + + resolverBody = Core(parameterIndex); + } + + // If we can't cache the query SQL, we can't pregenerate it; flow down to the generic RelationalCommandCache path. + // Note that in theory certain parameter nullability can be uncacheable, whereas others may be cacheable; so we could + // keep pregenerated SQLs where that works, and flow down to the generic RelationalCommandCache path otherwise. + if (!canCache) + { + resolver = null; + return false; + } + + resolver = Lambda(resolverBody, parameterDictionaryParameter); + return true; + + Expression Core(int parameterIndex) + { + var currentParameter = nullableParameterList[parameterIndex]; + Expression ifNull, ifNotNull; + ConditionalExpression ifThenElse; + + if (parameterIndex < numNullableParameters - 1) + { + var parameter = nullableParameterList[parameterIndex]; + parameters[parameter.Name] = null; + ifNull = Core(parameterIndex + 1); + if (!canCache) + { + return null!; + } + + parameters[parameter.Name] = GenerateNonNullParameterValue(parameter.Type); + ifNotNull = Core(parameterIndex + 1); + + ifThenElse = + IfThenElse( + Equal( + Property(parameterDictionaryParameter, "Item", Constant(currentParameter.Name)), + Constant(null, typeof(object))), + ifNull, + ifNotNull); + } + else + { + // We've reached the last parameter; generate the SQL and see if we can cache it. + ifNull = LastParameter(withNull: true); + if (!canCache) + { + return null!; + } + + ifNotNull = LastParameter(withNull: false); + + ifThenElse = + IfThenElse( + Equal( + Property(parameterDictionaryParameter, "Item", Constant(currentParameter.Name)), + Constant(null, typeof(object))), + Assign(resultParameter, ifNull), + Assign(resultParameter, ifNotNull)); + } + + return parameterIndex > 0 + ? Block(ifThenElse, resultParameter) + : Block(variables: [resultParameter], ifThenElse, resultParameter); + + Expression LastParameter(bool withNull) + { + var parameter = nullableParameterList[parameterIndex]; + parameters[parameter.Name] = withNull ? null : GenerateNonNullParameterValue(parameter.Type); + + return GenerateRelationalCommandExpression(parameters, out canCache); + } + } + + static object GenerateNonNullParameterValue(Type type) + { + // In general, the (2nd part of) the query pipeline doesn't care about actual values - it mostly looks a null vs. non-null. + // However, in some specific cases, it looks at actual parameters values - this happens e.g. for Contains over parameter, when + // actual values are integrated into the SQL. For these cases, SQL can't be cached in any case and so pregeneration isn't + // possible; but we still want to avoid casting exceptions, so we attempt to have a valid, correctly-typed value as the + // parameter, and this method attempts to do that in a reasonable way. + if (type == typeof(string)) + { + return string.Empty; + } + + if (type.IsArray) + { + return Array.CreateInstance(type.GetElementType()!, new int[type.GetArrayRank()]); + } + + try + { + return Activator.CreateInstance(type)!; + } + catch + { + return new object(); + } + } + + Expression GenerateRelationalCommandExpression(IReadOnlyDictionary parameters, out bool canCache) + { + var queryExpression = _relationalParameterBasedSqlProcessor.Optimize(select, parameters, out canCache); + if (!canCache) + { + return null!; + } + + var relationalCommandTemplate = _querySqlGeneratorFactory.Create().GetCommand(queryExpression); + + var liftableConstantContextParameter = Parameter(typeof(RelationalMaterializerLiftableConstantContext), "c"); + // TODO: Instead of instantiating RelationalCommand directly go through the provider's RelationalCommandBuilder (#33516) + return RelationalDependencies.RelationalLiftableConstantFactory.CreateLiftableConstant( + null!, // Not actually needed, as this is only used as a liftable constant + Lambda>( + New( + _relationalCommandConstructor ??= typeof(RelationalCommand) + .GetConstructor( + [ + typeof(RelationalCommandBuilderDependencies), + typeof(string), + typeof(IReadOnlyList) + ])!, + Property( + liftableConstantContextParameter, + _commandBuilderDependenciesProperty ??= typeof(RelationalMaterializerLiftableConstantContext) + .GetProperty(nameof(RelationalMaterializerLiftableConstantContext.CommandBuilderDependencies))!), + Constant(relationalCommandTemplate.CommandText), + NewArrayInit( + typeof(IRelationalParameter), + relationalCommandTemplate.Parameters.Cast().Select( + p => (Expression)New( + _typeMappedRelationalParameterConstructor ??= typeof(TypeMappedRelationalParameter) + .GetConstructor( + [ + typeof(string), + typeof(string), + typeof(RelationalTypeMapping), + typeof(bool?), + typeof(ParameterDirection) + ])!, + Constant(p.InvariantName), + Constant(p.Name), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(p.RelationalTypeMapping), + Constant(p.IsNullable, typeof(bool?)), + Constant(p.Direction))).ToArray())), + liftableConstantContextParameter), + "relationalCommandTemplate", + typeof(IRelationalCommandTemplate)); + } + } + } + + private sealed class SqlParameterLocator : ExpressionVisitor + { + private HashSet _parameters = null!; + + public IReadOnlySet LocateParameters(Expression selectExpression) + { + _parameters = new(); + Visit(selectExpression); + return _parameters; + } + + protected override Expression VisitExtension(Expression node) + { + if (node is SqlParameterExpression parameter) + { + _parameters.Add(parameter); + } + + return base.VisitExtension(node); + } } } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index fb5439908b3..d1e1e7cbae3 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -1046,9 +1046,24 @@ protected override Expression VisitNewArray(NewArrayExpression newArrayExpressio /// protected override Expression VisitParameter(ParameterExpression parameterExpression) - => parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true - ? new SqlParameterExpression(parameterExpression.Name, parameterExpression.Type, null) - : throw new InvalidOperationException(CoreStrings.TranslationFailed(parameterExpression.Print())); + { + if (parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true) + { + // If we're precompiling a query, nullability information about reference type parameters has been extracted by the + // funcletizer and stored on the query compilation context; use that information when creating the SqlParameterExpression. + if (_queryCompilationContext.NonNullableReferenceTypeParameters.Contains(parameterExpression.Name)) + { + Check.DebugAssert( + _queryCompilationContext.IsPrecompiling, + "Parameters can only be known to has non-nullable reference types in query precompilation."); + return new SqlParameterExpression(parameterExpression.Name, parameterExpression.Type, typeMapping: null, nullable: false); + } + + return new SqlParameterExpression(parameterExpression.Name, parameterExpression.Type, typeMapping: null); + } + + throw new InvalidOperationException(CoreStrings.TranslationFailed(parameterExpression.Print())); + } /// protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExpression) diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs index 40469671761..5e5ba92a6c7 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs @@ -21,8 +21,15 @@ public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typ { } - private SqlParameterExpression(string name, Type type, bool nullable, RelationalTypeMapping? typeMapping) - : base(type, typeMapping) + /// + /// Creates a new instance of the class. + /// + /// The parameter name. + /// The of the expression. + /// Whether this parameter can have null values. + /// The associated with the expression. + public SqlParameterExpression(string name, Type type, bool nullable, RelationalTypeMapping? typeMapping) + : base(type.UnwrapNullableType(), typeMapping) { Name = name; IsNullable = nullable; diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs index aca56014c1b..fc4ca3b3635 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.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 System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// @@ -23,9 +25,29 @@ public class SqlServerQueryCompilationContext : RelationalQueryCompilationContex QueryCompilationContextDependencies dependencies, RelationalQueryCompilationContextDependencies relationalDependencies, bool async, - bool precompiling, bool multipleActiveResultSetsEnabled) - : base(dependencies, relationalDependencies, async, precompiling) + : this( + dependencies, relationalDependencies, async, multipleActiveResultSetsEnabled, precompiling: false, + nonNullableReferenceTypeParameters: null) + { + _multipleActiveResultSetsEnabled = multipleActiveResultSetsEnabled; + } + + /// + /// 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. + /// + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + public SqlServerQueryCompilationContext( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies, + bool async, + bool multipleActiveResultSetsEnabled, + bool precompiling, + IReadOnlySet? nonNullableReferenceTypeParameters) + : base(dependencies, relationalDependencies, async, precompiling, nonNullableReferenceTypeParameters) { _multipleActiveResultSetsEnabled = multipleActiveResultSetsEnabled; } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContextFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContextFactory.cs index d7aa170c427..ea4b25e3315 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContextFactory.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContextFactory.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; @@ -47,9 +48,9 @@ public class SqlServerQueryCompilationContextFactory : IQueryCompilationContextF /// 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 QueryCompilationContext Create(bool async, bool precompiling) + public virtual QueryCompilationContext Create(bool async) => new SqlServerQueryCompilationContext( - Dependencies, RelationalDependencies, async, precompiling, _sqlServerConnection.IsMultipleActiveResultSetsEnabled); + Dependencies, RelationalDependencies, async, _sqlServerConnection.IsMultipleActiveResultSetsEnabled); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -57,6 +58,9 @@ public virtual QueryCompilationContext Create(bool async, bool precompiling) /// 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 QueryCompilationContext Create(bool async) - => throw new UnreachableException("The overload with `precompiling` should be called"); + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + public virtual QueryCompilationContext CreatePrecompiled(bool async, IReadOnlySet nonNullableReferenceTypeParameters) + => new SqlServerQueryCompilationContext( + Dependencies, RelationalDependencies, async, _sqlServerConnection.IsMultipleActiveResultSetsEnabled, precompiling: true, + nonNullableReferenceTypeParameters); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContext.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContext.cs index 4f6b189126a..587e8af2bee 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContext.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContext.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 System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; /// @@ -17,12 +19,28 @@ public class SqliteQueryCompilationContext : RelationalQueryCompilationContext /// 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 SqliteQueryCompilationContext( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies, + bool async) + : this(dependencies, relationalDependencies, async, precompiling: false, nonNullableReferenceTypeParameters: null) + { + } + + /// + /// 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. + /// + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] public SqliteQueryCompilationContext( QueryCompilationContextDependencies dependencies, RelationalQueryCompilationContextDependencies relationalDependencies, bool async, - bool precompiling) - : base(dependencies, relationalDependencies, async, precompiling) + bool precompiling, + IReadOnlySet? nonNullableReferenceTypeParameters) + : base(dependencies, relationalDependencies, async, precompiling, nonNullableReferenceTypeParameters) { } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContextFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContextFactory.cs index c088df764ff..9c3ceab20e7 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContextFactory.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryCompilationContextFactory.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 System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; /// @@ -39,10 +41,9 @@ public class SqliteQueryCompilationContextFactory : IQueryCompilationContextFact /// Creates a new . /// /// Specifies whether the query is async. - /// Indicates whether the query is being precompiled. /// The created query compilation context. - public QueryCompilationContext Create(bool async, bool precompiling) - => new SqliteQueryCompilationContext(Dependencies, RelationalDependencies, async, precompiling); + public QueryCompilationContext Create(bool async) + => new SqliteQueryCompilationContext(Dependencies, RelationalDependencies, async); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -50,6 +51,8 @@ public QueryCompilationContext Create(bool async, bool precompiling) /// 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 QueryCompilationContext Create(bool async) - => throw new UnreachableException("The overload with `precompiling` should be called"); + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + public virtual QueryCompilationContext CreatePrecompiled(bool async, IReadOnlySet nonNullableReferenceTypeParameters) + => new SqliteQueryCompilationContext( + Dependencies, RelationalDependencies, async, precompiling: true, nonNullableReferenceTypeParameters); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs index 41265f09819..0de552d83c4 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs @@ -575,14 +575,9 @@ private static Type GetProviderType(SqlExpression expression) _ => expression }; - private sealed class FakeMemberInfo : MemberInfo + private sealed class FakeMemberInfo(string name) : MemberInfo { - public FakeMemberInfo(string name) - { - Name = name; - } - - public override string Name { get; } + public override string Name { get; } = name; public override object[] GetCustomAttributes(bool inherit) => throw new NotSupportedException(); diff --git a/src/EFCore/Query/IQueryCompilationContextFactory.cs b/src/EFCore/Query/IQueryCompilationContextFactory.cs index 6206a0dc39f..8c095016a06 100644 --- a/src/EFCore/Query/IQueryCompilationContextFactory.cs +++ b/src/EFCore/Query/IQueryCompilationContextFactory.cs @@ -33,11 +33,9 @@ public interface IQueryCompilationContextFactory /// Creates a new . /// /// Specifies whether the query is async. - /// Indicates whether the query is being precompiled. + /// Names of parameters which have non-nullable reference types. /// The created query compilation context. [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] - QueryCompilationContext Create(bool async, bool precompiling) - => precompiling - ? throw new InvalidOperationException(CoreStrings.PrecompiledQueryNotSupported) - : Create(async); + QueryCompilationContext CreatePrecompiled(bool async, IReadOnlySet nonNullableReferenceTypeParameters) + => throw new InvalidOperationException(CoreStrings.PrecompiledQueryNotSupported); } diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index c905f8d45a5..e361987a8d8 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -74,7 +74,7 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor /// A cache of tree fragments that have already been parameterized, along with their parameter. This allows us to reuse the same /// query parameter twice when the same captured variable is referenced in the query. /// - private readonly Dictionary _parameterizedValues = new(ExpressionEqualityComparer.Instance); + private readonly Dictionary _parameterizedValues = new(ExpressionEqualityComparer.Instance); /// /// Used only when evaluating arbitrary QueryRootExpressions (specifically SqlQueryRootExpression), to force any evaluatable nested @@ -91,11 +91,14 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor private IQueryProvider? _currentQueryProvider; private State _state; private IParameterValues _parameterValues = null!; + private HashSet? _nonNullableReferenceTypeParameters; private readonly IModel _model; private readonly ContextParameterReplacer _contextParameterReplacer; private readonly IDiagnosticsLogger _logger; + private static readonly IReadOnlySet EmptyStringSet = new HashSet(); + private static readonly MethodInfo ReadOnlyCollectionIndexerGetter = typeof(ReadOnlyCollection).GetProperties() .Single(p => p.GetIndexParameters() is { Length: 1 } indexParameters && indexParameters[0].ParameterType == typeof(int)).GetMethod!; @@ -137,8 +140,8 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor } /// - /// Processes an expression tree, extracting parameters and evaluating evaluatable fragments as part of the pass. - /// Used for regular query execution (neither compiled nor pre-compiled). + /// Processes an expression tree, extracting parameters and evaluating evaluatable fragments as part of the pass. + /// Used for regular query execution (neither compiled nor pre-compiled). /// /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -149,15 +152,42 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor public virtual Expression ExtractParameters( Expression expression, IParameterValues parameterValues, - bool precompiledQuery, bool parameterize, bool clearParameterizedValues) + { + var result = ExtractParameters( + expression, parameterValues, parameterize, clearParameterizedValues, precompiledQuery: false, + out var nonNullableReferenceTypeParameters); + Check.DebugAssert( + nonNullableReferenceTypeParameters.Count == 0, + "Non-nullable reference type parameters can only be detected when precompiling."); + return result; + } + + /// + /// Processes an expression tree, extracting parameters and evaluating evaluatable fragments as part of the pass. + /// Used for regular query execution (neither compiled nor pre-compiled). + /// + /// + /// 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. + /// + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + public virtual Expression ExtractParameters( + Expression expression, + IParameterValues parameterValues, + bool parameterize, + bool clearParameterizedValues, + bool precompiledQuery, + out IReadOnlySet nonNullableReferenceTypeParameters) { Reset(clearParameterizedValues); _parameterValues = parameterValues; - _precompiledQuery = precompiledQuery; _parameterize = parameterize; _calculatingPath = false; + _precompiledQuery = precompiledQuery; var root = Visit(expression, out var state); @@ -169,6 +199,8 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor root = ProcessEvaluatableRoot(root, ref state); } + nonNullableReferenceTypeParameters = _nonNullableReferenceTypeParameters ?? EmptyStringSet; + return root; } @@ -1875,6 +1907,18 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state // Regular parameter extraction mode; client-evaluate the subtree and replace it with a query parameter. state = State.NoEvaluatability; + // TODO: #33508 + // TODO: This currently only knows about the NRT status of a directly captured variable, but not the NRT status of any + // TODO: larger expression composed on top of a captured variable (e.g. Where(b => b.Name == foo + "Bla")) + // TODO: This would require bubbling nullability information up the tree via State. + if (_precompiledQuery + && !evaluatableRoot.Type.IsValueType + && evaluatableRoot is MemberExpression { Member: IParameterNullabilityInfo { IsNonNullableReferenceType: true } }) + { + _nonNullableReferenceTypeParameters ??= []; + _nonNullableReferenceTypeParameters.Add(parameterName); + } + _parameterValues.AddParameter(parameterName, value); return _parameterizedValues[evaluatableRoot] = Parameter(evaluatableRoot.Type, parameterName); diff --git a/src/EFCore/Query/Internal/IParameterNullabilityInfo.cs b/src/EFCore/Query/Internal/IParameterNullabilityInfo.cs new file mode 100644 index 00000000000..53f6e112c8c --- /dev/null +++ b/src/EFCore/Query/Internal/IParameterNullabilityInfo.cs @@ -0,0 +1,21 @@ +// 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.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 interface IParameterNullabilityInfo +{ + /// + /// 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 bool IsNonNullableReferenceType { get; } +} diff --git a/src/EFCore/Query/Internal/IParameterValues.cs b/src/EFCore/Query/Internal/IParameterValues.cs index 8d52ce91f32..e9ba4c5a770 100644 --- a/src/EFCore/Query/Internal/IParameterValues.cs +++ b/src/EFCore/Query/Internal/IParameterValues.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 System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Query.Internal; /// diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index f91f6f77a5f..45b85cf0962 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -208,9 +208,11 @@ protected override Expression VisitExtension(Expression extensionExpression) // Apply defining query only when it is not custom query root && entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)) { + // TODO: #33509: merge NRT information (nonNullableReferenceTypeParameters) for parameters introduced by the query + // TODO: filter into the QueryCompilationContext.NonNullableReferenceTypeParameters var processedDefiningQueryBody = _funcletizer.ExtractParameters( - definingQuery.Body, _parameters, _queryCompilationContext.IsPrecompiling, parameterize: false, - clearParameterizedValues: false); + definingQuery.Body, _parameters, parameterize: false, clearParameterizedValues: false, + _queryCompilationContext.IsPrecompiling, out var nonNullableReferenceTypeParameters); processedDefiningQueryBody = _queryTranslationPreprocessor.NormalizeQueryableMethod(processedDefiningQueryBody); processedDefiningQueryBody = _nullCheckRemovingExpressionVisitor.Visit(processedDefiningQueryBody); processedDefiningQueryBody = @@ -1753,9 +1755,11 @@ private Expression ApplyQueryFilter(IEntityType entityType, NavigationExpansionE if (!_parameterizedQueryFilterPredicateCache.TryGetValue(rootEntityType, out var filterPredicate)) { filterPredicate = queryFilter; + // TODO: #33509: merge NRT information (nonNullableReferenceTypeParameters) for parameters introduced by the query + // TODO: filter into the QueryCompilationContext.NonNullableReferenceTypeParameters filterPredicate = (LambdaExpression)_funcletizer.ExtractParameters( - filterPredicate, _parameters, _queryCompilationContext.IsPrecompiling, parameterize: false, - clearParameterizedValues: false); + filterPredicate, _parameters, parameterize: false, clearParameterizedValues: false, + _queryCompilationContext.IsPrecompiling, out var nonNullableReferenceTypeParameters); filterPredicate = (LambdaExpression)_queryTranslationPreprocessor.NormalizeQueryableMethod(filterPredicate); // We need to do entity equality, but that requires a full method call on a query root to properly flow the diff --git a/src/EFCore/Query/Internal/QueryCompilationContextFactory.cs b/src/EFCore/Query/Internal/QueryCompilationContextFactory.cs index 857ef233ec4..6ab3373a5b3 100644 --- a/src/EFCore/Query/Internal/QueryCompilationContextFactory.cs +++ b/src/EFCore/Query/Internal/QueryCompilationContextFactory.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 System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Query.Internal; /// @@ -33,8 +35,8 @@ public QueryCompilationContextFactory(QueryCompilationContextDependencies depend /// 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 QueryCompilationContext Create(bool async, bool precompiling) - => new(Dependencies, async, precompiling); + public virtual QueryCompilationContext Create(bool async) + => new(Dependencies, async); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -42,6 +44,7 @@ public virtual QueryCompilationContext Create(bool async, bool precompiling) /// 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 QueryCompilationContext Create(bool async) - => throw new UnreachableException("The overload with `precompiling` should be called"); + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + public virtual QueryCompilationContext CreatePrecompiled(bool async, IReadOnlySet nonNullableReferenceTypeParameters) + => new(Dependencies, async, precompiling: true, nonNullableReferenceTypeParameters); } diff --git a/src/EFCore/Query/Internal/QueryCompiler.cs b/src/EFCore/Query/Internal/QueryCompiler.cs index 3dfa3aea333..724990671fd 100644 --- a/src/EFCore/Query/Internal/QueryCompiler.cs +++ b/src/EFCore/Query/Internal/QueryCompiler.cs @@ -135,8 +135,12 @@ var compiledQuery [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] public virtual Expression> PrecompileQuery(Expression query, bool async) { - query = ExtractParameters(query, _queryContextFactory.Create(), _logger, precompiledQuery: true); - return _database.CompileQueryExpression(query, async); + query = new ExpressionTreeFuncletizer(_model, _evaluatableExpressionFilter, _contextType, generateContextAccessors: false, _logger) + .ExtractParameters( + query, _queryContextFactory.Create(), parameterize: true, clearParameterizedValues: true, precompiledQuery: true, + out var nonNullableReferenceTypeParameters); + + return _database.CompileQueryExpression(query, async, nonNullableReferenceTypeParameters); } /// @@ -150,8 +154,7 @@ var compiledQuery IParameterValues parameterValues, IDiagnosticsLogger logger, bool compiledQuery = false, - bool precompiledQuery = false, bool generateContextAccessors = false) => new ExpressionTreeFuncletizer(_model, _evaluatableExpressionFilter, _contextType, generateContextAccessors: false, logger) - .ExtractParameters(query, parameterValues, precompiledQuery, parameterize: !compiledQuery, clearParameterizedValues: true); + .ExtractParameters(query, parameterValues, parameterize: !compiledQuery, clearParameterizedValues: true); } diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index a6809179cbd..c229a751455 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -53,6 +53,8 @@ public class QueryCompilationContext /// public static readonly Expression NotTranslatedExpression = new NotTranslatedExpressionType(); + private static readonly IReadOnlySet EmptySet = new HashSet(); + private readonly IQueryTranslationPreprocessorFactory _queryTranslationPreprocessorFactory; private readonly IQueryableMethodTranslatingExpressionVisitorFactory _queryableMethodTranslatingExpressionVisitorFactory; private readonly IQueryTranslationPostprocessorFactory _queryTranslationPostprocessorFactory; @@ -71,7 +73,7 @@ public class QueryCompilationContext public QueryCompilationContext( QueryCompilationContextDependencies dependencies, bool async) - : this(dependencies, async, precompiling: false) + : this(dependencies, async, precompiling: false, nonNullableReferenceTypeParameters: null) { } @@ -81,11 +83,13 @@ public class QueryCompilationContext /// Parameter object containing dependencies for this class. /// A bool value indicating whether it is for async query. /// Indicates whether the query is being precompiled. + /// Names of parameters which have non-nullable reference types. [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] public QueryCompilationContext( QueryCompilationContextDependencies dependencies, bool async, - bool precompiling) + bool precompiling, + IReadOnlySet? nonNullableReferenceTypeParameters) { Dependencies = dependencies; IsAsync = async; @@ -96,6 +100,7 @@ public class QueryCompilationContext ContextOptions = dependencies.ContextOptions; ContextType = dependencies.ContextType; Logger = dependencies.Logger; + NonNullableReferenceTypeParameters = nonNullableReferenceTypeParameters ?? EmptySet; _queryTranslationPreprocessorFactory = dependencies.QueryTranslationPreprocessorFactory; _queryableMethodTranslatingExpressionVisitorFactory = dependencies.QueryableMethodTranslatingExpressionVisitorFactory; @@ -164,6 +169,12 @@ public class QueryCompilationContext /// public virtual Type ContextType { get; } + /// + /// Names of parameters which have non-nullable reference types. + /// + [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] + public virtual IReadOnlySet NonNullableReferenceTypeParameters { get; } + /// /// Adds a tag to . /// diff --git a/src/EFCore/Query/QueryContext.cs b/src/EFCore/Query/QueryContext.cs index 9237d7ff653..08bad5a6903 100644 --- a/src/EFCore/Query/QueryContext.cs +++ b/src/EFCore/Query/QueryContext.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; diff --git a/src/EFCore/Storage/Database.cs b/src/EFCore/Storage/Database.cs index 95606b3df64..9614e156bf5 100644 --- a/src/EFCore/Storage/Database.cs +++ b/src/EFCore/Storage/Database.cs @@ -66,13 +66,16 @@ protected Database(DatabaseDependencies dependencies) /// public virtual Func CompileQuery(Expression query, bool async) => Dependencies.QueryCompilationContextFactory - .Create(async, precompiling: false) + .Create(async) .CreateQueryExecutor(query); /// [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] - public virtual Expression> CompileQueryExpression(Expression query, bool async) + public virtual Expression> CompileQueryExpression( + Expression query, + bool async, + IReadOnlySet nonNullableReferenceTypeParameters) => Dependencies.QueryCompilationContextFactory - .Create(async, precompiling: true) + .CreatePrecompiled(async, nonNullableReferenceTypeParameters) .CreateQueryExecutorExpression(query); } diff --git a/src/EFCore/Storage/IDatabase.cs b/src/EFCore/Storage/IDatabase.cs index 69ee1952b1d..ce0c1d974ad 100644 --- a/src/EFCore/Storage/IDatabase.cs +++ b/src/EFCore/Storage/IDatabase.cs @@ -63,8 +63,12 @@ public interface IDatabase /// /// The type of query result. /// The query to compile. + /// Names of parameters which have non-nullable reference types.. /// A value indicating whether this is an async query. /// An expression tree which can be used to execute the query. [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] - Expression> CompileQueryExpression(Expression query, bool async); + Expression> CompileQueryExpression( + Expression query, + bool async, + IReadOnlySet nonNullableReferenceTypeParameters); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalFixture.cs index e2ecee44abd..fd4d1f19e9d 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalFixture.cs @@ -17,7 +17,10 @@ public TestSqlLoggerFactory TestSqlLoggerFactory protected override IServiceCollection AddServices(IServiceCollection serviceCollection) => base.AddServices(serviceCollection) - .AddScoped(); + // Bomb if any query is executed that wasn't precompiled + .AddScoped() + // Don't pregenerate SQLs to make sure we're testing quotability of SQL expressions + .AddScoped(); public new RelationalTestStore TestStore => (RelationalTestStore)base.TestStore; @@ -31,4 +34,31 @@ protected override async Task SeedAsync(PrecompiledQueryRelationalTestBase.Preco } public abstract PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers { get; } + + public class NonSqlGeneratingShapedQueryCompilingExpressionVisitorFactory( + ShapedQueryCompilingExpressionVisitorDependencies dependencies, + RelationalShapedQueryCompilingExpressionVisitorDependencies relationalDependencies) + : IShapedQueryCompilingExpressionVisitorFactory + { + public ShapedQueryCompilingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) + => new NonSqlGeneratingShapedQueryCompilingExpressionVisitor( + dependencies, + relationalDependencies, + queryCompilationContext); + } + + /// + /// A replacement for which does not pregenerate + /// any SQL, ever. This means that we always generate the SQL as an expression tree in the interceptor, which allows us + /// to check that all SQL expressions are properly quotable. + /// + public class NonSqlGeneratingShapedQueryCompilingExpressionVisitor( + ShapedQueryCompilingExpressionVisitorDependencies dependencies, + RelationalShapedQueryCompilingExpressionVisitorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : RelationalShapedQueryCompilingExpressionVisitor(dependencies, relationalDependencies, queryCompilationContext) + { + protected override int MaxNullableParametersForPregeneratedSql + => int.MinValue; + } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs index 50290f7c1fa..d304fb03ee4 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs @@ -13,6 +13,10 @@ namespace Microsoft.EntityFrameworkCore.Query; // ReSharper disable InconsistentNaming +/// +/// General tests for precompiled queries. +/// See also for tests specifically related to SQL pregeneration. +/// public class PrecompiledQueryRelationalTestBase { public PrecompiledQueryRelationalTestBase(PrecompiledQueryRelationalFixture fixture, ITestOutputHelper testOutputHelper) diff --git a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalFixture.cs new file mode 100644 index 00000000000..247db65c602 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalFixture.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; +using Blog = Microsoft.EntityFrameworkCore.Query.PrecompiledSqlPregenerationQueryRelationalTestBase.Blog; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class PrecompiledSqlPregenerationQueryRelationalFixture + : SharedStoreFixtureBase, ITestSqlLoggerFactory +{ + protected override string StoreName + => "PrecompiledSqlPregenerationQueryTest"; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection) + // Bomb if any query is executed that wasn't precompiled + .AddScoped(); + + protected override async Task SeedAsync(PrecompiledSqlPregenerationQueryRelationalTestBase.PrecompiledQueryContext context) + { + context.Blogs.AddRange( + new Blog { Id = 8, Name = "Blog1" }, + new Blog { Id = 9, Name = "Blog2" }); + await context.SaveChangesAsync(); + } + + public abstract PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers { get; } +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalTestBase.cs new file mode 100644 index 00000000000..77ffb7c372e --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledSqlPregenerationQueryRelationalTestBase.cs @@ -0,0 +1,316 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations.Schema; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore.Query.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// Test suite for SQL pregeneration scenarios with precompiled queries. +/// See for general precompiled query tests not related to +/// SQL pregeneration. +/// +public class PrecompiledSqlPregenerationQueryRelationalTestBase +{ + public PrecompiledSqlPregenerationQueryRelationalTestBase(PrecompiledSqlPregenerationQueryRelationalFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + TestOutputHelper = testOutputHelper; + + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected virtual bool AlwaysPrintGeneratedSources + => false; + + [ConditionalFact] + public virtual Task No_parameters() + => Test("""var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();"""); + + [ConditionalFact] + public virtual Task Non_nullable_value_type() + => Test( + """ +int id = 8; +var blogs = await context.Blogs.Where(b => b.Id == id).ToListAsync(); +"""); + + [ConditionalFact] + public virtual Task Nullable_value_type() + => Test( + """ +int? id = 8; +var blogs = await context.Blogs.Where(b => b.Id == id).ToListAsync(); +""", + interceptorCodeAsserter: code => + { + Assert.DoesNotContain(nameof(RelationalCommandCache), code); + + AssertContains( + """ +if (parameters["__id_0"] == null) +{ + result = relationalCommandTemplate; +} +else +{ + result = relationalCommandTemplate0; +} +""", code); + }); + + [ConditionalFact] + public virtual Task Nullable_reference_type() + => Test( + """ +string? name = "bar"; +var blogs = await context.Blogs.Where(b => b.Name == name).ToListAsync(); +""", + interceptorCodeAsserter: code => + { + Assert.DoesNotContain(nameof(RelationalCommandCache), code); + + AssertContains( + """ +if (parameters["__name_0"] == null) +{ + result = relationalCommandTemplate; +} +else +{ + result = relationalCommandTemplate0; +} +""", code); + }); + + [ConditionalFact] + public virtual Task Non_nullable_reference_type() + => Test( + """ +string name = "bar"; +var blogs = await context.Blogs.Where(b => b.Name == name).ToListAsync(); +"""); + + [ConditionalFact] + public virtual Task Nullable_and_non_nullable_value_types() + => Test( + """ +int? id1 = 8; +int id2 = 9; +var blogs = await context.Blogs.Where(b => b.Id == id1 || b.Id == id2).ToListAsync(); +""", + interceptorCodeAsserter: code => + { + Assert.DoesNotContain(nameof(RelationalCommandCache), code); + + AssertContains( + """ +if (parameters["__id1_0"] == null) +{ + result = relationalCommandTemplate; +} +else +{ + result = relationalCommandTemplate0; +} +""", code); + }); + + [ConditionalFact] + public virtual Task Two_nullable_reference_types() + => Test( + """ +string? name1 = "foo"; +string? name2 = "bar"; +var blogs = await context.Blogs.Where(b => b.Name == name1 || b.Name == name2).ToListAsync(); +""", + interceptorCodeAsserter: code => + { + Assert.DoesNotContain(nameof(RelationalCommandCache), code); + + AssertContains( + """ +if (parameters["__name1_0"] == null) +{ + if (parameters["__name2_1"] == null) + { + result = relationalCommandTemplate; + } + else + { + result = relationalCommandTemplate0; + } +} +else +{ + if (parameters["__name2_1"] == null) + { + result = relationalCommandTemplate1; + } + else + { + result = relationalCommandTemplate2; + } +} +""", code); + }); + + [ConditionalFact] + public virtual Task Two_non_nullable_reference_types() + => Test( + """ +string name1 = "foo"; +string name2 = "bar"; +var blogs = await context.Blogs.Where(b => b.Name == name1 || b.Name == name2).ToListAsync(); +"""); + + [ConditionalFact] + public virtual Task Nullable_and_non_nullable_reference_types() + => Test( + """ +string? name1 = "foo"; +string name2 = "bar"; +var blogs = await context.Blogs.Where(b => b.Name == name1 || b.Name == name2).ToListAsync(); +""", + interceptorCodeAsserter: code => + { + Assert.DoesNotContain(nameof(RelationalCommandCache), code); + + AssertContains( + """ +if (parameters["__name1_0"] == null) +{ + result = relationalCommandTemplate; +} +else +{ + result = relationalCommandTemplate0; +} +""", code); + }); + + [ConditionalFact] + public virtual Task Too_many_nullable_parameters_prevent_pregeneration() + => Test( + """ +string? name1 = "foo"; +string? name2 = "bar"; +string? name3 = "baz"; +string? name4 = "baq"; +var blogs = await context.Blogs.Where(b => b.Name == name1 || b.Name == name2 || b.Name == name3 || b.Name == name4).ToListAsync(); +""", + interceptorCodeAsserter: code => Assert.Contains(nameof(RelationalCommandCache), code)); + + [ConditionalFact] + public virtual Task Many_non_nullable_parameters_do_not_prevent_pregeneration() + => Test( + """ +string name1 = "foo"; +string name2 = "bar"; +string name3 = "baz"; +string name4 = "baq"; +var blogs = await context.Blogs.Where(b => b.Name == name1 || b.Name == name2 || b.Name == name3 || b.Name == name4).ToListAsync(); +"""); + + #region Tests for the different querying enumerables + + [ConditionalFact] + public virtual Task Include_single_query() + => Test( + """ +var blogs = await context.Blogs + .Include(b => b.Posts) + .ToListAsync(); +"""); + + [ConditionalFact] + public virtual Task Include_split_query() + => Test( + """ +var blogs = await context.Blogs + .Include(b => b.Posts) + .AsSplitQuery() + .ToListAsync(); +"""); + + [ConditionalFact] + public virtual Task Final_GroupBy() + => Test("var blogs = await context.Blogs.GroupBy(b => b.Name).ToListAsync();"); + + #endregion Tests for the different querying enumerables + + public class PrecompiledQueryContext(DbContextOptions options) : DbContext(options) + { + public DbSet Blogs { get; set; } = null!; + } + + protected PrecompiledSqlPregenerationQueryRelationalFixture Fixture { get; } + protected ITestOutputHelper TestOutputHelper { get; } + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + /// + /// Normalizes line endings and strips all leading whitespace before checking substring containment. + /// + private void AssertContains(string expected, string actual) + => Assert.Contains( + Regex.Replace(expected.ReplaceLineEndings(), @"^\s*", "", RegexOptions.Multiline), + Regex.Replace(actual.ReplaceLineEndings(), @"^\s*", "", RegexOptions.Multiline)); + + protected virtual async Task Test( + string sourceCode, + Action? interceptorCodeAsserter = null, + Action>? errorAsserter = null, + [CallerMemberName] string callerName = "") + { + // By default, make sure there's no mention of RelationalCommandCache in the generated interceptor code. + // That means that SQL was pregenerated. + interceptorCodeAsserter ??= code => Assert.DoesNotContain(nameof(RelationalCommandCache), code); + + await Fixture.PrecompiledQueryTestHelpers.Test( + """ +await using var context = new Microsoft.EntityFrameworkCore.Query.PrecompiledSqlPregenerationQueryRelationalTestBase.PrecompiledQueryContext(dbContextOptions); + +""" + + sourceCode, + Fixture.ServiceProvider.GetRequiredService(), + typeof(PrecompiledQueryContext), + interceptorCodeAsserter, + errorAsserter, + TestOutputHelper, + AlwaysPrintGeneratedSources, + callerName); + } + + public class Blog + { + public Blog() + { + } + + public Blog(int id, string name) + { + Id = id; + Name = name; + } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public string? Name { get; set; } + + public List Posts { get; set; } = new(); + } + + public class Post + { + public int Id { get; set; } + public string? Title { get; set; } + + public Blog? Blog { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs index e9aa263029d..928c97253a0 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs @@ -93,7 +93,7 @@ public static async Task Test(DbContextOptions dbContextOptions) "TestCompilation", syntaxTrees: [syntaxTree], _metadataReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); IReadOnlyList? generatedFiles = null; diff --git a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs index f54a1e559f2..15b0f1a4087 100644 --- a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs +++ b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs @@ -574,8 +574,11 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase #pragma warning disable EF9100 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. typeof(RelationalMaterializerLiftableConstantContext).GetMethod("get_RelationalDependencies"), typeof(RelationalMaterializerLiftableConstantContext).GetMethod("set_RelationalDependencies"), + typeof(RelationalMaterializerLiftableConstantContext).GetMethod("get_CommandBuilderDependencies"), + typeof(RelationalMaterializerLiftableConstantContext).GetMethod("set_CommandBuilderDependencies"), typeof(RelationalMaterializerLiftableConstantContext).GetMethod("Deconstruct", [typeof(ShapedQueryCompilingExpressionVisitorDependencies).MakeByRefType()]), typeof(RelationalMaterializerLiftableConstantContext).GetMethod("Deconstruct", [typeof(ShapedQueryCompilingExpressionVisitorDependencies).MakeByRefType(), typeof(RelationalShapedQueryCompilingExpressionVisitorDependencies).MakeByRefType()]), + typeof(RelationalMaterializerLiftableConstantContext).GetMethod("Deconstruct", [typeof(ShapedQueryCompilingExpressionVisitorDependencies).MakeByRefType(), typeof(RelationalShapedQueryCompilingExpressionVisitorDependencies).MakeByRefType(), typeof(RelationalCommandBuilderDependencies).MakeByRefType()]), #pragma warning restore EF9100 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. ]; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqlServerTest.cs new file mode 100644 index 00000000000..b187615aee8 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqlServerTest.cs @@ -0,0 +1,268 @@ +// 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; + +// ReSharper disable InconsistentNaming + +public class PrecompiledSqlPregenerationQuerySqlServerTest( + PrecompiledSqlPregenerationQuerySqlServerTest.PrecompiledSqlPregenerationQuerySqlServerFixture fixture, + ITestOutputHelper testOutputHelper) + : PrecompiledSqlPregenerationQueryRelationalTestBase(fixture, testOutputHelper), + IClassFixture +{ + protected override bool AlwaysPrintGeneratedSources + => false; + + public override async Task No_parameters() + { + await base.No_parameters(); + + AssertSql( + """ +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = N'foo' +"""); + } + + public override async Task Non_nullable_value_type() + { + await base.Non_nullable_value_type(); + + AssertSql( + """ +@__id_0='8' + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Id] = @__id_0 +"""); + } + + public override async Task Nullable_value_type() + { + await base.Nullable_value_type(); + + AssertSql( + """ +@__id_0='8' (Nullable = true) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Id] = @__id_0 +"""); + } + + public override async Task Nullable_reference_type() + { + await base.Nullable_reference_type(); + + AssertSql( + """ +@__name_0='bar' (Size = 4000) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = @__name_0 +"""); + } + + public override async Task Non_nullable_reference_type() + { + await base.Non_nullable_reference_type(); + + AssertSql( + """ +@__name_0='bar' (Nullable = false) (Size = 4000) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = @__name_0 +"""); + } + + public override async Task Nullable_and_non_nullable_value_types() + { + await base.Nullable_and_non_nullable_value_types(); + + AssertSql( + """ +@__id1_0='8' (Nullable = true) +@__id2_1='9' + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Id] = @__id1_0 OR [b].[Id] = @__id2_1 +"""); + } + + public override async Task Two_nullable_reference_types() + { + await base.Two_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' (Size = 4000) +@__name2_1='bar' (Size = 4000) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = @__name1_0 OR [b].[Name] = @__name2_1 +"""); + } + + public override async Task Two_non_nullable_reference_types() + { + await base.Two_non_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' (Nullable = false) (Size = 4000) +@__name2_1='bar' (Nullable = false) (Size = 4000) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = @__name1_0 OR [b].[Name] = @__name2_1 +"""); + } + + public override async Task Nullable_and_non_nullable_reference_types() + { + await base.Nullable_and_non_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' (Size = 4000) +@__name2_1='bar' (Nullable = false) (Size = 4000) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = @__name1_0 OR [b].[Name] = @__name2_1 +"""); + } + + public override async Task Too_many_nullable_parameters_prevent_pregeneration() + { + await base.Too_many_nullable_parameters_prevent_pregeneration(); + + AssertSql( + """ +@__name1_0='foo' (Size = 4000) +@__name2_1='bar' (Size = 4000) +@__name3_2='baz' (Size = 4000) +@__name4_3='baq' (Size = 4000) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = @__name1_0 OR [b].[Name] = @__name2_1 OR [b].[Name] = @__name3_2 OR [b].[Name] = @__name4_3 +"""); + } + + public override async Task Many_non_nullable_parameters_do_not_prevent_pregeneration() + { + await base.Many_non_nullable_parameters_do_not_prevent_pregeneration(); + + AssertSql( + """ +@__name1_0='foo' (Nullable = false) (Size = 4000) +@__name2_1='bar' (Nullable = false) (Size = 4000) +@__name3_2='baz' (Nullable = false) (Size = 4000) +@__name4_3='baq' (Nullable = false) (Size = 4000) + +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] = @__name1_0 OR [b].[Name] = @__name2_1 OR [b].[Name] = @__name3_2 OR [b].[Name] = @__name4_3 +"""); + } + + #region Tests for the different querying enumerables + + public override async Task Include_single_query() + { + await base.Include_single_query(); + + AssertSql( + """ +SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title] +FROM [Blogs] AS [b] +LEFT JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId] +ORDER BY [b].[Id] +"""); + } + + public override async Task Include_split_query() + { + await base.Include_split_query(); + + AssertSql( + """ +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +ORDER BY [b].[Id] +""", + // + """ +SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id] +FROM [Blogs] AS [b] +INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId] +ORDER BY [b].[Id] +"""); + } + + public override async Task Final_GroupBy() + { + await base.Final_GroupBy(); + + AssertSql( + """ +SELECT [b].[Name], [b].[Id] +FROM [Blogs] AS [b] +ORDER BY [b].[Name] +"""); + } + + #endregion Tests for the different querying enumerables + + [ConditionalFact] + public virtual async Task Do_not_cache_is_respected() + { + // The "do not cache" flag in the 2nd part of the query pipeline is turned on in provider-specific situations, so we test it + // here in SQL Server; note that SQL Server compatibility mode is set low to trigger this. + await Test( + """ +string[] names = ["foo", "bar"]; +var blogs = await context.Blogs.Where(b => names.Contains(b.Name)).ToListAsync(); +""", + interceptorCodeAsserter: code => Assert.Contains(nameof(RelationalCommandCache), code)); + + AssertSql( + """ +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] IN (N'foo', N'bar') +"""); + } + + public class PrecompiledSqlPregenerationQuerySqlServerFixture : PrecompiledSqlPregenerationQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + builder = base.AddOptions(builder); + + // TODO: Figure out if there's a nice way to continue using the retrying strategy + var sqlServerOptionsBuilder = new SqlServerDbContextOptionsBuilder(builder); + sqlServerOptionsBuilder + .UseCompatibilityLevel(120) + .ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } + + public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => SqlServerPrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqliteTest.cs new file mode 100644 index 00000000000..072134128f5 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrecompiledSqlPregenerationQuerySqliteTest.cs @@ -0,0 +1,22 @@ +// 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 PrecompiledSqlPregenerationQuerySqlServerTest( + PrecompiledSqlPregenerationQuerySqlServerTest.PrecompiledSqlPregenerationQuerySqliteFixture fixture, + ITestOutputHelper testOutputHelper) + : PrecompiledSqlPregenerationQueryRelationalTestBase(fixture, testOutputHelper), + IClassFixture +{ + protected override bool AlwaysPrintGeneratedSources + => false; + + public class PrecompiledSqlPregenerationQuerySqliteFixture : PrecompiledSqlPregenerationQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => SqlitePrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.Tests/Query/Internal/NavigationExpandingExpressionVisitorTests.cs b/test/EFCore.Tests/Query/Internal/NavigationExpandingExpressionVisitorTests.cs index 6144b826051..35ca67f91d7 100644 --- a/test/EFCore.Tests/Query/Internal/NavigationExpandingExpressionVisitorTests.cs +++ b/test/EFCore.Tests/Query/Internal/NavigationExpandingExpressionVisitorTests.cs @@ -34,7 +34,7 @@ public TestNavigationExpandingExpressionVisitor() contextOptions: null, logger: null, new TestInterceptors() - ), async: false, precompiling: false), + ), async: false), null, null) {