From 124467e6d57f9959538977f68778d3a5d93cb444 Mon Sep 17 00:00:00 2001 From: Phuc Thai Date: Sat, 4 Nov 2023 00:11:23 +0700 Subject: [PATCH] Add sample groupby issue query in ef 3.1 --- EFCoreSamples.sln | 11 +- .../20231103163846_Init.Designer.cs | 73 +++++++++ .../Migrations/20231103163846_Init.cs | 61 ++++++++ .../BloggingContextModelSnapshot.cs | 71 +++++++++ QueryingIssues.EF3.1/Model.cs | 52 +++++++ QueryingIssues.EF3.1/Program.cs | 14 ++ .../QueryStackoverflowGroupBy.cs | 145 ++++++++++++++++++ .../QueryingIssues.EF3.1.csproj | 18 +++ QueryingIssues/Program.cs | 10 +- QueryingIssues/QueryStackoverflowGroupBy.cs | 111 ++++++++++++++ 10 files changed, 562 insertions(+), 4 deletions(-) create mode 100644 QueryingIssues.EF3.1/Migrations/20231103163846_Init.Designer.cs create mode 100644 QueryingIssues.EF3.1/Migrations/20231103163846_Init.cs create mode 100644 QueryingIssues.EF3.1/Migrations/BloggingContextModelSnapshot.cs create mode 100644 QueryingIssues.EF3.1/Model.cs create mode 100644 QueryingIssues.EF3.1/Program.cs create mode 100644 QueryingIssues.EF3.1/QueryStackoverflowGroupBy.cs create mode 100644 QueryingIssues.EF3.1/QueryingIssues.EF3.1.csproj create mode 100644 QueryingIssues/QueryStackoverflowGroupBy.cs diff --git a/EFCoreSamples.sln b/EFCoreSamples.sln index 99c3055..b8952a0 100644 --- a/EFCoreSamples.sln +++ b/EFCoreSamples.sln @@ -5,9 +5,11 @@ VisualStudioVersion = 17.7.34202.233 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Querying", "Querying", "{44F1D95E-F174-4676-8C14-D02B5E0DA764}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryingIssues.EF7", "QueryingIssues\QueryingIssues.EF7.csproj", "{3B3EF0D2-E8AC-453E-BA4D-9BEC27C000B1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryingIssues.EF7", "QueryingIssues\QueryingIssues.EF7.csproj", "{3B3EF0D2-E8AC-453E-BA4D-9BEC27C000B1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{2A4A63A3-BFC8-4D45-83AD-7F34CF694418}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logging", "Logging\Logging.csproj", "{2A4A63A3-BFC8-4D45-83AD-7F34CF694418}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryingIssues.EF3.1", "QueryingIssues.EF3.1\QueryingIssues.EF3.1.csproj", "{3A87659F-A062-463B-BD4C-55698E97218C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -23,6 +25,10 @@ Global {2A4A63A3-BFC8-4D45-83AD-7F34CF694418}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A4A63A3-BFC8-4D45-83AD-7F34CF694418}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A4A63A3-BFC8-4D45-83AD-7F34CF694418}.Release|Any CPU.Build.0 = Release|Any CPU + {3A87659F-A062-463B-BD4C-55698E97218C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A87659F-A062-463B-BD4C-55698E97218C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A87659F-A062-463B-BD4C-55698E97218C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A87659F-A062-463B-BD4C-55698E97218C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -30,6 +36,7 @@ Global GlobalSection(NestedProjects) = preSolution {3B3EF0D2-E8AC-453E-BA4D-9BEC27C000B1} = {44F1D95E-F174-4676-8C14-D02B5E0DA764} {2A4A63A3-BFC8-4D45-83AD-7F34CF694418} = {44F1D95E-F174-4676-8C14-D02B5E0DA764} + {3A87659F-A062-463B-BD4C-55698E97218C} = {44F1D95E-F174-4676-8C14-D02B5E0DA764} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {512678AA-D8DE-42B9-9E6F-45A28E1EC6CD} diff --git a/QueryingIssues.EF3.1/Migrations/20231103163846_Init.Designer.cs b/QueryingIssues.EF3.1/Migrations/20231103163846_Init.Designer.cs new file mode 100644 index 0000000..ab2628b --- /dev/null +++ b/QueryingIssues.EF3.1/Migrations/20231103163846_Init.Designer.cs @@ -0,0 +1,73 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace QueryingIssues.EF3._1.Migrations +{ + [DbContext(typeof(BloggingContext))] + [Migration("20231103163846_Init")] + partial class Init + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.32"); + + modelBuilder.Entity("Blog", b => + { + b.Property("BlogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("BlogId"); + + b.ToTable("Blogs"); + }); + + modelBuilder.Entity("Post", b => + { + b.Property("PostId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlogId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("NumberOfView") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("PostId"); + + b.HasIndex("BlogId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Post", b => + { + b.HasOne("Blog", "Blog") + .WithMany("Posts") + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/QueryingIssues.EF3.1/Migrations/20231103163846_Init.cs b/QueryingIssues.EF3.1/Migrations/20231103163846_Init.cs new file mode 100644 index 0000000..a69fcfe --- /dev/null +++ b/QueryingIssues.EF3.1/Migrations/20231103163846_Init.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace QueryingIssues.EF3._1.Migrations +{ + public partial class Init : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Blogs", + columns: table => new + { + BlogId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Url = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Blogs", x => x.BlogId); + }); + + migrationBuilder.CreateTable( + name: "Posts", + columns: table => new + { + PostId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(nullable: true), + Content = table.Column(nullable: true), + BlogId = table.Column(nullable: false), + CreatedAt = table.Column(nullable: false), + NumberOfView = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Posts", x => x.PostId); + table.ForeignKey( + name: "FK_Posts_Blogs_BlogId", + column: x => x.BlogId, + principalTable: "Blogs", + principalColumn: "BlogId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Posts_BlogId", + table: "Posts", + column: "BlogId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Posts"); + + migrationBuilder.DropTable( + name: "Blogs"); + } + } +} diff --git a/QueryingIssues.EF3.1/Migrations/BloggingContextModelSnapshot.cs b/QueryingIssues.EF3.1/Migrations/BloggingContextModelSnapshot.cs new file mode 100644 index 0000000..e0e3a8a --- /dev/null +++ b/QueryingIssues.EF3.1/Migrations/BloggingContextModelSnapshot.cs @@ -0,0 +1,71 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace QueryingIssues.EF3._1.Migrations +{ + [DbContext(typeof(BloggingContext))] + partial class BloggingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.32"); + + modelBuilder.Entity("Blog", b => + { + b.Property("BlogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("BlogId"); + + b.ToTable("Blogs"); + }); + + modelBuilder.Entity("Post", b => + { + b.Property("PostId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlogId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("NumberOfView") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("PostId"); + + b.HasIndex("BlogId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Post", b => + { + b.HasOne("Blog", "Blog") + .WithMany("Posts") + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/QueryingIssues.EF3.1/Model.cs b/QueryingIssues.EF3.1/Model.cs new file mode 100644 index 0000000..43ecf04 --- /dev/null +++ b/QueryingIssues.EF3.1/Model.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +public class BloggingContext : DbContext +{ + public static readonly ILoggerFactory MyLoggerFactory + = LoggerFactory.Create(builder => builder.AddConsole()); + + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + public string DbPath { get; } + + public BloggingContext() + { + var folder = Environment.SpecialFolder.LocalApplicationData; + var path = Environment.GetFolderPath(folder); + DbPath = System.IO.Path.Join(path, "blogging3.1.db"); + } + + // The following configures EF to create a Sqlite database file in the + // special "local" folder for your platform. + protected override void OnConfiguring(DbContextOptionsBuilder options) + => options + .UseLoggerFactory(MyLoggerFactory) // Enable EF Core logging + // .UseSqlServer( + // "Server=LAPTOP-IAJ1J0A2;Database=blogpost;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=true"); + .UseSqlite($"Data Source={DbPath}"); +} + +public class Blog +{ + public int BlogId { get; set; } + public string Url { get; set; } + + public List Posts { get; } = new List(); +} + +public class Post +{ + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + + public int BlogId { get; set; } + public Blog Blog { get; set; } + public DateTime CreatedAt { get; set; } + + public long NumberOfView { get; set; } +} diff --git a/QueryingIssues.EF3.1/Program.cs b/QueryingIssues.EF3.1/Program.cs new file mode 100644 index 0000000..920fb53 --- /dev/null +++ b/QueryingIssues.EF3.1/Program.cs @@ -0,0 +1,14 @@ +using System; + +namespace QueryingIssues.EF3._1 +{ + public class Program + { + static void Main(string[] args) + { + var query5 = new QueryStackoverflowGroupBy(); + query5.QueryNo59456026(); + query5.QueryNo59456026_Workaround(); + } + } +} diff --git a/QueryingIssues.EF3.1/QueryStackoverflowGroupBy.cs b/QueryingIssues.EF3.1/QueryStackoverflowGroupBy.cs new file mode 100644 index 0000000..1d06937 --- /dev/null +++ b/QueryingIssues.EF3.1/QueryStackoverflowGroupBy.cs @@ -0,0 +1,145 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ValueGeneration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QueryingIssues.EF3._1 +{ + public class QueryStackoverflowGroupBy + { + // https://stackoverflow.com/questions/69355421/retrieving-the-last-record-in-each-group-with-ef-core + // https://stackoverflow.com/questions/59456026/how-to-select-top-n-rows-for-each-group-in-a-entity-framework-groupby-with-ef-3/59468439#59468439 + public void QueryNo59456026() + { + Init(); + + using var context = new BloggingContext(); + + var data = context + .Posts + .AsNoTracking() + .GroupBy(post => post.BlogId) + .Select(group => new + { + BlogId = group.Key, + Post = group.OrderByDescending(post => post.CreatedAt).FirstOrDefault() + }) + .ToList(); + + foreach (var item in data) + { + Console.WriteLine($"Blog {item.BlogId} - Post ({item.Post?.PostId}): {item.Post?.Title}"); + } + } + + public void QueryNo59456026_Workaround() + { + Init(); + + using var context = new BloggingContext(); + + var unique = context + .Posts + .AsNoTracking() + .Select(post => new { post.BlogId }) + .Distinct() + .ToList(); + + var query = + from u in unique + from l in context + .Posts + .AsNoTracking() + .Where(post => post.BlogId == u.BlogId) + .OrderByDescending(post => post.CreatedAt) + .Take(1) + select l; + + + var data = query.ToList(); + + foreach (var post in data) + { + Console.WriteLine($"Blog {post.BlogId} - Post ({post.PostId}): {post.Title}"); + } + } + + public void Query() + { + Init(); + + using var context = new BloggingContext(); + + var result = context + .Blogs + .AsQueryable() + .Select(blog => new + { + BlogUrl = blog.Url, + Post = blog.Posts.OrderByDescending(post => post.CreatedAt).Select(post => new + { + post.PostId, + post.Title + }) + .FirstOrDefault() + }) + .ToList(); + + foreach (var blog in result) + { + Console.WriteLine($"Blog {blog.BlogUrl} - Post ({blog.Post?.PostId}): {blog.Post?.Title}"); + } + } + + private void Init() + { + using var context = new BloggingContext(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + var now = DateTime.Now; + // Generate 10 blogs + for (int i = 0; i < 10; i++) + { + var blog = new Blog { Url = $"https://example.com/blog/{i + 1}" }; + context.Blogs.Add(blog); + + // Generate 2 to 4 posts for each blog + var random = new Random(); + int postCount = random.Next(2, 5); + + for (int j = 0; j < postCount; j++) + { + now = now.AddMinutes(1); + var post = new Post + { + Title = $"Post {j + 1} for Blog {i + 1}", + Content = $"Content for Post {j + 1}", + CreatedAt = now, + NumberOfView = random.Next(100, 501) + }; + blog.Posts.Add(post); + } + } + + context.SaveChanges(); + + Print(context); + } + + private void Print(BloggingContext context) + { + foreach (var blog in context.Blogs) + { + Console.WriteLine($"Blog {blog.Url} "); + foreach (var post in blog.Posts) + { + Console.WriteLine($"Post ({post.PostId}): {post.Title} - {post.CreatedAt}"); + } + } + } + } +} diff --git a/QueryingIssues.EF3.1/QueryingIssues.EF3.1.csproj b/QueryingIssues.EF3.1/QueryingIssues.EF3.1.csproj new file mode 100644 index 0000000..abaebe0 --- /dev/null +++ b/QueryingIssues.EF3.1/QueryingIssues.EF3.1.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.1 + QueryingIssues.EF3._1 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/QueryingIssues/Program.cs b/QueryingIssues/Program.cs index 4bf870a..9104d44 100644 --- a/QueryingIssues/Program.cs +++ b/QueryingIssues/Program.cs @@ -2,12 +2,18 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using QueryingIssues; +using QueryingIssues.EF7; //var query1 = new QueryGitNo27285(); //await query1.Query(); //var query2 = new QueryStackoverflow58011931(); +//await query2.Query(); //await query2.Query_Workaround(); -var query3 = new QueryStackoverflow68023153(); -await query3.Query_Workaround(); +//var query3 = new QueryStackoverflow68023153(); +//await query3.Query(); +//await query3.Query_Workaround(); + +var query4 = new QueryStackoverflowGroupBy(); +query4.QueryNo59456026(); diff --git a/QueryingIssues/QueryStackoverflowGroupBy.cs b/QueryingIssues/QueryStackoverflowGroupBy.cs new file mode 100644 index 0000000..4a41d0d --- /dev/null +++ b/QueryingIssues/QueryStackoverflowGroupBy.cs @@ -0,0 +1,111 @@ +using Microsoft.EntityFrameworkCore.ValueGeneration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QueryingIssues.EF7 +{ + public class QueryStackoverflowGroupBy + { + // https://stackoverflow.com/questions/69355421/retrieving-the-last-record-in-each-group-with-ef-core + // https://stackoverflow.com/questions/59456026/how-to-select-top-n-rows-for-each-group-in-a-entity-framework-groupby-with-ef-3/59468439#59468439 + public void QueryNo59456026() + { + Init(); + + using var context = new BloggingContext(); + + var data = context + .Posts + .GroupBy(post => post.BlogId) + .Select(group => new + { + BlogId = group.Key, + Post = group.OrderByDescending(post => post.CreatedAt).FirstOrDefault() + }) + .ToList(); + + foreach (var item in data) + { + Console.WriteLine($"Blog {item.BlogId} - Post ({item.Post?.PostId}): {item.Post?.Title}"); + } + } + + public void Query() + { + Init(); + + using var context = new BloggingContext(); + + var result = context + .Blogs + .AsQueryable() + .Select(blog => new + { + BlogUrl = blog.Url, + Post = blog.Posts.OrderByDescending(post => post.CreatedAt).Select(post => new + { + post.PostId, + post.Title + }) + .FirstOrDefault() + }) + .ToList(); + + foreach ( var blog in result ) + { + Console.WriteLine($"Blog {blog.BlogUrl} - Post ({blog.Post?.PostId}): {blog.Post?.Title}"); + } + } + + private void Init() + { + using var context = new BloggingContext(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + var now = DateTime.Now; + // Generate 10 blogs + for (int i = 0; i < 10; i++) + { + var blog = new Blog { Url = $"https://example.com/blog/{i + 1}" }; + context.Blogs.Add(blog); + + // Generate 2 to 4 posts for each blog + var random = new Random(); + int postCount = random.Next(2, 5); + + for (int j = 0; j < postCount; j++) + { + now = now.AddMinutes(1); + var post = new Post + { + Title = $"Post {j + 1} for Blog {i + 1}", + Content = $"Content for Post {j + 1}", + CreatedAt = now, + NumberOfView = random.Next(100, 501) + }; + blog.Posts.Add(post); + } + } + + context.SaveChanges(); + + Print(context); + } + + private void Print(BloggingContext context) + { + foreach(var blog in context.Blogs) + { + Console.WriteLine($"Blog {blog.Url} "); + foreach(var post in blog.Posts) + { + Console.WriteLine($"Post ({post.PostId}): {post.Title} - {post.CreatedAt}"); + } + } + } + } +}