Skip to content

Commit 6449d9e

Browse files
authored
Add documentation for many-to-many relationship configuration.
Fixes #1978
1 parent 84c9763 commit 6449d9e

File tree

9 files changed

+221
-19
lines changed

9 files changed

+221
-19
lines changed

entity-framework/core/modeling/data-seeding.md

-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ There are several ways this can be accomplished in EF Core:
1818

1919
## Model seed data
2020

21-
> [!NOTE]
22-
> This feature is new in EF Core 2.1.
23-
2421
Unlike in EF6, in EF Core, seeding data can be associated with an entity type as part of the model configuration. Then EF Core [migrations](xref:core/managing-schemas/migrations/index) can automatically compute what insert, update or delete operations need to be applied when upgrading the database to a new version of the model.
2522

2623
> [!NOTE]

entity-framework/core/modeling/relationships.md

+50-1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,55 @@ With this configuration the columns corresponding to `ShippingAddress` will be m
267267
268268
### Many-to-many
269269

270-
Many-to-many relationships without an entity class to represent the join table are not yet supported. However, you can represent a many-to-many relationship by including an entity class for the join table and mapping two separate one-to-many relationships.
270+
Many to many relationships require a collection navigation property on both sides. They will be discovered by convention like other types of relationships.
271+
272+
[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=ManyToManyShared)]
273+
274+
The way this relationship is implemented in the database is by a join table that contains foreign keys to both `Post` and `Tag`. For example this is what EF will create in a relational database for the above model.
275+
276+
```sql
277+
CREATE TABLE [Posts] (
278+
[PostId] int NOT NULL IDENTITY,
279+
[Title] nvarchar(max) NULL,
280+
[Content] nvarchar(max) NULL,
281+
CONSTRAINT [PK_Posts] PRIMARY KEY ([PostId])
282+
);
283+
284+
CREATE TABLE [Tags] (
285+
[TagId] nvarchar(450) NOT NULL,
286+
CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
287+
);
288+
289+
CREATE TABLE [PostTag] (
290+
[PostId] int NOT NULL,
291+
[TagId] nvarchar(450) NOT NULL,
292+
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostId], [TagId]),
293+
CONSTRAINT [FK_PostTag_Posts_PostId] FOREIGN KEY ([PostId]) REFERENCES [Posts] ([PostId]) ON DELETE CASCADE,
294+
CONSTRAINT [FK_PostTag_Tags_TagId] FOREIGN KEY ([TagId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
295+
);
296+
```
297+
298+
Internally, EF creates an entity type to represent the join table that will be referred to as the join entity type. There is no specific CLR type that can be used for this, so `Dictionary<string, object>` is used. More than one many-to-many relationships can exist in the model, therefore the join entity type must be given a unique name, in this case `PostTag`. The feature that allows this is called shared-type entity type.
299+
300+
The many to many navigations are called skip navigations as they effectively skip over the join entity type. If you are employing bulk configuration all skip navigations can be obtained from `GetSkipNavigations`.
301+
302+
[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=Metadata)]
303+
304+
It is common to apply configuration to the join entity type. This action can be accomplished via `UsingEntity`.
305+
306+
[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=SharedConfiguration)]
307+
308+
[Model seed data](xref:core/modeling/data-seeding) 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.
309+
310+
[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=Seeding)]
311+
312+
Additional data can be stored in the join entity type, but for this it's best to create a bespoke CLR type. When configuring the relationship with a custom join entity type both foreign keys need to be specified explicitly.
313+
314+
[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyPayload.cs?name=ManyToManyPayload)]
315+
316+
> [!NOTE]
317+
> The ability to configure many-to-many relationships was added in EF Core 5.0, for previous version use the following approach.
318+
319+
You can also represent a many-to-many relationship by just adding the join entity type and mapping two separate one-to-many relationships.
271320

272321
[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToMany.cs?name=ManyToMany&highlight=11-14,16-19,39-46)]

entity-framework/core/what-is-new/ef-core-5.0/whatsnew.md

+17-7
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,23 @@ Unlike EF6, EF Core allows full customization of the join table. For example, th
133133
protected override void OnModelCreating(ModelBuilder modelBuilder)
134134
{
135135
modelBuilder
136-
.Entity<Community>()
137-
.HasMany(e => e.Members)
138-
.WithMany(e => e.Memberships)
139-
.UsingEntity<PersonCommunity>(
140-
b => b.HasOne(e => e.Member).WithMany().HasForeignKey(e => e.MembersId),
141-
b => b.HasOne(e => e.Membership).WithMany().HasForeignKey(e => e.MembershipsId))
142-
.Property(e => e.MemberSince).HasDefaultValueSql("CURRENT_TIMESTAMP");
136+
.Entity<Post>()
137+
.HasMany(p => p.Tags)
138+
.WithMany(p => p.Posts)
139+
.UsingEntity<PostTag>(
140+
j => j
141+
.HasOne(pt => pt.Tag)
142+
.WithMany()
143+
.HasForeignKey(pt => pt.TagId),
144+
j => j
145+
.HasOne(pt => pt.Post)
146+
.WithMany()
147+
.HasForeignKey(pt => pt.PostId),
148+
j =>
149+
{
150+
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
151+
j.HasKey(t => new { t.PostId, t.TagId });
152+
});
143153
}
144154
```
145155

samples/core/Modeling/FluentAPI/FluentAPI.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-preview.5.20278.2" />
11+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-rc.1.20451.13" />
1212
</ItemGroup>
1313

1414
</Project>

samples/core/Modeling/FluentAPI/IndexName.cs

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace EFModeling.FluentAPI.Relational.IndexName
44
{
5+
#pragma warning disable CS0618 // Type or member is obsolete
56
class MyContext : DbContext
67
{
78
public DbSet<Blog> Blogs { get; set; }
@@ -15,6 +16,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
1516
}
1617
#endregion
1718
}
19+
#pragma warning restore CS0618 // Type or member is obsolete
1820

1921
public class Blog
2022
{
+1-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
1+
using Microsoft.EntityFrameworkCore;
62

73
namespace EFModeling.FluentAPI
84
{
95
class Program
106
{
117
static void Main(string[] args)
128
{
13-
149
}
1510
}
1611
}

samples/core/Modeling/FluentAPI/Relationships/ManyToMany.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
using Microsoft.EntityFrameworkCore;
2+
using System;
23
using System.Collections.Generic;
34

45
namespace EFModeling.FluentAPI.Relationships.ManyToMany
56
{
67
#region ManyToMany
7-
class MyContext : DbContext
8+
public class MyContext : DbContext
89
{
10+
public MyContext(DbContextOptions<MyContext> options)
11+
: base(options)
12+
{
13+
}
14+
915
public DbSet<Post> Posts { get; set; }
1016
public DbSet<Tag> Tags { get; set; }
1117

@@ -44,6 +50,8 @@ public class Tag
4450

4551
public class PostTag
4652
{
53+
public DateTime PublicationDate { get; set; }
54+
4755
public int PostId { get; set; }
4856
public Post Post { get; set; }
4957

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using System;
3+
using System.Collections.Generic;
4+
5+
namespace EFModeling.FluentAPI.Relationships.ManyToManyPayload
6+
{
7+
#region ManyToManyPayload
8+
class MyContext : DbContext
9+
{
10+
public MyContext(DbContextOptions<MyContext> options)
11+
: base(options)
12+
{
13+
}
14+
15+
public DbSet<Post> Posts { get; set; }
16+
public DbSet<Tag> Tags { get; set; }
17+
18+
protected override void OnModelCreating(ModelBuilder modelBuilder)
19+
{
20+
modelBuilder.Entity<Post>()
21+
.HasMany(p => p.Tags)
22+
.WithMany(p => p.Posts)
23+
.UsingEntity<PostTag>(
24+
j => j
25+
.HasOne(pt => pt.Tag)
26+
.WithMany(t => t.PostTags)
27+
.HasForeignKey(pt => pt.TagId),
28+
j => j
29+
.HasOne(pt => pt.Post)
30+
.WithMany(p => p.PostTags)
31+
.HasForeignKey(pt => pt.PostId),
32+
j =>
33+
{
34+
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
35+
j.HasKey(t => new { t.PostId, t.TagId });
36+
});
37+
}
38+
}
39+
40+
public class Post
41+
{
42+
public int PostId { get; set; }
43+
public string Title { get; set; }
44+
public string Content { get; set; }
45+
46+
public ICollection<Tag> Tags { get; set; }
47+
public List<PostTag> PostTags { get; set; }
48+
}
49+
50+
public class Tag
51+
{
52+
public string TagId { get; set; }
53+
54+
public ICollection<Post> Posts { get; set; }
55+
public List<PostTag> PostTags { get; set; }
56+
}
57+
58+
public class PostTag
59+
{
60+
public DateTime PublicationDate { get; set; }
61+
62+
public int PostId { get; set; }
63+
public Post Post { get; set; }
64+
65+
public string TagId { get; set; }
66+
public Tag Tag { get; set; }
67+
}
68+
#endregion
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using System;
3+
using System.Collections.Generic;
4+
5+
namespace EFModeling.FluentAPI.Relationships.ManyToManyShared
6+
{
7+
public class MyContext : DbContext
8+
{
9+
public MyContext(DbContextOptions<MyContext> options)
10+
: base(options)
11+
{
12+
}
13+
14+
public DbSet<Post> Posts { get; set; }
15+
public DbSet<Tag> Tags { get; set; }
16+
17+
protected override void OnModelCreating(ModelBuilder modelBuilder)
18+
{
19+
#region SharedConfiguration
20+
modelBuilder
21+
.Entity<Post>()
22+
.HasMany(p => p.Tags)
23+
.WithMany(p => p.Posts)
24+
.UsingEntity(j => j.ToTable("PostTags"));
25+
#endregion
26+
27+
#region Metadata
28+
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
29+
{
30+
foreach (var skipNavigation in entityType.GetSkipNavigations())
31+
{
32+
Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
33+
}
34+
}
35+
#endregion
36+
37+
#region Seeding
38+
modelBuilder
39+
.Entity<Post>()
40+
.HasData(new Post { PostId = 1, Title = "First"});
41+
42+
modelBuilder
43+
.Entity<Tag>()
44+
.HasData(new Tag { TagId = "ef" });
45+
46+
modelBuilder
47+
.Entity<Post>()
48+
.HasMany(p => p.Tags)
49+
.WithMany(p => p.Posts)
50+
.UsingEntity(j => j.HasData(new { PostsPostId = 1, TagsTagId = "ef" }));
51+
#endregion
52+
}
53+
}
54+
55+
#region ManyToManyShared
56+
public class Post
57+
{
58+
public int PostId { get; set; }
59+
public string Title { get; set; }
60+
public string Content { get; set; }
61+
62+
public ICollection<Tag> Tags { get; set; }
63+
}
64+
65+
public class Tag
66+
{
67+
public string TagId { get; set; }
68+
69+
public ICollection<Post> Posts { get; set; }
70+
}
71+
#endregion
72+
}

0 commit comments

Comments
 (0)