EF Core 5 added a direct many-to-many relationship with zero configuration (hurrah!). This article describes how to set this direct many-to-many relationship and why (and how) you might want to configure this direct many-to-many. I also include the original many-to-many relationship (referred to as indirect many-to-many from now on) where you need to create the class that acts as the linking table between the two classes.
You might be mildly interested that this is the third iteration of this article. I wrote the first article on many-to-many on EF6.x in 2014, and another many-to-many article for EF Core in 2017. All of these got a lot of views, so I had to write a new article once EF Core 5 came out. I hope you find it useful.
All the information and the code come from Chapter 2 of the second edition of my book, Entity Framework Core in Action, which cover EF Core 5. In this book I build a book selling site, called Book App, where each book has two, many-to-many relationships:
- A direct many-to-many relationship to a Tag entity class (I refer to classes that EF Core maps to a database as entity classes). A Tag holds a category (for instance: Microsoft .NET or Web) which allows users to pick books by their topic.
- An indirect many-to-many relationship to Author entity class, which provides an ordered list of Author’s on the book, for instance: by Dino Esposito, Andrea Saltarello.
Here is an example of how it displays each book to the user – this is a fictitious book I used for many of my tests.

Overall summary and links to each section summary
For people who are in a hurry I have a ended each section with a summary. Here are the links to the summaries:
The overall summary is:
- Direct many-to many relationships are super simple to configure and use, but by default you can’t access the linking table.
- Indirect many-to many relationships takes more work to set up, but you can access the linking table. This allows you to put specific data in the linking table, such as an order in which you want to read them back.
Here are the individual summaries (with links).
- Direct many-to-many setup
- Normal setup. Really easy
- When you want to define the linking table. More work than an indirect M2M
- Direct many-to-many usage
- Querying. Really easy
- Add a new link. Really easy
- Remove a link. Really easy
- Create Book with Tags. Easy.
- Indirect many-to-many setup – configuring the linking table. Moderate
- Indirect many-to-many usage
- Querying. Easy.
- Add a new link. Easy.
- Remove a link. Easy.
- Create Book with Authors. Easy.
NOTE: All the code you see in this article comes the companion GitHub repo to my book Entity Framework Core in Action. Here is link to the directory with the entity classes are in, and many of code examples comes from the Ch03_ManyToManyUpdate unit test class and Ch03_ManyToManyCreate.
Setting the scene – the database and the query
Let’s start by seeing the finished database and how the query works. You can skip this, but maybe having an overall view of what is going on will help you when you are looking at the detailed part you are looking at the specific part you are interested in. Let’s start with the database.

This shows the two many-to-many – both have a linking table, but the direct many-to-many has its linking table created by EF Core.
Next, let’s see the many-to-many queries and how they relate to the book display in the figure below.

You can see that the Book’s Authors (top left) needs to be ordered – that Order property (a byte) is in the linking entity class. But for the Book’s Tags (bottom left), which don’t have an order, the query is much simpler to write because EF Core will automatically add the extra SQL needed to use the hidden linking table.
Now we get into the detail of setting up and using both of these types of many-to-many relationships.
Direct many-to-many setup – normal setup.
The setting up of the direct many-to-many relationship is done automatically (this is known as By Convention configuration in EF Core). And when you create your database via EF Core, then it will add the linking table for you.

This is super simple to do – so much easier than the indirect many-to-many. But if you want to add extra data in the linking table, say for ordering or filtering, then you either alter the direct many-to-many or use the indirect many-to-many approach.
NOTE: The direct many-to-many relationship is only automatically configured if you have a collection navigational property on both ends. If you only want a navigational property on one end, then you will need to use the Fluent API configure (see next section), for instance …HasMany(x => x.Tags) .WithMany() where the Tags has no navigational property back to the Books.
Direct many-to-many setup: When you want to define the linking table
You can define an entity class and configure the linking table, but I will say that if you are going to do that you might as well use the indirect approach as I think it’s easier to set up and use.
Typically, you would only define the linking table if you wanted to add extra data. There are two steps in this process:
1. Creating a class to map to the linking table
Your entity class must have the two primary/foreign key from each ends of the many-to-many link, in this case it’s the BookId and the TagId. The code below defines the minimum properties to be the linking table – can add extra properties as normal, but I leave that you to do that.
public class BookTag
{
public int BookId { get; set; }
[Required]
[MaxLength(40)]
public string TagId { get; set; }
//You can add extra properties here
//relationships
public Book Book { get; private set; }
public Tag Tag { get; private set; }
}
You could add properties such as the Order property needed for the Author ordering, or maybe a property to use for soft delete. That’s up to you and doesn’t affect the configuration step that comes next.
2. Configuring the linking table in the OnModelCreating method
Now you have to configure the many-to-many linking class/table with the UsingEntity method in the OnModelCreating method in your application’s DbContext, as shown in the code below.
public class EfCoreContext : DbContext
{
//Other code left out to focus on many-to-many
protected override OnModelCreating(ModelBuilder modelBuilder)
{
//Other configuration left out to focus on many-to-many
modelBuilder.Entity<Book>().HasMany(x => x.Tags)
.WithMany(x => x.Books)
.UsingEntity<BookTag>(
x => x.HasOne(x => x.Tag)
.WithMany().HasForeignKey(x => x.TagId),
x => x.HasOne(x => x.Book)
.WithMany().HasForeignKey(x => x.BookId));
}
}
You can see the EF Core document on this here.
NOTE: I really recommend an excellent video produced by the EF Core team which has a long section on the new, direct many-to-many, including how to configure it to include extra data.
Direct many-to-many usage – querying
Querying the direct many-to-many relationships is quite normal. Here are some queries
- Load all the Books with their Tags
var books = context.Books.Include(b => b.Tags).ToList()
- Get all the books with the TagId (which holds the category name)
var books = context.Books.Tags.Select(t => t.TagId).ToList()
EF Core will detect that your query is using a direct many-to-many relationship and add the extra SQL to use the hidden linking table to get the correct entity instances on the other end of the many-to-many relationship.
Direct many-to-many usage: Add a new link
To add another many-to-many link to an existing entity class is easy – you just add the existing entry into the direct many-to-many navigational collection property. The code below shows how to add an existing Tag to a book that already had one Tag already.
var book = context.Books
.Include(p => p.Tags)
.Single(p => p.Title == "Quantum Networking");
var existingTag = context.Tags
.Single(p => p.TagId == "Editor's Choice");
book.Tags.Add(existingTag);
context.SaveChanges();
When you add the existing Tag into the Tags collection EF Core works out you want a linking entry created between the Book and the Tag. It then creates that new link.
A few things to say about this:
- You should load the existing Tags using the Include method, otherwise you could lose any existing links to Tags.
- You MUST load the existing Tag from the database to add to the Tags navigational collection. If you simply created a new Tag, then EF Core will add that new Tag to the database.
ADVANCE NOTES on navigational collection properties
Point 1: Let me explain why I say “You should load the existing Tags…” above. There are two situations:
- If you add an empty navigational collection on the initialization of the class, then you don’t have add the Include method, as an Add will work (but I don’t recommend this – see below).
- If your navigational collection is null after construction, then you MUST load the navigational collection, otherwise your code will fail.
Overall, I recommend loading the navigational collection using the Include method even if you have navigational collection has been set to an empty collection because the entity doesn’t match the database, which I try not to do as a future refactor might assume it did match the database.
Point 2: If you are adding a new entry (or removing an existing linking relationship) in a collection with LOTs of items in the collection, then you might have a performance issue with using an Include. In this case you can create (or delete for remove link – see below) the linking table entry. For a direct many-to-many relationship, you would need to create a property bag of the right form to add.
NOTE These ADVANCE NOTES also apply to the adding a new indirect many-to-many link.
Direct many-to-many usage: Remove a link
Removing a link to an entity that is already in the navigation property collection you simply remove that entity instance from the collection. The code below shows removing an existing Tag using the Remove method.
var book = context.Books
.Include(p => p.Tags)
.First();
var tagToRemove = book.Tags
.Single(x => x.TagId == "Editor's Choice");
book.Tags.Remove(tagToRemove);
context.SaveChanges();
This just like the adding of a link, but in this case EF Core works out you what linking entity that needs to be deleted to remove this relationship.
Direct many-to-many usage: Create Book with tags
To add tags when you first create a book you just add each tag to the Tags collection. The code below adds two existing Tags to a new book (note that I haven’t set up the Author – see this part for how you do that).
var existingTag1 = context.Tags.Single(t => t.TagId == "Tag1");
var existingTag2 = context.Tags.Single(t => t.TagId == "Tag2");
var newBook = new Book()
{
Title = "My Book",
//... other property settings left out
//Set your Tags property to an empty collection
Tags = new List<Tag>()
};
newBook.Tags.Add(existingTag1);
newBook.Tags.Add(existingTag2);
context.Add(newBook);
context.SaveChanges();
Indirect many-to-many setup – configuring the linking table
An indirect many-to-many relationship takes a bit more work, but it does allow you to use extra data that you can put into the linking table. The figure below shows the three entity classes, Book, BookAuthor, and Author, with define the many-to-many relationship.
This is more complex because you need to define the linking entity class, BookAuthor, so that you can add extra data in the linking table and also excess that extra data when you query the data.

EF Core will automatically detect the relationships because of all the navigational properties. But the one thing it can’t automatically detect is the composite primary key in the BookAuthor entity class. This code below shows how to do that.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BookAuthor>()
.HasKey(x => new {x.BookId, x.AuthorId});
}
NOTE: Like the direct many-to-many configuration, if you leave out any of the four navigational properties, then it won’t set up that part of the many-to-many. You will then have to add Fluent API commands to set up the relationships.
Indirect many-to-many usage – querying
The indirect query is more complex, but that’s because you want to order the Author’s Names.
- Load all the Books with their BookAuthor and Author entity classes
var books = context.Books
.Include(book => book.AuthorsLink).ThenInclude(ba => ba.Authors
.ToList(); - Load all the Books with their BookAuthor and Author entity classes, and make sure the Authors are in the right order
var books = context.Books
.Include(book => book.AuthorsLink.OrderBy(ba => ba.Order))
.ThenInclude(ba => ba.Authors
.ToList(); - Get all the Books’ Title with the authors names ordered and then returned as a comma delimitated string
var books = context.Books.Select(book => new
{
Title = book.Title,
AuthorsString = string.Join(", ",
book.AuthorsLink.OrderBy(ba => ba.Order)
.Select(ba => ba.Author.Name))
}).ToList();
NOTE: ordering within the Include method is also a new feature in EF Core 5.
Indirect many-to-many usage – add a new link
To add a new many-to-many relationship link you need to add a new instance of the linking entity class, in our example that is a BookAuthor entity class, and set up the two relationships, in this example filling in the Book and Author singleton navigational properties. This is shown in the code below, where we set the Order to a value that adds the new Author on the end (the first Author has an Order of 0, the second Author is 1, and so on).
var existingBook = context.Books
.Include(p => p.AuthorsLink)
.Single(p => p.Title == "Quantum Networking");
var existingAuthor = context.Authors
.Single(p => p.Name == "Martin Fowler");
existingBook.AuthorsLink.Add(new BookAuthor
{
Book = existingBook,
Author = existingAuthor,
// We set the Order to add this new Author on the end
Order = (byte) book.AuthorsLink.Count
});
context.SaveChanges();
A few things to say about this (the first two are the same as the direct many-to-many add):
- You should load the Book’s AuthorsLink using the Include method, otherwise you will lose any existing links to Authors.
- You MUST load the existing Author from the database to add to the BookAuthor linking entity. If you simply created a new Author, then EF Core will add that new Author to the database.
- Technically you don’t need to set the BookAuthor’s Book navigational property because you added the new BookAuthor instance to the Book’s AuthorsLink, which also tells EF Core that this BookAuthor is linked to the Book. I put it in to make it clear what the Book navigational does.
Indirect many-to-many usage – removing a link
To remove a many-to-many link, you need to remove (delete) the linking entity. In this example I have a book with two Authors, and I remove the link to the last Author – see the code below.
var existingBook = context.Books
.Include(book => book.AuthorsLink
.OrderBy(x => x.Order))
.Single(book => book.BookId == bookId);
var linkToRemove = existingBook.AuthorsLink.Last();
context.Remove(linkToRemove);
context.SaveChanges();
This works, but you have the problem of making sure the Order values are correct. In the example code I deleted the last BookAuthor linking entity so it wasn’t a problem, but if I deleted any BookAuthor other than the last I should recalculate the Order values for all the Authors, otherwise a later update might get the Order of the Authors wrong.
NOTE: You can remove the BookAuthor by removing it from the Book’s AuthorsLink collection, like you did for the direct many-to-many remove. Both approches work.
Indirect many-to-many usage – Create Book with Authors
To add Authors when you first create a book you need to add a BookAuthor linking class for each author in the book, setting the Order property to define the order that the Authors should be displayed in. The code below adds two existing Authors to a new book.
var existingAuthor1 = context.Authors
.Single(a => a.Name == "Author1");
var existingAuthor2 = context.Authors
.Single(a => a.Name == "Author2");
var newBook = new Book()
{
Title = "My Book",
//... other property settings left out
//Set your AuthorsLink property to an empty collection
AuthorsLink = new List<BookAuthor>()
};
newBook.AuthorsLink.Add(new BookAuthor
{
Book = newBook,
Author = existingAuthor1,
Order = 0 //First author
});
newBook.AuthorsLink.Add(new BookAuthor
{
Book = newBook,
Author = existingAuthor2,
Order = 1 //second author
});
context.Add(newBook);
context.SaveChanges();
Conclusion
So, since EF Core 5, you have two ways to set up a many-to-many – the original indirect approach (Book-BookAuthor-Author) and the new direct (Book-Tags) approach. The new direct many-to-many is really easy to use, but as you have seen sometimes using the original indirect approach is the way to go when you want to do more than a simple link between to entity classes.
If you didn’t find this link before, I really recommend an excellent video produced by the EF Core team which has a long section on the new, direct many-to-many, including how to configure it to include extra data.
All the best with your EF Core coding and do have a look at my GitHub page to see the various libraries I have created to help build and test EF Core applications.
Happy coding.
Hey Jon! Many thanks for this article. I am looking to use direct option but I cannot change the name of the primary keys (they are set to Id only, without mention of the name of the entities). The linking table is created automatically with the migration but when I try to select an entity and use Include, it does not find the linked entity and I receive an error about id.entity column not existing. I believe this is because of the difference of naming for the primary keys. What do you think? Did you experience something similar?
Hi Max,
In the direct option the primary keys can be Id (see this section of the EF Core docs), so there must be something else.
I wonder if you haven’t registered the HasMany / WithMany parts to your primary entity class in your OnModelCreating method?, e.g.
I have to say I prefer “indirect” way. We have really complex db and sometimes it’s good to have access to that “connecting table” from dbcontext.
It’s a really nice article! One significant part is missing though, about how to actually update the linking table records for an existing Entity. Like if we had 2 tags and it became 1 or 3
Hi Dmytro,
You are right. I showed how to add new entity to many-to-many relationship, but I didn’t show how to remove a many-to-many relationship.
Here is a quick answer as I’m busy on other things at the moment.
Direct many-to-many
For indirect many-to-many
Hope that helps.
Hi John,
I’m following your example on “Direct many-to-many usage: Add a new link”and is not working for me by only having SaveChanges(); it does not throw an error but it doesn’t add the row in the join table.
So I added Context.Update() which worked the 1st time but the 2nd time I try to add another existing company I get a duplicate PK error
var organization = Context.Organizations.Include(o => o.Companies).Single(o => o.Id == organizationId);
var existingCompany = Context.Companies.Single(c => c.Id == companyId);
organization.Companies.Add(existingCompany);
Context.Update(organization);
Context.SaveChanges();
but if do the following then it works but if I’m not wrong this is what you recommend not to do, right?
organization.Companies = new List<Company> {existingCompany};
Context.Update(organization);
Context.SaveChanges();
Any idea if I’m missing something?
Thanks in advance
Hi Kar,
I assume your many-to-many is between Company and Organization – correct? What I don’t see is a property in your Company which has a collections of Organizations. For the direct many-to-many setup both ends needs a collection of the other end of the many-to-many.
If your Organization contains many Companies, but a Company can only be in one Organization, then its a one-to-many relationship.
The Update method is only used in very specific situations, like getting a entity from a API and wanting to update database. In your case you shouldn’t need Update.
Not sure I have solved your problem, but there are some things to think about than might help to work out what is happening
Hi Jon, thanks for the reply. Yes, it is a many-to-many company and organization and I do have the collection of Organization in Company and the collection of companies in Organization like this:
public class Organization
{
[Key]
public Guid Id { get; set; }
[Required]
public string Name { get; set; }
public ICollection<Company> Companies { get; set; }
}
public class Company
{
[Key]
public Guid Id { get; set; }
[Required]
public string Name { get; set; }
public ICollection<Organization> Organizations { get; set; }
}
and this is my code that doesn’t work, it does not add any record:
var organization = Context.Organizations.Include(o => o.Companies).Single(o => o.Id == organizationId);
var existingCompany = Context.Companies.Single(c => c.Id == companyId);
organization.Companies.Add(existingCompany);
Context.SaveChanges();
If I add the “Context.Update” and lets say I add an existing company “Company ABC” it works but then if add another existing company for example “Company XYZ” to the organization it throws a duplicate id error. It looks that it tries to add again the “Company ABC”
Any idea why Context.SaveChanges(); is not working?
Hi Jon,
I found the problem. I had UseQueryTrackingBehavior as no tracking in the startup. I removed it and worked fine.
Thanks for your help and for this article, it helped a lot.
Regards
Hi Kar,
Glad you fixed it. It didn’t make sense before, but UseQueryTrackingBehavior as no tracking would stop you doing updates.
Hi Jon! Nice article!
In the following note you mention that if we only want to have a navigational property on one end then we should use “HasMany(x => x.Tags).WithMany()” but there doesn’t seem to exist a parameterless WithMany function in EF Core 5.0.10. Is there another way of doing this?
“NOTE: The direct many-to-many relationship is only automatically configured if you have a collection navigational property on both ends. If you only want a navigational property on one end, then you will need to use the Fluent API configure (see next section), for instance …HasMany(x => x.Tags) .WithMany() where the Tags has no navigational property back to the Books.”
Regards
Hi Michael,
The …HasMany(x => x.Tags) .WithMany() is shortening, possibly the best one 🙁 . Have a look at the configuration code under the 2. Configuring the linking table in the OnModelCreating method section for the full config of a many-to-many.
Hi Jon! Thanks for the article.
I have a question. I already have set the direct many-to-many aproach.
But, what if a need another relational table based on the same relations?
Let me explain:
Book -> BookAuthor <- Author (Normal)
Book -> BookAuthorView <-Author (Normal but with diferent relation table name and amount of rows)
Basically i need the same direct relation but persisting diferent amount of data.
Can i indicate EF somehow to use diferent relational table?
Regards
Hi Seva,
I haven’t done it but I think you can – have a look at the EF Core video about many-to-many relationships as I seem to remember shay did have two many-to-many links to the same entities (I think, I might be wrong).
I am setting up a direct many-to-many connection. I have a FollowUp object and an ActionTaken object with a many-to-many table between them named FollowUpActionTaken. When I configured it using the normal setup and ran it, it threw an error saying it couldn’t find the table ActionTakenFollowUp. Is there a simple way to get it to get it to swap the name components of that table?
Hi Rono,
I’m not sure what you mean by ‘the normal setup’ but for the direct many-to-many relationship you need to use the configuration shown here, which is different to the indirect many-to-many relationship configuration.
Hi Jon !
Nice article, I have followed your instructions in the direct many to many configuration, but I encounter an error in the intellisense.
I have two models, Movie and WatchList; I create the join table MovieWatchList, and when I try to insert a movie in the WatchList navigation property “Movies” it throws an error in the Add() method. It says that it cannot convert from type Movie to MovieWatchList..
What do you think is the problem? How can I solve it?
Hi Ramim,
If you are using direct many-to-many (Book to Tag in this article), then a) you don’t need to create a joining table because EF Core will create it. Your WatchList navigation property would be of type ICollection<Movie>. If you want to define a joining class I show how to configure that. (I would have though direct many-to-many was a good choice for your Movie and WatchList).
If you are using an indirect many-to-many (Book to Author in this article), you have to create your WatchList navigation property would be of type ICollection<MovieWatchList> and you would need to add a MovieWatchList type (see the Book/Author examples).
So, I have great news! I managed to successfully “add” a Movie into the WatchList by creating a “new” Join table, MovieWatchList, instance that passes two objects as values, one Movie and the other WatchList. After checking the database, the join table successfully creates the tuple with the values.
By the way I am applying the indirect apporach.
Now, when I load my WatchList view, I would like to iterate through the WatchList navigation property, Movies, to render each movie via a partial. I pass each movie instance, stored in the navigation property, to a partial. Here comes the problem, when I get to the partial, I would use Movie as a model, but an error is thrown saying that a MovieWatchList model is expected rather than a Movie model. How can I sort that out?
Regards,
Ramim.
I have to leave that for you to work out as I’m busy. I’m sure you can work it out and you will know more that way.
All the best.
Hi, great article – this helped far more than the Microsoft documentation!
This is the first time I’ve used this new functionality in EF Core 5 and I have run into an issue – in my application, when following the steps equivalently to yours in realtion to ‘Direct many-to-many usage: Add a new link’, I get ‘Object reference not set to an instance of an object’ on the line:
I have exactly the same Model structure – i.e. a Navigation Property on both tables.
I have debugged the code and the ‘existingTag’ in your example loads fine – I can see all details associated.
Any idea what may be wrong, or am I heading for a StackOverflow question? 🙂
Thanks, Luke
Hi Luke,
Its hard to debug code remotely, but ‘Object reference not set to an instance of an object’ could be (most likely first):
If you put a breakpoint on the line that fails, then you can look at all the variables and find which one is null.
PS. Glad you found this article useful.
Hi Jon, thanks for that, I’m actually adding to a new book object which hasn’t yet been written to the DB. I’m wondering whether the I need to do a ‘context.SaveChanges’ before it’ll accept the tag mapping?
I covered that in the section called Direct many-to-many usage: Create Book with tags.
Got it, all working – thanks Jon!
Indirect many-to-many usage – add a new linkTypo: instead
book.AuthorsLink.Add(…
it should be
existingBook.AuthorsLink…
Nice article!
Hi Michael,
Thanks for pointing that error out. I have fixed it!
Love that content. This is the besth explaining i have seen ever !!!
Hi Berk,
Glad you found it useful. I have created articles on many-to-many relationships for EF6, EF Core 2 and now EF Core 5. People seem to find them useful.
it was so hard to get the related data on many to many configuration. its much simple with ur article,thanks again.
This has been so helpful, thank you! Regarding this statement:
Does this mean we have to identify and manage potential duplicates manually? Is there a best practice for this? Say for example if I am adding another book and some of the tags already exist.
Hi Kyle,
You should be OK, as EF Core is clever enough to handle this.
My tests says that the normal updating of a direct-many-to-many EF Core won’t try to add a link to a entity that is already in the list. For example, if the Book has a Tag with the TagId of say “XXX” and you load that same Tag and add it to the Tags collection, then EF Core works out that it has that Tag and won’t try and create a extra link. See this unit test which checks this.
If you create a entity with the same primary key as a existing entity (I describe one way next), then the fact that a linking entry with the same composite key will mean either in EF Core or in the database will throw an exception. I tested this by adding a new Tag, but with the same TagId as an existing Tag in the Book, which threw an exception. See this unit test that shows this example.
Ok that makes sense. So we still have to do some work to check for duplicates by finding existing entities that would match the proposed tags before saving changes to the DB. That’s still easier than managing the join table itself within the code.
I was wondering about this as I’m building this exact scenario of a database of books along with tags related to each book. I am working on migrating everything to code first and EF Core 5. Thanks.
Hi Kyle,
Glad that was useful.
If you are building a database of books you might like the EF Core community standup video where I improve the performance of book-selling app in stages. Lots of examples of how to improve the performance of EF Core queries.
Would it be possible to add an example of inserting a new book record with links to one or more existing authors and tags. The example s in this article show updating/inserting/removing authors/tags. But I would like to see how to add a book which is written by two existing authors and has one or more existing tags.
TIA, Guido
Good idea. I am busy this week but I will add something next weekend. I’ll send you a update when its done.
Quick version: load the existing author, add a new BookAuthor with its Author property set to the existing Author, and add that BookAuthor to the Book’s AuthorsLink collection.
Hi Guido,
I have updated the article to show creating a new book with a) Tags and b) Links to Authors. I’m sure you can combine the two.
Any examples with many to many with same table? Eg. a product can have many related products
Hi Garnett,
Yes, that is possible using the same approaches shown in this article. The only thing that is going to be a problem is the primary/foreign keys. Typically you would call them something like BookId and AuthorId and EF Core can work out that they are foreign keys for a Book and an Author. But if both ends of the linking table go to the same type, then you would have to have primary/foreign keys called something like ProductId1 and ProductId2, and the navigational links would be names something like Product1 and Product2.
Therefore you would need to define the relationships because EF Core can’t work them out by looking at their names. Fluent API HasMany(…).HasForeignKey(…).WithMany(…).HasForeignKey(…) commands would work or I think the [ForeignKey(…)] attributes would work.