📜 ⬆️ ⬇️

Domain Driven Design: Value Objects and Entity Framework Core in practice

On Habré, and not only a decent amount of articles about Domain Driven Design are written - both in general about architecture, and with examples on .Net. But at the same time, such an important part of this architecture as Value Objects is often poorly mentioned.

In this article I will try to reveal the nuances of the implementation of Value Objects in .Net Core using the Entity Framework Core.

There is a lot of code under the cut.

Some theory


The core of the Domain Driven Design architecture is the Domain - the subject area to which the developed software is applied. Here is all the business logic of the application, which usually interacts with various data. Data can be of two types:
')

An Entity Object defines an entity in business logic and necessarily has an identifier by which an Entity can be found or compared with another Entity. If two entities have the same identifier, they are the same entity. Almost always change.
Value Object is an immutable type, the value of which is set at creation and does not change throughout the life of the object. Does not have an identifier. If two VOs are structurally the same, they are equivalent.

Entity may contain other Entity and VO. Other VOs may be included in the VO, but not Entity.

Thus, the domain logic should work exclusively with Entity and VO - this guarantees its consistency. Basic data types, such as string, int, etc. Often they can not act as a VO, because they can simply violate the state of the domain - which is almost a disaster in the framework of DDD.

Example. The Person class which has scored dirty teeth in various manuals often is shown like this:

public class Person { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } 

Simple and clear - ID, name and age, where is it possible to make a mistake?

And there can be several errors here - for example, from the point of view of business logic, the name is obligatory, there can not be zero length or more than 100 characters and should not contain special characters, punctuation, etc. And age can not be less than 10 or more than 120 years.

From the point of view of a programming language, 5 is a completely normal integer, similarly, an empty string. But the domain is already in an incorrect state.

Go to practice


At this point, we know that the VO must be immutable and contain a value that is valid for business logic.

Immunity is achieved by initializing the readonly property when an object is created.
Validation of the value occurs in the constructor (Guard clause). It is desirable to make the check itself publicly available - so that other layers can validate data received from the client (the same browser).

Let's create a VO for Name and Age. In addition, let's complicate the task a bit - add a PersonalName that combines FirstName and LastName, and apply it to Person.

Name
 public class Name { private static readonly Regex ValidationRegex = new Regex( @"^[\p{L}\p{M}\p{N}]{1,100}\z", RegexOptions.Singleline | RegexOptions.Compiled); public Name(String value) { if (!IsValid(value)) { throw new ArgumentException("Name is not valid"); } Value = value; } public String Value { get; } public static Boolean IsValid(String value) { return !String.IsNullOrWhiteSpace(value) && ValidationRegex.IsMatch(value); } public override Boolean Equals(Object obj) { return obj is Name other && StringComparer.Ordinal.Equals(Value, other.Value); } public override Int32 GetHashCode() { return StringComparer.Ordinal.GetHashCode(Value); } } 


PersonalName
 public class PersonalName { protected PersonalName() { } public PersonalName(Name firstName, Name lastName) { if (firstName == null) { throw new ArgumentNullException(nameof(firstName)); } if (lastName == null) { throw new ArgumentNullException(nameof(lastName)); } FirstName = firstName; LastName = lastName; } public Name FirstName { get; } public Name LastName { get; } public String FullName => $"{FirstName} {LastName}"; public override Boolean Equals(Object obj) { return obj is PersonalName personalName && EqualityComparer<Name>.Default.Equals(FirstName, personalName.FirstName) && EqualityComparer<Name>.Default.Equals(LastName, personalName.LastName); } public override Int32 GetHashCode() { return HashCode.Combine(FirstName, LastName); } public override String ToString() { return FullName; } } 


Age
 public class Age { public Age(Int32 value) { if (!IsValid(value)) { throw new ArgumentException("Age is not valid"); } Value = value; } public Int32 Value { get; } public static Boolean IsValid(Int32 value) { return 10 <= value && value <= 120; } public override Boolean Equals(Object obj) { return obj is Age other && Value == other.Value; } public override Int32 GetHashCode() { return Value.GetHashCode(); } } 


And finally, Person:

 public class Person { public Person(PersonalName personalName, Age age) { if (personalName == null) { throw new ArgumentNullException(nameof(personalName)); } if (age == null) { throw new ArgumentNullException(nameof(age)); } Id = Guid.NewGuid(); PersonalName= personalName; Age = age; } public Guid Id { get; private set; } public PersonalName PersonalName{ get; set; } public Age Age { get; set; } } 

Thus, we cannot create a Person without a full name or age. Also, we cannot create a “wrong” name or “wrong” age. A good programmer will check the received data in the controller using the methods Name.IsValid (“John”) and Age.IsValid (35) and in case of incorrect data he will inform the client about this.

If we make it a rule to use only Entity and VO everywhere in the model, then we will save ourselves from a large number of errors - incorrect data simply will not fall into the model.

Persistence


Now we need to save our data in the data warehouse and retrieve them on request. As the ORM, we will use the Entity Framework Core, the data warehouse is MS SQL Server.

DDD clearly defines: Persistence is a subspecies of the infrastructure layer, because it hides in itself a concrete implementation of data access.

The domain does not need to know anything about Persistence, so it defines only the interfaces of the repositories.

And Persistence contains specific implementations, mapping configurations, as well as the UnitOfWork object.

There are two opinions, whether it is worth creating repositories and Unit of Work.

On the one hand, no, not necessary, because in the Entity Framework Core this is all already implemented. If we have a layered architecture like DAL -> Business Logic -> Presentation, which is based on data storage - then why not use the features of EF Core directly.

But the domain in DDD does not depend on data storage and the ORM used - these are all implementation subtleties that are encapsulated in Persistence and no one is interested. If we provide DbContext to other layers, then we immediately reveal the implementation details, tightly tied to the selected ORM and get DAL - as the basis of all business logic, but this should not be. Roughly speaking, the domain should not notice a change in the ORM and even the loss of Persistence as a layer.

So, the Persons repository interface in the domain:

 public interface IPersons { Task Add(Person person); Task<IReadOnlyList<Person>> GetList(); } 

and its implementation in Persistence:

 public class EfPersons : IPersons { private readonly PersonsDemoContext _context; public EfPersons(UnitOfWork unitOfWork) { if (unitOfWork == null) { throw new ArgumentNullException(nameof(unitOfWork)); } _context = unitOfWork.Context; } public async Task Add(Person person) { if (person == null) { throw new ArgumentNullException(nameof(person)); } await _context.Persons.AddAsync(person); } public async Task<IReadOnlyList<Person>> GetList() { return await _context.Persons.ToListAsync(); } } 

It would seem nothing complicated, but there is a problem. The Entity Framework Core “out of the box” works only with basic types (string, int, DateTime, etc.) and knows nothing about PersonalName and Age. Let's teach EF Core to understand our Value Objects.

Configuration


The Fluent API is best suited for configuring Entity in DDD. Attributes are not suitable, as the domain should not know anything about the mapping nuances.

Create in Persistence a class with the base configuration PersonConfiguration:

 internal class PersonConfiguration : IEntityTypeConfiguration<Person> { public void Configure(EntityTypeBuilder<Person> builder) { builder.ToTable("Persons"); builder.HasKey(p => p.Id); builder.Property(p => p.Id).ValueGeneratedNever(); } } 

and connect it to DbContext:

 protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ApplyConfiguration(new PersonConfiguration()); } 

Mapping


That section for which this material is written.

At the moment there are two more or less convenient ways of mapping non-standard classes to base types - Value Conversions and Owned Types.

Value conversions


This feature appeared in the Entity Framework Core 2.1 and allows you to define the conversion between the two types of data.

Let's write a converter for Age (in this section all the code is in PersonConfiguration):

 var ageConverter = new ValueConverter<Age, Int32>( v => v.Value, v => new Age(v)); builder .Property(p => p.Age) .HasConversion(ageConverter) .HasColumnName("Age") .HasColumnType("int") .IsRequired(); 

Simple and concise syntax, but not without flaws:

  1. Cannot convert null;
  2. It is not possible to convert one property into several columns in a table and vice versa;
  3. EF Core cannot convert a LINQ expression with this property to an SQL query.

I will dwell on the last point in more detail. Add a method to the repository that returns a list of Person older than the specified age:

 public async Task<IReadOnlyList<Person>> GetOlderThan(Age age) { if (age == null) { throw new ArgumentNullException(nameof(age)); } return await _context.Persons .Where(p => p.Age.Value > age.Value) .ToListAsync(); } 

There is a condition on age, but EF Core cannot convert it to SQL query and, having reached Where (), loads the entire table into the application memory and only then, using LINQ, fulfills the condition p.Age.Value> age.Value .

In general, Value Conversions is a simple and fast version of mapping, but you need to remember this feature of EF Core, otherwise, at some point, when you query into large tables, the memory may run out.

Owned types


Owned Types appeared in the Entity Framework Core 2.0 and came to replace Complex Types from the usual Entity Framework.

Let's make Age as Owned Type:

 builder.OwnsOne(p => p.Age, a => { a.Property(u => u.Value).HasColumnName("Age"); a.Property(u => u.Value).HasColumnType("int"); a.Property(u => u.Value).IsRequired(); }); 

Not bad. And Owned Types do not have some of the disadvantages of Value Conversions, namely points 2 and 3.

2. It is possible to convert one property into several columns in a table and vice versa.

What you need for PersonalName, although the syntax is already a bit overloaded:

 builder.OwnsOne(b => b.PersonalName, pn => { pn.OwnsOne(p => p.FirstName, fn => { fn.Property(x => x.Value).HasColumnName("FirstName"); fn.Property(x => x.Value).HasColumnType("nvarchar(100)"); fn.Property(x => x.Value).IsRequired(); }); pn.OwnsOne(p => p.LastName, ln => { ln.Property(x => x.Value).HasColumnName("LastName"); ln.Property(x => x.Value).HasColumnType("nvarchar(100)"); ln.Property(x => x.Value).IsRequired(); }); }); 

3. EF Core can convert a LINQ expression with this property into a SQL query.
Add a sort by LastName and FirstName when loading the list:

 public async Task<IReadOnlyList<Person>> GetList() { return await _context.Persons .OrderBy(p => p.PersonalName.LastName.Value) .ThenBy(p => p.PersonalName.FirstName.Value) .ToListAsync(); } 

Such an expression will be correctly converted to a SQL query and the sorting will be performed on the SQL server side, and not in the application.

Of course, there are drawbacks.

  1. Nowhere is the problem with null;
  2. Owned Types fields cannot be readonly and must have a protected or private setter.
  3. Owned Types are implemented as regular Entity, which means:
    • They have an identifier (as a shadow property, i.e. it does not appear in the domain class);
    • EF Core tracks all changes in Owned Types, just like for regular Entity.

On the one hand, this is not what Value Objects should be. They should not have any identifiers. VOs should not be tracked for changes - because they are initially immutable, properties of the parent Entity, but not properties of VO should be tracked.

On the other hand, these are implementation details that can be omitted, but, again, you should not forget. Tracking changes affects performance. If this is not noticeable with samples of single Entity (for example, by Id) or small lists, then with a sample of large lists of “heavy” Entity (many VO properties), the performance will be very noticeable because of tracking.

Presentation


We figured out how to implement Value Objects in the domain and repository. It's time to use it all. Let's create two simplest pages - with the list of Person and the form of adding Person.

The controller code without Action methods looks like this:

 public class HomeController : Controller { private readonly IPersons _persons; private readonly UnitOfWork _unitOfWork; public HomeController(IPersons persons, UnitOfWork unitOfWork) { if (persons == null) { throw new ArgumentNullException(nameof(persons)); } if (unitOfWork == null) { throw new ArgumentNullException(nameof(unitOfWork)); } _persons = persons; _unitOfWork = unitOfWork; } // Actions private static PersonModel CreateModel(Person person) { return new PersonModel { FirstName = person.PersonalName.FirstName.Value, LastName = person.PersonalName.LastName.Value, Age = person.Age.Value }; } } 

Add an Action to get the Person list:

 [HttpGet] public async Task<IActionResult> Index() { var persons = await _persons.GetList(); var result = new PersonsListModel { Persons = persons .Select(CreateModel) .ToArray() }; return View(result); } 

View
 @model PersonsListModel @{ ViewData["Title"] = "Persons List"; } <div class="text-center"> <h2 class="display-4">Persons</h2> </div> <table class="table"> <thead> <tr> <td><b>Last name</b></td> <td><b>First name</b></td> <td><b>Age</b></td> </tr> </thead> @foreach (var p in Model.Persons) { <tr> <td>@p.LastName</td> <td>@p.FirstName</td> <td>@p.Age</td> </tr> } </table> 


Nothing complicated - we loaded the list, created a Data-Transfer Object (PersonModel) for each

Person and sent to the appropriate View.

Result


The addition of Person is much more interesting:

 [HttpPost] public async Task<IActionResult> AddPerson(PersonModel model) { if (model == null) { return BadRequest(); } if (!Name.IsValid(model.FirstName)) { ModelState.AddModelError(nameof(model.FirstName), "FirstName is invalid"); } if (!Name.IsValid(model.LastName)) { ModelState.AddModelError(nameof(model.LastName), "LastName is invalid"); } if (!Age.IsValid(model.Age)) { ModelState.AddModelError(nameof(model.Age), "Age is invalid"); } if (!ModelState.IsValid) { return View(); } var firstName = new Name(model.FirstName); var lastName = new Name(model.LastName); var person = new Person( new PersonalName(firstName, lastName), new Age(model.Age)); await _persons.Add(person); await _unitOfWork.Commit(); var persons = await _persons.GetList(); var result = new PersonsListModel { Persons = persons .Select(CreateModel) .ToArray() }; return View("Index", result); } 

View
 @model PersonDemo.Models.PersonModel @{ ViewData["Title"] = "Add Person"; } <h2 class="display-4">Add Person</h2> <div class="row"> <div class="col-md-4"> <form asp-action="AddPerson"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="FirstName" class="control-label"></label> <input asp-for="FirstName" class="form-control" /> <span asp-validation-for="FirstName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="LastName" class="control-label"></label> <input asp-for="LastName" class="form-control" /> <span asp-validation-for="LastName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Age" class="control-label"></label> <input asp-for="Age" class="form-control" /> <span asp-validation-for="Age" class="text-danger"></span> </div> <div class="form-group"> <input type="submit" value="Create" class="btn btn-primary" /> </div> </form> </div> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } 


Here there is a mandatory validation of incoming data:

 if (!Name.IsValid(model.FirstName)) { ModelState.AddModelError(nameof(model.FirstName), "FirstName is invalid"); } 

If this is not done, then when creating a VO with an incorrect value, ArgumentException will be thrown out (remember the Guard Clause in the VO constructors). With verification, it is much easier to send the user a message that one of the values ​​is incorrect.

Result


Here you need to make a small digression - in Asp Net Core there is a regular way of data validation - using attributes. But in DDD, this method of validation is not correct for several reasons:


Let's go back to AddPerson (). After validating the data, PersonalName, Age, and then Person are created. Next, add the object to the repository and save the changes (Commit). It is very important that Commit not be called in the EfPersons repository. The task of the repository is to perform some action with the data, no more. Commit is done only from the outside, when exactly - the programmer decides. Otherwise, a situation is possible when an error occurs in the middle of a certain business iteration — but some of the data is saved, and some is not. We get the domain in the “broken” state. If Commit is done at the very end, then if an error occurs, the transaction will simply be rolled back.

Conclusion


I gave examples of the implementation of Value Objects in general and the nuances of mapping in the Entity Framework Core. I hope that the material will be useful in understanding how to use the elements of Domain Driven Design in practice.

Full source code of the PersonsDemo - GitHub project

The material does not disclose the problem of interaction with optional (nullable) Value Objects - if PersonalName or Age were not mandatory Person properties. I wanted to describe it in this article, but it already came out a bit overloaded. If there is an interest in this issue - write in the comments, there will be a continuation.

Fans of “beautiful architectures” in general, and Domain Driven Design in particular, highly recommend the Enterprise Craftsmanship resource.

There are many useful articles about the correct architecture and examples of implementation on .Net. Some ideas were borrowed there, successfully implemented in “combat” projects and partially reflected in this article.

Also used official documentation on Owned Types and Value Conversions .

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


All Articles