- If the join table has state and behaviour then use a join class.
Yes, but if the join table has additional state (i.e. not just the FKs) of its own, that generally means that the domain to some degree recognizes this join class as an entity. It makes no sense to track state for something which you don't acknowledge exists.
Barring auxiliary things like audit fields etc. I'm focusing on any data that the domain has active knowledge of, other than the FKs of the join class.
- If the join table has a name that is recognised by domain experts then use a join class
If by "recognized by domain experts" you mean "considered to be part of the domain", then yes. But just because two people can acknowledge the existence of something doesn't mean the domain needs to acknowledge it as well.
Although I would argue that it ceases to be a "join class" when it is acknowledged to be a domain object by its own right (whether part of an aggregate or its own aggregate is irrelevant).
To me, "join class" implies that this class is merely a technical implementation detail to store the relationship state and has no real function on its own.
- Else don't use a join class (like in the example above)
It's fairly logical that if you don't need something, then you don't need to create and maintain it. Or at least that doing so would be utterly pointless.
Exploring the issue
The overarching problem in your question is that you are letting your knowledge of an external resource decide the design of your domain. And that's causing you a lot of confusion now, and will cause a lot of grief in this codebase's future.
The fact that EF Core is currently unable to handle many-to-many relationships is an implementation detail, more specifically an implementation detail of your infrastructure/persistence layer (depends on whether you lump these together or not - that distinction is irrelevant here).
Unsurprisingly, the domain structure shouldn't be decided based on an implementation detail of another layer. That's pretty much the definition of why you have a domain: to be agnostic of specific implementations or dependencies.
You need to respect the order of operations: first decide the domain, then decide how external resources handle the domain.
Shape your domain the way it makes sense to you. If a many-to-many makes sense in your domain, use it; regardless of what EF Core does or doesn't support.
But there are other possibilities here, e.g. what is in effect a many-to-many might only be represented by your domain using a seemingly one-to-many. For example, if your domain lists the students in a given course, but never lists the courses of a given student. If your underlying data source is relational, you're going to want to be using a many-to-many even though the domain never fully utilizes that aspect).
This example is probably not applicable to your specific scenario, but the point I'm trying to make is that this is a perfectly valid domain setup if it matches your business requirements.
The persistence layer
Let's assume you've designed your domain layer to use a many-to-many relationship.
Now that your domain design has been decided, the persistence layer can be designed. How the underlying data source is designed (i.e. the database structure) is freely choosable by the persistence layer.
However, the end responsibility of the persistence layer includes being able to convert your data (whatever structure it uses) to the domain objects whose structure has already been decided.
It's therefore advisable for the database structure to closely resemble the domain structure to make things easier, but that is not a fixed requirement. You can change it however you need to.
Since EF Core doesn't support many-to-many, this is where we get to that point where your persistence layer is now forced to have a different data structure than your domain. That leads to the inevitable consequence of needing to map that different structure back to the domain structure.
- The domain decides its own structure based on what it needs. DO NOT let implementation details of your persistence layer (or any other infrastructure layer) decide your domain structure.
- The persistence layer is free to use whatever database structure it wants. In this case, EF Core's inability to handle many-to-many relationships forces your database structure to no longer map one-to-one to your domain structure.
- It is the persistence layer's responsibility to map the database structure to the domain (and vice versa).
- While EF can reuse your domain objects as its entities1, the domain objects should never be designed with that in mind. The domain objects are defined as they are needed by the domain. If EF is able to reuse them as entities, great! If not, then your persistence layer should create its own entities and map them to the domain objects. At no point should the domain be changed because EF can/cannot use them as entities.
Course belong to the domain
- Since your database structure is different, your persistence layer will have a (different)
CourseRegistration if you will) entities.
- The persistence layer will need to create a mapping so it's able to return (domain)
Student and (domain)
Course objects as the domain demands them to be.
If and when EF Core decides to support many-to-many relationships in the same way that Entity Framework used to; then your persistence layer can be simplified as it can more closely resemble the domain structure again.
However, it's important to notice that if and when this happens and you simplify your persistence layer, you shouldn't be making any changes to the domain.
What you are suggesting in your answer would mean that your domain would need to change (i.e. the join table could be removed), which is exactly what a good domain design would have avoided from the get to.
1 I suspect several people will contest this advice. This is something that not everyone is going to agree on.
I generally follow Jason Taylor's Clean Architecture philosophy, which approaches EF as a framework rather than a library, therefore allowing it to cross the layer boundaries.
The gist of it is that you don't have to create separate repositories and entities when what EF already provides suffices for your use case. You should only create them if there is added value to having them.
This applies in both directions: the domain having access to EF Core's
DbSet, and the persistence layer reusing the domain objects as its entities. The same principle applies in either case.
This philosophy is a response to the general "you must always fully abstract your persistence layer" philosophy which often ends up creating more work/problems than it actually solves.
It's okay to disagree with this approach, but I'm not going to get into discussions on which is objectively superior. Both approaches have their use cases, and different contexts can have different requirements.