diff --git a/test/EFCore.Relational.Specification.Tests/Query/ToSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/ToSqlQueryTestBase.cs new file mode 100644 index 00000000000..d755a6b4c25 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/ToSqlQueryTestBase.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class ToSqlQueryTestBase : NonSharedModelTestBase +{ + protected override string StoreName + => "ToSqlQueryTests"; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] // Issue #27629 + public virtual async Task Entity_type_with_navigation_mapped_to_SqlQuery(bool async) + { + var contextFactory = await InitializeAsync(seed: c => + { + var author = new Author { Name = "Toast", Posts = { new() { Title = "Sausages of the world!"} } }; + c.Add(author); + c.SaveChanges(); + + var postStat = new PostStat { Count = 10, Author = author }; + author.PostStat = postStat; + c.Add(postStat); + c.SaveChanges(); + }); + + using var context = contextFactory.CreateContext(); + + var authors = await + (from o in context.Authors + select new + { + Author = o, + PostCount = o.PostStat!.Count + }).ToListAsync(); + + Assert.Single(authors); + Assert.Equal("Toast", authors[0].Author.Name); + Assert.Equal(10, authors[0].PostCount); + } + + protected class Context27629 : DbContext + { + public Context27629(DbContextOptions options) + : base(options) + { + } + + public DbSet Authors + => Set(); + + public DbSet Posts + => Set(); + + public DbSet PostStats + => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + builder => + { + builder.ToTable("Authors"); + builder.Property(o => o.Name).HasMaxLength(50); + }); + + modelBuilder.Entity( + builder => + { + builder.ToTable("Posts"); + builder.Property(o => o.Title).HasMaxLength(50); + builder.Property(o => o.Content).HasMaxLength(500); + + builder + .HasOne(o => o.Author) + .WithMany(o => o.Posts) + .HasForeignKey(o => o.AuthorId) + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity( + builder => + { + builder + .ToSqlQuery("SELECT * FROM PostStats") + .HasKey(o => o.AuthorId); + + builder + .HasOne(o => o.Author) + .WithOne().HasForeignKey(o => o.AuthorId) + .OnDelete(DeleteBehavior.ClientCascade); + }); + } + } + + protected class Author + { + public long Id { get; set; } + public string Name { get; set; } = null!; + public List Posts { get; } = new(); + public PostStat? PostStat { get; set; } + } + + protected class Post + { + public long Id { get; set; } + public long AuthorId { get; set; } + public Author Author { get; set; } = null!; + public string? Title { get; set; } + public string? Content { get; set; } + } + + protected class PostStat + { + public long AuthorId { get; set; } + public Author Author { get; set; } = null!; + public long? Count { get; set; } + } + + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected void ClearLog() + => TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ToSqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ToSqlQuerySqlServerTest.cs new file mode 100644 index 00000000000..73362d151c3 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ToSqlQuerySqlServerTest.cs @@ -0,0 +1,29 @@ +// 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; + +public class ToSqlQuerySqlServerTest : ToSqlQueryTestBase +{ + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Entity_type_with_navigation_mapped_to_SqlQuery(bool async) + { + await base.Entity_type_with_navigation_mapped_to_SqlQuery(async); + + AssertSql( + @"SELECT [a].[Id], [a].[Name], [a].[PostStatAuthorId], [m].[Count] AS [PostCount] +FROM [Authors] AS [a] +LEFT JOIN ( + SELECT * FROM PostStats +) AS [m] ON [a].[PostStatAuthorId] = [m].[AuthorId]"); + } + + private void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ToSqlQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ToSqlQuerySqliteTest.cs new file mode 100644 index 00000000000..7dab8a91b7d --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ToSqlQuerySqliteTest.cs @@ -0,0 +1,29 @@ +// 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; + +public class ToSqlQuerySqliteTest : ToSqlQueryTestBase +{ + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Entity_type_with_navigation_mapped_to_SqlQuery(bool async) + { + await base.Entity_type_with_navigation_mapped_to_SqlQuery(async); + + AssertSql( + @"SELECT ""a"".""Id"", ""a"".""Name"", ""a"".""PostStatAuthorId"", ""m"".""Count"" AS ""PostCount"" +FROM ""Authors"" AS ""a"" +LEFT JOIN ( + SELECT * FROM PostStats +) AS ""m"" ON ""a"".""PostStatAuthorId"" = ""m"".""AuthorId"""); + } + + private void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); +}