diff --git a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs index 92de54d3dc0..0a949e20fd4 100644 --- a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs @@ -10,6 +10,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Utilities; // ReSharper disable once CheckNamespace diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 6fdc56d9045..e70312a710f 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -72,6 +72,7 @@ public static readonly IDictionary RelationalServi { typeof(IMethodCallTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMemberTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ISqlExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IRelationalQueryStringFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ICommandBatchPreparer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IModificationCommandBatchFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsModelDiffer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -167,6 +168,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); ServiceCollectionMap.GetInfrastructure() .AddDependencySingleton() diff --git a/src/EFCore.Relational/Query/IRelationalQueryStringFactory.cs b/src/EFCore.Relational/Query/IRelationalQueryStringFactory.cs new file mode 100644 index 00000000000..7145a58aa37 --- /dev/null +++ b/src/EFCore.Relational/Query/IRelationalQueryStringFactory.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.Data.Common; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// Implemented by database providers to generate the query string for . + /// + /// + /// This interface is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public interface IRelationalQueryStringFactory + { + /// + /// Returns a formatted query string for the given command. + /// + /// The command that represents the query. + /// The formatted string. + string Create([NotNull] DbCommand command); + } +} diff --git a/src/EFCore.Relational/Query/IRelationalQueryingEnumerable.cs b/src/EFCore.Relational/Query/IRelationalQueryingEnumerable.cs deleted file mode 100644 index 175767a9b6a..00000000000 --- a/src/EFCore.Relational/Query/IRelationalQueryingEnumerable.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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; -using System.Data.Common; - -namespace Microsoft.EntityFrameworkCore.Query -{ - /// - /// - /// Interface that can be implemented by a database provider's implementation to - /// provide the query string for debugging purposes. - /// - /// - /// This interface is typically used by database providers (and other extensions). It is generally - /// not used in application code. - /// - /// - public interface IRelationalQueryingEnumerable : IQueryingEnumerable - { - /// - /// - /// Creates a set up to execute this query. - /// - /// - /// Warning: there is no guarantee that executing this command directly will result in the same behavior as if EF Core had - /// executed the command. - /// - /// - /// Note that DbCommand is an object. The caller is responsible for disposing the returned - /// command. - /// - /// - /// The newly created command. - DbCommand CreateDbCommand(); - } -} diff --git a/src/EFCore.Relational/Query/Internal/IRelationalQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/IRelationalQueryingEnumerable.cs new file mode 100644 index 00000000000..294c631109c --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/IRelationalQueryingEnumerable.cs @@ -0,0 +1,24 @@ +// 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.Data.Common; + +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 IRelationalQueryingEnumerable : IQueryingEnumerable + { + /// + /// 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. + /// + DbCommand CreateDbCommand(); + } +} diff --git a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs index 9659176960c..12eacb94e7f 100644 --- a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs @@ -6,13 +6,11 @@ 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 { @@ -107,22 +105,7 @@ public virtual DbCommand CreateDbCommand() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual string ToQueryString() - { - using var command = CreateDbCommand(); - - 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(); - } + => _relationalQueryContext.RelationalQueryStringFactory.Create(CreateDbCommand()); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Query/Internal/RelationalQueryStringFactory.cs b/src/EFCore.Relational/Query/Internal/RelationalQueryStringFactory.cs new file mode 100644 index 00000000000..9a8385c72ce --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalQueryStringFactory.cs @@ -0,0 +1,40 @@ +// 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.Data.Common; +using System.Text; +using Microsoft.EntityFrameworkCore.Storage.Internal; + +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 RelationalQueryStringFactory : IRelationalQueryStringFactory + { + /// + /// 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 Create(DbCommand command) + { + if (command.Parameters.Count == 0) + { + return command.CommandText; + } + + var builder = new StringBuilder(); + foreach (DbParameter parameter in command.Parameters) + { + builder.Append("-- ").AppendLine(parameter.FormatParameter(logParameterValues: true)); + } + + return builder.Append(command.CommandText).ToString(); + } + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryContext.cs b/src/EFCore.Relational/Query/RelationalQueryContext.cs index 14ccb558bb1..f96ac87719f 100644 --- a/src/EFCore.Relational/Query/RelationalQueryContext.cs +++ b/src/EFCore.Relational/Query/RelationalQueryContext.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.Data.Common; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; @@ -38,6 +39,12 @@ public RelationalQueryContext( /// protected virtual RelationalQueryContextDependencies RelationalDependencies { get; } + /// + /// A factory for creating a readable query string from a + /// + public virtual IRelationalQueryStringFactory RelationalQueryStringFactory + => RelationalDependencies.RelationalQueryStringFactory; + /// /// Gets the active relational connection. /// diff --git a/src/EFCore.Relational/Query/RelationalQueryContextDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryContextDependencies.cs index cf72078f02f..cecbf64cbd0 100644 --- a/src/EFCore.Relational/Query/RelationalQueryContextDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalQueryContextDependencies.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.Data.Common; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -55,11 +56,14 @@ public sealed class RelationalQueryContextDependencies /// [EntityFrameworkInternal] public RelationalQueryContextDependencies( - [NotNull] IRelationalConnection relationalConnection) + [NotNull] IRelationalConnection relationalConnection, + [NotNull] IRelationalQueryStringFactory relationalQueryStringFactory) { Check.NotNull(relationalConnection, nameof(relationalConnection)); + Check.NotNull(relationalQueryStringFactory, nameof(relationalQueryStringFactory)); RelationalConnection = relationalConnection; + RelationalQueryStringFactory = relationalQueryStringFactory; } /// @@ -67,12 +71,25 @@ public RelationalQueryContextDependencies( /// public IRelationalConnection RelationalConnection { get; } + /// + /// A factory for creating a readable query string from a + /// + public IRelationalQueryStringFactory RelationalQueryStringFactory { get; } + /// /// Clones this dependency parameter object with one service replaced. /// /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public RelationalQueryContextDependencies With([NotNull] IRelationalConnection relationalConnection) - => new RelationalQueryContextDependencies(relationalConnection); + => new RelationalQueryContextDependencies(relationalConnection, RelationalQueryStringFactory); + + /// + /// Clones this dependency parameter object with one service replaced. + /// + /// A replacement for the current dependency of this type. + /// A new parameter object with the given service replaced. + public RelationalQueryContextDependencies With([NotNull] IRelationalQueryStringFactory relationalQueryStringFactory) + => new RelationalQueryContextDependencies(RelationalConnection, relationalQueryStringFactory); } } diff --git a/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs b/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs index 5c826eb6830..7f0adf7b49d 100644 --- a/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs +++ b/src/EFCore.Relational/Storage/Internal/DbParameterCollectionExtensions.cs @@ -31,7 +31,9 @@ public static class DbParameterCollectionExtensions public static string FormatParameters( [NotNull] this DbParameterCollection parameters, bool logParameterValues) - => FormatParameterList(parameters, logParameterValues).Join(); + => parameters + .Cast() + .Select(p => FormatParameter(p, logParameterValues)).Join(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -39,24 +41,17 @@ public static string FormatParameters( /// 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( - p.ParameterName, - logParameterValues ? p.Value : "?", - logParameterValues, - p.Direction, - p.DbType, - p.IsNullable, - p.Size, - p.Precision, - p.Scale)); - } + public static string FormatParameter([NotNull] this DbParameter parameter, bool logParameterValues) + => FormatParameter( + parameter.ParameterName, + logParameterValues ? parameter.Value : "?", + logParameterValues, + parameter.Direction, + parameter.DbType, + parameter.IsNullable, + parameter.Size, + parameter.Precision, + parameter.Scale); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index e6d7fe1994c..730176fa978 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -68,6 +68,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer([NotNull] this ISer .TryAdd() .TryAdd() .TryAdd() + .TryAdd() // New Query Pipeline .TryAdd() diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryStringFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryStringFactory.cs new file mode 100644 index 00000000000..4ce8381fea8 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryStringFactory.cs @@ -0,0 +1,180 @@ +// 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; +using System.Data.Common; +using System.Data.SqlTypes; +using System.Globalization; +using System.Text; +using JetBrains.Annotations; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.SqlServer.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 SqlServerQueryStringFactory : IRelationalQueryStringFactory + { + private readonly IRelationalTypeMappingSource _typeMapper; + + /// Initializes a new instance of the class. + public SqlServerQueryStringFactory([NotNull] IRelationalTypeMappingSource typeMapper) + { + _typeMapper = typeMapper; + } + + /// + /// 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 Create(DbCommand command) + { + if (command.Parameters.Count == 0) + { + return command.CommandText; + } + + var builder = new StringBuilder(); + foreach (DbParameter parameter in command.Parameters) + { + var typeName = TypeNameBuilder.CreateTypeName(parameter); + var typeMapping = _typeMapper.FindMapping(typeName); + + builder + .Append("DECLARE ") + .Append(parameter.ParameterName) + .Append(' ') + .Append(typeName) + .Append(" = ") + .Append( + (parameter.Value == DBNull.Value + || parameter.Value == null) + ? "NULL" + : parameter.Value is SqlBytes sqlBytes + ? new SqlServerByteArrayTypeMapping(typeName).GenerateSqlLiteral(sqlBytes.Value) + : typeMapping != null + ? typeMapping.GenerateSqlLiteral(parameter.Value) + : parameter.Value.ToString()) + .AppendLine(";"); + } + + return builder + .AppendLine() + .Append(command.CommandText).ToString(); + } + } + + internal static class TypeNameBuilder + { + private static StringBuilder AppendSize(this StringBuilder builder, DbParameter parameter) + { + if (parameter.Size > 0) + { + builder + .Append('(') + .Append(parameter.Size.ToString(CultureInfo.InvariantCulture)) + .Append(')'); + } + + return builder; + } + + private static StringBuilder AppendSizeOrMax(this StringBuilder builder, DbParameter parameter) + { + if (parameter.Size > 0) + { + builder.AppendSize(parameter); + } + else if (parameter.Size == -1) + { + builder.Append("(max)"); + } + + return builder; + } + + private static StringBuilder AppendPrecision(this StringBuilder builder, DbParameter parameter) + { + if (parameter.Precision > 0) + { + builder + .Append('(') + .Append(parameter.Precision.ToString(CultureInfo.InvariantCulture)) + .Append(')'); + } + + return builder; + } + + private static StringBuilder AppendPrecisionAndScale(this StringBuilder builder, DbParameter parameter) + { + if (parameter.Precision > 0 + && parameter.Scale > 0) + { + builder + .Append('(') + .Append(parameter.Precision.ToString(CultureInfo.InvariantCulture)) + .Append(',') + .Append(parameter.Scale.ToString(CultureInfo.InvariantCulture)) + .Append(')'); + } + + return builder.AppendPrecision(parameter); + } + + public static string CreateTypeName(DbParameter parameter) + { + if (parameter is SqlParameter sqlParameter) + { + var builder = new StringBuilder(); + return (sqlParameter.SqlDbType switch + { + SqlDbType.BigInt => builder.Append("bigint"), + SqlDbType.Binary => builder.Append("binary").AppendSize(parameter), + SqlDbType.Bit => builder.Append("bit"), + SqlDbType.Char => builder.Append("char").AppendSize(parameter), + SqlDbType.Date => builder.Append("date"), + SqlDbType.DateTime => builder.Append("datetime"), + SqlDbType.DateTime2 => builder.Append("datetime2").AppendPrecision(parameter), + SqlDbType.DateTimeOffset => builder.Append("datetimeoffset").AppendPrecision(parameter), + SqlDbType.Decimal => builder.Append("decimal").AppendPrecisionAndScale(parameter), + SqlDbType.Float => builder.Append("float").AppendSize(parameter), + SqlDbType.Image => builder.Append("image"), + SqlDbType.Int => builder.Append("int"), + SqlDbType.Money => builder.Append("money"), + SqlDbType.NChar => builder.Append("nchar").AppendSize(parameter), + SqlDbType.NText => builder.Append("ntext"), + SqlDbType.NVarChar => builder.Append("nvarchar").AppendSizeOrMax(parameter), + SqlDbType.Real => builder.Append("real"), + SqlDbType.SmallDateTime => builder.Append("smalldatetime"), + SqlDbType.SmallInt => builder.Append("smallint"), + SqlDbType.SmallMoney => builder.Append("smallmoney"), + SqlDbType.Structured => builder.Append("structured"), + SqlDbType.Text => builder.Append("text"), + SqlDbType.Time => builder.Append("time").AppendPrecision(parameter), + SqlDbType.Timestamp => builder.Append("rowversion"), + SqlDbType.TinyInt => builder.Append("tinyint"), + SqlDbType.Udt => builder.Append(sqlParameter.UdtTypeName), + SqlDbType.UniqueIdentifier => builder.Append("uniqueIdentifier"), + SqlDbType.VarBinary => builder.Append("varbinary").AppendSizeOrMax(parameter), + SqlDbType.VarChar => builder.Append("varchar").AppendSizeOrMax(parameter), + SqlDbType.Variant => builder.Append("sql_variant"), + SqlDbType.Xml => builder.Append("xml"), + _ => builder.Append("sql_variant") + }).ToString(); + } + + return "sql_variant"; + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalFixtureBase.cs index 7053f61c263..cc6088551ff 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalFixtureBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalFixtureBase.cs @@ -1,6 +1,9 @@ // 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 Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; using Microsoft.EntityFrameworkCore.TestUtilities; namespace Microsoft.EntityFrameworkCore.Query @@ -8,5 +11,15 @@ namespace Microsoft.EntityFrameworkCore.Query public abstract class ComplexNavigationsQueryRelationalFixtureBase : ComplexNavigationsQueryFixtureBase { public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new RelationalQueryAsserter( + CreateContext, + new ComplexNavigationsDefaultData(), + entitySorters, + entityAsserters, + CanExecuteQueryString); } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalFixture.cs index ec7314bcc7e..b7c4d06c0b2 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalFixture.cs @@ -1,6 +1,9 @@ // 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 Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; using Microsoft.EntityFrameworkCore.TestUtilities; namespace Microsoft.EntityFrameworkCore.Query @@ -13,5 +16,15 @@ public abstract class GearsOfWarQueryRelationalFixture : GearsOfWarQueryFixtureB protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Query.Name; + + protected override QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new RelationalQueryAsserter( + CreateContext, + new GearsOfWarData(), + entitySorters, + entityAsserters, + CanExecuteQueryString); } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindQueryRelationalFixture.cs index 6dadb83e87a..b291033b272 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindQueryRelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindQueryRelationalFixture.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 Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.TestModels.Northwind; @@ -26,5 +27,15 @@ protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Query.Name; protected override Type ContextType => typeof(NorthwindRelationalContext); + + protected override QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new RelationalQueryAsserter( + CreateContext, + new NorthwindData(), + entitySorters, + entityAsserters, + CanExecuteQueryString); } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/RelationalOwnedQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/RelationalOwnedQueryTestBase.cs index a2749fa6933..152c6fb9b2e 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/RelationalOwnedQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/RelationalOwnedQueryTestBase.cs @@ -1,6 +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.Collections.Generic; using Microsoft.EntityFrameworkCore.TestUtilities; namespace Microsoft.EntityFrameworkCore.Query @@ -16,6 +18,16 @@ protected RelationalOwnedQueryTestBase(TFixture fixture) public abstract class RelationalOwnedQueryFixture : OwnedQueryFixtureBase { public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new RelationalQueryAsserter( + CreateContext, + new OwnedQueryData(), + entitySorters, + entityAsserters, + CanExecuteQueryString); } } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/SpatialQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/SpatialQueryRelationalFixture.cs index 8d4861bef26..f9d4c96f35c 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/SpatialQueryRelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/SpatialQueryRelationalFixture.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 Microsoft.EntityFrameworkCore.TestModels.SpatialModel; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,5 +18,13 @@ public TestSqlLoggerFactory TestSqlLoggerFactory protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Query.Name; + + protected override QueryAsserter CreateQueryAsserter() + => new RelationalQueryAsserter( + CreateContext, + new SpatialData(GeometryFactory), + entitySorters: null, + entityAsserters: null, + CanExecuteQueryString); } } diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalQueryAsserter.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalQueryAsserter.cs new file mode 100644 index 00000000000..a9fd486cb58 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/RelationalQueryAsserter.cs @@ -0,0 +1,84 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.TestUtilities +{ + public class RelationalQueryAsserter : QueryAsserter + where TContext : DbContext + { + private readonly bool _canExecuteQueryString; + + public RelationalQueryAsserter( + Func contextCreator, + ISetSource expectedData, + Dictionary entitySorters, + Dictionary entityAsserters, + bool canExecuteQueryString) + : base(contextCreator, expectedData, entitySorters, entityAsserters) + { + _canExecuteQueryString = canExecuteQueryString; + } + + protected override void AssertRogueExecution(int expectedCount, IQueryable queryable) + { + var dependencies = ExecuteOurDbCommand(expectedCount, queryable); + + if (_canExecuteQueryString) + { + ExecuteTheirDbCommand(queryable, dependencies); + } + } + + private static (DbConnection, DbTransaction, int, int) ExecuteOurDbCommand(int expectedCount, IQueryable queryable) + { + using var command = queryable.CreateDbCommand(); + var count = ExecuteReader(command); + + // There may be more rows returned than entity instances created, but there + // should never vbe fewer. + Assert.True(count >= expectedCount); + + return (command.Connection, command.Transaction, command.CommandTimeout, count); + } + + private static void ExecuteTheirDbCommand( + IQueryable queryable, + (DbConnection, DbTransaction, int, int) commandDependencies) + { + var (connection, transaction, timeout, expectedCount) = commandDependencies; + + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = queryable.ToQueryString(); + command.CommandTimeout = timeout; + + var count = ExecuteReader(command); + + Assert.Equal(expectedCount, count); + } + + private static int ExecuteReader(DbCommand command) + { + using var reader = command.ExecuteReader(); + + // Not materializing objects here since automatic creation of objects does not + // work for some SQL types, such as geometry/geography + var count = 0; + if (reader.HasRows) + { + while (reader.Read()) + { + count++; + } + } + + return count; + } + } +} diff --git a/test/EFCore.Specification.Tests/FixtureBase.cs b/test/EFCore.Specification.Tests/FixtureBase.cs index d260734b6f0..495a20b55fc 100644 --- a/test/EFCore.Specification.Tests/FixtureBase.cs +++ b/test/EFCore.Specification.Tests/FixtureBase.cs @@ -20,6 +20,9 @@ public virtual DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builde .Log(CoreEventId.SensitiveDataLoggingEnabledWarning) .Log(CoreEventId.PossibleUnintendedReferenceComparisonWarning)); + + protected virtual bool CanExecuteQueryString => false; + protected virtual void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { } diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryFixtureBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryFixtureBase.cs index c3df17706f7..be196eaa9f7 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryFixtureBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryFixtureBase.cs @@ -163,12 +163,17 @@ protected ComplexNavigationsQueryFixtureBase() } }.ToDictionary(e => e.Key, e => (object)e.Value); - QueryAsserter = new QueryAsserter( + QueryAsserter = CreateQueryAsserter(entitySorters, entityAsserters); + } + + protected virtual QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new QueryAsserter( CreateContext, new ComplexNavigationsDefaultData(), entitySorters, entityAsserters); - } public QueryAsserterBase QueryAsserter { get; set; } @@ -293,7 +298,7 @@ public override ComplexNavigationsContext CreateContext() return context; } - private class ComplexNavigationsDefaultData : ComplexNavigationsData + protected class ComplexNavigationsDefaultData : ComplexNavigationsData { public override IQueryable Set() { diff --git a/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs index 522b14c396a..14aa4b25d70 100644 --- a/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs @@ -605,14 +605,18 @@ public FunkyDataQueryFixtureBase() } } }.ToDictionary(e => e.Key, e => (object)e.Value); - ; - QueryAsserter = new QueryAsserter( + QueryAsserter = CreateQueryAsserter(entitySorters, entityAsserters); + } + + protected virtual QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new QueryAsserter( CreateContext, new FunkyDataData(), entitySorters, entityAsserters); - } protected override string StoreName { get; } = "FunkyDataQueryTest"; diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs index 9556263438e..44079039793 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs @@ -227,12 +227,17 @@ protected GearsOfWarQueryFixtureBase() } }.ToDictionary(e => e.Key, e => (object)e.Value); - QueryAsserter = new QueryAsserter( + QueryAsserter = CreateQueryAsserter(entitySorters, entityAsserters); + } + + protected virtual QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new QueryAsserter( CreateContext, new GearsOfWarData(), entitySorters, entityAsserters); - } public QueryAsserterBase QueryAsserter { get; set; } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindQueryFixtureBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindQueryFixtureBase.cs index 2ee3a398cd0..931b3b9d85c 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindQueryFixtureBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindQueryFixtureBase.cs @@ -30,12 +30,17 @@ protected NorthwindQueryFixtureBase() var entityAsserters = new Dictionary(); - QueryAsserter = new QueryAsserter( + QueryAsserter = CreateQueryAsserter(entitySorters, entityAsserters); + } + + protected virtual QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new QueryAsserter( CreateContext, new NorthwindData(), entitySorters, entityAsserters); - } protected override string StoreName { get; } = "Northwind"; diff --git a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index a6b5b4e7244..e7b8ecddfb9 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -652,12 +652,17 @@ public OwnedQueryFixtureBase() }.ToDictionary(e => e.Key, e => (object)e.Value); ; - QueryAsserter = new QueryAsserter( + QueryAsserter = CreateQueryAsserter(entitySorters, entityAsserters); + } + + protected virtual QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new QueryAsserter( CreateContext, new OwnedQueryData(), entitySorters, entityAsserters); - } protected override string StoreName { get; } = "OwnedQueryTest"; diff --git a/test/EFCore.Specification.Tests/Query/SpatialQueryFixtureBase.cs b/test/EFCore.Specification.Tests/Query/SpatialQueryFixtureBase.cs index b7391b8f08a..d7fa8e3699b 100644 --- a/test/EFCore.Specification.Tests/Query/SpatialQueryFixtureBase.cs +++ b/test/EFCore.Specification.Tests/Query/SpatialQueryFixtureBase.cs @@ -15,12 +15,15 @@ public abstract class SpatialQueryFixtureBase : SharedStoreFixtureBase( + QueryAsserter = CreateQueryAsserter(); + } + + protected virtual QueryAsserter CreateQueryAsserter() + => new QueryAsserter( CreateContext, new SpatialData(GeometryFactory), entitySorters: null, entityAsserters: null); - } public QueryAsserterBase QueryAsserter { get; set; } diff --git a/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs b/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs index e7be046b491..2deead9a468 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs @@ -36,6 +36,10 @@ public QueryAsserter( _includeResultAsserter = new IncludeQueryResultAsserter(_entitySorters, _entityAsserters); } + protected virtual void AssertRogueExecution(int expectedCount, IQueryable queryable) + { + } + public override async Task AssertSingleResultTyped( Func actualSyncQuery, Func> actualAsyncQuery, @@ -88,6 +92,8 @@ public override async Task AssertQuery( ? await query.ToArrayAsync() : query.ToArray(); + AssertRogueExecution(actual.Length, query); + var expected = expectedQuery(ExpectedData).ToArray(); if (!assertOrder @@ -156,6 +162,8 @@ public override async Task AssertQueryScalar( ? await query.ToArrayAsync() : query.ToArray(); + AssertRogueExecution(actual.Length, query); + var expected = expectedQuery(ExpectedData).ToArray(); TestHelpers.AssertResults( @@ -189,6 +197,8 @@ public override async Task AssertQueryScalar( ? await query.ToArrayAsync() : query.ToArray(); + AssertRogueExecution(actual.Length, query); + var expected = expectedQuery(ExpectedData).ToArray(); TestHelpers.AssertResults( @@ -225,6 +235,8 @@ public override async Task> AssertIncludeQuery( ? await query.ToListAsync() : query.ToList(); + AssertRogueExecution(actual.Count, query); + var expected = expectedQuery(ExpectedData).ToList(); if (!assertOrder diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerFixture.cs index e2c30b68fdf..1010c529bb2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerFixture.cs @@ -8,5 +8,7 @@ namespace Microsoft.EntityFrameworkCore.Query public class ComplexNavigationsQuerySqlServerFixture : ComplexNavigationsQueryRelationalFixtureBase { protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected override bool CanExecuteQueryString => true; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs index 1fe8a957676..1500fdff948 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs @@ -346,8 +346,9 @@ public override string FromSqlRaw_queryable_with_parameters_and_closure() ) AS [c] WHERE [c].[ContactTitle] = @__contactTitle_1"); - Assert.Equal(@"-- p0='London' (Size = 4000) --- @__contactTitle_1='Sales Representative' (Size = 4000) + Assert.Equal(@"DECLARE p0 nvarchar(4000) = N'London'; +DECLARE @__contactTitle_1 nvarchar(4000) = N'Sales Representative'; + 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 diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs index e470d926334..28611e2633e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs @@ -1,7 +1,10 @@ // 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.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.FunkyDataModel; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit.Abstractions; @@ -454,6 +457,18 @@ public class FunkyDataQuerySqlServerFixture : FunkyDataQueryFixtureBase public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected override bool CanExecuteQueryString => true; + + protected override QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new RelationalQueryAsserter( + CreateContext, + new FunkyDataData(), + entitySorters, + entityAsserters, + CanExecuteQueryString); } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs index 60a91321fb2..b6fe33a99f4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerFixture.cs @@ -15,6 +15,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con base.OnModelCreating(modelBuilder, context); modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); + } + + protected override bool CanExecuteQueryString => true; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQuerySqlServerFixture.cs index 978037227e1..4f8cf6f389d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQuerySqlServerFixture.cs @@ -12,6 +12,8 @@ public class NorthwindQuerySqlServerFixture : NorthwindQueryRe { protected override ITestStoreFactory TestStoreFactory => SqlServerNorthwindTestStoreFactory.Instance; + protected override bool CanExecuteQueryString => true; + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index ebebb048325..18f0dcfbb91 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -52,7 +52,8 @@ FROM [Customers] AS [c] WHERE [c].[City] = @__city_0"); Assert.Equal( - @"-- @__city_0='London' (Size = 4000) + @"DECLARE @__city_0 nvarchar(4000) = N'London'; + 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); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index ca56df0ca66..68eef30073d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -1653,6 +1653,8 @@ private void AssertSql(params string[] expected) public class OwnedQuerySqlServerFixture : RelationalOwnedQueryFixture { protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected override bool CanExecuteQueryString => true; } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerFixture.cs index a7d4ee9fca5..1fb8632011a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerFixture.cs @@ -27,6 +27,8 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build return optionsBuilder; } + protected override bool CanExecuteQueryString => true; + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs index 4b39beb2c65..ad229f0d9ea 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs @@ -1,6 +1,9 @@ // 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 Microsoft.EntityFrameworkCore.TestModels.FunkyDataModel; using Microsoft.EntityFrameworkCore.TestUtilities; namespace Microsoft.EntityFrameworkCore.Query @@ -17,6 +20,16 @@ public class FunkyDataQuerySqliteFixture : FunkyDataQueryFixtureBase public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + + protected override QueryAsserter CreateQueryAsserter( + Dictionary entitySorters, + Dictionary entityAsserters) + => new RelationalQueryAsserter( + CreateContext, + new FunkyDataData(), + entitySorters, + entityAsserters, + CanExecuteQueryString); } } }