Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Understand why EF Core thinks it needs a setter for TagsInPostsData in gRPC model #23703

Closed
JeepNL opened this issue Dec 16, 2020 · 10 comments · Fixed by #24553
Closed

Understand why EF Core thinks it needs a setter for TagsInPostsData in gRPC model #23703

JeepNL opened this issue Dec 16, 2020 · 10 comments · Fixed by #24553
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@JeepNL
Copy link

JeepNL commented Dec 16, 2020

  • ASP.NET Core 5.01 (Blazor/Wasm, Kestrel Hosted)
  • Entity Framework Core 5.01 / SQLite
  • Visual Studio v16.9.0 Preview 2.0

I'm trying to configure a many-to-many relationship in my (Shared Folder) protobuf file with the video.proto code sample below. This code compiles and auto generates the ApplicationDbContextModelSnapshot.cs / xxxx_InitialCreate.cs and creates 'automagically' the extra (needed) table "TagVideo" (see below).

I'm trying to add model seed data as described in the EF Core 5.x ' Many-to-Many' samples in Microsoft Docs

I'm having difficulties implementing this, mainly because a) I'm learning 😉 & b) because EF Core auto generates the code/table I do not have the 'TagVideo' class. I don't know how to configure this in ApplicationDbContext.cs / in my Blazor web application.

File: video.proto

syntax = "proto3";
option csharp_namespace = "Mediatheek.Shared.Protos";
import "google/protobuf/empty.proto";
package protovideoservice;

service ProtoVideoService{
    rpc GetVideos(google.protobuf.Empty) returns (Videos);
}

message Video {
    int32 video_id = 1;
    string title = 2;
    string description = 3;
    // Many to Many (Tags in Video)
    repeated Tag tags = 27;
}
message Videos {
    repeated Video videos = 1;
}

message Tag {
	string tag_id = 1; // STRING!! = DESCRIPTION !!
	// Many to Many (Videos with Tag)
	repeated Video videos = 3;
}
message Tags {
    repeated Tag tags = 1;
}

In ApplicationDbContextModelSnapshot.cs

        modelBuilder.Entity("TagVideo", b =>
            {
                b.Property<string>("TagsTagId")
                    .HasColumnType("TEXT");

                b.Property<int>("VideosVideoId")
                    .HasColumnType("INTEGER");

                b.HasKey("TagsTagId", "VideosVideoId");

                b.HasIndex("VideosVideoId");

                b.ToTable("TagVideo");
            });

        modelBuilder.Entity("TagVideo", b =>
            {
                b.HasOne("Mediatheek.Shared.Protos.Tag", null)
                    .WithMany()
                    .HasForeignKey("TagsTagId")
                    .OnDelete(DeleteBehavior.Cascade)
                    .IsRequired();

                b.HasOne("Mediatheek.Shared.Protos.Video", null)
                    .WithMany()
                    .HasForeignKey("VideosVideoId")
                    .OnDelete(DeleteBehavior.Cascade)
                    .IsRequired();
            });

In 20201216131125_InitialCreate.cs

        migrationBuilder.CreateTable(
            name: "TagVideo",
            columns: table => new
            {
                TagsTagId = table.Column<string>(type: "TEXT", nullable: false),
                VideosVideoId = table.Column<int>(type: "INTEGER", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_TagVideo", x => new { x.TagsTagId, x.VideosVideoId });
                table.ForeignKey(
                    name: "FK_TagVideo_Tags_TagsTagId",
                    column: x => x.TagsTagId,
                    principalTable: "Tags",
                    principalColumn: "TagId",
                    onDelete: ReferentialAction.Cascade);
                table.ForeignKey(
                    name: "FK_TagVideo_Videos_VideosVideoId",
                    column: x => x.VideosVideoId,
                    principalTable: "Videos",
                    principalColumn: "VideoId",
                    onDelete: ReferentialAction.Cascade);
            });
@ajcvickers
Copy link
Contributor

/cc @JeremyLikness

@JeepNL
Copy link
Author

JeepNL commented Dec 19, 2020

[Update: OLD, Deleted this repo and created a new repo, see update below]

I've created a sample GitHub repo for my question where you can see where I get an error adding seed data for "Join entity type configuration". Error: "No backing field could be found for property 'Tag.Videos' and the property does not have a setter."

Sample code copied from Microsoft Docs: "Model seed data can be provided for the join entity type by using anonymous types"

I've updated my 'Videos.proto' to this.

Everything compiles, and EF Core creates the TagVideo join table & seeds the Videos & Tags data when I comment out the 'join entity' code below. (last 'modelBuilder'). I'm unable to seed the join table data.

Part of the sample project ApplicationDbContext.cs file where the error occurs:

// seed Videos
modelBuilder
	.Entity<Video>()
	.HasData(
		new Video { VideoId = 1, Title = "First Video", Description = "First Description" },
		new Video { VideoId = 2, Title = "Second Video", Description = "Second Description" },
		new Video { VideoId = 3, Title = "Third Video", Description = "Third Description" }
	);

// seed Tags
modelBuilder
	.Entity<Tag>()
	.HasData(
		new Tag { TagId = "FirstTag" },
		new Tag { TagId = "SecondTag" },
		new Tag { TagId = "ThirdTag" }
	);

// This doesn't work
// See: Join entity type configuration:
// https://docs.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#join-entity-type-configuration
// "Model seed data can be provided for the join entity type by using anonymous types. You can examine the model debug view to determine the property names created by convention."

// Error in PMC when: Add-Migration InitialCreate -OutputDir "Data/Migrations"; Update-Database;
// Ërror: "No backing field could be found for property 'Tag.Videos' and the property does not have a setter."
modelBuilder
	.Entity<Video>()
	.HasMany(p => p.Tags)
	.WithMany(p => p.Videos)
	.UsingEntity(j => j.HasData(
		new { VideosVideoId = 1, TagsTagId = "FirstTag" })
	);

@JeepNL
Copy link
Author

JeepNL commented Dec 20, 2020

This is my problem:

I think I should have something like the code below in ApplicationDbContext.cs

But in Video.proto (Video.proto should start with a lowercase 'v' actually, didn't update it yet in the source) there's no definition (class) for 'TagVideo', the table which EFCore builds 'automagically'.

So I can't use public DbSet<TagVideo> TagsVideos { get; set; } as well

public DbSet<TagVideo> TagsVideos { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	modelBuilder.Entity<TagsVideos>().HasKey(x => new { x.TagsTagId, x.VideosVideId});

	base.OnModelCreating(modelBuilder);
}

@JeepNL
Copy link
Author

JeepNL commented Dec 29, 2020

I watched the EF Core Tour Pt2 on Channel9 "Deep Dive in Many to Many" and updated my code repository. Actually I've created a new one:

The 3 most important files are:

This is a working Blazor WASM (Kestrel Hosted with SQLite DB) repo as long as I do not run (comment out) the code below in SeedData.cs (adding records to the join table). EF Core generates the Join Table PostsTags (automagically 😉) with the 'table definitions' provided in blog.proto, when I run 'Migrations'.

Screenshot 2020-12-29 144823

When I try to add records to the Join Table (in SeedData.cs), it results in an error.

// Uncomment the 3 lines below to see the error.
tag1.PostsInTagsData.AddRange(new[] { post1 });
tag2.PostsInTagsData.AddRange(new[] { post1, post3 });
tag3.PostsInTagsData.AddRange(new[] { post2, post3 });

The error is:

crit: Microsoft.AspNetCore.Hosting.Diagnostics[6]
      Application startup exception
      System.InvalidOperationException: No backing field could be found for property 'Tag.PostsInTagsData' and the property does not have a setter.

Adding other records to the tables (Authors, Posts, Tags) works. See /Server/Data/SeedData.cs

@JeremyLikness
Copy link
Member

I propose we close this issue.

@JeepNL it looks to me like the problem is with the generated entities. The RepeatedField implementation doesn't give EF Core the access needed to populate the list. The problem is related to this protobuf issue. I'm not a gRPC expert but in looking at existing implementations of gRPC + EF Core, it seems developers are defining a class separate from the generated one and using that for EF Core with a helper method to move the values into the gRPC-generated class for messaging.

Not the best answer but let me know if you have any other questions and I will close to keep open accordingly.

@JeepNL
Copy link
Author

JeepNL commented Dec 30, 2020

@JeremyLikness In the issue you mentioned (and sub issues in it) people are talking about gRPC and problems with JSON serialization/deserialization if I understand correctly. But probably I don't, because I don't get why they want to use JSON serialization/deserialization with gRPC. I like gRPC just because it isn't REST.

I love the combination EF Core/gRPC for (in my case Blazor WASM) web apps, because it's fast, it's contract based, it does binary wire transfer, it's imo easier to use than JSON/REST (serialization/deserialization), has more possibilities (client/server streaming) and EF Core already autogenerates a lot of code from a protobuf (definition/contract) file.

Maybe I'm one of the early adopters using this combination (gRPC/EFCore) where most people still are using REST, but I'm pretty sure this will change in the near future because of the many pro's with using gRPC. I'm searching all over the internet how to use Many to Many relationships with EF Core and gRPC but I can't find the info I'm looking for. I'm watching videos how to use EF Core in general (i.e. your ON.NET EF Core Tour EF Core episodes, love them!) and I just bought the book "Entity Framework Core in Action Second Edition" (MEAP / Manning / Jon P Smith)

I know I've a lot to learn about C# and EF Core, but so far I've written several (simple) prototypes which are using EF Core/gRPC and it so much fun writing them and I've learned already a lot how to use EF Core with gRPC for database access except how to implement Many to Many relationships.

You say "the RepeatedField implementation doesn't give EF Core the access needed to populate the list", is there any way I can add the necessary code somewhere so EF Core can populate the list? EF Core already 'automagically' creates the Join Table from the RepeatedFields in the protobuf file, but (if I understand correctly) can't use it yet. I'm not looking for a step by step example (although that would be nice 😉) but maybe links to articles/docs/github code where I can learn how to configure it. Or maybe I should use Stack Overflow for these types of questions instead if GitHub. I don't know, maybe this is the wrong place for these types of questions.

But I think more questions will come in the future about how to use Many to Many relationships with EF Core and gRPC because of the benefits of gRPC over REST. That's also why I've created a special GitHub repo for this question so maybe I can help other devs with it.

@JeremyLikness
Copy link
Member

JeremyLikness commented Jan 4, 2021

Notes for triage:

Issue is at https://github.com/dotnet/efcore/blob/main/src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs#HL93-HL101

Call stack:

System.InvalidOperationException
  HResult=0x80131509
  Message=No backing field could be found for property 'Tag.PostsInTagsData' and the property does not have a setter.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrCollectionAccessorFactory.Create(IPropertyBase navigation, IEntityType targetType) in /_/src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs:line 100
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrCollectionAccessorFactory.Create(INavigationBase navigation) in /_/src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs:line 52
   at Microsoft.EntityFrameworkCore.Metadata.Internal.SkipNavigation.<>c.<get_CollectionAccessor>b__45_0(SkipNavigation n) in /_/src/EFCore/Metadata/Internal/SkipNavigation.cs:line 326
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory) in /_/src/Shared/NonCapturingLazyInitializer.cs:line 24
   at Microsoft.EntityFrameworkCore.Metadata.Internal.SkipNavigation.get_CollectionAccessor() in /_/src/EFCore/Metadata/Internal/SkipNavigation.cs:line 325
   at Microsoft.EntityFrameworkCore.Metadata.ISkipNavigation.Microsoft.EntityFrameworkCore.Metadata.INavigationBase.GetCollectionAccessor() in /_/src/EFCore/Metadata/ISkipNavigation.cs:line 51
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.AddToCollection(INavigationBase navigationBase, InternalEntityEntry value, Boolean forMaterialization) in /_/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs:line 823
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.AddToCollection(InternalEntityEntry entry, INavigationBase navigation, InternalEntityEntry value, Boolean fromQuery) in /_/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs:line 1279
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.InitialFixup(InternalEntityEntry entry, Boolean fromQuery) in /_/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs:line 675
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.StateChanged(InternalEntityEntry entry, EntityState oldState, Boolean fromQuery) in /_/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs:line 555
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.StateChanged(InternalEntityEntry entry, EntityState oldState, Boolean fromQuery) in /_/src/EFCore/ChangeTracking/Internal/InternalEntityEntryNotifier.cs:line 68
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.FireStateChanged(EntityState oldState) in /_/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs:line 346
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties) in /_/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs:line 329
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey) in /_/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs:line 140
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node) in /_/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs:line 102
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode) in /_/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs:line 40
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey) in /_/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs:line 51
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.SetEntityState(InternalEntityEntry entry, EntityState entityState) in /_/src/EFCore/Internal/InternalDbSet.cs:line 558
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Add(TEntity entity) in /_/src/EFCore/Internal/InternalDbSet.cs:line 202
    ...
    [Call Stack Truncated]

/cc @ajcvickers

@ajcvickers
Copy link
Contributor

@JeepNL @JeremyLikness Finally got time to dig into this. Firstly, I'm impressed that EF Core is able to get this far with mapping against the generated entity types by convention. However, the RepeatedField properties do not have field names that can be found by convention. This can be fixed by the following configuration:

modelBuilder.Entity<Post>().Navigation(e => e.TagsInPostsData).HasField("tagsInPostsData_");
modelBuilder.Entity<Tag>().Navigation(e => e.PostsInTagsData).HasField("postsInTagsData_");

I want to keep this issue open to investigate why EF Core thinks it needs the setter or field access here, since it looks like the collections are initialized eagerly.

@JeepNL
Copy link
Author

JeepNL commented Jan 12, 2021

@ajcvickers No problem! And we just had the Christmas/New Year holidays, so it is very understandable, I'm just glad I've found something which seems to be more an issue than a question so I'm not wasting your time. (this is my 'imposter syndrome' speaking, but no worries, I'm embracing it 😉) Tomorrow I'll try your modelBuilder code suggestion, it's here 6:35 PM, Amsterdam, The Netherlands. Thank you for that!

JeepNL added a commit to JeepNL/BlazorWasmGrpcBlog that referenced this issue Jan 13, 2021
JeepNL added a commit to JeepNL/BlazorWasmGrpcBlog that referenced this issue Jan 13, 2021
@JeepNL
Copy link
Author

JeepNL commented Jan 13, 2021

Wow! I actually got goosebumps when I ran the App. This is so great!

I've updated my BlazorWasmGrpcBlog GitHub Repo with some additional comments and links to the EF Core Deep Dive Pt2 video and your answer here.

I've renamed TagsInPostsData in TagsInPostData (Post singular) and PostsInTagsData in PostsInTagData (/Shared/Protos/blog.proto) e.g. multiple tags in one post and vice versa

@ajcvickers ajcvickers changed the title [Question] EF Core 5.x - gRPC/Protobuf - Many to Many Relationship (SQLite) Understand why EF Core thinks it needs a setter for TagsInPostsData in gRPC model Jan 15, 2021
@ajcvickers ajcvickers added this to the 6.0.0 milestone Jan 15, 2021
ajcvickers added a commit that referenced this issue Mar 31, 2021
Fixes #23703

At some point we started eagerly throwing when attempting to build a setter delegate. This should be lazy because we don't always need a setter.

Fixes #23901

Detects "propertyName_" as a backing field.
@ajcvickers ajcvickers modified the milestones: 6.0.0, 6.0.0-preview4 Mar 31, 2021
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Mar 31, 2021
ajcvickers added a commit that referenced this issue Mar 31, 2021
Fixes #23703

At some point we started eagerly throwing when attempting to build a setter delegate. This should be lazy because we don't always need a setter.

Fixes #23901

Detects "propertyName_" as a backing field.
@ajcvickers ajcvickers removed this from the 6.0.0-preview4 milestone Nov 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants