EF Core 2.1 vs NHibernate 5.1: DDD perspective

UPDATE 3/5/2020: I’ve expanded on this article and updated its content for EF Core 3.1 in my new Pluralsight course DDD and EF Core: Preserving Encapsulation.

That was probably a long wait for those of you who follow my blog. But, better late than never, so here it is: another comparison of Entity Framework and NHibernate, in which I bash EF Core and present it as an unbiased review. Just kidding, I do try to be unbiased here to the best of my skills.

EF Core vs NHibernate: Preface

EF Core has made a lot of progress and it took me quite a while to catch up with it (this, and totally not my procrastination was the reason for the delay in publishing the comparison). And although I spent quite some time researching the topic, I could still miss something. If you see anything I’ve written is incorrect or incomplete, please, share in the comments below. I’ll update the article and then delete your comment to seem as though I knew this from the very beginning.

So, what’s the deal here? We’ll compare EF Core and NHibernate from the Domain-Driven Design (DDD) perspective. And when it comes to domain modeling, the two most important things you need to focus on are:

  • Encapsulation, and
  • Separation of concerns.

Encapsulation stands for protecting data integrity. You do that by preventing clients of a class from setting its internals into an invalid or inconsistent state. The main rule here is that the domain model must maintain its invariants at all times. In Object-Oriented Programming, the two major techniques that help you achieve encapsulation are:

  • Information hiding, and
  • Bundling data and operations together.

Encapsulation

That’s the essence of encapsulation. BTW, I wrote a whole course about building encapsulated domain models which I regard as one of my best Pluralsight courses, definitely check it out: Refactoring from Anemic Domain Model Towards a Rich One.

Separation of concerns (also known as Persistence Ignorance and Domain Model Isolation) stands for stripping your domain model from all non-domain-related stuff. The reasoning behind this principle is that you can only keep track of so many things at a time. The domain model itself is usually quite complicated already. The more you isolate it from concerns that don’t relate to domain knowledge, the better. And because Domain Model is the heart of any (line-of-business) application, separation of concerns applied to this specific area pays for itself manyfold.

Given the DDD focus of this comparison, I will only consider features that affect encapsulation and separation of concerns.

For those of you who might wonder why bother with this at all and not just separate the domain model into domain and persistence models and keep the domain model encapsulated this way: it doesn’t work out well. In complex applications, the amount of effort required to build a separate persistence model doesn’t justify the improvements in terms of purity. The effort is too large, the benefits are too small.

The only use case where it’s reasonable is with legacy databases. Trying to bridge the gap between such database’s structure and the domain model is almost impossible, so you are pretty much forced into building a separate persistence model. In all other cases, consider relying on the plain ORM and accepting its shortcomings if any. Here I wrote about it in more detail: Having the domain model separated from the persistence model.

Alright, let’s start.

We’ll start with something simple - referencing a related entity. Let’s say you have a many-to-one relationship between Student and Course. You can implement this relationship directly by introducing a foreign key into your domain model:

public class Student : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public int FavoriteCourseId { get; private set; } // Foreign key
}

public class Course : Entity
{
    public string Title { get; private set; }
    public int Credits { get; private set; }
}

But that would violate the separation of concerns principle. You’re introducing a concern that has nothing to do with the domain you are working on. Replace FavoriteCourseId with a proper member:

public class Student : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public virtual Course FavoriteCourse { get; private set; }
}

public class Course : Entity
{
    public string Title { get; private set; }
    public int Credits { get; private set; }
}

To read more about it, see Link to an aggregate: reference or Id?.

The Entity Framework team has introduced a nice feature, Shadow Properties, that allows you to do that. And EF Core 2.1 has brought back Lazy Loading that allows you not to worry about manually fetching the related entities beforehand.

The code above works just fine in EF Core without additional configuration. It automatically creates a shadow property with a foreign key (FavoriteCourseId) that doesn’t show up in the domain model. All you deal with is the nice and clean navigation property.

There is one small drawback with EF Core’s implementation here, though. When you refer to the Id of the navigation property, like this:

Student student = context.Students.Find(id);
int favoriteCourseId = student.FavoriteCourse.Id;

it triggers loading of the related object (favorite course). It’s not ideal because this Id is already loaded alongside with the student’s other data. But you don’t often refer to entities’ Ids like that, so it’s a minor issue.

Needless to say, NHibernate allows you to do this as well.

I must add that, in the future, the EF team plans to rewrite the lazy loading mechanism. The current implementation (2.1) relies on dynamic proxies, just as EF4, EF5, EF6, and NHibernate do. Which means the underlying class is not a true POCO and must adhere to the requirements from the ORM. They aren’t that big but still annoying. In particular: you need to declare the class’ properties as virtual and have a parameter-less constructor (which can be made non-public). The future implementation will use Roslyn to weave assemblies as part of the build process.

We’ll see how it goes. If implemented, it’s going to be an exciting news as this would push the separation of concerns even further.

For now, the score of #1 Referencing a related entity is: EF Core vs NHibernate - 0.9 : 1.

#2: Working with disconnected graphs of objects

The work with graphs of objects always was a weak spot in Entity Framework. Before EF Core, if you were to add a new entity to the context, EF would mark all its children as added as well. This was a big pain as you could have objects in the graph that already existed in the database. Trying to insert such objects resulted in an exception. For example, this code didn’t work in EF6:

Course course = context.Courses.First();
var student = new Student("First Name", "Last name", course);
context.Students.Add(student); // EF6 tries to insert course along with student
context.SaveChanges();

// Student.cs
public class Student : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public virtual Course FavoriteCourse { get; set; }
    
    public Student(string firstName, string lastName, Course favoriteCourse)
    {
        FirstName = firstName;
        LastName = lastName;
        FavoriteCourse = favoriteCourse;
    }
}

EF tried to insert course along with student. And of course, because the course was already in the database, this code threw an exception.

To fix the problem, you had to manually indicate the state of the course entity:

Course course = context.Courses.First();
var student = new Student("First Name", "Last name", course);
context.Entry(course).State = EntityState.Unchanged; // or EntityState.Modified
context.Students.Add(student);
context.SaveChanges();

In this particular example, it’s not that bad. But when you have complex aggregates, this starts to get overwhelming really fast. People even came up with a generalized mechanism for determining the state of domain objects and communicating that state to EF before each SaveChanges.

Thankfully, the team has fixed this and in version 2.1 the code above works just fine without the need to tell EF the state of the object. If the object has a default (zero) Id, it’s marked as added. Otherwise - as modified.

But here’s the catch. It doesn’t work with disconnected (detached) objects. If the root entity has any children that are detached from the context, you are back to square one: you have to manually guide EF Core through which of the objects are new and which are not.

And this use case comes up a lot if you work with rich domain models. The enumeration pattern, representing reference data as code - all that uses domain objects outside the scope of a particular database context.

This code throws an exception:

Course courseCached;
using (SchoolContext context = new SchoolContext(optionsBuilder.Options))
{
    courseCached = context.Courses.Last();
}

using (SchoolContext context = new SchoolContext(optionsBuilder.Options))
{
    Student student = context.Students.Find(id);
    student.FavoriteCourse = courseCached;
    context.SaveChanges(); // Throws. Must use context.Entry(courseCached).State = EntityState.Detached;
}

To avoid the error, you must set the state of courseCached like in good old days.

Not sure why the EF team can’t apply the same mechanism to detached objects but let’s hope the fix won’t take them another couple years.

Of course, NHibernate did the automatic state resolution from the very beginning, so nothing particularly interesting to talk about here.

The score of #2: Working with disconnected graphs of objects is: EF Core vs NHibernate - 0 : 1.

#3: Mapping backing fields

The ability to map to backing fields was introduced in EF Core 1.1. On the surface, it looks like a huge win for EF and one could only wonder why it took them so long to roll it out.

However, with a closer inspection, the situation here is much worse. This feature is unusable outside the narrow scope of simple use cases.

But let’s be coherent. I’ll write about what works first, describe some small issues around this implementation, and then continue with why it is unusable from the Domain-Driven Design perspective.

Alright, so the biggest benefit of mapping directly to backing fields is the ability to encapsulate collections in your domain model. That is something that wasn’t possible in EF6, at least not without some serious violation of separation of concerns.

Here’s an example:

public class Student : Entity
{
    private readonly List<Enrollment> _enrollments = new List<Enrollment>();
    public virtual IReadOnlyList<Enrollment> Enrollments => _enrollments.ToList();
    
    public void AddEnrollment(Course course, Grade grade)
    {
        var enrollment = new Enrollment
        {
            Course = course,
            Student = this,
            Grade = grade
        };
        _enrollments.Add(enrollment);
    }
}

// Usage example
using (SchoolContext context = new SchoolContext(optionsBuilder.Options))
{
    Student student = context.Students.Find(1);
    student.AddEnrollment(context.Courses.First(), Grade.B);
    context.SaveChanges();
}

The Enrollments collection’s property here is read-only (has no setter), which it always should be, and the collection itself is read-only too, to prohibit the clients of this code from altering it. All modifications to the collection should be done using AddEnrollment and DeleteEnrollment methods on the Student class itself.

The explicit mapping is somewhat awkward but the good news is you can omit it. The mapping works out of the box, by convention. Here’s the explicit version:

modelBuilder.Entity<Student>(x =>
{
    // ...
    x.Metadata.FindNavigation("Enrollments").SetPropertyAccessMode(PropertyAccessMode.Field);
});

So it looks like we have a nice degree of encapsulation here.

Not so fast.

One (small) problem with this feature is that EF Core requires the navigation property’s type has to be a sub-type of the backing field’s.

For example, this won’t work:

public class Student : Entity
{
    // The field's type is ICollection, not List
    private readonly ICollection<Enrollment> _enrollments = new List<Enrollment>();
    public virtual IReadOnlyList<Enrollment> Enrollments => _enrollments.ToList();
}

because Collection doesn’t inherit from IReadOnlyList.

This is completely unnecessary. You shouldn’t be worrying about the property at all when binding to the backing field.

Another small-ish issue here is that EF Core allows you to bind to a concrete collection type (List in our case). This shouldn’t be allowed. Binding to concrete collections means that you won’t be able to intercept calls to it, such as

int count = Enrollments.Count;

and won’t be able to translate such calls to the corresponding SQL queries like

SELECT COUNT(*) FROM dbo.Enrollment WHERE StudentID = @StudentID

Instead, you will always have to load the full collection into the memory first.

But that’s just nitpicking on my part. I don’t remember any use cases off the top of my head where such optimizations would be useful.

And that brings us to the main issue: EF Core doesn’t intercept calls to backing fields.

Let’s take an example. Let’s say that there’s an invariant in your domain model: no student can have more than 5 enrollments. How would you implement it? Well, with something like this:

public class Student : Entity
{
    private readonly List<Enrollment> _enrollments = new List<Enrollment>();
    public virtual IReadOnlyList<Enrollment> Enrollments => _enrollments.ToList();
    
    public void AddEnrollment(Course course, Grade grade)
    {
        if (_enrollments.Count >= 5) // Invariant protection
            throw new InvalidOperationException();

        var enrollment = new Enrollment
        {
            Course = course,
            Student = this,
            Grade = grade
        };
        _enrollments.Add(enrollment);
    }
}

This way, you introduce a precondition in AddEnrollment which protects the integrity of the domain model.

The problem here is that this check always passes, no matter how many enrollments there are already. That’s because the enrollments collection is not initialized until you explicitly call the Enrollments navigation property.

It’s actually not an issue with the backing fields themselves but rather with the approach to lazy loading the EF team has chosen early on. If you worked with NHibernate, you might remember that it requires you to mark all public members (including methods) as virtual, not only navigation properties like in EF Core. That is done for this exact reason - to intercept any calls to the backing fields and initialize the collections before you start using them.

In other words, NHibernate requires you to make AddEnrollment virtual to override its runtime behavior: it initializes _enrollments before passing the control to your code. EF Core doesn’t do that.

This problem comes from the very first version of Entity Framework, and it’s astonishing that the EF Core team hasn’t done anything about it since then given that Oren Eini wrote about it almost 10 years ago. Sometimes, it seems as though the EF team is forbidden from looking at competitors’ (read: NHibernate’s) code bases and reading their authors’ blogs.

Now. One way to avoid this problem within AddEnrollment is to call the Count property on the navigation property, not the underlying field:

public class Student : Entity
{
    public void AddEnrollment(Course course, Grade grade)
    {
        if (Enrollments.Count >= 5) // Call to the navigation property instead of the backing field
            throw new InvalidOperationException();
        
        // ...

        _enrollments.Add(enrollment);
    }
}

Fair enough. Although, it’s unclear why we call the property in one case and the backing field in the other.

However, you won’t be able to work around this issue in this scenario:

public class Student : Entity
{
    private readonly List<Enrollment> _enrollments = new List<Enrollment>();
    public virtual IReadOnlyList<Enrollment> Enrollments => _enrollments.ToList();
    
    public void DeleteEnrollment(Enrollment enrollment)
    {
        _enrollments.Remove(enrollment);
    }
}

The enrollments collection is always empty here. To fix the issue you need to introduce a dirty hack:

public class Student : Entity
{
    public void DeleteEnrollment(Enrollment enrollment)
    {
        int hack = Enrollments.Count; // Force to initialize the backing field
    
        _enrollments.Remove(enrollment);
    }
}

Needless to say, this is something that’s too easy to overlook. This code also violates the separation of concerns principles: it mixes the domain concerns with an ORM one.

Because the issue here is with the way EF implements lazy loading, you can also overcome it by always loading all children collections eagerly. And it will work in simple cases. In more complex scenarios, however, you can have deep hierarchical aggregates with multiple collections. You can also have different update use cases, most of which would require only one of those collections. Loading the full aggregate graph eagerly all the time would entail drastic dips in performance and lazy loading is essential to avoid this.

This issue might be fixed when the EF team rolls out the new lazy loading implementation but who knows how much time it will take and whether it will fix it at all.

With NHibernate, mapping to backing fields is as simple as this (no additional hacks required):

public class StudentMap : ClassMap<Student>
{
    public StudentMap()
    {
        HasMany(x => x.Enrollments).Access.CamelCaseField(Prefix.Underscore);
    }
}

I’ll give EF Core a couple points for the effort, though. The score of #3: Mapping backing fields is: EF Core vs NHibernate - 0.2 : 1.

#4: Deleting orphaned entities

EF Core finally supports deletion of orphaned entities. This is useful when you’ve got an aggregate root that needs to have full control over its children’s lifetimes. So basically the situation we have with students and their enrollments. When deleting an enrollment from the student’s collection of enrollments, it should be deleted from the database too as they don’t make sense without the parent entity.

Here’s how to configure it in EF Core:

modelBuilder.Entity<Student>(x =>
{
    // ...
    x.HasMany(p => p.Enrollments).WithOne(p => p.Student).OnDelete(DeleteBehavior.Cascade);
});

The NHibernate’s version:

public class StudentMap : ClassMap<Student>
{
    public StudentMap()
    {
        HasMany(x => x.Enrollments).Cascade.AllDeleteOrphan();
    }
}

EF Core gets a full point here, good job. The score of #4: Deleting orphaned entities is: EF Core vs NHibernate - 1 : 1.

#5: Single-valued Value Objects

Here they come, Value Objects. Value Object is a concept that assumes immutability, no identity, and inability to live outside of the host entity. Read more about them in this article: Entity vs Value Object: the ultimate list of differences.

It’s hard to overestimate how important they are for building a rich domain model. Read this article to learn why, I won’t rehash it here.

The whole topic of Value Objects can be split into single-valued (those that consist of a single value) and multi-valued Value Objects. That’s because these two types are handled differently by EF Core and NHibernate. We’ll talk about single-valued Value Objects in this section.

EF Core 2.1 has introduced a new feature called Value Conversions and it’s a perfect fit for Single-valued Value Objects.

Let’s take an example. Let’s say that each student has an email. Email is a domain concept and you don’t want to fall into the trap of primitive obsession here by representing emails as strings. And so you want the domain class to look like this:

public class Student : Entity
{
    public Email Email { get; private set; }
}

public class Email : ValueObject
{
    private readonly string _value;

    private Email(string value)
    {
        _value = value;
    }

    public static Email Create(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            throw new InvalidOperationException();
        if (!Regex.IsMatch(email, @"^(.+)@(.+)$"))
            throw new InvalidOperationException();

        return new Email(email);
    }

    public static implicit operator string(Email email)
    {
        return email._value;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return _value;
    }
}

(BTW, I’m using the ValueObject base class from this article here.)

Thanks to the Value Conversions feature, this entity is very easy to map to the database now. You only need to do this:

modelBuilder.Entity<Student>(x =>
{
    // ...
    x.Property(p => p.Email)
        .HasConversion(p => (string)p, p => Email.Create(p));
});

where the first delegate tells EF how to convert the value from Email to string, and the second - how to do the backward conversion.

With NHibernate, you need to introduce a backing field for this purpose:

public class Student : Entity
{
    private string _email;
    public Email Email
    {
        get => Email.Create(_email);
        protected set => _email = value;
    }
}

public class StudentMap : ClassMap<Student>
{
    public StudentMap()
    {
        // ...
        Map(x => x.Email).CustomType<string>().Access.CamelCaseField(Prefix.Underscore);
    }
}

As you can see, the EF Core’s syntax is cleaner as it doesn’t require a backing field in the domain entity. Therefore, the score of #5: Single-valued Value Objects is: EF Core vs NHibernate - 1 : 0.8.

#6: Multi-valued Value Objects

With multi-valued Value Objects, the situation for EF Core is not as great.

You can map multi-valued Value Objects using Owned Entity Types. And the EF Core team decided to make some strange design decisions with this feature.

Internally, owned types are implemented as regular entities which means:

  • They have an Id property (declared as a shadow property, i.e. it doesn’t appear in the domain class), and

  • EF Core tracks changes in them just like it does changes in regular entities.

This is not how Value Objects work. They shouldn’t have an Id and EF’s Change Tracker should attribute all changes in them to their parent entities, not owned types themselves.

Alright, these are internal implementation details and you shouldn’t care about them per se. What you should care about is the limitations those details entail. So, what are they?

First of all, it’s nullability. Let me show that with an example. Let’s say that students in our domain model have an address, and this address is allowed to be null:

public class Student : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public virtual Address Address { get; private set; } // Can be null
    
    protected Student()
    {
    }

    public Student(string firstName, string lastName, Address address = null)
    {
        FirstName = firstName;
        LastName = lastName;
        Address = address;
    }
}

public class Address : ValueObject
{
    public string City { get; }
    public string Street { get; }

    public Address(string city, string street)
    {
        City = city;
        Street = street;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return City;
        yield return Street;
    }
}

If you try to create a student without this address:

using (SchoolContext context = new SchoolContext(optionsBuilder.Options))
{
    var student = new Student("First Name", "Last name");
    context.Students.Add(student);
    context.SaveChanges();
}

you will get an exception:

An unhandled exception of type ‘System.InvalidOperationException’ occurred in Microsoft.EntityFrameworkCore.dll The entity of type ‘Student’ is sharing the table ‘Student’ with entities of type ‘Address’, but there is no entity of this type with the same key value ‘{Id: -2147482647}’ that has been marked as ‘Added’.

Why this exception? Because EF Core tries to support storing owned types in separate tables out of the box, and this imposes some limitations. In particular, it’s unclear how to differentiate between a null owned type stored in the parent entity’s table and an owned entity with its own table that has the Id of -2147482647 and all columns set to null. A quite artificial limitation that could be easily avoided had EF Core not tried to do so much magic, if you ask me.

The proposed workaround here is to use the Null Object pattern. In other words, if a student has no address, still create one but set all its fields to nulls:

using (SchoolContext context = new SchoolContext(optionsBuilder.Options))
{
    var emptyAddress = new Address(null, null);
    var student = new Student("First Name", "Last name", emptyAddress);
    context.Students.Add(student);
    context.SaveChanges();
}

This just doesn’t work from the DDD perspective, though. The Address value object might have its own invariants and one of them could be that the city and the street must not be null. So, another violation of the domain model’s encapsulation here.

How NHibernate deals with this, you might ask? For starters, it doesn’t treat value objects as entities with their own Ids. Second, if the Address property is null in the domain class, NHibernate will set all columns that relate to this value object to nulls on saving it to the database.

And it will apply the same convention when materializing the student from the database. Here’s the corresponding mapping for NHibernate:

public class StudentMap : ClassMap<Student>
{
    public StudentMap()
    {
        // ...
        Component(x => x.Address, y =>
        {
            y.Map(x => x.Street);
            y.Map(x => x.City);
        });
    }
}

But does it mean NHibernate’s components don’t support storing Value Objects in separate tables? That’s right. And that’s a good thing. A feature should do only one thing and it should do it well. If you want to store value objects in separate tables, you can host it inside an entity, although I don’t recommend doing it for 1-to-1 relationships. It’s much easier to store them in the parent entity’s table instead.

This issue should be fixed in EF Core 3.0, though. The team is going to mimic NHibernate’s behavior with regards to nulls.

The next limitation is that if you have a hierarchy of domain entities, you won’t be able to use multi-valued Value Objects declared in the derived class. So, something like this won’t be possible either:

public class Person : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
}

public class Student : Person
{
    public virtual Address Address { get; private set; }
}

There were a lot more of issues with owned types in EF Core 2.0 which are fixed in version 2.1. The problem is that the fixes seem superficial and don’t work together well.

For example. EF Core 2.1 now supports read-only owned types. And you don’t even have to declare a parameter-less constructor in them, EF Core will find the appropriate constructor given that you define one that sets up all of the properties. Here’s the Address value object once again and the corresponding mapping:

public class Address : ValueObject
{
    public string City { get; }
    public string Street { get; }

    public Address(string city, string street) // Must have this ...
    {
        City = city;
        Street = street;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return City;
        yield return Street;
    }
}

modelBuilder.Entity<Address>(x =>
{
    x.Property(p => p.City).UsePropertyAccessMode(PropertyAccessMode.Field); // ... and these
    x.Property(p => p.Street).UsePropertyAccessMode(PropertyAccessMode.Field);
});

Note the use of UsePropertyAccessMode, this does the trick.

You can now also have your value objects contain a navigation property, like for example:

public class Address : ValueObject
{
    public string City { get; set; }
    public string Street { get; set; }
    public Country Country { get; set; }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return City;
        yield return Street;
        yield return Country;
    }
}

And that’s great. A long-awaited enhancement. But these two features don’t work together. In other words, you can’t define the value object like the following which is both immutable and contains a navigation property:

public class Address : ValueObject
{
    public string City { get; }
    public string Street { get; }
    public Country Country { get; }

    public Address(string city, string street, Country country)
    {
        City = city;
        Street = street;
        Country = country;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return City;
        yield return Street;
        yield return Country;
    }
}

And I assume that’s just tip of the iceberg. I haven’t worked with EF Core in production, just toyed with it in my spare time, and who knows how many more of such pitfalls there are. All because the team tries to fit a square peg in a round hole (implement owned types as regular entities). Just do it the same way NHibernate implemented its Component feature already.

And yes, NHibernate supports read-only Value Objects, allows you to declare them in derived types, behaves well when you introduce a navigation property in them, and even allows for adding a collection of properties (not that you should ever do that, though).

So once again we have a feature that’s usable in simple scenarios only. The score of #6: Multi-valued Value Objects is: EF Core vs NHibernate - 0.4 : 1.

#7: Domain Events

Domain events is an important part of your model in DDD. They represent domain-specific changes in your domain. I talked about them in my DDD in Practice Pluralsight course and also wrote about them here: Domain events: simple and reliable solution. But let me recap it real quick.

The best way to implement domain events is using the pattern that I call “commit before dispatching”. In other words, when you dispatch the events after the database transaction is complete. To do that, you need to introduce a collection of domain events into the base entity class:

public class Order
{
    private Customer _customer;
    private List<OrderLine> _lines;

    public Order(IEnumerable<ProductLine> lines, Customer customer)
    {
        _customer = customer;
        _lines = lines
            .Select(x => new OrderLine(x.Product, x.Quantity, x.Price))
            .ToList();

        AddDomainEvent(new OrderSubmitted(this)); // Method in the base Entity or AggregateRoot class
    }
}

In order to dispatch those events, you need to be able to listen for changes in your domain objects. This is how you can do that with NHibernate:

internal class EventListener : 
    IPostInsertEventListener, 
    IPostDeleteEventListener, 
    IPostUpdateEventListener, 
    IPostCollectionUpdateEventListener
{
    public void OnPostUpdate(PostUpdateEvent ev)
    {
        DispatchEvents(ev.Entity as AggregateRoot);
    }

    public void OnPostDelete(PostDeleteEvent ev)
    {
        DispatchEvents(ev.Entity as AggregateRoot);
    }

    public void OnPostInsert(PostInsertEvent ev)
    {
        DispatchEvents(ev.Entity as AggregateRoot);
    }

    public void OnPostUpdateCollection(PostCollectionUpdateEvent ev)
    {
        DispatchEvents(ev.AffectedOwnerOrNull as AggregateRoot);
    }

    private void DispatchEvents(AggregateRoot aggregateRoot)
    {
        if (aggregateRoot == null)
            return;

        foreach (IDomainEvent domainEvent in aggregateRoot.DomainEvents)
        {
            DomainEvents.Dispatch(domainEvent);
        }

        aggregateRoot.ClearEvents();
    }
}

Note 2 things here:

  • The dispatch is done after the transaction is committed.

  • You listen for not only changes in the object themselves, but also for changes in their collections. It’s important because you might want to update a collection of children and not the parent entity itself, and this should still be considered a change from the DDD perspective.

In EF Core, you can do the same using the following code:

using (SchoolContext context = new SchoolContext(optionsBuilder.Options))
{
    // Do something here

    List<IDomainEvent> domainEvents = context.ChangeTracker
        .Entries()
        .Where(x => x.Entity is Entity)
        .SelectMany(x => ((Entity)x.Entity).DomainEvents)
        .ToList();

    context.SaveChanges();

    foreach (IDomainEvent domainEvent in domainEvents)
    {
        DomainEvents.Dispatch(domainEvent);
    }
}

The score of #7: Domain Events is: EF Core vs NHibernate - 1 : 1.

#8: Many-to-many relationships

EF Core still doesn’t support many-to-many relationships. I personally don’t use many-to-many relationships often but sometimes, they do pop up, and it’s nice to have this feature in the ORM.

Let’s take the following example:

EF Core vs NHibernate: Many-to-many

The reason why I don’t use many-to-many relationships that much is because it’s pretty rare when the intermediate table contains only the (composite) primary key. More often than not, this relationship contains some additional information, and so it makes sense to elevate it to its own entity.

For example here:

EF Core vs NHibernate: Many-to-many with additional column

you can see the additional column StudentSince. And because it’s generally not a good idea for fully-fledged entities to have composite keys, you can see it now has its own dedicated primary key. Alright, so EF Core doesn’t support many-to-many relationships and you have to always introduce an entity for the intermediate table. Which actually wouldn’t be that bad (unless of course you port a legacy project from EF6 with lots of already existing many-to-many relationships, in which case it would). It wouldn’t be that bad if you could encapsulate that additional entity and hide this technical inconvenience from the clients of your domain model.

But you can’t.

First of all, you must have explicit foreign key properties (StudentId and InstructorId) defined in that entity:

public class Student2Instructor
{
    public int StudentId { get; set; } // Must have this
    public virtual Student Student { get; set; }

    public int InstructorId { get; set; } // And this
    public virtual Instructor Instructor { get; set; }
}

But that’s a minor issue. The more important one is that this class must show up in the domain model. You can’t hide it there using a trick like this:

public class Student : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }

    private readonly List<Student2Instructor> _student2Instructors =
        new List<Student2Instructor>();
    public virtual IReadOnlyList<Instructor> Instructors =>
        _student2Instructors.Select(x => x.Instructor).ToList();
}

That’s because in EF Core, for some reason, the type of the property has to match the type of the underlying backing field, and there’s no way to tweak the mapping to make it work.

And so you have to come up with a much less appealing workaround:

public class Student : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }

    private readonly List<Student2Instructor> _student2Instructors =
        new List<Student2Instructor>();
    public virtual IReadOnlyList<Student2Instructor> Student2Instructors =>
        _student2Instructors.ToList();
    public virtual IReadOnlyList<Instructor> Instructors =>
        Student2Instructors.Select(x => x.Instructor).ToList();
}

Clearly, the Student2Instructors collection is not something you’d like to see in your domain model, it’s another ORM concern leaking into it.

And that’s not all. You are also having an N+1 problem here:

using (SchoolContext context = new SchoolContext(optionsBuilder.Options))
{
    Student student = context.Students.Find(1); // Triggers a load - OK
    IReadOnlyList<Instructor> instructors = student.Instructors; // Triggers a load - OK
    Instructor instructor1 = instructors[0]; // Triggers a load - !NOT OK!
    Instructor instructor2 = instructors[1]; // Triggers a load - !NOT OK!
}

Instead of just 2 database roundtrips (assuming lazy loading is on) for the student itself and its collection of instructors, EF Core loads each instructor separately.

This one is not an issue with the many-to-many mappings but rather with the lack of proper mapping settings. But the lack of it becomes especially painful when working with many-to-many relationships. In NHibernate, you can explicitly specify which relationships to map eagerly, load lazily or even not load at all. In EF Core, there’s no such flexibility.

With NHibernate, you can map many-to-many relationships, and it just works exactly as you’d expect it to:

public class Student : Entity
{
    public virtual string FirstName { get; private set; }
    public virtual string LastName { get; private set; }

    private readonly IList<Instructor> _instructors = new List<Instructor>();
    public virtual IReadOnlyList<Instructor> Instructors => _instructors.ToList();
}

public class StudentMap : ClassMap<Student>
{
    public StudentMap()
    {
        HasManyToMany<Instructor>(Reveal.Member<Student>("_instructors"))
            .Access.Field()
            .Table("Student2Instructor");
    }
}

And even if for some reason you want to introduce an explicit class for the intermediate table, you are still able to encapsulate the work with it. It doesn’t have to show up in the domain model.

The score of #8: Many-to-many relationships EF Core vs NHibernate – 0 : 1.

Conclusion

The total score is: EF Core vs NHibernate - 4.5 : 7.8.

The EF Core team is putting a lot of effort, and EF Core is slowly approaching parity with NHibernate. But it’s still light years behind, and I don’t even mention all other, non-DDD related features.

And frankly, I’m not sure it will ever reach the parity. How hard can it be to look at NHibernate at copy its functionality, at least in the key areas? And still, with all the available resources, Microsoft’s flagman ORM is not even close to the competitor that’s being developed by only a handful of people.

And if you insist that there’s no one true way and that maybe the EF Core team has its own view on how ORMs should work, just look at EF’s progression. The more recent the release, the closer it is to NHibernate in terms of key design decisions. The team eventually comes to the same conclusions (see ## 2, 3, 4, 6), except that in NHibernate, they were implemented more than a decade ago.

Microsoft should have adopted NHibernate from the very beginning, and not tried to reinvent the wheel. This train is long gone of course and no one is going to abandon EF Core, so the next best thing would be to try to speed up the convergence and finally start looking at how others solve the same problems. Implement proper multi-valued Value Objects (read: copy NHibernate’s Component feature), the work with disconnected graphs of objects, lazy loading. That would be a good start.

Stop working on features that shouldn’t have made it to the ORM in the first place. Such as the soft deletion feature for example. Soft deletion should be the domain’s responsibility, not something you delegate to the ORM.

Developers, try out NHibernate. I’ve been using it for building highly encapsulated and clean domain models for many years. And now that it supports both async operations and .NET Standard, there’s no reason not to. The combination that often works best for me is: NHibernate for commands (write operations) and Dapper for queries (read operations).

If you’d like to look at examples of those domain models, check out these GitHub repositories of mine: one, two. I also recommend you watch these two of my Pluralsight courses: Domain-Driven Design in Practice and Refactoring from Anemic Domain Model Towards a Rich One.

Alright, the comparison turned out to be quite harsh, but someone had to say all this. Hopefully, one day there will be no real difference between the two ORMs in terms of DDD, or maybe EF Core would even do that much better than NHibernate.

UPDATE 6/23/2018

  • Updated #7 (EF Core actually supports this case).
  • Added #8: Many-to-many relationships.

UPDATE 3/5/2020

I’ve expanded on this article and updated its content for EF Core 3.1 in my new Pluralsight course DDD and EF Core: Preserving Encapsulation.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus