Skip to content

Commit

Permalink
Query: Throw error for unhandled derived query root expression (#26579)
Browse files Browse the repository at this point in the history
Query root design

Core exposes QueryRootExpression which is base class for any query root so we can access EntityType out of it. This is needed for nav expansion and any potential future use case.
Since any derived query root would derive from it, core assembly translates it only when type is exact match to QueryRootExpression.
Relational layer also processes QueryRootExpression when they are mapped to default SqlQuery, which also uses exact type match now.

Apart from QueryRootExpression, no other kind of query root which can appear in different providers should be derivable (else they need to use exact type match too).
This rule makes relational ones sealed class. In case any one needs to derive from it, they need to add additional processing anyway.

Provider specific derived query roots can be non-sealed. If anyone is deriving from it then they should be using their derived provider which process those nodes too and if the derived provider wasn't used and shipped provider is used then it is an error from user perspective. If derived query root is used on other provider (targeting diff database) then it will fail since even the base shipped query root is unknown.

Resolves #26502
  • Loading branch information
smitpatel committed Nov 19, 2021
1 parent 3335a1a commit 7ddc346
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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.
/// </summary>
public class FromSqlQueryRootExpression : QueryRootExpression
public sealed class FromSqlQueryRootExpression : QueryRootExpression
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -55,15 +55,15 @@ public class FromSqlQueryRootExpression : QueryRootExpression
/// 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.
/// </summary>
public virtual string Sql { get; }
public string Sql { get; }

/// <summary>
/// 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.
/// </summary>
public virtual Expression Argument { get; }
public Expression Argument { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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.
/// </summary>
public class TableValuedFunctionQueryRootExpression : QueryRootExpression
public sealed class TableValuedFunctionQueryRootExpression : QueryRootExpression
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -41,15 +41,15 @@ public class TableValuedFunctionQueryRootExpression : QueryRootExpression
/// 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.
/// </summary>
public virtual IStoreFunction Function { get; }
public IStoreFunction Function { get; }

/// <summary>
/// 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.
/// </summary>
public virtual IReadOnlyCollection<Expression> Arguments { get; }
public IReadOnlyCollection<Expression> Arguments { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
_sqlExpressionFactory.Select(
fromSqlQueryRootExpression.EntityType,
new FromSqlExpression(
fromSqlQueryRootExpression.EntityType.GetDefaultMappings().Single().Table.Name.Substring(0, 1)
fromSqlQueryRootExpression.EntityType.GetDefaultMappings().Single().Table.Name[..1]
.ToLowerInvariant(),
fromSqlQueryRootExpression.Sql,
fromSqlQueryRootExpression.Argument)));
Expand Down Expand Up @@ -135,7 +135,8 @@ protected override Expression VisitExtension(Expression extensionExpression)
return CreateShapedQueryExpression(entityType, queryExpression);

case QueryRootExpression queryRootExpression
when queryRootExpression.EntityType.GetSqlQueryMappings().FirstOrDefault(m => m.IsDefaultSqlQueryMapping)?.SqlQuery is
when queryRootExpression.GetType() == typeof(QueryRootExpression)
&& queryRootExpression.EntityType.GetSqlQueryMappings().FirstOrDefault(m => m.IsDefaultSqlQueryMapping)?.SqlQuery is
ISqlQuery sqlQuery:
return Visit(
new FromSqlQueryRootExpression(
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,9 @@
<data name="QueryUnableToTranslateStringEqualsWithStringComparison" xml:space="preserve">
<value>Translation of the 'string.Equals' overload with a 'StringComparison' parameter is not supported. See https://go.microsoft.com/fwlink/?linkid=2129535 for more information.</value>
</data>
<data name="QueryUnhandledQueryRootExpression" xml:space="preserve">
<value>Query root of type '{type}' wasn't handled by provider code. This issue happens when using a provider specific method on a different provider where it is not supported.</value>
</data>
<data name="RecursiveOnConfiguring" xml:space="preserve">
<value>An attempt was made to use the context instance while it is being configured. A DbContext instance cannot be used inside 'OnConfiguring' since it is still being configured at this point. This can happen if a second operation is started on this context instance before a previous operation completed. Any instance members are not guaranteed to be thread safe.</value>
</data>
Expand Down
19 changes: 14 additions & 5 deletions src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,21 @@ protected virtual void AddTranslationErrorDetails(string details)

/// <inheritdoc />
protected override Expression VisitExtension(Expression extensionExpression)
=> extensionExpression switch
{
if (extensionExpression is QueryRootExpression queryRootExpression)
{
ShapedQueryExpression _ => extensionExpression,
QueryRootExpression queryRootExpression => CreateShapedQueryExpression(queryRootExpression.EntityType),
_ => base.VisitExtension(extensionExpression),
};
// Query roots must be processed.
if (extensionExpression.GetType() == typeof(QueryRootExpression))
{
// This requires exact type match on query root to avoid processing derived query roots.
return CreateShapedQueryExpression(queryRootExpression.EntityType);
}

throw new InvalidOperationException(CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName()));
}

return base.VisitExtension(extensionExpression);
}

/// <inheritdoc />
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
Expand Down
266 changes: 266 additions & 0 deletions test/EFCore.CrossStore.FunctionalTests/QueryTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.


// ReSharper disable InconsistentNaming
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Xunit;

namespace Microsoft.EntityFrameworkCore
{
public class QueryTest
{
public static IEnumerable<object[]> IsAsyncData = new[] { new object[] { false }, new object[] { true } };

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task AsSplitQuery_does_not_throw_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.Include(e => e.Posts).AsSplitQuery();
if (async)
{
await query.ToListAsync();
}
else
{
query.ToList();
}
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task AsSingleQuery_does_not_throw_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.Include(e => e.Posts).AsSingleQuery();
if (async)
{
await query.ToListAsync();
}
else
{
query.ToList();
}
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task FromSqlRaw_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.FromSqlRaw("Select 1");

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(FromSqlQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task FromSqlInterpolated_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.FromSqlInterpolated($"Select 1");

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(FromSqlQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalAsOf_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.TemporalAsOf(DateTime.Now);

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalAsOfQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalAll_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.TemporalAll();

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalAllQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalBetween_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.TemporalBetween(DateTime.Now, DateTime.Now.AddDays(7));

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalBetweenQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalContainedIn_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.TemporalContainedIn(DateTime.Now, DateTime.Now.AddDays(7));

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalContainedInQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalFromTo_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.TemporalFromTo(DateTime.Now, DateTime.Now.AddDays(7));

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalFromToQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalAsOf_throws_for_Sqlite(bool async)
{
using var context = new SqliteQueryContext();
var query = context.Blogs.TemporalAsOf(DateTime.Now);

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalAsOfQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalAll_throws_for_Sqlite(bool async)
{
using var context = new SqliteQueryContext();
var query = context.Blogs.TemporalAll();

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalAllQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalBetween_throws_for_Sqlite(bool async)
{
using var context = new SqliteQueryContext();
var query = context.Blogs.TemporalBetween(DateTime.Now, DateTime.Now.AddDays(7));

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalBetweenQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalContainedIn_throws_for_Sqlite(bool async)
{
using var context = new SqliteQueryContext();
var query = context.Blogs.TemporalContainedIn(DateTime.Now, DateTime.Now.AddDays(7));

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalContainedInQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalFromTo_throws_for_Sqlite(bool async)
{
using var context = new SqliteQueryContext();
var query = context.Blogs.TemporalFromTo(DateTime.Now, DateTime.Now.AddDays(7));

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(TemporalFromToQueryRootExpression)), message);
}

private class Blog
{
public int Id { get; set; }
public List<Post> Posts { get; set; }
}

private class Post
{
public int Id { get; set; }
public Blog Blog { get; set; }
}

private abstract class QueryContextBase : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
}
}

private class InMemoryQueryContext : QueryContextBase
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseInMemoryDatabase(nameof(InMemoryQueryContext));
}


private class SqliteQueryContext : QueryContextBase
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlite(((RelationalTestStore)SqliteTestStoreFactory.Instance.Create(nameof(SqliteQueryContext))).ConnectionString);
}

private class SqlServerQueryContext : QueryContextBase
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlite(((RelationalTestStore)SqlServerTestStoreFactory.Instance.Create(nameof(SqlServerQueryContext))).ConnectionString);
}
}
}

0 comments on commit 7ddc346

Please sign in to comment.