From 33df6fc8eb339227ad0f305b2c1e5a0f2db4d476 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 12 Dec 2021 12:23:18 +0100 Subject: [PATCH] Add AtTimeZoneExpression and some relevant translations Closes #26199 Closes #26971 --- All.sln.DotSettings | 1 + .../Query/QuerySqlGenerator.cs | 38 ++++- .../Query/SqlExpressionFactory.cs | 4 + .../Query/SqlExpressionVisitor.cs | 13 +- .../SqlExpressions/AtTimeZoneExpression.cs | 91 ++++++++++++ .../Query/SqlNullabilityProcessor.cs | 21 +++ .../SqlServerDbFunctionsExtensions.cs | 46 ++++++ .../SqlServerServiceCollectionExtensions.cs | 1 + ...rchConditionConvertingExpressionVisitor.cs | 17 +++ .../SqlServerDateTimeMethodTranslator.cs | 50 +++++++ .../Query/GearsOfWarQuerySqlServerTest.cs | 139 ++++++++++++++++++ 11 files changed, 419 insertions(+), 2 deletions(-) create mode 100644 src/EFCore.Relational/Query/SqlExpressions/AtTimeZoneExpression.cs diff --git a/All.sln.DotSettings b/All.sln.DotSettings index 9aeadf36333..4e7bf4fbf52 100644 --- a/All.sln.DotSettings +++ b/All.sln.DotSettings @@ -285,6 +285,7 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True True True True diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 1d8e6ae2bcc..626b2e8e15e 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -895,6 +895,42 @@ protected override Expression VisitIn(InExpression inExpression) return inExpression; } + /// + protected override Expression VisitAtTimeZone(AtTimeZoneExpression atTimeZoneExpression) + { + var requiresBrackets = RequiresParentheses(atTimeZoneExpression, atTimeZoneExpression.Operand); + + if (requiresBrackets) + { + _relationalCommandBuilder.Append("("); + } + + Visit(atTimeZoneExpression.Operand); + + if (requiresBrackets) + { + _relationalCommandBuilder.Append(")"); + } + + _relationalCommandBuilder.Append(" AT TIME ZONE "); + + requiresBrackets = RequiresParentheses(atTimeZoneExpression, atTimeZoneExpression.TimeZone); + + if (requiresBrackets) + { + _relationalCommandBuilder.Append("("); + } + + Visit(atTimeZoneExpression.TimeZone); + + if (requiresBrackets) + { + _relationalCommandBuilder.Append(")"); + } + + return atTimeZoneExpression; + } + /// /// Gets a SQL operator for a SQL binary operation. /// @@ -914,7 +950,7 @@ protected virtual bool RequiresParentheses(SqlExpression outerExpression, SqlExp { switch (innerExpression) { - case LikeExpression: + case AtTimeZoneExpression or LikeExpression: return true; case SqlUnaryExpression sqlUnaryExpression: diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 1f0ff84c295..eeddeae5ab0 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -54,6 +54,7 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) return sqlExpression switch { + AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping), CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping), CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping), DistinctExpression e => ApplyTypeMappingOnDistinct(e, typeMapping), @@ -69,6 +70,9 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) }; } + private SqlExpression ApplyTypeMappingOnAtTimeZone(AtTimeZoneExpression atTimeZoneExpression, RelationalTypeMapping? typeMapping) + => new AtTimeZoneExpression(atTimeZoneExpression.Operand, atTimeZoneExpression.TimeZone, atTimeZoneExpression.Type, typeMapping); + private SqlExpression ApplyTypeMappingOnLike(LikeExpression likeExpression) { var inferredTypeMapping = (likeExpression.EscapeChar == null diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index f5d9da82cc5..c4872f20a41 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -24,6 +24,9 @@ protected override Expression VisitExtension(Expression extensionExpression) case ShapedQueryExpression shapedQueryExpression: return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)); + case AtTimeZoneExpression atTimeZoneExpression: + return VisitAtTimeZone(atTimeZoneExpression); + case CaseExpression caseExpression: return VisitCase(caseExpression); @@ -115,6 +118,14 @@ protected override Expression VisitExtension(Expression extensionExpression) return base.VisitExtension(extensionExpression); } + + /// + /// Visits the children of the sql "at time zone" expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitAtTimeZone(AtTimeZoneExpression atTimeZoneExpression); + /// /// Visits the children of the case expression. /// @@ -277,7 +288,7 @@ protected override Expression VisitExtension(Expression extensionExpression) protected abstract Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression); /// - /// Visits the children of the sql fragent expression. + /// Visits the children of the sql fragment expression. /// /// The expression to visit. /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. diff --git a/src/EFCore.Relational/Query/SqlExpressions/AtTimeZoneExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/AtTimeZoneExpression.cs new file mode 100644 index 00000000000..f4b75295467 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/AtTimeZoneExpression.cs @@ -0,0 +1,91 @@ +// 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.SqlExpressions; + +/// +/// +/// An expression that represents an AT TIME ZONE operation in a SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class AtTimeZoneExpression : SqlExpression +{ + /// + /// Creates a new instance of the class. + /// + /// The operand on which to perform the time zone conversion. + /// The time zone to convert to. + /// The of the expression. + /// The associated with the expression. + public AtTimeZoneExpression( + SqlExpression operand, + SqlExpression timeZone, + Type type, + RelationalTypeMapping? typeMapping) + : base(type, typeMapping) + { + Operand = operand; + TimeZone = timeZone; + } + + /// + /// The input operand on which to apply the time zone. + /// + public virtual SqlExpression Operand { get; } + + /// + /// The time zone to be applied. + /// + public virtual SqlExpression TimeZone { get; } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var operand = (SqlExpression)visitor.Visit(Operand); + var timeZone = (SqlExpression)visitor.Visit(TimeZone); + + return Update(operand, timeZone); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual AtTimeZoneExpression Update(SqlExpression operand, SqlExpression timeZone) + => operand != Operand || timeZone != TimeZone + ? new AtTimeZoneExpression(operand, timeZone, Type, TypeMapping) + : this; + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Operand); + + expressionPrinter.Append(" AT TIME ZONE "); + + expressionPrinter.Visit(TimeZone); + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is AtTimeZoneExpression atTimeZoneExpression + && Equals(atTimeZoneExpression)); + + private bool Equals(AtTimeZoneExpression atTimeZoneExpression) + => base.Equals(atTimeZoneExpression) + && Operand.Equals(atTimeZoneExpression.Operand) + && TimeZone.Equals(atTimeZoneExpression.TimeZone); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Operand, TimeZone); +} diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 9d03eaf7d2b..ae575a52c66 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -357,6 +357,8 @@ protected virtual SelectExpression Visit(SelectExpression selectExpression) var nullValueColumnsCount = _nullValueColumns.Count; var result = sqlExpression switch { + AtTimeZoneExpression sqlAtTimeZoneExpression + => VisitAtTimeZone(sqlAtTimeZoneExpression, allowOptimizedExpansion, out nullable), CaseExpression caseExpression => VisitCase(caseExpression, allowOptimizedExpansion, out nullable), CollateExpression collateExpression @@ -413,6 +415,25 @@ protected virtual SqlExpression VisitCustomSqlExpression( => throw new InvalidOperationException( RelationalStrings.UnhandledExpressionInVisitor(sqlExpression, sqlExpression.GetType(), nameof(SqlNullabilityProcessor))); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual SqlExpression VisitAtTimeZone( + AtTimeZoneExpression atTimeZoneExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + var operand = Visit(atTimeZoneExpression.Operand, out var operandNullable); + var timeZone = Visit(atTimeZoneExpression.TimeZone, out var timeZoneNullable); + + nullable = operandNullable || timeZoneNullable; + + return atTimeZoneExpression.Update(operand, timeZone); + } + /// /// Visits a and computes its nullability. /// diff --git a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs index d41be8cd35e..f4de9db02ab 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs @@ -1438,4 +1438,50 @@ public static bool IsNumeric( this DbFunctions _, string expression) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsNumeric))); + + /// + /// Converts to the corresponding datetimeoffset in the target . + /// Corresponds to the SQL Server's AT TIME ZONE construct. + /// + /// + /// + /// Note that the of is not taken into account when performing the + /// conversion; the offset for the provided time zone is simply applied as-is. + /// + /// + /// See Database functions, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// + /// The instance. + /// The value to convert to datetimeoffset. + /// A valid SQL Server time zone ID. + /// The datetimeoffset resulting from the conversion. + /// SQL Server documentation for AT TIME ZONE. + public static DateTimeOffset AtTimeZone( + this DbFunctions _, + DateTime dateTime, + string timeZone) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(AtTimeZone))); + + /// + /// Converts to the time zone specified by . + /// Corresponds to the SQL Server's AT TIME ZONE construct. + /// + /// + /// See Database functions, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The instance. + /// The value on which to perform the time zone conversion. + /// A valid SQL Server time zone ID. + /// The datetimeoffset resulting from the conversion. + /// SQL Server documentation for AT TIME ZONE. + public static DateTimeOffset AtTimeZone( + this DbFunctions _, + DateTimeOffset dateTimeOffset, + string timeZone) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(AtTimeZone))); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 37129cd8f8e..6d79e7872fe 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -123,6 +123,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 497ff74aef3..9a5bb6c9c91 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -306,6 +306,23 @@ protected override Expression VisitSelect(SelectExpression selectExpression) : selectExpression; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitAtTimeZone(AtTimeZoneExpression atTimeZoneExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + var operand = (SqlExpression)Visit(atTimeZoneExpression.Operand); + var timeZone = (SqlExpression)Visit(atTimeZoneExpression.TimeZone); + _isSearchCondition = parentSearchCondition; + + return atTimeZoneExpression.Update(operand, timeZone); + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs index f5469dbb8cf..f962d6d84e7 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs @@ -31,6 +31,14 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) })!, "millisecond" } }; + private static readonly MethodInfo AtTimeZoneDateTimeOffsetMethodInfo = typeof(SqlServerDbFunctionsExtensions) + .GetRuntimeMethod( + nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTimeOffset), typeof(string) })!; + + private static readonly MethodInfo AtTimeZoneDateTimeMethodInfo = typeof(SqlServerDbFunctionsExtensions) + .GetRuntimeMethod( + nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTime), typeof(string) })!; + private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly IRelationalTypeMappingSource _typeMappingSource; @@ -89,6 +97,48 @@ public SqlServerDateTimeMethodTranslator( instance.TypeMapping); } + if (method == AtTimeZoneDateTimeOffsetMethodInfo || method == AtTimeZoneDateTimeMethodInfo) + { + var (operand, timeZone) = (arguments[1], arguments[2]); + + RelationalTypeMapping? resultTypeMapping = null; + + // The AT TIME ZONE construct bubbles up the precision of its operand, so when invoked over datetime2(2) it returns a + // datetimeoffset(2). So if the operand has a type mapping, bubble it up accordingly, otherwise allow the result type mapping + // to be inferred. + if (operand.TypeMapping is { } operandTypeMapping) + { + switch (operandTypeMapping.StoreTypeNameBase) + { + case "datetimeoffset": + resultTypeMapping = operandTypeMapping; + break; + case "datetime" or "datetime2" or "smalldatetime": + resultTypeMapping = _typeMappingSource.FindMapping( + typeof(DateTimeOffset), "datetimeoffset", precision: operandTypeMapping.Precision); + break; + default: + Check.DebugAssert( + false, + $"Unknown operand type mapping '{operandTypeMapping.StoreTypeNameBase}' when translating EF.Functions.AtTimeZone"); + break; + } + } + + if (operand is SqlConstantExpression) + { + // Our constant representation for datetime/datetimeoffset is an untyped string literal, which the AT TIME ZONE expression + // does not accept. Type it explicitly. + operand = _sqlExpressionFactory.Convert(operand, operand.Type); + } + + return new AtTimeZoneExpression( + operand, + _sqlExpressionFactory.ApplyTypeMapping(timeZone, _typeMappingSource.FindMapping("varchar")), + typeof(DateTimeOffset), + resultTypeMapping); + } + return null; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index a3ebf9f0952..28fec1e11a5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -2534,6 +2534,145 @@ FROM [Missions] AS [m] WHERE [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'"); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_AtTimeZone_datetimeoffset_constant(bool async) + { + using var context = CreateContext(); + + var query = context.Set().Where( + m => EF.Functions.AtTimeZone(m.Timeline, "UTC") == new DateTimeOffset(2, 3, 1, 13, 0, 0, TimeSpan.Zero)); + + var missions = async + ? await query.ToListAsync() + : query.ToList(); + + var mission = Assert.Single(missions); + Assert.Equal(2, mission.Id); + + AssertSql( + @"SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE ([m].[Timeline] AT TIME ZONE 'UTC') = '0002-03-01T13:00:00.0000000+00:00'"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_AtTimeZone_datetimeoffset_parameter(bool async) + { + using var context = CreateContext(); + + var dateTime = new DateTimeOffset(2, 3, 1, 13, 0, 0, TimeSpan.Zero); + var timeZone = "UTC"; + var query = context.Set().Where(m => m.Timeline == EF.Functions.AtTimeZone(dateTime, timeZone)); + + var missions = async + ? await query.ToListAsync() + : query.ToList(); + + var mission = Assert.Single(missions); + Assert.Equal(2, mission.Id); + + AssertSql( + @"@__dateTime_1='0002-03-01T13:00:00.0000000+00:00' +@__timeZone_2='UTC' (Size = 8000) (DbType = AnsiString) + +SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE [m].[Timeline] = (@__dateTime_1 AT TIME ZONE @__timeZone_2)"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_AtTimeZone_datetimeoffset_column(bool async) + { + using var context = CreateContext(); + + var query = context.Set() + .Where(m => EF.Functions.AtTimeZone(m.Timeline, "UTC") == new DateTimeOffset(2, 3, 1, 13, 0, 0, TimeSpan.Zero)); + + var missions = async + ? await query.ToListAsync() + : query.ToList(); + + var mission = Assert.Single(missions); + Assert.Equal(2, mission.Id); + + AssertSql( + @"SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE ([m].[Timeline] AT TIME ZONE 'UTC') = '0002-03-01T13:00:00.0000000+00:00'"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_AtTimeZone_datetime_constant(bool async) + { + using var context = CreateContext(); + + var query = context.Set().Where(m => m.Timeline == EF.Functions.AtTimeZone(new DateTime(10, 5, 3, 12, 0, 0), "UTC")); + + var missions = async + ? await query.ToListAsync() + : query.ToList(); + + var mission = Assert.Single(missions); + Assert.Equal(3, mission.Id); + + AssertSql( + @"SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE [m].[Timeline] = (CAST('0010-05-03T12:00:00.0000000' AS datetime2) AT TIME ZONE 'UTC')"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_AtTimeZone_datetime_parameter(bool async) + { + using var context = CreateContext(); + + var dateTime = new DateTime(10, 5, 3, 12, 0, 0); + var timeZone = "UTC"; + var query = context.Set().Where(m => m.Timeline == EF.Functions.AtTimeZone(dateTime, timeZone)); + + var missions = async + ? await query.ToListAsync() + : query.ToList(); + + var mission = Assert.Single(missions); + Assert.Equal(3, mission.Id); + + AssertSql( + @"@__dateTime_1='0010-05-03T12:00:00.0000000' +@__timeZone_2='UTC' (Size = 8000) (DbType = AnsiString) + +SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Duration], [m].[Rating], [m].[Timeline] +FROM [Missions] AS [m] +WHERE [m].[Timeline] = (@__dateTime_1 AT TIME ZONE @__timeZone_2)"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Where_AtTimeZone_datetime_column(bool async) + { + using var context = CreateContext(); + + var query = context.Set() + .Where(ct => EF.Functions.AtTimeZone(ct.IssueDate, "UTC") == new DateTimeOffset(15, 3, 7, 0, 0, 0, TimeSpan.Zero)); + + var missions = async + ? await query.ToListAsync() + : query.ToList(); + + var mission = Assert.Single(missions); + Assert.Equal(Guid.Parse("A7BE028A-0CF2-448F-AB55-CE8BC5D8CF69"), mission.Id); + + AssertSql( + @"SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] +FROM [Tags] AS [t] +WHERE ([t].[IssueDate] AT TIME ZONE 'UTC') = '0015-03-07T00:00:00.0000000+00:00'"); + } + public override async Task Orderby_added_for_client_side_GroupJoin_composite_dependent_to_principal_LOJ_when_incomplete_key_is_used( bool async) {