ASP.NET Core 5 Blazor WASM、gRPC、Entity Framework Core 5:多对多导致堆栈溢出

ASP.NET Core 5 Blazor WASM, gRPC, Entity Framework Core 5: many-to-many results in stack overflow

信息:

示例项目:(可在 GitHub 获得完整的工作源代码) 它是博客 cms 的(简单)原型,在 posts 和标签。

当我尝试 return 带有来自 BlogService.cs 的标签的 post 列表时,应用程序停止并出现 'stack overflow' 错误。在我看来,它就像一个参考循环,就像你在使用 json 时得到的一样。但也许我错了,我不知道。 (使用 Json.Net 您需要设置 ReferenceLoopHandling = ReferenceLoopHandling.Ignore 才能进行多对多工作)

或者我可以更改 LINQ 查询,以便 BlogService.cs 中的 .Include(tipd => tipd.TagsInPostData) 仅 return 每个 post 中的标签(一层深)并且不' 尝试解析每个标记中的所有 post,依此类推。

我仍在学习 C#、EF Core、gRPC、LINQ 并且英语不是我的第一语言,所以我希望你能理解我的问题。如果没有,请说出来,我会努力做得更好。

(部分)BlogService.cs,完整来源 here

public override async Task<Posts> GetPosts(Empty request, ServerCallContext context)
{
    var posts = new Posts();
    var allPosts = await dbContext.Posts
        .Where(ps => ps.PostStat == PostStatus.Published)
        .Include(pa => pa.PostAuthor)
        .Include(pe => pe.PostExt)
        //.Include(tipd => tipd.TagsInPostData) // TODO: [ERROR] / doesn't work: results in a stack overflow.
        .OrderByDescending(dc => dc.DateCreated)
        .ToListAsync();
    posts.PostsData.AddRange(allPosts);
    return posts;
}

EF Core 从中创建 table 的 类(包括连接 table,EF Core 创建那个 'automagically')是从 protobuf 文件创建的blog.proto。为了将记录添加到连接 table 与 gRPC 一起工作,我询问了 a question at GitHub 并且我在 ApplicationDbContext.cs

中相应地修改了我的代码

(部分)ApplicationDbContext.cs,完整来源 here

modelBuilder
    .Entity<Post>()
    .HasMany(e => e.TagsInPostData)
    .WithMany(e => e.PostsInTagData)
    .UsingEntity<Dictionary<string, object>>(
        "PostsTags", // (Join) Table Name.(Renames EF Core autogenerated 'PostTag' table to 'PostsTags' table)
        b => b.HasOne<Tag>().WithMany().HasForeignKey("TagId"), // Field Name.
        b => b.HasOne<Post>().WithMany().HasForeignKey("PostId") // Field Name.
    );

// Adding data to Many to Many Join Table 'PostsTags' now works because of just the 2 lines below.
// See: https://github.com/dotnet/efcore/issues/23703#issuecomment-758801618
modelBuilder.Entity<Post>().Navigation(e => e.TagsInPostData).HasField("tagsInPostData_");
modelBuilder.Entity<Tag>().Navigation(e => e.PostsInTagData).HasField("postsInTagData_");

(部分)blog.proto,完整来源 here

message Post { // For Public Access
    int32 post_id = 1;
    int32 author_id = 2;
    string title = 3;
    string date_created = 4; // DateTime (UTC) string because of SQLite
    PostStatus post_stat = 5; // enum
    PostExtended post_ext = 6; // one to one
    Author post_author = 7; // Post with one author, one to one
    repeated Tag tags_in_post_data = 8; // Post with many Tags
}
message Posts {
    repeated Post posts_data = 1;
}

/*
    Many to Many Tags in auto generated table "PostsTags"
    EF Core auto generates 'PostTag' table, Renaming it to 'PostsTags' is done in ApplicationDbContext
    Because of "message Post" with "repeated Tag tags_data"
    and "message Tag" with "repeated Post posts_data"
    EF Core creates "PostTag" table 'automagically'.
*/

message Tag {
    //string tag_id = 1; // Tag itself: string
    int32 tag_id = 1;
    string name = 2;
    repeated Post posts_in_tag_data = 3; // Tag with many Posts
}
message Tags {
    repeated Tag tags_data = 1;
}

错误:

info: 15-1-2021 14:08:44.437 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 5.0.1 initialized 'ApplicationDbContext' using provider 'Microsoft.EntityFrameworkCore.Sqlite' with options: SensitiveDataLoggingEnabled
info: 15-1-2021 14:08:45.908 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "p"."PostId", "p"."AuthorId", "p"."DateCreated", "p"."PostStat", "p"."Title", "a"."AuthorId", "a"."DateCreated", "a"."Name", "p0"."PostId", "p0"."Content", "p0"."Ts", "t0"."PostId", "t0"."TagId", "t0"."TagId0", "t0"."Name"
      FROM "Posts" AS "p"
      INNER JOIN "Authors" AS "a" ON "p"."AuthorId" = "a"."AuthorId"
      LEFT JOIN "PostsExtented" AS "p0" ON "p"."PostId" = "p0"."PostId"
      LEFT JOIN (
          SELECT "p1"."PostId", "p1"."TagId", "t"."TagId" AS "TagId0", "t"."Name"
          FROM "PostsTags" AS "p1"
          INNER JOIN "Tags" AS "t" ON "p1"."PostId" = "t"."TagId"
      ) AS "t0" ON "p"."PostId" = "t0"."TagId"
      WHERE "p"."PostStat" = 1
      ORDER BY "p"."DateCreated" DESC, "p"."PostId", "a"."AuthorId", "p0"."PostId", "t0"."PostId", "t0"."TagId", "t0"."TagId0"
Stack overflow.
   at System.Text.UTF8Encoding.GetByteCount(System.String)
   at Google.Protobuf.CodedOutputStream.ComputeStringSize(System.String)
   at BlazorWasmGrpcBlog.Shared.Protos.Post.CalculateSize()
   at Google.Protobuf.CodedOutputStream.ComputeMessageSize(Google.Protobuf.IMessage)
   at Google.Protobuf.FieldCodec+<>c__32`1[[System.__Canon, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].<ForMessage>b__32_4(System.__Canon)

完整源代码的快速链接:

我现在可以使用下面的代码了。我不知道这是否是正确的方法,或者我是否可以简化它,我还必须了解更多有关 LINQ 和映射对象的知识。

我在 Github Repo 发布了一个问题 grpc-dotnet and the answer James Newton-King 给了我实际上帮助了我很多:“Protobuf 序列化程序不支持引用循环。" 因此,我知道我必须修改我的查询,我不应该寻找解决 gRPC 中引用循环的解决方案。

我在 Github 更新了我的 sample project 工作代码:

(部分)/Server/Services/BlogService.cs(完整source

public override async Task<Posts> GetPosts(Empty request, ServerCallContext context)
{
    var postsQuery = await dbContext.Posts.AsSplitQuery() // trying/testing ".AsSplitQuery()"
                                                          //var postsQuery = await dbContext.Posts
        .Where(ps => ps.PostStat == PostStatus.Published)
        .Include(pa => pa.PostAuthor)
        .Include(pe => pe.PostExtended)
        .Include(tipd => tipd.TagsInPostData)
        .OrderByDescending(dc => dc.DateCreated)
        .AsNoTracking().ToListAsync();

    // The Protobuf serializer doesn't support reference loops
    // see: https://github.com/grpc/grpc-dotnet/issues/1177#issuecomment-763910215
    //var posts = new Posts();
    //posts.PostsData.AddRange(allPosts); // so this doesn't work
    //return posts

    Posts posts = new();
    foreach (var p in postsQuery)
    {
        Post post = new()
        {
            PostId = p.PostId,
            Title = p.Title,
            DateCreated = p.DateCreated,
            PostStat = p.PostStat,
            PostAuthor = p.PostAuthor,
            PostExtended = p.PostExtended,
        };

        // Just add all the tags to each post, this isn't a reference loop.
        List<Tag> tags = p.TagsInPostData.Select(t => new Tag { TagId = t.TagId }).ToList();
        post.TagsInPostData.AddRange(tags);

        // Add Post (now with tags) to posts
        posts.PostsData.Add(post);
    }
    return posts;
}

这是结果: