From a09033f10ce0454c18a8d9227375ae4fd6535ead Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Wed, 18 Aug 2021 15:30:32 -0700 Subject: [PATCH] Query: Allow using STET overload for Set in query filter/defining query - Update with correct query root when replacing with runtime entity type in query filter Resolves #24601 --- .../Internal/FromSqlQueryRootExpression.cs | 13 ++ .../Internal/FromSqlQueryRootExpression.cs | 13 ++ .../TableValuedFunctionQueryRootExpression.cs | 13 ++ .../TemporalAllQueryRootExpression.cs | 14 +++ .../TemporalAsOfQueryRootExpression.cs | 13 ++ .../TemporalBetweenQueryRootExpression.cs | 13 ++ .../TemporalContainedInQueryRootExpression.cs | 13 ++ .../TemporalFromToQueryRootExpression.cs | 13 ++ .../QueryFilterRewritingConvention.cs | 60 ++++++++- .../Conventions/RuntimeModelConvention.cs | 2 +- src/EFCore/Properties/CoreStrings.Designer.cs | 10 +- src/EFCore/Properties/CoreStrings.resx | 5 +- src/EFCore/Query/QueryRootExpression.cs | 12 ++ .../Query/SharedTypeQueryInMemoryTest.cs | 51 ++++++++ .../SharedTypeQueryRelationalTestBase.cs | 53 ++++++++ .../Query/SharedTypeQueryTestBase.cs | 69 +++++++++++ .../Query/QueryBugsTest.cs | 20 +-- .../Query/SharedTypeQuerySqlServerTest.cs | 41 ++++++ .../Query/SharedTypeQuerySqliteTest.cs | 12 ++ .../QueryFilterRewritingConventionTest.cs | 117 ++++++++++++++++++ 20 files changed, 540 insertions(+), 17 deletions(-) create mode 100644 test/EFCore.InMemory.FunctionalTests/Query/SharedTypeQueryInMemoryTest.cs create mode 100644 test/EFCore.Relational.Specification.Tests/Query/SharedTypeQueryRelationalTestBase.cs create mode 100644 test/EFCore.Specification.Tests/Query/SharedTypeQueryTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/SharedTypeQuerySqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/SharedTypeQuerySqliteTest.cs create mode 100644 test/EFCore.Tests/Metadata/Conventions/QueryFilterRewritingConventionTest.cs diff --git a/src/EFCore.Cosmos/Query/Internal/FromSqlQueryRootExpression.cs b/src/EFCore.Cosmos/Query/Internal/FromSqlQueryRootExpression.cs index 2ac394e4c4a..2d8a289516b 100644 --- a/src/EFCore.Cosmos/Query/Internal/FromSqlQueryRootExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/FromSqlQueryRootExpression.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Utilities; @@ -81,6 +82,18 @@ public FromSqlQueryRootExpression( public override Expression DetachQueryProvider() => new FromSqlQueryRootExpression(EntityType, Sql, Argument); + /// + /// 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 override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new FromSqlQueryRootExpression(entityType, Sql, Argument); + /// /// 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.Relational/Query/Internal/FromSqlQueryRootExpression.cs b/src/EFCore.Relational/Query/Internal/FromSqlQueryRootExpression.cs index ca7dc4cae04..0e55b6e88bf 100644 --- a/src/EFCore.Relational/Query/Internal/FromSqlQueryRootExpression.cs +++ b/src/EFCore.Relational/Query/Internal/FromSqlQueryRootExpression.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; @@ -80,6 +81,18 @@ public FromSqlQueryRootExpression( public override Expression DetachQueryProvider() => new FromSqlQueryRootExpression(EntityType, Sql, Argument); + /// + /// 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 override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new FromSqlQueryRootExpression(entityType, Sql, Argument); + /// /// 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.Relational/Query/Internal/TableValuedFunctionQueryRootExpression.cs b/src/EFCore.Relational/Query/Internal/TableValuedFunctionQueryRootExpression.cs index cbda431c941..f4db217eae5 100644 --- a/src/EFCore.Relational/Query/Internal/TableValuedFunctionQueryRootExpression.cs +++ b/src/EFCore.Relational/Query/Internal/TableValuedFunctionQueryRootExpression.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; @@ -76,6 +77,18 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) : 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. + /// + public override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new TableValuedFunctionQueryRootExpression(entityType, Function, Arguments); + /// /// 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/TemporalAllQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalAllQueryRootExpression.cs index 55b9577cc0b..64b9ad36893 100644 --- a/src/EFCore.SqlServer/Query/Internal/TemporalAllQueryRootExpression.cs +++ b/src/EFCore.SqlServer/Query/Internal/TemporalAllQueryRootExpression.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; @@ -46,6 +48,18 @@ public TemporalAllQueryRootExpression(IAsyncQueryProvider queryProvider, IEntity public override Expression DetachQueryProvider() => new TemporalAllQueryRootExpression(EntityType); + /// + /// 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 override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new TemporalAllQueryRootExpression(entityType); + /// /// 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/TemporalAsOfQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalAsOfQueryRootExpression.cs index 3bb596cf117..1c9224de48d 100644 --- a/src/EFCore.SqlServer/Query/Internal/TemporalAsOfQueryRootExpression.cs +++ b/src/EFCore.SqlServer/Query/Internal/TemporalAsOfQueryRootExpression.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; @@ -58,6 +59,18 @@ public TemporalAsOfQueryRootExpression( public override Expression DetachQueryProvider() => new TemporalAsOfQueryRootExpression(EntityType, PointInTime); + /// + /// 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 override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new TemporalAsOfQueryRootExpression(entityType, PointInTime); + /// /// 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/TemporalBetweenQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalBetweenQueryRootExpression.cs index 9ae48d502f6..786d6883ffa 100644 --- a/src/EFCore.SqlServer/Query/Internal/TemporalBetweenQueryRootExpression.cs +++ b/src/EFCore.SqlServer/Query/Internal/TemporalBetweenQueryRootExpression.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; @@ -54,6 +55,18 @@ public TemporalBetweenQueryRootExpression( public override Expression DetachQueryProvider() => new TemporalBetweenQueryRootExpression(EntityType, From, To); + /// + /// 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 override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new TemporalBetweenQueryRootExpression(entityType, From, To); + /// /// 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/TemporalContainedInQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalContainedInQueryRootExpression.cs index 692e74de653..eaba68ae511 100644 --- a/src/EFCore.SqlServer/Query/Internal/TemporalContainedInQueryRootExpression.cs +++ b/src/EFCore.SqlServer/Query/Internal/TemporalContainedInQueryRootExpression.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; @@ -54,6 +55,18 @@ public TemporalContainedInQueryRootExpression( public override Expression DetachQueryProvider() => new TemporalContainedInQueryRootExpression(EntityType, From, To); + /// + /// 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 override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new TemporalContainedInQueryRootExpression(entityType, From, To); + /// /// 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/TemporalFromToQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalFromToQueryRootExpression.cs index d3504fad25e..f0318b40aba 100644 --- a/src/EFCore.SqlServer/Query/Internal/TemporalFromToQueryRootExpression.cs +++ b/src/EFCore.SqlServer/Query/Internal/TemporalFromToQueryRootExpression.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; @@ -54,6 +55,18 @@ public TemporalFromToQueryRootExpression( public override Expression DetachQueryProvider() => new TemporalFromToQueryRootExpression(EntityType, From, To); + /// + /// 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 override QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new TemporalFromToQueryRootExpression(entityType, From, To); + /// /// 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/Metadata/Conventions/QueryFilterRewritingConvention.cs b/src/EFCore/Metadata/Conventions/QueryFilterRewritingConvention.cs index 2bf808df950..796e9e3e2da 100644 --- a/src/EFCore/Metadata/Conventions/QueryFilterRewritingConvention.cs +++ b/src/EFCore/Metadata/Conventions/QueryFilterRewritingConvention.cs @@ -3,8 +3,11 @@ using System; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Utilities; @@ -93,7 +96,8 @@ protected override Expression VisitMember(MemberExpression memberExpression) && memberExpression.Type.GetGenericTypeDefinition() == typeof(DbSet<>) && _model != null) { - return new QueryRootExpression(FindEntityType(memberExpression.Type)!); + var entityClrType = memberExpression.Type.GetGenericArguments()[0]; + return new QueryRootExpression(FindEntityType(entityClrType)!); } return base.VisitMember(memberExpression); @@ -111,14 +115,62 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && methodCallExpression.Type.GetGenericTypeDefinition() == typeof(DbSet<>) && _model != null) { - return new QueryRootExpression(FindEntityType(methodCallExpression.Type)!); + IEntityType? entityType; + var entityClrType = methodCallExpression.Type.GetGenericArguments()[0]; + if (methodCallExpression.Arguments.Count == 1) + { + // STET Set method + var entityTypeName = methodCallExpression.Arguments[0].GetConstantValue(); + entityType = (IEntityType?)_model.FindEntityType(entityTypeName); + } + else + { + entityType = FindEntityType(entityClrType); + } + + if (entityType == null) + { + if (_model.IsShared(entityClrType)) + { + throw new InvalidOperationException(CoreStrings.InvalidSetSharedType(entityClrType.ShortDisplayName())); + } + + var findSameTypeName = ((IModel)_model).FindSameTypeNameWithDifferentNamespace(entityClrType); + //if the same name exists in your entity types we will show you the full namespace of the type + if (!string.IsNullOrEmpty(findSameTypeName)) + { + throw new InvalidOperationException(CoreStrings.InvalidSetSameTypeWithDifferentNamespace(entityClrType.DisplayName(), findSameTypeName)); + } + else + { + throw new InvalidOperationException(CoreStrings.InvalidSetType(entityClrType.ShortDisplayName())); + } + } + + if (entityType.IsOwned()) + { + var message = CoreStrings.InvalidSetTypeOwned( + entityType.DisplayName(), entityType.FindOwnership()!.PrincipalEntityType.DisplayName()); + + throw new InvalidOperationException(message); + } + + if (entityType.ClrType != entityClrType) + { + var message = CoreStrings.DbSetIncorrectGenericType( + entityType.ShortName(), entityType.ClrType.ShortDisplayName(), entityClrType.ShortDisplayName()); + + throw new InvalidOperationException(message); + } + + return new QueryRootExpression(entityType); } return base.VisitMethodCall(methodCallExpression); } - private IEntityType? FindEntityType(Type dbSetType) - => ((IModel)_model!).FindRuntimeEntityType(dbSetType.GetGenericArguments()[0]); + private IEntityType? FindEntityType(Type entityClrType) + => ((IModel)_model!).FindRuntimeEntityType(entityClrType); } } } diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 64c7538cece..7d707fd222e 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -613,7 +613,7 @@ public Expression Rewrite(Expression expression) /// protected override Expression VisitExtension(Expression extensionExpression) => extensionExpression is QueryRootExpression queryRootExpression - ? new QueryRootExpression(_model.FindEntityType(queryRootExpression.EntityType.Name)!) + ? queryRootExpression.UpdateEntityType(_model.FindEntityType(queryRootExpression.EntityType.Name)!) : base.VisitExtension(extensionExpression); } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index bf75269fa0a..f74efdb1b76 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2290,6 +2290,14 @@ public static string QueryInvalidMaterializationType(object? projection, object? GetString("QueryInvalidMaterializationType", nameof(projection), nameof(queryableType)), projection, queryableType); + /// + /// The replacement entity type: {entityType} does not have same name and CLR type as entity type this query root represents. + /// + public static string QueryRootDifferentEntityType(object? entityType) + => string.Format( + GetString("QueryRootDifferentEntityType", nameof(entityType)), + entityType); + /// /// Translation of '{expression}' failed. Either the query source is not an entity type, or the specified property does not exist on the entity type. /// @@ -2848,7 +2856,7 @@ public static string ValueGenWithConversion(object? entityType, object? property => string.Format( GetString("ValueGenWithConversion", nameof(entityType), nameof(property), nameof(converter)), entityType, property, converter); - + /// /// Calling '{visitMethodName}' is not allowed. Visit the expression manually for the relevant part in the visitor. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index d12f4d5378f..4511a3a4dc0 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -972,7 +972,7 @@ The foreign key property '{entityType}.{property}' was created in shadow state because a conflicting property with the simple name '{baseName}' exists in the entity type, but is either not mapped, is already used for another relationship, or is incompatible with the associated primary key type. See https://aka.ms/efcore-relationships for information on mapping relationships in EF Core. - Warning CoreEventId.ShadowForeignKeyPropertyCreated string string + Warning CoreEventId.ShadowForeignKeyPropertyCreated string string string The property '{entityType}.{property}' was created in shadow state because there are no eligible CLR members with a matching name. @@ -1310,6 +1310,9 @@ The query contains a projection '{projection}' of type '{queryableType}'. Collections in the final projection must be an 'IEnumerable<T>' type such as 'List<T>'. Consider using 'ToList' or some other mechanism to convert the 'IQueryable<T>' or 'IOrderedEnumerable<T>' into an 'IEnumerable<T>'. + + The replacement entity type: {entityType} does not have same name and CLR type as entity type this query root represents. + Translation of '{expression}' failed. Either the query source is not an entity type, or the specified property does not exist on the entity type. diff --git a/src/EFCore/Query/QueryRootExpression.cs b/src/EFCore/Query/QueryRootExpression.cs index 09a497c0615..43b1edf6ac7 100644 --- a/src/EFCore/Query/QueryRootExpression.cs +++ b/src/EFCore/Query/QueryRootExpression.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; @@ -66,6 +67,17 @@ public QueryRootExpression(IEntityType entityType) public virtual Expression DetachQueryProvider() => new QueryRootExpression(EntityType); + /// + /// Updates entity type associated with this query root with equivalent optimized version. + /// + /// The entity type to replace with. + /// New query root containing given entity type. + public virtual QueryRootExpression UpdateEntityType(IEntityType entityType) + => entityType.ClrType != EntityType.ClrType + || entityType.Name != EntityType.Name + ? throw new InvalidOperationException(CoreStrings.QueryRootDifferentEntityType(entityType.DisplayName())) + : new QueryRootExpression(entityType); + /// public override ExpressionType NodeType => ExpressionType.Extension; diff --git a/test/EFCore.InMemory.FunctionalTests/Query/SharedTypeQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/SharedTypeQueryInMemoryTest.cs new file mode 100644 index 00000000000..e4fb54aa9d1 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/Query/SharedTypeQueryInMemoryTest.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class SharedTypeQueryInMemoryTest : SharedTypeQueryTestBase + { + protected override ITestStoreFactory TestStoreFactory => InMemoryTestStoreFactory.Instance; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Can_use_shared_type_entity_type_in_ToInMemoryQuery(bool async) + { + var contextFactory = await InitializeAsync( + seed: c => c.Seed()); + + using var context = contextFactory.CreateContext(); + + var data = context.Set(); + + Assert.Equal("Maumar", Assert.Single(data).Value); + } + + private class MyContextInMemory24601 : MyContext24601 + { + public MyContextInMemory24601(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.SharedTypeEntity>("STET", + b => + { + b.IndexerProperty("Id"); + b.IndexerProperty("Value"); + }); + + modelBuilder.Entity().HasNoKey() + .ToInMemoryQuery(() => Set>("STET").Select(e => new ViewQuery24601 { Value = (string)e["Value"] })); + } + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/SharedTypeQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/SharedTypeQueryRelationalTestBase.cs new file mode 100644 index 00000000000..52b41fa30f2 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/SharedTypeQueryRelationalTestBase.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public abstract class SharedTypeQueryRelationalTestBase : SharedTypeQueryTestBase + { + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected void ClearLog() => TestSqlLoggerFactory.Clear(); + + protected void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Can_use_shared_type_entity_type_in_query_filter_with_from_sql(bool async) + { + var contextFactory = await InitializeAsync( + seed: c => c.Seed()); + + using var context = contextFactory.CreateContext(); + var query = context.Set(); + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Empty(result); + } + + protected class MyContextRelational24601 : MyContext24601 + { + public MyContextRelational24601(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .HasQueryFilter(e => Set>("STET") + .FromSqlRaw("Select * from STET").Select(i => (string)i["Value"]).Contains(e.Value)); + } + } + } +} diff --git a/test/EFCore.Specification.Tests/Query/SharedTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SharedTypeQueryTestBase.cs new file mode 100644 index 00000000000..c7187105e5d --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/SharedTypeQueryTestBase.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public abstract class SharedTypeQueryTestBase : NonSharedModelTestBase + { + public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } }; + + protected override string StoreName => "SharedTypeQueryTests"; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Can_use_shared_type_entity_type_in_query_filter(bool async) + { + var contextFactory = await InitializeAsync( + seed: c => c.Seed()); + + using var context = contextFactory.CreateContext(); + var query = context.Set(); + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Empty(result); + } + + protected class MyContext24601 : DbContext + { + public MyContext24601(DbContextOptions options) + : base(options) + { + } + + public void Seed() + { + Set>("STET").Add(new Dictionary + { + ["Value"] = "Maumar" + }); + + SaveChanges(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.SharedTypeEntity>("STET", + b => + { + b.IndexerProperty("Id"); + b.IndexerProperty("Value"); + }); + + modelBuilder.Entity().HasNoKey() + .HasQueryFilter(e => Set>("STET").Select(i => (string)i["Value"]).Contains(e.Value)); + } + } + + protected class ViewQuery24601 + { + public string Value { get; set; } + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 27bffb56ff4..b591b079372 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -10159,7 +10159,7 @@ public class JsonResult #endregion - #region Issue24569 + #region Issue25400 [ConditionalTheory] [InlineData(true)] @@ -10171,14 +10171,14 @@ public virtual async Task NoTracking_split_query_creates_only_required_instances using (var context = contextFactory.CreateContext()) { - Test24569.ConstructorCallCount = 0; + Test25400.ConstructorCallCount = 0; - var query = context.Set().AsNoTracking().OrderBy(e => e.Id); + var query = context.Set().AsNoTracking().OrderBy(e => e.Id); var test = async ? await query.FirstOrDefaultAsync() : query.FirstOrDefault(); - Assert.Equal(1, Test24569.ConstructorCallCount); + Assert.Equal(1, Test25400.ConstructorCallCount); AssertSql( @"SELECT TOP(1) [t].[Id], [t].[Value] @@ -10189,7 +10189,7 @@ FROM [Tests] AS [t] protected class MyContext25400 : DbContext { - public DbSet Tests { get; set; } + public DbSet Tests { get; set; } public MyContext25400(DbContextOptions options) : base(options) @@ -10198,27 +10198,27 @@ public MyContext25400(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity().HasKey(e => e.Id); + modelBuilder.Entity().HasKey(e => e.Id); } public void Seed() { - Tests.Add(new Test24569(15)); + Tests.Add(new Test25400(15)); SaveChanges(); } } - protected class Test24569 + protected class Test25400 { public static int ConstructorCallCount = 0; - public Test24569() + public Test25400() { ++ConstructorCallCount; } - public Test24569(int value) + public Test25400(int value) { Value = value; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SharedTypeQuerySqlServerTest.cs new file mode 100644 index 00000000000..07f8b2d53ff --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SharedTypeQuerySqlServerTest.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class SharedTypeQuerySqlServerTest : SharedTypeQueryRelationalTestBase + { + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + public override async Task Can_use_shared_type_entity_type_in_query_filter(bool async) + { + await base.Can_use_shared_type_entity_type_in_query_filter(async); + + AssertSql( + @"SELECT [v].[Value] +FROM [ViewQuery24601] AS [v] +WHERE EXISTS ( + SELECT 1 + FROM [STET] AS [s] + WHERE ([s].[Value] = [v].[Value]) OR ([s].[Value] IS NULL AND [v].[Value] IS NULL))"); + } + + public override async Task Can_use_shared_type_entity_type_in_query_filter_with_from_sql(bool async) + { + await base.Can_use_shared_type_entity_type_in_query_filter_with_from_sql(async); + + AssertSql( + @"SELECT [v].[Value] +FROM [ViewQuery24601] AS [v] +WHERE EXISTS ( + SELECT 1 + FROM ( + Select * from STET + ) AS [s] + WHERE ([s].[Value] = [v].[Value]) OR ([s].[Value] IS NULL AND [v].[Value] IS NULL))"); + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/SharedTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/SharedTypeQuerySqliteTest.cs new file mode 100644 index 00000000000..cf34bec702d --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/SharedTypeQuerySqliteTest.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class SharedTypeQuerySqliteTest : SharedTypeQueryRelationalTestBase + { + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.Tests/Metadata/Conventions/QueryFilterRewritingConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/QueryFilterRewritingConventionTest.cs new file mode 100644 index 00000000000..3433547c35e --- /dev/null +++ b/test/EFCore.Tests/Metadata/Conventions/QueryFilterRewritingConventionTest.cs @@ -0,0 +1,117 @@ +// 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 +// ReSharper disable UnusedMember.Local +using System; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + public class QueryFilterRewritingConventionTest + { + [ConditionalFact] + public virtual void QueryFilter_containing_db_set_with_not_included_type() + { + var modelBuilder = new InternalModelBuilder(new Model()); + Expression> lambda = (Blog e) => new MyContext().Set().Single().Id == e.Id; + modelBuilder.Entity(typeof(Blog), ConfigurationSource.Explicit) + .HasQueryFilter(lambda, ConfigurationSource.Explicit); + + Assert.Equal( + CoreStrings.InvalidSetType(typeof(Post).ShortDisplayName()), + Assert.Throws(() => RunConvention(modelBuilder)).Message); + } + + [ConditionalFact] + public virtual void QueryFilter_containing_db_set_with_shared_type_without_name() + { + var modelBuilder = new InternalModelBuilder(new Model()); + modelBuilder.SharedTypeEntity("Post1", typeof(Post), ConfigurationSource.Explicit); + Expression> lambda = (Blog e) => new MyContext().Set().Single().Id == e.Id; + modelBuilder.Entity(typeof(Blog), ConfigurationSource.Explicit) + .HasQueryFilter(lambda, ConfigurationSource.Explicit); + + Assert.Equal( + CoreStrings.InvalidSetSharedType(typeof(Post).ShortDisplayName()), + Assert.Throws(() => RunConvention(modelBuilder)).Message); + } + + [ConditionalFact] + public virtual void QueryFilter_containing_db_set_of_incorrect_type() + { + var modelBuilder = new InternalModelBuilder(new Model()); + modelBuilder.SharedTypeEntity("Post1", typeof(Post), ConfigurationSource.Explicit); + Expression> lambda = (Blog e) => new MyContext().Set("Post1").Single().Id == e.Id; + modelBuilder.Entity(typeof(Blog), ConfigurationSource.Explicit) + .HasQueryFilter(lambda, ConfigurationSource.Explicit); + + Assert.Equal( + CoreStrings.DbSetIncorrectGenericType("Post1", typeof(Post).ShortDisplayName(), typeof(Blog).ShortDisplayName()), + Assert.Throws(() => RunConvention(modelBuilder)).Message); + } + + [ConditionalFact] + public virtual void QueryFilter_containing_db_set_of_owned() + { + var modelBuilder = new InternalModelBuilder(new Model()); + modelBuilder.Entity(typeof(Owner), ConfigurationSource.Explicit) + .HasOwnership(typeof(Blog), "Blog", ConfigurationSource.Explicit); + + Expression> lambda = (Owner e) => new MyContext().Set().Single().Id == e.Id; + modelBuilder.Entity(typeof(Owner), ConfigurationSource.Explicit) + .HasQueryFilter(lambda, ConfigurationSource.Explicit); + + Assert.Equal( + CoreStrings.InvalidSetTypeOwned(typeof(Blog).ShortDisplayName(), typeof(Owner).ShortDisplayName()), + Assert.Throws(() => RunConvention(modelBuilder)).Message); + } + + private void RunConvention(InternalModelBuilder modelBuilder) + { + var context = new ConventionContext(modelBuilder.Metadata.ConventionDispatcher); + + new QueryFilterRewritingConvention(CreateDependencies()) + .ProcessModelFinalizing(modelBuilder, context); + + Assert.False(context.ShouldStopProcessing()); + } + + private ProviderConventionSetBuilderDependencies CreateDependencies() + => InMemoryTestHelpers.Instance.CreateContextServices().GetRequiredService(); + + protected class Blog + { + public int Id { get; set; } + } + + protected class Post + { + public int Id { get; set; } + } + + protected class Owner + { + public int Id { get; set; } + public Blog Blog { get; set; } + } + + protected class MyContext : DbContext + { + public MyContext() + { + } + } + } +}