Recently, I began to get acquainted with the concept of DDD, and the question arose of how to act correctly in CREATE and UPDATE operations when data comes through the Web API. For example, on some form on a web page, we fill in the fields, click "Save." A POST request is sent to the address / api / customers with the form data:

{ "Id": "93967a3e-384f-459a-8b50-0a0f4cc66d66", "firstName": "Иван", "lastName": "Череззаборногузадерищенко", "zipCode": 245876, "city": "Самара", "street": "Николая Панова", "houseNumber": 64, "appartmentsNumber": 62, } 

On the Web API side, it is deserialized into a DTO class:

 public class CustomerDTO { public Guid Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int ZipCode { get; set; } public string City { get; set; } public string Street { get; set; } public int HouseNumber { get; set; } public int AppartmentsNumber { get; set; } public Guid CustomerStateId { get; set; } } 

We have the following domain model (done very roughly to demonstrate an example):

 public class Customer: Entity, IAggreagtionRoot { public Customer(Name name) { Name = name; } public Name Name { get; private set; } public Address Address { get; private set; } public CustomerState State { get; private set; } public void ChangeState(CustomerState newState) { //some logic... } public void ChangeAddress(Address newAddress) { //some logic... Address = newAddress; } public void ChangeName(Name newName) { //some logic... Name = newName; } } public class CustomerState: Entity { public static readonly CustomerState Regular = new CustomerState(new Guid("7beb8006-1b70-4d47-bb95-1976a2c18e9a"), "Regular"); public static readonly CustomerState VIP = new CustomerState(new Guid("c27e9e0c-a2dc-4093-80f5-e75b66997746"), "VIP"); public CustomerState(Guid id, string name) { Id = id; Name = name; } public string Name { get; private set; } } public class Address: ValueType { public Address(int zipCode, string city, string street, int houseNumber, int appartmentsNumber) { ZipCode = zipCode; City = city; Street = street; HouseNumber = houseNumber; AppartmentsNumber = appartmentsNumber; } public int ZipCode { get; private set; } public string City { get; private set; } public string Street { get; private set; } public int HouseNumber { get; private set; } public int AppartmentsNumber { get; private set; } } public class Name: ValueType { public Name(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; private set; } public string LastName { get; private set; } } 

Question 1. How to change the Customer object from the DTO object? The following comes to mind:

 public class CustomerService { private CustomerRepository _customerRepository; public CustomerService(CustomerRepository customerRepository) { _customerRepository = customerRepository; } public void UpdateCustomer(CustomerDTO customerDto) { Guid customerId = customerDto.Id; Customer customer = _customerRepository.GetById(customerId); //Как мне дейстовать здесь? customer.ChangeName(new Name(customerDto.FirstName, customerDto.LastName)); customer.ChangeAddress(new Address(customerDto.ZipCode, customerDto.City, customerDto.Street, customerDto.HouseNumber, customerDto.AppartmentsNumber)); if(customerDto.CustomerStateId == CustomerState.VIP.Id) customer.ChangeState(CustomerState.VIP); if (customerDto.CustomerStateId == CustomerState.Regular.Id) customer.ChangeState(CustomerState.Regular); //Что-то вроде этого? //Может быть согласовать дизайн UI с дизайном доменной модели, и не позволять //изменять статус с помощью полей формы, и изменять его отдельными действиями (например, специальными кнопками, //которые инициируют POST-запрос www.site.ru/api/customers/2432352/PromoteToVip ? _customerRepository.Update(customer); } } 

Question 2. What should I do if I want to send only the changes, and not the entire object? For example, only the house number has changed, and in order to minimize the data being sent, I want to send such a request:

 { "houseNumber": 164 } 

How in this case should I change the Value-type Address if its constructor requires all the parameters? Maybe something like this ?:

 Customer customer = _customerRepository.GetById(customerId); var zipCode = customerDto.ZipCode == 0 ? customer.Address.ZipCode : customerDto.ZipCode; var street = string.IsNullOrEmpty(customerDto.Street) ? customer.Address.Street : customerDto.Street; var city = string.IsNullOrEmpty(customerDto.City) ? customer.Address.City : customerDto.City; var appartmentsNumber = customerDto.AppartmentsNumber == 0 ? customer.Address.AppartmentsNumber : customerDto.AppartmentsNumber; var houseNumber = customerDto.HouseNumber == 0 ? customer.Address.HouseNumber : customerDto.HouseNumber; customer.ChangeAddress(new Address(zipCode, city, street, houseNumber, appartmentsNumber)); 

Question 3. What if an object has many properties, say 40. For example, 20 of them do not affect the consistency of the model (ie, purely informational, and you do not need to monitor their change). To do for each method of setting values ​​(which I feel sad), or just open them for editing (to make the setter public) to the external code?

PS I think that the public void ChangeState (CustomerState newState) method should be removed according to the canons of DDD, and instead introduce methods like PromoteToVip () and DowngradeToRegular (), but let it be as it is in this example.

    1 answer 1

    I will answer in order from simple to complex.

    Question number 2 : how to organize sending only changes, and not the entire object as a whole. REST has a well-established practice about it. Operation update detail to replace and modify .

    First, replace allows you to change the entire object. It corresponds to the HTTP method PUT . The second, modify , allows you to change individual properties. It corresponds to the HTTP method PATCH .

    To the question of how to change the type of Address , whose constructor requires all the parameters, I will answer: follow the subject area. Ask the experts what the address is for them and whether it is possible to change its components independently of each other.

    It may be that the address is broken down into components only for convenience of input, but in fact, inside the domain, the address is always needed entirely. Then you can explicitly specify this feature to REST clients by listing the address in a separate structure:

    { "Id": "93967a3e-384f-459a-8b50-0a0f4cc66d66", "firstName": "Иван", "lastName": "Череззаборногузадерищенко", "address": { "zipCode": 245876, "city": "Самара", "street": "Николая Панова", "houseNumber": 64, "appartmentsNumber": 62, }, }

    By marking the fields of the Address object as mandatory, you actually tell the client the following: the address can either be transferred entirely or not transmitted at all (if we are talking about the modify operation).

    A short comment on the implementation. As far as I can see, you have C #, and, therefore, with a high degree of probability, the library NewtonJson. You can not get separate DTO-classes for value objects, just mark the Address(string zip, string city, string house, string apartment) constructor Address(string zip, string city, string house, string apartment) JsonConstructor attribute. NewtownJson will be able to assemble an object from JSON using such a constructor.

    Question number 3 : the method of setting the value or setter? The opposition here is contrived: the setter is syntactic sugar for the installation method, which allows you to make the code cleaner.

    If there are properties that need to be set together, maybe these are value objects?

     customer.Name = new Name(customerDto.FirstName, customerDto.LastName); customer.Address = new Address(customerDto.ZipCode, customerDto.City, customerDto.Street, customerDto.HouseNumber, customerDto.AppartmentsNumber); 

    You can use methods in a scenario where a client has to constantly change several properties, and all the time they are different. For example, if you had an order object, which would have several statuses , on which the object could pass:

     order.State = OrderState.Applied; order.LastModified = timeProvider.Now; order.Applier = httpContext.CurrentUser; 

    Such constructions should be replaced with one method:

     order.Apply(httpContext.CurrentUser); 

    This is exactly the essence of encapsulation : the client cannot destroy the state of the object from the outside by changing only a part of the required properties. Compare with the name and address above.

    Question number 1 : change the essence of the DTO-model. In your code, everything is quite correct. The design of the UI must of course be coordinated with the domain model, because both must correspond to the user's ideas. On the contrary, if you have too difficult to translate UI objects in essence, it is a bell that somewhere you have moved away from DDD.

    Use language tools to express your thoughts. In particular, such a complex code:

     if(customerDto.CustomerStateId == CustomerState.VIP.Id) customer.ChangeState(CustomerState.VIP); if (customerDto.CustomerStateId == CustomerState.Regular.Id) customer.ChangeState(CustomerState.Regular); 

    You can replace the simple:

     customer.State = (CustomerState)customerDto.CustomerStateId; 

    C # understands perfectly well that each enum value contains an integer.

    • CustomerState is not enum, but : Entity and an int may not be trivial to bring to it - Grundy
    • @Grundy, yes, I saw, thanks. For now, I'll leave the answer the same, because in my opinion, CustomState should not be an entity. It seems that data on the level of the subject area from the presentation level leaked that Id and Name , but they are not at the level of the subject area. - Mark Shevchenko