From ee680a084e5669d37c4ab4ef2d6b0ac3c73745ae Mon Sep 17 00:00:00 2001 From: Halil ibrahim Kalkan Date: Mon, 18 Feb 2019 15:44:36 +0300 Subject: [PATCH] Added Product Management section. --- docs/en/Samples/Microservice-Demo.md | 238 +++++++++++++++++- ...vice-sample-product-module-in-solution.png | Bin 0 -> 12817 bytes 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 docs/en/images/microservice-sample-product-module-in-solution.png diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index 810d8ea207..7cca79c4fc 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -1055,9 +1055,245 @@ Swagger UI is configured and is the default page for this service. If you naviga ## Modules +ABP provides a strong infrastructure to make modular application development easier by providing services and architecture (see the [module development best practices guide](../Best-Practices/Index.md)). + +This solution demonstrate how to use [prebuilt application modules](../Modules/Index.md) in a distributed architecture. The solution also includes a simple "Product Management" module to show the implementation of a well layered module example. + ### Product Management -TODO +Product Management is a module that consists of several layers and packages/projects: + +![microservice-sample-product-module-in-solution](../images/microservice-sample-product-module-in-solution.png) + +* `ProductManagement.Domain.Shared` contains constants and types shared among all layers. +* `ProductManagement.Domain` contains the domain logic and defines entities, domain services, domain events, business/domain exceptions. +* `ProductManagement.Application.Contracts` contains application service interfaces and DTOs. +* `ProductManagement.Application` contains the implementation of application services. +* `ProductManagement.EntityFrameworkCore` contains DbConext and other EF Core related classes and configuration. +* `ProductManagement.HttpApi` contains API Controllers. +* `ProductManagement.HttpApi.Client` contains C# proxies to directly use the HTTP API remotely. Uses [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework. +* `ProductManagement.Web` contains the UI elements (pages, scripts, styles... etc). + +By the help of this layering, it is possible to use the same module as a package reference in a monolithic application or use as a service that runs in another server. It is possible to separate UI (Web) and API layers, so they run in different servers. + +In this solution, Web layer runs in the Backend Admin Application while API layer is hosted by the Product microservice. + +This tutorial will highlight some important aspects of the module. But, it's suggested to see the source code for a better understanding. + +#### Domain Layer + +`Product` is the main [Aggregate Root](../Entities.md) of this module: + +````csharp +public class Product : AuditedAggregateRoot +{ + /// + /// A unique value for this product. + /// ProductManager ensures the uniqueness of it. + /// It can not be changed after creation of the product. + /// + [NotNull] + public string Code { get; private set; } + + [NotNull] + public string Name { get; private set; } + + public float Price { get; private set; } + + public int StockCount { get; private set; } + + //... +} +```` + +All of its properties have private setters which prevents any direct change of the properties from out of the class. Product class ensures its own integrity and validity by its own constructors and methods. + +It has two constructors: + +````csharp +private Product() +{ + //Default constructor is needed for ORMs. +} + +internal Product( + Guid id, + [NotNull] string code, + [NotNull] string name, + float price = 0.0f, + int stockCount = 0) +{ + Check.NotNullOrWhiteSpace(code, nameof(code)); + + if (code.Length >= ProductConsts.MaxCodeLength) + { + throw new ArgumentException( + $"Product code can not be longer than {ProductConsts.MaxCodeLength}" + ); + } + + Id = id; + Code = code; + SetName(Check.NotNullOrWhiteSpace(name, nameof(name))); + SetPrice(price); + SetStockCountInternal(stockCount, triggerEvent: false); +} + +```` + +Default (**parameterless**) constructor is private and is not used in the application code. It is needed because most ORMs requires a parameterless constructor on deserializing entities while getting from the database. + +Second constructor is **internal** that means it can only be used inside the domain layer. This enforces to use the `ProductManager` while creating a new `Product`. Because, `ProductManager` should implement a business rule on a new product creation. This constructor only requires the minimal required arguments to create a new product with some optional arguments. It checks some simple business rules to ensure that the entity is created as a valid product. + +Rest of the class has methods to manipulate properties of the entity. Example: + +````csharp +public Product SetPrice(float price) +{ + if (price < 0.0f) + { + throw new ArgumentException($"{nameof(price)} can not be less than 0.0!"); + } + + Price = price; + return this; +} + +```` + +`SetPrice` method is used to change the price of the product in a safe manner (by checking a validation rule). + +`SetStockCount` is another method that is used to change stock count of a product: + +````csharp +public Product SetStockCount(int stockCount) +{ + return SetStockCountInternal(stockCount); +} + +private Product SetStockCountInternal(int stockCount, bool triggerEvent = true) +{ + if (StockCount < 0) + { + throw new ArgumentException($"{nameof(stockCount)} can not be less than 0!"); + } + + if (StockCount == stockCount) + { + return this; + } + + if (triggerEvent) + { + AddDistributedEvent(new ProductStockCountChangedEto(StockCount, stockCount)); + } + + StockCount = stockCount; + return this; +} + +```` + +This method also triggers a **distributed event** with the `ProductStockCountChangedEto` parameter (Eto is a conventional postfix stands for **E**vent **T**ransfer **O**bject, but not required) to notify listeners that stock count of a product has changed. Any subscriber can receive this event and perform an action based on that knowledge. + +Events are distributed by RabbitMQ for this solution. But ABP is message broker independent by providing necessary abstractions (see the [Event Bus](../Event-Bus.md) document). + +As said before, this module forces to always use the `ProductManager` to create a new `Product`. `ProductManager` is a simple domain service defined as shown: + +````csharp +public class ProductManager : DomainService +{ + private readonly IRepository _productRepository; + + public ProductManager(IRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task CreateAsync( + [NotNull] string code, + [NotNull] string name, + float price = 0.0f, + int stockCount = 0) + { + var existingProduct = + await _productRepository.FirstOrDefaultAsync(p => p.Code == code); + + if (existingProduct != null) + { + throw new ProductCodeAlreadyExistsException(code); + } + + return await _productRepository.InsertAsync( + new Product( + GuidGenerator.Create(), + code, + name, + price, + stockCount + ) + ); + } +} +```` + +* It checks if given code is used before. Throws `ProductCodeAlreadyExistsException` so. +* If uses the `GuidGenerator` (`IGuidGenerator`) service to create a new `Guid`. +* It inserts the entity to the repository. + +So, with this design, uniqueness of the product code is guaranteed. + +`ProductCodeAlreadyExistsException` is a domain/business exception defined as like below: + +````csharp +public class ProductCodeAlreadyExistsException : BusinessException +{ + public ProductCodeAlreadyExistsException(string productCode) + : base("PM:000001", $"A product with code {productCode} has already exists!") + { + + } +} +```` + +`PM:000001` is a code for the exception type that is sent to the clients, so they can understand the error type. Not implemented for this case, but it is also possible to localize business exceptions. See the [exception handling documentation](../Exception-Handling.md). + +#### Application Layer + +Application layer of this module has two services: + +* `ProductAppService` is mainly used by the Backend Admin Application to manage (create, update, delete...) products. It requires permission to perform any operation. +* `PublicProductAppService` is used by the Public Web Site to show list of products to the visitors. It does not require any permission since most of the visitors are not logged in to the application. + +Notice that; instead of putting two application service into the same project, it might be a better principle to have separated application layers per application. But we unified them for simplicity in this solution. + +As an example, `ProductAppService` has the following method to update a product: + +````csharp +[Authorize(ProductManagementPermissions.Products.Update)] +public async Task UpdateAsync(Guid id, UpdateProductDto input) +{ + var product = await _productRepository.GetAsync(id); + + product.SetName(input.Name); + product.SetPrice(input.Price); + product.SetStockCount(input.StockCount); + + return ObjectMapper.Map(product); +} +```` + +* It defines the required permission (*ProductManagementPermissions.Products.Update* is a constant with value `ProductManagement.Update`) to perform this operation. +* Gets the id of the product and a DTO contains the values to update. +* Gets the related product entity from the repository. +* Uses the related methods (like `SetName`) of the `Product` class to change properties, because they are with private setters and the only way to change a value is to use an entity method. +* Returns an updated `ProductDto` to the client (client may need it for some reason) by using the [ObjectMapper](../Object-To-Object-Mapping.md). + +The implementation may vary based on the requirements. This implementation follows the [best practices offered here](../Best-Practices/Application-Services.md). + +#### Other Layers + +See other layers from the source code. ## Infrastructure diff --git a/docs/en/images/microservice-sample-product-module-in-solution.png b/docs/en/images/microservice-sample-product-module-in-solution.png new file mode 100644 index 0000000000000000000000000000000000000000..27e841cdd569dfc7833650dbc9e0511fb80cef03 GIT binary patch literal 12817 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