diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 21f9e5c07d1..1c0ee7f0ebf 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -12,9 +13,15 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.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 partial class CosmosShapedQueryCompilingExpressionVisitor { - private sealed class QueryingEnumerable : IEnumerable, IAsyncEnumerable + private sealed class QueryingEnumerable : IEnumerable, IAsyncEnumerable, IQueryingEnumerable { private readonly CosmosQueryContext _cosmosQueryContext; private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -48,6 +55,32 @@ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToke public IEnumerator GetEnumerator() => new Enumerator(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public string ToQueryString() + { + var sqlQuery = _querySqlGeneratorFactory.Create().GetSqlQuery( + (SelectExpression)new InExpressionValuesExpandingExpressionVisitor( + _sqlExpressionFactory, _cosmosQueryContext.ParameterValues).Visit(_selectExpression), + _cosmosQueryContext.ParameterValues); + + if (sqlQuery.Parameters.Count == 0) + { + return sqlQuery.Query; + } + + var builder = new StringBuilder(); + foreach (var parameter in sqlQuery.Parameters) + { + builder + .Append("-- ") + .Append(parameter.Name) + .Append("='") + .Append(parameter.Value) + .AppendLine("'"); + } + + return builder.Append(sqlQuery.Query).ToString(); + } + private sealed class Enumerator : IEnumerator { private IEnumerator _enumerator; diff --git a/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs b/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs index 5dfae7d1bec..78790302664 100644 --- a/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs +++ b/src/EFCore.InMemory/Properties/InMemoryStrings.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Reflection; @@ -21,6 +21,12 @@ public static class InMemoryStrings private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.EntityFrameworkCore.InMemory.Properties.InMemoryStrings", typeof(InMemoryStrings).Assembly); + /// + /// There is no query string because the in-memory provider does not use a string-based query language. + /// + public static string NoQueryStrings + => GetString("NoQueryStrings"); + /// /// Attempted to update or delete an entity that does not exist in the store. /// diff --git a/src/EFCore.InMemory/Properties/InMemoryStrings.resx b/src/EFCore.InMemory/Properties/InMemoryStrings.resx index 80390db54ed..c194012f7d9 100644 --- a/src/EFCore.InMemory/Properties/InMemoryStrings.resx +++ b/src/EFCore.InMemory/Properties/InMemoryStrings.resx @@ -1,17 +1,17 @@  - @@ -125,6 +125,9 @@ Transactions are not supported by the in-memory store. See http://go.microsoft.com/fwlink/?LinkId=800142 Warning InMemoryEventId.TransactionIgnoredWarning + + There is no query string because the in-memory provider does not use a string-based query language. + Attempted to update or delete an entity that does not exist in the store. diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 82c7b5297c8..1eb85c97120 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -7,15 +7,22 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.InMemory.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.InMemory.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 partial class InMemoryShapedQueryCompilingExpressionVisitor { - private sealed class QueryingEnumerable : IAsyncEnumerable, IEnumerable + private sealed class QueryingEnumerable : IAsyncEnumerable, IEnumerable, IQueryingEnumerable { private readonly QueryContext _queryContext; private readonly IEnumerable _innerEnumerable; @@ -44,6 +51,8 @@ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToke IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public string ToQueryString() => InMemoryStrings.NoQueryStrings; + private sealed class Enumerator : IEnumerator { private IEnumerator _enumerator; diff --git a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs index c8b70945ee0..43142a9782a 100644 --- a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs @@ -6,11 +6,13 @@ using System.Collections.Generic; using System.Data.Common; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; namespace Microsoft.EntityFrameworkCore.Query.Internal { @@ -20,7 +22,7 @@ namespace Microsoft.EntityFrameworkCore.Query.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 class QueryingEnumerable : IEnumerable, IAsyncEnumerable + public class QueryingEnumerable : IEnumerable, IAsyncEnumerable, IQueryingEnumerable { private readonly RelationalQueryContext _relationalQueryContext; private readonly RelationalCommandCache _relationalCommandCache; @@ -30,6 +32,12 @@ public class QueryingEnumerable : IEnumerable, IAsyncEnumerable private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + /// + /// 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 QueryingEnumerable( [NotNull] RelationalQueryContext relationalQueryContext, [NotNull] RelationalCommandCache relationalCommandCache, @@ -48,12 +56,71 @@ public QueryingEnumerable( _logger = logger; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// public virtual IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new AsyncEnumerator(this, cancellationToken); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// public virtual IEnumerator GetEnumerator() => new Enumerator(this); + + /// + /// 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. + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string ToQueryString() + { + using var command = _relationalCommandCache + .GetRelationalCommand(_relationalQueryContext.ParameterValues) + .CreateCommand( + new RelationalCommandParameterObject( + _relationalQueryContext.Connection, + _relationalQueryContext.ParameterValues, + null, + null, + null), + Guid.Empty, + (DbCommandMethod)(-1)); + + if (command.Parameters.Count == 0) + { + return command.CommandText; + } + + var builder = new StringBuilder(); + foreach (var parameter in command.Parameters.FormatParameterList(logParameterValues: true)) + { + builder.Append("-- ").AppendLine(parameter); + } + + return builder.Append(command.CommandText).ToString(); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// public static int[] BuildIndexMap([CanBeNull] IReadOnlyList columnNames, [NotNull] DbDataReader dataReader) { if (columnNames == null) diff --git a/src/EFCore.Relational/Storage/IRelationalCommand.cs b/src/EFCore.Relational/Storage/IRelationalCommand.cs index cc938ffe4de..f222caae70a 100644 --- a/src/EFCore.Relational/Storage/IRelationalCommand.cs +++ b/src/EFCore.Relational/Storage/IRelationalCommand.cs @@ -1,9 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace Microsoft.EntityFrameworkCore.Storage { @@ -84,5 +87,24 @@ Task ExecuteScalarAsync( Task ExecuteReaderAsync( RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken = default); + + /// + /// + /// Called by the execute methods to create a for the given + /// and configure timeouts and transactions. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// Parameters for this method. + /// The command correlation ID. + /// The method that will be called on the created command. + /// The created command. + DbCommand CreateCommand( + RelationalCommandParameterObject parameterObject, + Guid commandId, + DbCommandMethod commandMethod) => throw new NotImplementedException(); } } diff --git a/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs b/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs index 96958888f47..5c826eb6830 100644 --- a/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs +++ b/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Globalization; @@ -30,7 +31,19 @@ public static class DbParameterCollectionExtensions public static string FormatParameters( [NotNull] this DbParameterCollection parameters, bool logParameterValues) - => parameters + => FormatParameterList(parameters, logParameterValues).Join(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IEnumerable FormatParameterList( + [NotNull] this DbParameterCollection parameters, + bool logParameterValues) + { + return parameters .Cast() .Select( p => FormatParameter( @@ -42,8 +55,8 @@ public static string FormatParameters( p.IsNullable, p.Size, p.Precision, - p.Scale)) - .Join(); + p.Scale)); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Storage/RelationalCommand.cs b/src/EFCore.Relational/Storage/RelationalCommand.cs index afc027b7b9a..c734e0aec25 100644 --- a/src/EFCore.Relational/Storage/RelationalCommand.cs +++ b/src/EFCore.Relational/Storage/RelationalCommand.cs @@ -561,7 +561,7 @@ await logger.CommandErrorAsync( /// /// - /// Template method called by the execute methods to + /// Called by the execute methods to /// create a for the given and configure /// timeouts and transactions. /// @@ -574,7 +574,7 @@ await logger.CommandErrorAsync( /// The command correlation ID. /// The method that will be called on the created command. /// The created command. - protected virtual DbCommand CreateCommand( + public virtual DbCommand CreateCommand( RelationalCommandParameterObject parameterObject, Guid commandId, DbCommandMethod commandMethod) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 99d7a9013cc..62f19a7200c 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -24,6 +24,26 @@ namespace Microsoft.EntityFrameworkCore /// public static class EntityFrameworkQueryableExtensions { + /// + /// + /// Generates a string representation of the query used. This string may not be suitable for direct execution is intended only + /// for use in debugging. + /// + /// + /// This is only typically supported by queries generated by Entity Framework Core. + /// + /// + /// The query source. + /// The query string for debugging. + public static string ToQueryString([NotNull] this IQueryable source) + { + Check.NotNull(source, nameof(source)); + + return source.Provider.Execute(source.Expression) is IQueryingEnumerable queryingEnumerable + ? queryingEnumerable.ToQueryString() + : CoreStrings.NotQueryingEnumerable; + } + #region Any/All /// diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index b6e92ddb7bb..677a7dfa10c 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Reflection; @@ -62,6 +62,12 @@ public static string ModelNotFinalized([CanBeNull] object method) public static string NoElements => GetString("NoElements"); + /// + /// The given 'IQueryable' does not support generation of query strings. + /// + public static string NotQueryingEnumerable + => GetString("NotQueryingEnumerable"); + /// /// The value provided for argument '{argumentName}' must be a valid value of enum type '{enumType}'. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 4087a42448b..8192768d23d 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -132,6 +132,9 @@ Sequence contains no elements. + + The given 'IQueryable' does not support generation of query strings. + The value provided for argument '{argumentName}' must be a valid value of enum type '{enumType}'. diff --git a/src/EFCore/Query/IQueryingEnumerable.cs b/src/EFCore/Query/IQueryingEnumerable.cs new file mode 100644 index 00000000000..373c02b122d --- /dev/null +++ b/src/EFCore/Query/IQueryingEnumerable.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// Interface that can be implemented by a database provider's implementation to + /// provide the query string for debugging purposes. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public interface IQueryingEnumerable + { + /// + /// A string representation of the query used. This string may not be suitable for direct execution is intended only + /// for use in debugging. + /// + /// The query string. + string ToQueryString(); + } +} diff --git a/src/EFCore/Query/Internal/EntityQueryable`.cs b/src/EFCore/Query/Internal/EntityQueryable`.cs index 5bac44da27a..7a20dcaf9a6 100644 --- a/src/EFCore/Query/Internal/EntityQueryable`.cs +++ b/src/EFCore/Query/Internal/EntityQueryable`.cs @@ -10,6 +10,7 @@ using System.Threading; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query.Internal @@ -137,5 +138,13 @@ IList IListSource.GetList() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// bool IListSource.ContainsListCollection => false; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual QueryDebugView DebugView => new QueryDebugView(() => Expression.Print(), this.ToQueryString); } } diff --git a/src/EFCore/Query/Internal/QueryDebugView.cs b/src/EFCore/Query/Internal/QueryDebugView.cs new file mode 100644 index 00000000000..afeddb6b97b --- /dev/null +++ b/src/EFCore/Query/Internal/QueryDebugView.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using JetBrains.Annotations; + +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 class QueryDebugView + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Func _toExpressionString; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Func _toQueryString; + + /// + /// 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 QueryDebugView( + [NotNull] Func toExpressionString, + [NotNull] Func toQueryString) + { + _toExpressionString = toExpressionString; + _toQueryString = toQueryString; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string Expression => _toExpressionString(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string Query => _toQueryString(); + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index 315a149e2df..93f8d255878 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -391,9 +391,9 @@ FROM root c WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""CustomerID""] = ""ALFKI""))"); } - public override async Task Where_simple_closure(bool async) + public override async Task Where_simple_closure(bool async) { - await base.Where_simple_closure(async); + var queryString = await base.Where_simple_closure(async); AssertSql( @"@__city_0='London' @@ -401,6 +401,14 @@ public override async Task Where_simple_closure(bool async) SELECT c FROM root c WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""City""] = @__city_0))"); + + Assert.Equal( + @"-- @__city_0='London' +SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""City""] = @__city_0))", queryString, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + + return null; } public override async Task Where_indexer_closure(bool async) diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs index 5bacb2662b3..392873074c7 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs @@ -1,8 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.InMemory.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; using Xunit.Abstractions; @@ -38,6 +38,14 @@ public override Task Where_equals_on_null_nullable_int_types(bool async) { return base.Where_equals_on_null_nullable_int_types(async); } + public override async Task Where_simple_closure(bool async) + { + var queryString = await base.Where_simple_closure(async); + + Assert.Equal(InMemoryStrings.NoQueryStrings, queryString ); + + return null; + } // Casting int to object to string is invalid for InMemory public override Task Like_with_non_string_column_using_double_cast(bool async) => Task.CompletedTask; diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs index e7bf2c39043..9b70e6ac292 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs @@ -199,14 +199,19 @@ public virtual void FromSqlRaw_queryable_simple_columns_out_of_order_and_not_eno } [ConditionalFact] - public virtual void FromSqlRaw_queryable_composed() + public virtual string FromSqlRaw_queryable_composed() { using var context = CreateContext(); - var actual = context.Set().FromSqlRaw(NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) - .Where(c => c.ContactName.Contains("z")) - .ToArray(); + var queryable = context.Set().FromSqlRaw(NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + .Where(c => c.ContactName.Contains("z")); + + var queryString = queryable.ToQueryString(); + + var actual = queryable.ToArray(); Assert.Equal(14, actual.Length); + + return queryString; } [ConditionalFact] @@ -541,20 +546,23 @@ public virtual void FromSqlRaw_queryable_with_null_parameter() } [ConditionalFact] - public virtual void FromSqlRaw_queryable_with_parameters_and_closure() + public virtual string FromSqlRaw_queryable_with_parameters_and_closure() { var city = "London"; var contactTitle = "Sales Representative"; using var context = CreateContext(); - var actual = context.Set().FromSqlRaw( + var queryable = context.Set().FromSqlRaw( NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - .Where(c => c.ContactTitle == contactTitle) - .ToArray(); + .Where(c => c.ContactTitle == contactTitle); + var queryString = queryable.ToQueryString(); + var actual = queryable.ToArray(); Assert.Equal(3, actual.Length); Assert.True(actual.All(c => c.City == "London")); Assert.True(actual.All(c => c.ContactTitle == "Sales Representative")); + + return queryString; } [ConditionalFact] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index 58dcba53bcf..46886e10f05 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -53,14 +53,17 @@ public virtual Task Where_as_queryable_expression(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Where_simple_closure(bool async) + public virtual async Task Where_simple_closure(bool async) { var city = "London"; - return AssertQuery( + await AssertQuery( async, ss => ss.Set().Where(c => c.City == city), entryCount: 6); + + using var context = CreateContext(); + return context.Set().Where(c => c.City == city).ToQueryString(); } [ConditionalTheory] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs index 3457beacbd2..1fe8a957676 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Data.Common; using System.Linq; using Microsoft.Data.SqlClient; @@ -42,16 +43,20 @@ public override void FromSqlRaw_queryable_simple_columns_out_of_order_and_extra_ @"SELECT ""Region"", ""PostalCode"", ""PostalCode"" AS ""Foo"", ""Phone"", ""Fax"", ""CustomerID"", ""Country"", ""ContactTitle"", ""ContactName"", ""CompanyName"", ""City"", ""Address"" FROM ""Customers"""); } - public override void FromSqlRaw_queryable_composed() + public override string FromSqlRaw_queryable_composed() { - base.FromSqlRaw_queryable_composed(); + var queryString = base.FromSqlRaw_queryable_composed(); - AssertSql( - @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + var expected = @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM ( SELECT * FROM ""Customers"" ) AS [c] -WHERE CHARINDEX(N'z', [c].[ContactName]) > 0"); +WHERE CHARINDEX(N'z', [c].[ContactName]) > 0"; + + AssertSql(expected); + Assert.Equal(expected, queryString); + + return null; } public override void FromSqlRaw_queryable_composed_after_removing_whitespaces() @@ -327,9 +332,9 @@ public override void FromSqlRaw_queryable_with_null_parameter() SELECT * FROM ""Employees"" WHERE ""ReportsTo"" = @p0 OR (""ReportsTo"" IS NULL AND @p0 IS NULL)"); } - public override void FromSqlRaw_queryable_with_parameters_and_closure() + public override string FromSqlRaw_queryable_with_parameters_and_closure() { - base.FromSqlRaw_queryable_with_parameters_and_closure(); + var queryString = base.FromSqlRaw_queryable_with_parameters_and_closure(); AssertSql( @"p0='London' (Size = 4000) @@ -340,6 +345,16 @@ public override void FromSqlRaw_queryable_with_parameters_and_closure() SELECT * FROM ""Customers"" WHERE ""City"" = @p0 ) AS [c] WHERE [c].[ContactTitle] = @__contactTitle_1"); + + Assert.Equal(@"-- p0='London' (Size = 4000) +-- @__contactTitle_1='Sales Representative' (Size = 4000) +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM ( + SELECT * FROM ""Customers"" WHERE ""City"" = @p0 +) AS [c] +WHERE [c].[ContactTitle] = @__contactTitle_1", queryString); + + return null; } public override void FromSqlRaw_queryable_simple_cache_key_includes_query_string() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index f4f88a3d620..ebebb048325 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; using Xunit.Abstractions; namespace Microsoft.EntityFrameworkCore.Query @@ -39,9 +40,9 @@ FROM [Orders] AS [o] WHERE ([c].[CustomerID] = [o].[CustomerID]) AND ([o].[CustomerID] = N'ALFKI'))"); } - public override async Task Where_simple_closure(bool async) + public override async Task Where_simple_closure(bool async) { - await base.Where_simple_closure(async); + var queryString = await base.Where_simple_closure(async); AssertSql( @"@__city_0='London' (Size = 4000) @@ -49,6 +50,14 @@ public override async Task Where_simple_closure(bool async) SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[City] = @__city_0"); + + Assert.Equal( + @"-- @__city_0='London' (Size = 4000) +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[City] = @__city_0", queryString, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + + return null; } public override async Task Where_indexer_closure(bool async) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindWhereQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindWhereQuerySqliteTest.cs index 126e3931fda..189df6c98fe 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindWhereQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindWhereQuerySqliteTest.cs @@ -24,6 +24,26 @@ public override Task Where_datetimeoffset_now_component(bool async) public override Task Where_datetimeoffset_utcnow_component(bool async) => AssertTranslationFailed(() => base.Where_datetimeoffset_utcnow_component(async)); + public override async Task Where_simple_closure(bool async) + { + var queryString = await base.Where_simple_closure(async); + + AssertSql( + @"@__city_0='London' (Size = 6) + +SELECT ""c"".""CustomerID"", ""c"".""Address"", ""c"".""City"", ""c"".""CompanyName"", ""c"".""ContactName"", ""c"".""ContactTitle"", ""c"".""Country"", ""c"".""Fax"", ""c"".""Phone"", ""c"".""PostalCode"", ""c"".""Region"" +FROM ""Customers"" AS ""c"" +WHERE ""c"".""City"" = @__city_0"); + + Assert.Equal( + @"-- @__city_0='London' (Size = 6) +SELECT ""c"".""CustomerID"", ""c"".""Address"", ""c"".""City"", ""c"".""CompanyName"", ""c"".""ContactName"", ""c"".""ContactTitle"", ""c"".""Country"", ""c"".""Fax"", ""c"".""Phone"", ""c"".""PostalCode"", ""c"".""Region"" +FROM ""Customers"" AS ""c"" +WHERE ""c"".""City"" = @__city_0", queryString, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); + + return null; + } + public override async Task Where_datetime_now(bool async) { await base.Where_datetime_now(async);