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

On Habré and not only a decent amount of articles has been written about Domain Driven Design - 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 uncover the nuances of implementing Value Objects in .Net Core using the Entity Framework Core.

Under a cat there is a lot of code.

Bit of theory


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

  • Entity Object
  • Value Object (hereinafter - VO)

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

Entity may contain other Entity and VO. VOs may include other VOs, 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 cannot 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. In the various manuals, the Person class, which has gotten sick of everyone, is often shown like this:

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

Simple and clear - identifier, name and age, where can you make a mistake?

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

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

Let's move on to practice


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

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

Let's create a VO for Name and Age. Additionally, we complicate the task a bit - add a PersonalName combining FirstName and LastName, and apply this 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.Default.Equals(FirstName, personalName.FirstName) &&
               EqualityComparer.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; }
}

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

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

Persistence


Now we need to save our data in the data warehouse and get it on request. We will use Entity Framework Core as the ORM, and the data warehouse is MS SQL Server.

DDD clearly defines: Persistence is a subspecies of the infrastructure layer because it hides a specific implementation of data access.

The domain does not need to know anything about Persistence, this determines only the interfaces of the repositories.

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

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

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

But the domain in DDD does not depend on data storage and the ORM used - these are all the subtleties of implementation that are encapsulated in Persistence and are of no interest to anyone else. If we provide DbContext to other layers, then we immediately disclose the implementation details, tightly bind 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 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> 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> GetList() {
        return await _context.Persons.ToListAsync();
    }
}

It would seem nothing complicated, but there is a problem. Out of the box Entity Framework Core 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 most suitable for configuring Entity in DDD. Attributes are not suitable, since the domain does not need to know anything about the nuances of mapping.

Create a class in Persistence with the basic configuration PersonConfiguration:

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

and plug it into the DbContext:

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

Mapping


The section for which this material was written.

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

Value conversions


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

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

var ageConverter = new ValueConverter(
    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. Unable to convert null;
  2. It is not possible to convert a single property into multiple 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 over a given age:

public async Task> 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 for age, but EF Core will not be able to convert it into an SQL query and, reaching Where (), it will load the entire table into the application memory and, only then, using LINQ, it will fulfill the condition p.Age.Value> age.Value .

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

Owned types


Owned Types appeared in Entity Framework Core 2.0 and replaced Complex Types from the regular 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 disadvantages of Value Conversions, namely points 2 and 3.

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

What is needed 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 an SQL query.
Add sorting by LastName and FirstName when loading the list:


public async Task> 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 an SQL query and sorting is performed on the SQL server side, and not in the application.

Of course, there are also disadvantages.

  1. Problems with null have not gone away;
  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 (like a shadow property, i.e. it does not appear in the domain class);
    • EF Core tracks all changes in Owned Types, exactly the same as for regular Entity.

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

On the other hand, these are implementation details that can be omitted, but again, do 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 selection of large lists of “heavy” Entity (many VO-properties), the performance drawdown will be very noticeable precisely because of tracking.

Presentation


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

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 Action to get the Person list:

[HttpGet]
public async Task 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";
}

Persons

@foreach (var p in Model.Persons) { }
Last nameFirst nameAge
@p.LastName@p.FirstName@p.Age


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

Person and sent it to the corresponding View.

Result


Much more interesting is the addition of Person:

[HttpPost]
public async Task 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";
}

Add Person

@section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }


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, an ArgumentException will be thrown (remember Guard Clause in the VO constructors). With verification, it is much easier to send a message to the user 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:

  • Attribute capabilities may not be enough for validation logic;
  • Any business logic, including rules for validating parameters, is set exclusively by the domain. He has a monopoly on this and all other layers must reckon with this. Attributes can be used, but you should not rely on them. If the attribute skips incorrect data, then we will again get an exception when creating a VO.

Back to AddPerson (). After data validation, 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 is not invoked 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 — some of the data is saved and some are not. We receive the domain in the "broken" state. If Commit is done at the very end, then if the error occurs, the transaction will simply roll 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 apply the elements of Domain Driven Design in practice.

The complete source code for the PersonsDemo project is GitHub.

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

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

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

The official documentation for Owned Types and Value Conversions was also used .

Also popular now: