首页 > 解决方案 > ASP.NET Core 5 Blazor WASM、gRPC、Entity Framework Core 5:多对多导致堆栈溢出

问题描述

信息:

示例项目: ( GitHub 上提供的完整工作源)它是博客 cms 的(简单)原型,在帖子和标签之间具有多对多关系。

当我尝试从应用程序返回带有标签的帖子列表时,BlogService.cs会出现“堆栈溢出”错误。在我看来,它就像一个参考循环,就像你在使用 json 时得到的一样。但也许我错了,我不知道。(使用 Json.Net,您需要设置ReferenceLoopHandling = ReferenceLoopHandling.Ignore为多对多工作)

或者我可以对 LINQ 查询进行更改,以便只返回每个帖子.Include(tipd => tipd.TagsInPostData)BlogService.cs的标签(一层深)并且不尝试解析每个标签中的所有帖子等等。

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

(部分)BlogService.cs,完整来源在这里

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 从中创建表的类(包括连接表,EF Core 会“自动”创建该表)是从 protobuf 文件中创建的blog.proto。为了使用 gRPC 将记录添加到连接表中,我在 GitHub 上提出了一个问题,并相应地修改了我的代码ApplicationDbContext.cs

(部分)ApplicationDbContext.cs,完整来源在这里

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,完整来源在这里

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)

完整源代码的快速链接:

标签: c#linqentity-framework-coreblazorgrpc

解决方案


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

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

我已经使用以下代码在 Github 上更新了我的工作示例项目:

(部分)/Server/Services/ BlogService.cs(完整源代码

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;
}

这是结果:

在此处输入图像描述


推荐阅读