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

Posted

技术标签:

【中文标题】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 【发布时间】:2021-01-15 14:47:16 【问题描述】:

信息:

使用:Visual Studio v16.9.0 Preview 2.0 和 .NET SDK 5.0.200 Kestrel 使用 SQLite DB 托管 Blazor WASM / gRPC / EF Core 项目。

示例项目:(可在GitHub 获得完整的工作源代码)它是博客 cms 的(简单)原型,帖子和标签之间存在多对多关系。

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

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

我还在学习 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 从中创建表的类(包括连接表,EF Core 会“自动”创建该表)是从 protobuf 文件 blog.proto 创建的。为了使用 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)

完整源代码的快速链接:

sourceblog.proto sourceApplicationDbContext.cs sourceBlogService.cs sourceSeedData.cs

【问题讨论】:

【参考方案1】:

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

我在 Github Repo grpc-dotnet 上发布了一个问题,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;

结果如下:

【讨论】:

以上是关于ASP.NET Core 5 Blazor WASM、gRPC、Entity Framework Core 5:多对多导致堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

Asp.NET Core进阶 第四篇 Asp.Net Core Blazor框架

在 ASP.NET Core 站点中实现 Blazor - 组件不在视图中呈现

Blazor——Asp.net core的新前端框架

.NET Core 3.0 Preview 6中对ASP.NET Core和Blazor的更新

从 ASP.NET Core Blazor 中的 .NET 方法调用 JavaScript 函数

从模板 Visual Studio 创建 Blazor(ASP.NET Core 托管)项目