-
Notifications
You must be signed in to change notification settings - Fork 68
Linear design approach
Peasy was designed to free ourselves from complications we often face with domain-driven designs, and favors a more linear approach to design and development.
What do we mean by linear approach? Basically we mean keeping 1-1 mappings between DTOs and data store entities (database tables, documents, etc.) because doing so greatly simplifies insert, update, and delete logic within the peasy framework by keeping your intentions clear. This can be attributed to the fact that during one of these operations, you don't have to inspect a DTO for the existence of children (the case for inserts and deletes) or the existence of dirty children (the case for updates).
To illustrate this, suppose your database contains tables to represent customers, orders, and order items. Assume that we have DTOs to represent these tables and their relations to each other by defining a Customer DTO that exposes a list of Order DTOs, and each Order DTO exposing a list of OrderItem DTOs.
Here's a code sample:
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int ID { get; set; }
public int CustomerID { get; set; }
public decimal Amount { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderItem> OrderItems { get; set; }
}
public class OrderItem
{
public int ID { get; set; }
public int OrderID { get; set; }
public decimal Amount { get; set; }
public int StatusID { get; set; }
}
Now let's suppose you have created a customer service, exposing an UpdateCommand method that accepts an instance of a Customer DTO as an argument. To update it, you are now responsible for looping through each Order DTO in the Customer.Orders list, checking for new orders (which would call for an insert operation), checking to see that orders haven't been removed (which would call for a delete operation), or checking to see that data has changed (which would call for an update operation). This same logic then would have to be applied to OrderItem DTO instances stored in each Order DTO.
While you could just ignore children during an update operation against the customers service, you are still left with a customer DTO with dangling children properties that have little to no context within an update operation, which smells.
You could also create different Customer DTOs that provide different properties and expose children of different nesting levels. However, you would then be left with many different Customer DTO implementations that leave a developer unsure when to use which one, etc.
An exception to all of this would be an implementation of CQRS, where nested DTOs would be acceptable as you would have specific handlers executing specific commands that accept specific DTOs as arguments. While the peasy framework could easily support a CQRS implementation, we are really arguing from a more traditional CRUD style type of application.
When you keep your DTO designs linear, your intentions about its usage are always clear. Doing so will also help to keep your service class methods simple, as they'll focus on doing one thing. Therefore, it is recommended that DTOs map directly to a data store entity (database table, document, etc.).
While a DTO can map across multiple data store entities providing children collections, it is encouraged to keep a 1-1 mapping between DTO and data store entity (database table, document, etc.).
When needing to support an insert, update, or delete scenario involving data from different data store entities, it is a peasy best practice to orchestrate this logic in a custom command implementation. Let's describe a scenario for what needs to happen when updating an order.
When updating an order, it is required that an order amount is set to the sum of the amount of all of its associated order items. Further, let's assume that after setting the amount that the order and its associated order items need to be updated in a data store, succeeding or failing as a whole (atomically). How do you orchestrate this within peasy?
Let's take a look at an example.
Keeping true to the linear design approach, here are the DTOs without children properties:
public class Order : IDomainObject<int>
{
public int ID { get; set; }
public int CustomerID { get; set; }
public decimal Amount { get; set; }
public DateTime OrderDate { get; set; }
}
public class OrderItem : IDomainObject<int>
{
public int ID { get; set; }
public int OrderID { get; set; }
public decimal Amount { get; set; }
public int StatusID { get; set; }
}
Here is a custom command that orchestrates updating the order amount and saving changes to the order and associated order items:
public class UpdateOrderCommand : CommandBase
{
private OrderItemService _itemsService;
private Order _order;
private IEnumerable<OrderItem> _orderItems;
private OrderService _orderService;
public UpdateOrderCommand(Order order,
IEnumerable<OrderItem> orderItems,
OrderService orderService,
OrderItemService itemsService)
{
_order = order;
_orderItems = orderItems;
_orderService = orderService;
_itemsService = itemsService;
}
protected override async Task OnExecuteAsync()
{
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
_order.Amount = _orderItems.Sum(i => i.Amount);
await _orderService.UpdateCommand(_order).ExecuteAsync();
foreach (var orderItem in _orderItems)
{
await _itemsService.UpdateCommand(orderItem).ExecuteAsync();
}
scope.Complete();
}
}
}
The UpdateOrderCommand orchestrates the updating of the order amount field, and saving changes to the order as well as its associated order items. To ensure that the orchestration occurs atomically, we execute the logic within a transaction scope.
By creating the UpdateOrderCommand, the intention of what must occur when an order is updated becomes clear. The command reads: when an order is updated, it's amount should be set to the sum of the amounts of all associated order items. Further, it is required that it's associated order items are updated along with it, and in the event that any update fails, all of the updates are rolled back. This type of explicitness was one of the major design goals of peasy.
Now that the UpdateOrderCommand is defined, we can expose it via a custom service class method, or consume it directly via dependency injection or creating an instance of it.