java – DDD, Aggregate root without ORM, how to save?

Question:

public class Order
{
   List<OrderItem> Items {get; private set;}

   public AddItem(OrderItem item)
   {
       //логика добавления

       items.Add(item);
   }
}

Following the DDD methodology, all domain logic is located within the domain and is not carried out into separate services. The question is how to save changes to the aggregation root without using ORM , or using microOrm – Dapper. How are you doing?

Answer:

In accordance with the principles of DDD, storage of entities is in charge of repositories, they are also repositories that allow you to read and write aggregates . In this case, the order repository must be able to read and write the order aggregate, which includes the order items .

Since the way of storing entities can change with a high degree of probability, we declare in the domain not the implementation of the repository, but only the interface:

public interface IOrderRepository
{
    Order Create();

    Order ReadById(int id);

    void Update(Order order);

    void DeleteById(ind id);
}

The implementation of this interface will reside in the infrastructure layer of the application (Eric Evans term). Not in DDD, this layer is called the data access layer . In accordance with the principle of data hiding, the internal data of the Order entity cannot be reached from the infrastructure layer.

Example: the Order entity has a CreatedAt property, that is, the creation date . According to the rules of the subject area, this property is read-only , that is, it has a getter method, but does not have a setter method. When the order repository loads data from the database, it must set the values ​​of all properties, including CreatedAt . But it cannot do this because it cannot change the value of a read-only property.

A similar problem is described in GoF, and there for its solutions are used Memento pattern (Guardian). It allows you to save the state of the object and restore it later. In the classical scheme, the state is stored in a form that is not available for analysis and understanding, but we need something different.

If we are using a DBMS, we want the state to be in a form that is convenient to store in the DBMS. To do this, we introduce DTOs for our entities. These objects are also on the domain layer because they are part of its interface to the infrastructure layer.

public class OrderDto
{
    public int Id { get; set; }

    public DateTime CreatedAt { get; set; }

    public OrderItemDto[] Items { get; set; }
}

public class OrderItemDto
{
    public int Id { get; set; }

    public int ProductId { get; set; }

    public decimal Count { get; set; }

    public decimal Amount { get; set; }
}

public class Order
{
     private readonly OrderDto dto;
     private readonly List<OrderItem> items;

     public int Id { get { return dto.Id; } }

     public DateTime CreatedAt { get { return dto.CreatedAt; } }

     public IReadOnlyCollection<OrderItem> Items { . . . }

     internal Order(OrderDto dto)
     {
         this.dto = dto;

         items = new List<OrderItem>();
         foreach (var orderItemDto in dto.Items)
             items.Add(new OrderItem(orderItemDto));
     }

     public void AddItem(Product product, decimal count)
     {
         var itemDto = new OrderItemDto
         {
             Id = 0,
             OrderId = this.Id,
             ProductId = product.Id,
             Count = count,
             Amount = product.Price * count
         };

         dto.Items.Add(itemDto);

         var item = new OrderItem(itemDto);
         items.Add(item);
     }
}

public class OrderItem
{
    internal OrderItem(OrderItemDto dto)
    {
        . . .
    }

    . . .
}

As such, the repository loads data from the database, transforms it into a DTO object, and then creates a domain object from the DTO object. Or it gets the entity of the domain, transforms it into a DTO object, and saves it.

public class AdoOrderRepository : IOrderRepository
{
    private readonly SqlConnection connection;

    public AdoOrderRepository(SqlConnection connection)
    {
        this.connection = connection;
    }

    public Order ReadById(int id)
    {
        using (var command = connection.CreateCommand())
        {
            // загружаем данные заказа включая агрегированные позиции
            // в объект OrderDto
            OrderDto orderDto = . . .;

            // волшебным образом преобразуем OrderDto в Order
            Order order = . . .;

            return order;
        }
    }

    public void Update(Order order)
    {
        using (var command = connection.CreateCommand())
        {
            // волшебным образом преобразуем Order в OrderDto
            OrderDto orderDto = . . .;

            // обновляем данные из OrderDto
            . . .
        }
    }
}

OrderItem fix it again: the domain layer describes domain entities ( Order , OrderItem ), separately it describes the repository interfaces ( IOrderRepository ) that need to be implemented in the infrastructure layer ( AdoOrderRepository ), and, finally, it describes DTO objects ( OrderDto , OrderItemDto ) that contain all the fields that the ( Order ) entity wants to store for a long time. Entities ( Order ) contain logic and hide the implementation, DTO objects contain only data and no logic.

Repository implementations know how to work with DTO objects, in particular, they can send a read request to the database and write the results to a DTO object, since a DTO object is very simple. But how do they convert a DTO object to an entity, and vice versa? In the example code I provided above, I wrote that this happens magically .

It's time to figure out exactly how. Domain objects themselves, such as Order , could save and restore their state. But they have the main function – these are Domain Orders. Adding a second function would violate the single responsibility principle. We need a separate class that needs to have access to the state of the Order entity. But one class does not need to know the implementation details of the other class.

Except when this class is nested.

public class Order
{
    private readonly OrderDto dto;

    private Order(OrderDto dto)
    {
        this.dto = dto;
    }

    . . .

    public static class DtoMapper
    {
        public static void Map(Order order, OrderDto orderDto)
        {
            . . .
        }

        public static void Map(OrderDto orderDto, Order order)
        {
            . . .
        }
    }
}

The inner class is described as public, so repository implementations at the infrastructure level can access it.

With this approach, it doesn't matter how the repositories are implemented: through a large ORM like Entity Framework, through a simple microORM Dapper, or through ADO.

You need to be careful with aggregated objects such as the OrderItem / OrderItemDto . Inside we have an OrderItemDto collection, outside OrderItem , and they must match. This task is not very difficult.

Finally, I would like to illustrate the difference between domain objects and DTOs, because sometimes it seems that there is so much in common between them that DTOs could be abandoned. Sometimes there is really a lot in common, but sometimes not.

If we have a user entity that has a password, then in the domain it looks like this:

public class User
{
    public int Id { get; }

    public DateTime CreatedAt { get; }

    public string Login { get; }

    bool ValidatePassword(string password);

    void ChangePassword(string oldPassword, string newPassword);
}

The DTO object looks radically different for it.

public class UserDto
{
    public int Id { get; set; }

    public DateTime CreatedAt { get; set; }

    public string Login { get; set; }

    public byte[] PasswordHash { get; set; }
}

As you can see, the DTO object provides access to raw data, in this case to the password hash. The domain object hides the hash not only for writing, but also for reading to avoid possible security problems. It provides the ValidatePassword and ChangePassword methods that tell us the scenarios for using the User object.

That is why DTO objects and domain entities, despite some duplication of fields, belong to different levels and have different purposes.

There are some tips for how to design DTO objects. Ideally, they should be made so that they can be immediately used in Entity Framework, NHibernate, or Dapper. This means that you can use foreign keys , navigation properties , and attributes like [Key] , [TableName] from the System.Components.Annotations namespace. This is optional, but will make it easier to implement the repositories. On the other hand, it is best not to target ORM-specific attributes and solutions such as [Index] . This leaves DTOs unbound to specific ORMs and can quickly move from EF / SQL to MongoDB or Redis.

Finally, after an endless theory, I will give a concrete answer to the question – how exactly to save changes to the aggregation root without using an ORM. Fill method AdoOrderRepository.Update :

public void Update(Order order)
{
    var orderDto = new OrderDto();
    Order.DtoMapper.Map(order, orderDto);

    using (var transaction = connection.BeginTransaction())
    {
        var orderDto = new OrderDto();
        Order.DtoMapper.Map(order, orderDto);

        var command = connection.CreateCommand();
        command.CommandText = "UPDATE [Orders] SET CreatedAt = @CreatedAt WHERE Id = @Id";
        // Поле CreatedAt только для чтения, так что я просто
        // иллюстрирую идею.
        command.Parameters.Add("@CreatedAt", orderDto.CreatedAt);
        command.Parameters.Add("@Id", orderDto.Id);

        command.ExecuteNonQuery();

        command.CommandText = "UPDATE [OrderItems] SET Count = @Count, Amount = @Amount WHERE Id = @Id";
        foreach (var item in dto.Items.Where(x => x.Id != 0))
        {
            command.Parameters.Clear();
            command.Parameters.Add("@Count", item.Count);
            command.Parameters.Add("@Amount", item.Amount);
            command.Parameters.Add("@Id", item.Id);

            command.ExecuteNonQuery();
        }

        command.CommandText = "INSERT [OrderItems] (OrderId, ProductId, Count, Amount) VALUES (@OrderId, @ProductId, @Count, @Amount)";
        foreach (var item in dto.Items.Where(x => x.Id == 0))
        {
            command.Parameters.Clear();
            command.Parameters.Add("@OrderId", item.OrderId);
            command.Parameters.Add("@ProductId", item.ProductId);
            command.Parameters.Add("@Count", item.Count);
            command.Parameters.Add("@Amount", item.Amount);

            item.Id = (int)command.ExecuteScalar();
        }

        transaction.Commit();
    }

    Order.DtoMapper.Map(orderDto, order);
}

In accordance with DDD, we must save not only the root of the aggregate, but also all aggregated entities, which we do in this method. We create a transaction to ensure consistency of changes. The code turned out to be cumbersome, but simple. To get rid of the bulkiness, you can just use Dapper.

Scroll to Top