0

Domain Driven Design states that you should have a domain model, which reflects the ubiquitious language used by the domain experts. When using ORMs and many to many relationships I am use to doing this:

public class Student
{
    public Student() 
    {
        this.Courses = new HashSet<Course>();
    }

    public int StudentId { get; set; }
    [Required]
    public string StudentName { get; set; }

    public virtual ICollection<Course> Courses { get; set; }
}

public class Course
{
    public Course()
    {
        this.Students = new HashSet<Student>();
    }

    public int CourseId { get; set; }
    public string CourseName { get; set; }

    public virtual ICollection<Student> Students { get; set; }
}

However, with EF Core you must create a join class as described here: https://github.com/aspnet/EntityFrameworkCore/issues/1368. In the case of the above; I would have to create a class called: StudentCourse.

What is the most appropriate way to map an ORM to a Domain Model assuming there are no technical constraints like with EF Core were you must currently have a join class? My thinking is:

1) If the join table has state and behaviour then use a join class.

2) If the join table has a name that is recognised by domain experts then use a join class e.g. a Person could be eligible for loans - here Eligibility is the join class and is a term recognised by the domain. In the case of the above; StudentCourse is not recognised by the domain.

3) Else don't use a join class (like in the example above).

14
  • 5
    ORM to a Domain Modelnone. DDD is totally agnostic to how the data is stored. You are trying to converge both domain model and persistence model in a single model. DDD is not data-centric, is domain centric. In other words, we first do model the business (domain) and later we decide how to persist it
    – Laiv
    Nov 29, 2018 at 14:10
  • 2
    if you are using CQRS, then the command model maps to the database or does it? this is your assumption. The truth is that if you model a system following DDD, the persistence is a mere implementation detail. It could be a (R)elational (D)ata (M)odel or it could be a (D)ocument (D)ata (M)odel. Or it could be binaries stored somehow into somewhere. DDD shift from data-centric models to domain-centric models. When you converge domain and persistence in the same model, the ORM is likely to impose its constraints. The common one is to break encapsulation, what it's totally contrary to DDD
    – Laiv
    Nov 29, 2018 at 14:35
  • 2
    @w0051977 Laiv is completely right. The domain model is not supposed to be a "data" model, at least not in object-orientation. Most of the projects I see do that anyway too. That just means that the idea didn't penetrate our old habits yet. I must say, that I'm not a fan of a "completely isolated" (as in: "persistence agnostic") domain model. I think objects should contain business-relevant behavior, and most of those contain some form of persistence implicitly. Nov 30, 2018 at 8:45
  • 1
    A strong hint you're doing something wrong is that you're currently trying to find out concepts in the domain from the database representation. This shows you're looking at the problem backward. Your domain model should never be influenced by technical constraints in DDD. Nov 30, 2018 at 17:20
  • 1
    @Vincent Savard, I would just like to see an example to see how change tracking is approached without the ORM mapped to the domain model.
    – w0051977
    Nov 30, 2018 at 17:43

3 Answers 3

2

The overarching paradigm that DDD seeks to provide is one focused on the behavior of your system. Everything else is an implementation detail. As such, one does not model the domain according to any specific persistence mechanism, rather, models the persistence mechanism according to the domain. Including some sort of "join" object in your domain for the sake of easing persistence to an RDBMS creates an unnecessary coupling and pollutes/obfuscates your model with persistence concerns.

You specific problem is caused by mixing the data model that an ORM requires with the domain model that your application requires. There is no DDD solution to employ here other than to decouple the former from the latter above.

ORMs are a great tool (cohesive mechanism) for generating a necessarily anemic data model (methods cannot be serialized within reason) and mapping to/from a necessarily anemic data store. Put another way, an ORM only provides a more OO experience for developers and offers a (leaky) abstraction over a data store. That's all. That is the goal of every ORM (what does it stand for again?).

That said, there are two types of applications:

The overwhelming majority of systems are trivial. That is, they simply collect and persist data then turn around and display it back to the user. Trivial applications boil down to CRUD with a few rules sprinkled here and there regarding (mostly) the format/range of data being entered. In such systems it's not usually necessary to isolate a domain model (or isolate only small portions of it) because there are so few rules.

The second type of application is the one that truly benefits from an isolated domain. It is an application with lots of business rules. Often this kind of application defines many types of object that aren't persisted at all, or includes lots of Aggregates. This makes isolating a domain paramount. An ORM's data model can only truly provide the consistency boundaries an Aggregate requires through discipline (not structure) because all of the Entities are made available (and mutable).

Whether or not you have "seen" this kind of application is beside the point, and does not invalidate the fundamental principals of clean architecture. Every decision is a trade off. A good software architect understands that and moves accordingly. If the costs of creating an isolated domain model are not worth the benefits, move forward accordingly. But understand the trade off. That's what systems design is all about: finding the perfect balance between business concerns and technical concerns. It's about pragmatism. Leave idealism to academics and jr. developers, and utilitarianism to CEOs. We are here to provide balance. That is our value.

3
  • Thanks. Do you know any DDD projects on Github that contain an isolated domain model? I know of many DDD projects on GitHub, however they all map the domain model straight to the database
    – w0051977
    Nov 30, 2018 at 17:40
  • @w0051977 If you have an actual question about how one implements an isolated domain model or a related concept, I am more than happy to expound on that. I have no interest in engaging with such a sardonic question. It is simply beneath me to take your implication seriously. Nov 30, 2018 at 20:11
  • Not sure how my comment is sardonic (disrespectful?), but thanks for answering anyway - always grateful for answers.
    – w0051977
    Nov 30, 2018 at 20:28
0

Direct feedback

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

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

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

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.


To summarize

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

Concretely:

  • Student and Course belong to the domain
  • Since your database structure is different, your persistence layer will have a (different) Student, (different) Course, and StudentCourse (or 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.

-1

I do agree with @VincentSavard that although it is true that the Change Tracking is an implementation detail with regarding to DDD. It is still a detail that needs to be tackled. And it is very true that there simply aren't articles/code samples that illustrate this (or I just haven't found them).

Anyway I have written a sample application which uses DDD with separate domain & persistence models while still keeping the benefits of ORMs Change Tracking feature.

If you wanna give it a read: Change Tracking while doing DDD

Source code is available at: GitHub

Share it if you found it helpful.

1
  • Any reason for down voting? Aug 2, 2020 at 19:22

Not the answer you're looking for? Browse other questions tagged or ask your own question.