📜 ⬆️ ⬇️

Entity Framework 6 (7) vs NHibernate 4: a DDD perspective

The network already has quite a few comparisons of the Entity Framework and NHibernate, but all of them mostly focus on the technical side of the issue. In this article, I would like to compare these two technologies in terms of Domain Driven Design (DDD). We will look at a few code examples and see how these two ORMs allow us to cope with difficulties.



Layered architecture (Onion architecture)


')
image

Today, it is quite common practice to use Onion (layered) architecture to design complex systems. It allows you to isolate domain logic from the rest of the system so that we can focus on the most important parts of the application.

Isolation of domain logic means that domain classes can interact only with other domain classes . This is one of the most important principles to follow in order to make the code clean and coherent.

The picture below shows the onion architecture using the classic n-tier scheme.

image

Persistence ignorance


When using the ORM, it is important to maintain a good degree of isolation between the domain logic and the logic of storing data in the database (Persistence Ignorance). This means that the code should be structured in such a way that all the logic relating to storing data in the database is removed from the domain classes. Ideally, domain entities should not contain any information about how they are stored in the database . Following this rule allows you to adhere to the principle of a single duty and, thus, keep the code simple and supported.

If you write code similar to the example below, you are on the wrong path:

public class MyEntity { // Perstisted in the DB public int Id { get; set; } public string Name { get; set; } // Not persisted public bool Flag { get; set; } } 


If the domain logic is separated from the data retention logic, it can be said that the domain classes are persistence ignorant. This means that you can change the way in which you save data to the database without affecting the domain logic. Persistence ignorance is a prerequisite for isolating domain logic.

Example 1: Deleting a child entity from an aggregate root


Let's look at code samples from real projects.

image

The figure above shows a unit containing two classes. The Order class is the root of the aggregate. This means that Order controls the lifetime of the objects in the Lines collection. If the Order is deleted, the objects from this collection will be deleted along with it; OrderLine cannot exist without an Order object.

Suppose we need to implement a method that removes one of the items in the order. Here is how we can implement this if our code is not associated with any ORM:

 public ICollection<OrderLine> Lines { get; private set; } public void RemoveLine(OrderLine line) { Lines.Remove(line); } 


You simply delete the position from the collection, and that's it. As long as the order is the root of an aggregate, customers of this class can access its positions only through a reference to the Order object. If it does not have this position, we can assume that it is deleted, because other objects cannot keep references to child objects of the aggregate.

If you try to execute this code using the Entity Framework, you will get an exception:

It is not a nillable.

In the Entity Framework, there is no way to set the mapping so that deleted items from the collection (orphaned) are automatically removed from the database. You need to do it yourself:

 public virtual ICollection<OrderLine> Lines { get; set; } public virtual void RemoveLine(OrderLine line, OrdersContext db) { Lines.Remove(line); db.OrderLines.Remove(line); } 


Passing OrdersContext to the domain object method violates the principle of responsibility sharing , since class Order in this case contains information about how it is stored in the database.

Here's how it can be done in NHibernate:

 public virtual IList<OrderLine> Lines { get; protected set; } public virtual void RemoveLine(OrderLine line) { Lines.Remove(line); } 


Note that this code is almost inefficient to the code that we would write if we didn’t need to save data to the database. You can tell NHibernate to remove positions from the database automatically after they are removed from the collection:

 public class OrderMap : ClassMap<Order> { public OrderMap() { Id(x => x.Id); HasMany(x => x.Lines).Cascade.AllDeleteOrphan().Inverse(); } } 


Example 2: Link to related entity


image

Suppose that in one of the classes we need to add a link to the associated class. Here is a sample code, not tied to any ORM:

 public class OrderLine { public Order Order { get; private set; } // Other members } 


Here is how this is done in the default Entity Framework:

 public class OrderLine { public virtual Order Order { get; set; } public int OrderId { get; set; } // Other members } 


The default method in NHibernate is:

 public class OrderLine { public virtual Order Order { get; set; } // Other members } 


As you can see, by default, in the Entity Framework, you need to add an additional property with an identifier in order to associate two entities. This approach violates the principle of sole responsibility: identifiers are part of the implementation of how data is stored in the database; domain objects should not contain such information. The Entity Framework encourages you to work directly with database terms, while in this case the best solution would be to create a single Order property and give up the rest of the work of the ORM itself.

Moreover, this code violates the principle do not repeat yourself . Declaring both OrderId and Order allows the OrderLine class to very easily go to the inconsistent state:

 Order = order; // An order with Id == 1 OrderId = 2; 


It should be noted that EF still allows you to declare references to related entities without specifying a separate identifier property, but this approach has two drawbacks:
- Access to the identifier of the associated object (i.e. orderLine.Order.Id) results in loading the entire object from the database, despite the fact that this identifier is already in memory. NHibernate, in turn, is smart enough and does not load the associated object from the database if the client accesses its Id.
- Entity Framework encourages developers to use identifiers. Partly due to the fact that this is the default way to refer to related entities, partly due to the fact that all the examples in the documentation use this approach. In NHibernate, by contrast, the default way to declare a link to a related entity is a link to an object of that entity.

Example 3: Read-only collection of related entities


If you want a collection of positions in the order with a read-only collection for customers of this class, you can write the following code (a version that is not tied to any ORM):

 private List<OrderLine> _lines; public IReadOnlyList<OrderLine> Lines { get { return _lines.ToList(); } } 


The official way to do this in EF is:

 public class Order { protected virtual ICollection<OrderLine> LinesInternal { get; set; } public virtual IReadOnlyList<OrderLine> Lines { get { return LinesInternal.ToList(); } } public class OrderConfiguration : EntityTypeConfiguration<Order> { public OrderConfiguration() { HasMany(p => p.LinesInternal).WithRequired(x => x.Order); } } } public class OrdersContext : DbContext { protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new Order.OrderConfiguration()); } } 


Obviously, this is not the way you want to use when designing a domain model, since it directly mixes the infrastructure logic with the domain one. The unofficial way is not much better:

 public class Order { public static Expression<Func<Order, ICollection<OrderLine>>> LinesExpression = f => f.LinesInternal; protected virtual ICollection<OrderLine> LinesInternal { get; set; } public virtual IReadOnlyList<OrderLine> Lines { get { return LinesInternal.ToList(); } } } public class OrdersContext : DbContext { protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Order>() .HasMany(Order.LinesExpression); } } 


Again, the Order class contains the infrastructure code. In the Entity Framework there is no way that would allow the separation of the infrastructure code from the domain code in such cases.

Here's how to do this with NHibernate:

 public class Order { private IList<OrderLine> _lines; public virtual IReadOnlyList<OrderLine> Lines { get { return _lines.ToList(); } } } public class OrderMap : ClassMap<Order> { public OrderMap() { HasMany<OrderLine>(Reveal.Member<Order>(“Lines”)) .Access.CamelCaseField(Prefix.Underscore); } } 


Again, the code above is almost identical to code that is not tied to any ORM. The Order class here is clean and does not contain any information about how it is stored in the database, which allows us to focus on the subject area.

Code using NHibernate has one flaw. It is prone to errors when refactoring, because The property name is specified as a string. However, this is a reasonable compromise, because This approach allows you to clearly separate the logic of the subject area from the logic of storing data in the database. In addition, such errors are quite easily caught by integration tests.

Example 4: Unit of Work Pattern


Below is the code from the task I was working on a couple of years ago. I omitted details for krakost, but the main idea should be clear:

 public IList<int> MigrateCustomers(IEnumerable<CustomerDto> customerDtos, CancellationToken token) { List<int> ids = new List<int>(); using (ISession session = CreateSession()) using (ITransaction transaction = session.BeginTransaction()) { foreach (CustomerDto dto in customerDtos) { token.ThrowIfCancellationRequested(); Customer customer = CreateCustomer(dto); session.Save(customer); ids.Add(customer.Id); } transaction.Commit(); } return ids; } 


The method takes some data, converts it into domain objects and stores thereafter. It returns a list of customer identifiers. External code can mark the migration process using the CancellationToken passed as a parameter.

If you use the Entity Framework for this task, it will insert records into the database as they are saved in the session in order to get Id, since The only available strategy for generating integer identifiers in EF is database identity. This approach works quite well in most cases, but it has a significant drawback: it violates the principle of the Unit of Work . If the calling code cancels the execution of the method, EF will have to delete all the records inserted in the database at the time of the cancellation, which results in a significant performance drop.

With NHibernate, you can choose the Hi / Lo strategy, so the records will simply not be saved in the database until the session is closed. In this case, identifiers are generated on the client, so there is no need to save records in the database in order to receive them. NHibernate can help save a significant amount of time in tasks of this type.

Example 5: Work with cached objects


Suppose that the list of your clients does not change often and you decide to cache it so as not to refer to the database each time to receive it. Suppose also that the order class has a precondition according to which each order should belong to one of the customers. You can implement this precondition using the constructor:

 public Order(Customer customer) { Customer = customer; } 


Thus, we can be sure that no order will be created without specifying its customer.

With the Entity Framework, you cannot assign a reference to a detached object to a new object. If you write the code as in the example above, EF will try to insert the client into the database, since he was not attached to the current context. To fix this, you need to explicitly indicate that the customer already exists in the database:

 public Order(Customer customer, OrdersContext context) { context.Entry(customer).State = EntityState.Unchanged; Customer = customer; } 


Again, this approach violates the principle of sole responsibility and establishes a relationship between the domain and infrastructure logic. Unlike EF, NHibernate can determine whether a customer object is new by its identifier and does not attempt to insert it into the database if it is already present there. The version of the code that uses NHibernate is the same as the version without ORM.

results


There is an easy way to measure how well ORM allows you to isolate domain logic from the logic of storing data in the database. The closer the code that uses ORM to the code that is not tied to any ORM, the better this ORM allows to share responsibilities in the code .

EF is too closely tied to the database. When you write code with the Entity Framework, you constantly have to think in terms of foreign-key constraints and relationships between tables. Without a clean and isolated domain model, your attention is constantly distracted, you cannot focus on the logic of the domain.

The peculiarity here is that you may not notice these distractions until your system grows to a sufficiently large size. And when this happens, it becomes really difficult to maintain the old level of development of new functionality due to the increased cost of maintaining the current code.

In sum, NHibernate is still ahead of the Entity Framework. In addition to a better degree of isolation of the domain code from the infrastructure logic, NHibernate has quite a few useful features that are not found in the Entity Framework: level 2 cache, strategies for simultaneous access to data in the database (concurrency strategies), more flexible ways of data mapping, etc. . Perhaps the only thing that EF boasts is a non-casting I / O when working with a database (async database operations).

What is the matter? The Entity Framework has been under development for many years, hasn’t it? Unfortunately, apparently, nobody in the EF team considers ORM from the point of view of DDD. The Entity Framework team is still thinking about ORM in terms of data, and not in terms of model. This means that, despite the fact that the Entity Framework is a good tool in many simple situations, if you are working on more complex projects, NHibernate will be the best choice for you.

As for me, this situation is very disappointing. With all the support that EF receives from Microsoft and with all the confidence that developers put into it due to the fact that this is a recommended (and default for many) ORM, Entity Framework could be better. Apparently, once again, Microsoft did not take into account the experience of the community and went their own way.

Update


The program manager of the Entity Framework team at Microsoft Rowan Miller responded that some of the problems described above will be fixed in EF7. Although this is only a part, let's hope that the EF will change for the better in the future.

Link to original article: NHibernate vs Entity Framework

Source: https://habr.com/ru/post/262407/


All Articles