It is well suited for rich models, but we can also do this for anemic models. With a rich domain model, you delegate the job of reconstructing the model to the ORM and immediately start calling its methods.The ORM also recreates the anemic model, but those classes just contain data, so you need to call other pieces of the software that contain the logic to manipulate those objects.In the code sample, the data abstraction layer now contains only the data access abstractions, such as the repositories, and it references the new Model project that is now the persisted model.Conceptually, it cleans up a few things:
- The data abstraction layer’s only responsibility is to contain data access abstractions.
- The domain layer’s only responsibility is implementing the domain services and the logic that is not part of that rich model.
- In the case of an anemic model, the domain layer’s responsibility would be to encapsulate all the domain logic.
- The Model project contains the entities.
Once again, I skip publishing most of the code here as it is irrelevant to the overall concept. If you think reading the code would help, you can consult and explore the sample on GitHub (https://adpg.link/9F5C). Using an IDE to browse the code should help you understand the flow, and as with the abstract layer, the dependencies between the projects, classes, and interfaces are the key to this.Nevertheless, here is the StockService class that uses that shared model so you can peek at some code that directly relates to the explanations:
namespace Domain.Services;
public class StockService : IStockService
{
private readonly IProductRepository _repository;
public StockService(IProductRepository repository)
{
_repository = repository ??
throw new ArgumentNullException(nameof(repository));
}
In the preceding code, we are injecting an implementation of the IProductRepository interface we use in the next two methods. Next, we look at the AddStockAsync method:
public async Task<int> AddStockAsync(int productId, int amount, CancellationToken cancellationToken)
{
var product = await _repository.FindByIdAsync(productId, cancellationToken);
if (product == null)
{
throw new ProductNotFoundException(productId);
}
product.AddStock(amount);
await _repository.UpdateAsync(product, cancellationToken);
return product.QuantityInStock;
}
The fun starts in the preceding code, which does the following:
- The repository recreates the product (model) that contains the logic.
- It validates that the product exists.
- It uses that model and calls the AddStock method (encapsulated domain logic).
- It tells the repository to update the product.
- It returns the updated product’s QuantityInStock to the consumer of the service.
Next, we explore the RemoveStockAsync method:
public async Task<int> RemoveStockAsync(int productId, int amount, CancellationToken cancellationToken)
{
var product = await _repository.FindByIdAsync(productId, cancellationToken);
if (product == null)
{
throw new ProductNotFoundException(productId);
}
product.RemoveStock(amount);
await _repository.UpdateAsync(product, cancellationToken);
return product.QuantityInStock;
}
}
We applied the same logic as the AddStock method to the RemoveStock method, but it calls the Product.RemoveStock method instead. From the StockService class, we can see the service gating the access to the domain model (the product), fetching and updating the model through the abstract data layer, manipulating the model by calling its methods, and returning domain data (an int in this case, but could be an object).
This type of design can be either very helpful or undesirable. Too many projects depending on and exposing a shared model can lead to leaking part of that model to consumers, for example exposing properties that shouldn’t be, exposing the whole domain model as output, or the very worst, exposing it as an input and opening exploitable holes and unexpected bugs.
Be careful not to expose your shared model to the presentation layer consumers.
Pushing logic into the model is not always possible or desirable, which is why we are exploring multiple types of domain models and ways to share them. Making a good design is often about options and deciding what option to use for each scenario. There are also tradeoffs to make between flexibility and robustness.The rest of the code is similar to the abstract layer project. Feel free to explore the source code (https://adpg.link/9F5C) and compare it with the other projects. The best way to learn is to practice, so play with the samples, add features, update the current features, remove stuff, or even build your own project. Understanding these concepts will help you apply them to different scenarios, sometimes creating unexpected but efficient constructs.Now, let’s look at the final evolution of layering: Clean Architecture.