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

Add documentation for many-to-many relationship configuration. #2716

Merged
merged 1 commit into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions entity-framework/core/modeling/data-seeding.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ There are several ways this can be accomplished in EF Core:

## Model seed data

> [!NOTE]
> This feature is new in EF Core 2.1.

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.

> [!NOTE]
Expand Down
51 changes: 50 additions & 1 deletion entity-framework/core/modeling/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,55 @@ With this configuration the columns corresponding to `ShippingAddress` will be m

### Many-to-many

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.
Many to many relationships require a collection navigation property on both sides. They will be discovered by convention like other types of relationships.

[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=ManyToManyShared)]

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.

```sql
CREATE TABLE [Posts] (
[PostId] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Content] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([PostId])
);

CREATE TABLE [Tags] (
[TagId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
);

CREATE TABLE [PostTag] (
[PostId] int NOT NULL,
[TagId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostId], [TagId]),
CONSTRAINT [FK_PostTag_Posts_PostId] FOREIGN KEY ([PostId]) REFERENCES [Posts] ([PostId]) ON DELETE CASCADE,
CONSTRAINT [FK_PostTag_Tags_TagId] FOREIGN KEY ([TagId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);
```

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.

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`.

[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=Metadata)]

It is common to apply configuration to the join entity type. This action can be accomplished via `UsingEntity`.

[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=SharedConfiguration)]

[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.

[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs?name=Seeding)]

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.

[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToManyPayload.cs?name=ManyToManyPayload)]

> [!NOTE]
> The ability to configure many-to-many relationships was added in EF Core 5.0, for previous version use the following approach.

You can also represent a many-to-many relationship by just adding the join entity type and mapping two separate one-to-many relationships.

[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Relationships/ManyToMany.cs?name=ManyToMany&highlight=11-14,16-19,39-46)]
24 changes: 17 additions & 7 deletions entity-framework/core/what-is-new/ef-core-5.0/whatsnew.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,23 @@ Unlike EF6, EF Core allows full customization of the join table. For example, th
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Community>()
.HasMany(e => e.Members)
.WithMany(e => e.Memberships)
.UsingEntity<PersonCommunity>(
b => b.HasOne(e => e.Member).WithMany().HasForeignKey(e => e.MembersId),
b => b.HasOne(e => e.Membership).WithMany().HasForeignKey(e => e.MembershipsId))
.Property(e => e.MemberSince).HasDefaultValueSql("CURRENT_TIMESTAMP");
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany()
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany()
.HasForeignKey(pt => pt.PostId),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
```

Expand Down
2 changes: 1 addition & 1 deletion samples/core/Modeling/FluentAPI/FluentAPI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-preview.5.20278.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-rc.1.20451.13" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions samples/core/Modeling/FluentAPI/IndexName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace EFModeling.FluentAPI.Relational.IndexName
{
#pragma warning disable CS0618 // Type or member is obsolete
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
Expand All @@ -15,6 +16,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
}
#endregion
}
#pragma warning restore CS0618 // Type or member is obsolete

public class Blog
{
Expand Down
7 changes: 1 addition & 6 deletions samples/core/Modeling/FluentAPI/Program.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace EFModeling.FluentAPI
{
class Program
{
static void Main(string[] args)
{

}
}
}
10 changes: 9 additions & 1 deletion samples/core/Modeling/FluentAPI/Relationships/ManyToMany.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;

namespace EFModeling.FluentAPI.Relationships.ManyToMany
{
#region ManyToMany
class MyContext : DbContext
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}

public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

Expand Down Expand Up @@ -44,6 +50,8 @@ public class Tag

public class PostTag
{
public DateTime PublicationDate { get; set; }

public int PostId { get; set; }
public Post Post { get; set; }

Expand Down
69 changes: 69 additions & 0 deletions samples/core/Modeling/FluentAPI/Relationships/ManyToManyPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;

namespace EFModeling.FluentAPI.Relationships.ManyToManyPayload
{
#region ManyToManyPayload
class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}

public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}

public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
public DateTime PublicationDate { get; set; }

public int PostId { get; set; }
public Post Post { get; set; }

public string TagId { get; set; }
public Tag Tag { get; set; }
}
#endregion
}
72 changes: 72 additions & 0 deletions samples/core/Modeling/FluentAPI/Relationships/ManyToManyShared.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;

namespace EFModeling.FluentAPI.Relationships.ManyToManyShared
{
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}

public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region SharedConfiguration
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.ToTable("PostTags"));
#endregion

#region Metadata
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var skipNavigation in entityType.GetSkipNavigations())
{
Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
}
}
#endregion

#region Seeding
modelBuilder
.Entity<Post>()
.HasData(new Post { PostId = 1, Title = "First"});

modelBuilder
.Entity<Tag>()
.HasData(new Tag { TagId = "ef" });

modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.HasData(new { PostsPostId = 1, TagsTagId = "ef" }));
#endregion
}
}

#region ManyToManyShared
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public ICollection<Post> Posts { get; set; }
}
#endregion
}