public class Order { List<OrderItem> Items {get; private set;} public AddItem(OrderItem item) { //логика добавления items.Add(item); } } 

Following the DDD methodology, all domain logic resides within the domain and is not made into separate services. The question is how to save the changes of the aggregation root without using the ORM , well, or using microOrm - Dapper. How are you doing?

  • 2
    Too many clever words - by asking in this language, you will receive a response for a long time. Well, the question is - why не используя ORM - why didn’t you please? What is microOrm - such a technology has not been heard, and how does it differ radically from ORM, that you really don't want to use ORM? - Goncharov Alexander
  • one
    Look towards CQRS (division of responsibility into commands and requests). When individual commands will be responsible for changing the data, it doesn’t matter what you use for DAL, since the command implementation is actually hidden. In the above case for saving the Order, you can write the implementation of the Save command as you see fit, without being tied to the ORM. - Primus Singularis
  • @PrimusSingularis, it is clear that for example the AddItemToOrderCommand command will be, inside we will get the Order, create an Item by putting it in the Order. That's the question, how after all the manipulations save the Order with all the changes. It is clear that you can simply put the item purely sql insert. How to do it, how do fathers do (by Feng Shui) best practics - coder
  • @coder If you make the AddItemToOrderCommand command, then it will save the Order, or make the generic SaveCommand <> command, which will be engaged in saving - Primus Singularis

1 answer 1

In accordance with the principles of DDD, the storage of entities is managed by the 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 down an order that includes order items .

Since the method of storing entities can change with high 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 be located 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 entity Order cannot be accessed from the infrastructure layer.

Example: the Order entity has the property CreatedAt , 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 no 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 the Memento ( Keeper ) pattern is used to solve it. 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 inaccessible for analysis and understanding, but we need something else.

If we use a DBMS, we want the state to be in a form that is convenient to store in the DBMS. To do this, we introduce DTO objects for our entities. These objects are also located in the domain layer, since they are part of its interface for 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) { . . . } . . . } 

In this form, the repository loads data from the database, converts it into a DTO object, and then creates a domain object from the DTO object. Or he gets the essence of the domain, converts 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 . . . } } } 

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

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

It's time to figure out exactly how. The domain objects themselves, such as Order , could save and restore their state. But they have a main function — they are Domain Orders. Adding a second function would violate the sole responsibility principle. We need a separate class that must have access to the state of the Order entity. But one class should not know the details of the implementation of another 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 the implementation of the repositories at the infrastructure level can access it.

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

OrderItem must be taken with aggregated objects, such as the OrderItem / OrderItemDto . Inside we have the OrderItemDto collection, outside the OrderItem , and they should match. This task is not very difficult.

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

If we have an entity user who has a password, then, in the subject area, 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 for it looks radically different.

 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 allows access to raw data, in this case, the password hash. The domain object hides the hash, not only for writing but also for reading, in order to avoid possible security problems. It provides the ValidatePassword and ChangePassword methods, which prompt us with scripts for using the User object.

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

There are some tips on how to design DTO objects. Ideally, they should be made such that they can be immediately used in the 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 not necessary, but it will simplify the implementation of repositories. On the other hand, it is better not to focus on ORM-specific attributes and solutions, such as [Index] . This way you leave DTO objects unbound to specific ORMs and can quickly go from EF / SQL to MongoDB or Redis.

Finally, after the infinite theory, I will give a specific answer to the question of how exactly to save the changes of the aggregation root without using the ORM. I will add 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 are doing in this method. Create a transaction to ensure consistency of changes. The code is bulky, but simple. To get rid of bulkiness, just you can use Dapper.

  • Thanks for such a detailed answer, I did not quite understand where a new ItemOrder is added. The idea with dto is clear, but I didn’t quite understand about adding a new item to the database in an existing order. - coder
  • Implementations may be different, I give one of the possible, it is close to how we write the code in my project. A new ItemOrder added in the Order.AddItem method, I added a little to illustrate what is happening. I also added the AdoOrderRepository.Update method to show that new product lines are inserted using INSERT , not UPDATE . - Mark Shevchenko
  • Mark thanks again, a very intelligible answer, this question is still interesting in your implementation, it turns out you are running through the entire item collection and updating even those that have not changed, is it too expensive? or is it just for example and use another way? - coder
  • Yes, of course, this is only an illustration. In the working code we have EF, which we doped a bit with files in the slowest places. When writing to the database, only modified properties are updated. On the other hand, it depends on the subject area. In some cases, database updates are not so massive, and you can make the code easier by simply updating everything. - Mark Shevchenko
  • Do I need to store a link to OrderDto in Order ? It seems to be enough to restore your condition and "throw away" dto. - andreycha