@ -23,7 +23,7 @@ First things first! Let's setup your development environment before creating the
## Creating a New Solution
> 🛈 This document uses [ABP Studio](../studio/index.md) to create new ABP solutions. **ABP Studio** is in the beta version now. If you have any issues, you can use the [ABP CLI](../cli/index.md) to create new solutions. You can also use the [getting started page](https://abp.io/get-started) to easily build ABP CLI commands for new project creations.
> 🛈 This document uses [ABP Studio](../studio/index.md) to create new ABP solutions. You can also use the [ABP CLI](../cli/index.md) to create new solutions and use the [getting started page](https://abp.io/get-started) to easily build ABP CLI commands.
> ABP startup solution templates have many options for your specific needs. If you don't understand an option that probably means you don't need it. We selected common defaults for you, so you can leave these options as they are.
@ -31,7 +31,7 @@ Assuming that you have [installed and logged in](../studio/installation.md) to t
Select the *File* -> *New Solution* in the main menu, or click the *New solution* button on the Welcome screen to open the *Create new solution* wizard:
Select the *File* -> *New Solution* in the main menu, or click the *New solution* button on the *Welcome* screen to open the *Create new solution* wizard:
> Currently, ABP Studio is in its beta phase and available for everyone. To access the beta version, kindly visit [this web page](https://abp.io/studio).
This document explains how to install the ABP Studio tool.
## Pre-requirements
@ -10,7 +9,11 @@ Before you begin the installation process for ABP Studio, ensure that your syste
### Node
Make sure [Node.js](https://nodejs.org/en) is installed on your system. If you have not installed Node.js, you can download the `v22+` version from the official [Node.js website](https://nodejs.org/en/download/prebuilt-installer).
### Docker
ABP Studio needs [Docker](https://www.docker.com/) for [Kubernetes](https://kubernetes.io/) operations. Install Docker by following the guidelines on the official [Docker website](https://docs.docker.com/get-docker/).
### WireGuard (Optional)
ABP Studio needs [WireGuard](https://www.wireguard.com/) for Kubernetes operations. You can find the installation instructions for your specific operating system below:
**For Windows:**
@ -19,9 +22,6 @@ Installation instructions for your Windows operating system are on the official
**For macOS:**
Installation instructions for your macOS operating system are on the official [WireGuard website](https://www.wireguard.com/install/#macos-homebrew-and-macports-basic-cli-homebrew-userspace-go-homebrew-tools-macports-userspace-go-macports-tools).
### Docker
ABP Studio needs [Docker](https://www.docker.com/) for [Kubernetes](https://kubernetes.io/) operations. Install Docker by following the guidelines on the official [Docker website](https://docs.docker.com/get-docker/).
## Installation
Now you have met the pre-requirements, follow the steps below to install ABP Studio:
In this first part of this tutorial, we will create a new ABP solution with modularity enabled.
## Getting Started with a new ABP Solution
Follow the *[Get Started](../../get-started/single-layer-web-application.md)* guide to create a single layer web application with the following configuration:
* **Solution name**: `ModularCrm`
* **UI Framework**: ASP.NET Core MVC / Razor Pages
* **Database Provider**: Entity Framework Core
You can select the other options based on your preference.
You can select the other options based on your preference but at the **Modularity** step, check the _Setup as a modular solution_ option and add a new **Standard Module** named `ModularCrm.Catalog`:
Since modularity is a key aspect of the ABP Framework, it provides an option to create a modular system from the beginning. Here, you're creating a `ModularCrm.Catalog` module using the *Standard Module* template.
> **Note:** This tutorial will guide you through creating two modules: `Catalog` and `Ordering`. We've just created the `Catalog` module in the _Modularity_ step. You could also create the `Ordering` module at this stage. However, we'll create the `Ordering` module later in this tutorial to better demonstrate ABP Studio's module management capabilities and to simulate a more realistic development workflow where modules are typically added incrementally as the application evolves.
> **Please complete the [Get Started](../../get-started/single-layer-web-application.md) guide and run the web application before going further.**
## The Solution Structure
The initial solution structure should be like the following in ABP Studio's *[Solution Explorer](../../studio/solution-explorer.md)*:
Initially, you see a `ModularCrm` solution and a `ModularCrm` module under that solution.
Initially, you see a `ModularCrm` solution, a `ModularCrm` module under that solution (our main single layer application), and a `modules` folder that contains the `ModularCrm.Catalog` module and its sub .NET projects.
> An ABP Studio module is typically a .NET solution and an ABP Studio solution is an umbrella concept for multiple .NET Solutions (see the [concepts](../../studio/concepts.md) document for more).
The `ModularCrm` module is the core of your application, built as a single-layer ASP.NET Core Web application. You can expand the `ModularCrm` module to see:
The `ModularCrm` module is the core of your application, built as a single-layer ASP.NET Core Web application. On the other hand, the `ModularCrm.Catalog` module consist of four packages (.NET projects) and used to implement the catalog module's functionality.
## Catalog Module's Packages
Here are the .NET projects (ABP Studio packages) of the Catalog module:
- `ModularCrm.Catalog`: The main module project that contains your [entities](../../framework/architecture/domain-driven-design/entities.md), [application service](../../framework/architecture/domain-driven-design/application-services.md) implementations and other business objects
- `ModularCrm.Catalog.Contracts`: Basically contains [application service](../../framework/architecture/domain-driven-design/application-services.md) interfaces and [DTOs](../../framework/architecture/domain-driven-design/data-transfer-objects.md)
- `ModularCrm.Catalog.Tests`: Unit and integration tests (if you selected the _Include Tests_ option)
- `ModularCrm.Catalog.UI`: Contains user interface pages components for the module
## Summary
We've created the initial single layer monolith solution. In the next part, we will learn how to create a new application module and install it to the main application.
You've created the initial single layer monolith modular solution with a Catalog module included. In the next part, you will learn how install the Catalog module to the main application.
In this part, you will create a new product management module and install it in the main CRM application.
In this part, you will install the `ModularCrm.Catalog` module to the main application, which was created in the [previous part](part-01.md).
## Creating Solution Folders
## Creating an ABP Studio Solution Folder
You can create solution folders and sub-folders in *Solution Explorer* to organize your solution components better. Right-click to the solution root on the *Solution Explorer* panel, and select *Add* -> *New Folder* command:
Before installing the `ModularCrm.Catalog` module to the main application, let's create a "main" folder and move the `ModularCrm` module (the main application) to it to tidy up the solution structure. Right-click to the solution root on the *Solution Explorer* panel, and select *Add* -> *New Folder* command:
Create a `main` and a `modules` folder using the *New Folder* command, then move the `ModularCrm` module into the `main` folder (simply by drag & drop). The *Solution Explorer* panel should look like the following figure now:
After the folder is created, now you can move the `ModularCrm` module to the `main` folder (simply by drag & drop). The _Solution Explorer_ panel should look like the following figure now:
This command opens a new dialog to define the properties of the new module. You can use the following values to create a new module named `ModularCrm.Products`:
* May not contain a UI and leaves the UI development to the final application.
* May contain a single UI implementation that is typically in the same technology as the main application.
* May contain more than one UI implementation if you want to create a reusable application module and you want to make that module usable by different applications with different UI technologies. For example, all of [pre-built ABP modules](https://abp.io/modules) support multiple UI options.
In this tutorial, we are selecting the MVC UI since we are building that module only for our `ModularCrm` solution and we are using the MVC UI in our application. So, select the MVC UI and click the *Next* button.
### Selecting the Database Provider
The next step is to select the database provider (or providers) you want to support with your module:
Since our main application is using Entity Framework Core and we will use the `ModularCrm.Products` module only for that main application, we can select the *Entity Framework Core* option and click the *Next* button.
Lastly, you can uncheck the *Include Tests* option if you don't want to include test projects in your module. Click the *Create* button to create the new module.
### Exploring the New Module
After adding the new module, the *Solution Explorer* panel should look like the following figure:
The new `ModularCrm.Products` module has been created and added to the solution. The `ModularCrm.Products` module has a separate and independent .NET solution. Right-click the `ModularCrm.Products` module and select the *Open with* -> *Explorer* command:
As seen in the preceding figure, the `ModularCrm.Products` solution consists of several layers, each has own responsibility.
### Installing the Product Module to the Main Application
A module does not contain an executable application inside. The `Modular.Products.Web` project is just a class library project, not an executable web application. A module should be installed in an executable application to run it.
A module does not contain an executable application inside. The `ModularCrm.Catalog.UI` project is just a class library project, not an executable web application. A module should be installed in an executable application to run it.
> **Ensure that the web application is not running in [Solution Runner](../../studio/running-applications.md) or in your IDE. Installing a module to a running application will produce errors.**
@ -108,19 +42,17 @@ The product module has yet to be related to the main application. Right-click on
The *Import Module* command opens a dialog as shown below:
Select the `ModularCrm.Products` module and check the *Install this module* option. If you don't check that option, it only imports the module but doesn't set project dependencies. Importing a module without installation can be used to set up your project dependencies manually. We want to make it automatically, so check the *Install this module* option.
Select the `ModularCrm.Catalog` module and check the *Install this module* option. If you don't check that option, it only imports the module but doesn't set project dependencies. Importing a module without installation can be used to set up your project dependencies manually. We want to make it automatically, so check the *Install this module* option.
When you click the *OK* button, ABP Studio opens the *Install Module* dialog:
This dialog simplifies installing a multi-layer module to a single-layer application. It automatically determines which package of the `ModularCrm.Products` module should be installed to which package of the main application.
Select the `ModularCrm.Catalog` and `ModularCrm.Catalog.UI` packages from the left area and ensure the `ModularCrm` package from the middle area was checked as shown in the preceding figure. Finally, click _OK_.
The default package match is good for this tutorial, so you can click the *OK* button to proceed.
### Building the Main Application
## Building the Main Application
After the installation, build the entire solution by right-clicking on the `ModularCrm` module (under the `main` folder) and selecting the *Dotnet CLI* -> *Graph Build* command:
@ -132,12 +64,10 @@ Graph Build is a dotnet CLI command that recursively builds all the referenced d
### Run the Main Application
Open the *Solution Runner* panel, click the *Play* button (near to the solution root), right-click the `ModularCrm` application and select the *Browse* command. It will open the web application in the built-in browser. Then you can navigate to the *Products* page on the main menu of the application to see the Products page that is coming from the `ModularCrm.Products` module:
Open the *Solution Runner* panel, click the *Play* button (near to the solution root), right-click the `ModularCrm` application and select the *Browse* command. It will open the web application in the built-in browser. Then you can navigate to the *Catalog* page on the main menu of the application to see the Catalog page that is coming from the `ModularCrm.Catalog` module:
In this part, we've created a new module to manage products in our modular application. Then we installed the new module to the main application and run the solution to test if it has successfully installed.
In the next part, you will learn how to create entities, services and a basic user interface for the products module.
In this part, you installed the `ModularCrm.Catalog` module to the main application and run the solution to test if it has successfully installed. In the [next part](part-03.md), you will learn how to create entities, services and a basic user interface for the catalog module.
In this part, you will learn how to create entities and services and a basic user interface for the products module.
In this part, you will learn how to create entities, services and a basic user interface for the catalog module.
> **This module's functionality will be minimal to focus on modularity.** You can follow the [Book Store tutorial](../book-store/index.md) to learn building more real-world applications with ABP.
@ -22,21 +22,21 @@ If it is still running, please stop the web application before continuing with t
## Creating a `Product` Entity
Open the `ModularCrm.Products` module in your favorite IDE. You can right-click the `ModularCrm.Products` module and select the *Open With* -> *Visual Studio* command to open the `ModularCrm.Products` module's .NET solution with Visual Studio. If you can not find your IDE in the *Open with* list, open with the *Explorer*, then open the `.sln` file with your IDE:
Open the `ModularCrm.Catalog` module in your favorite IDE. You can right-click the `ModularCrm.Catalog` module and select the *Open With* -> *Visual Studio Code* command to open the `ModularCrm.Catalog` module's .NET solution with Visual Studio. If you can not find your IDE in the *Open with* list, open with the *Explorer*, then open the `.sln` file with your IDE:
Add a new `Product` class under the `ModularCrm.Products.Domain` project (Right-click the `ModularCrm.Products.Domain` project, select *Add* -> *Class*):
Add a new `Product` class under the `ModularCrm.Catalog` project:
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace ModularCrm.Products;
namespace ModularCrm.Catalog;
public class Product : AggregateRoot<Guid>
{
@ -45,78 +45,80 @@ public class Product : AggregateRoot<Guid>
}
````
> Note that in this tutorial, we create classes directly in the project's root folder to keep things simple. It is up to you to create subfolders (namespaces) in your project to achieve finer code organization, especially for large modules.
## Mapping Entity to Database
The next step is to configure the Entity Framework Core `DbContext` class and the database for the new entity.
### Add a `DbSet` Property
Open the `ProductsDbContext` in the `ModularCrm.Products.EntityFrameworkCore` project and add a new `DbSet` property for the `Product` entity. The final `ProductsDbContext.cs` file content should be the following:
Open the `CatalogDbContext` under the **Data** folder in the same project and add a new `DbSet` property for the `Product` entity. The final `CatalogDbContext.cs` file content should be the following:
public interface ICatalogDbContext : IEfCoreDbContext
{
DbSet<Product> Products { get; set; }
}
````
Having such an `IProductsDbContext` interface allows us to decouple our repositories (and other classes) from the concrete `ProductsDbContext` class. This provides flexibility to the final application to merge multiple `DbContext`s into a single `DbContext` to manage database migrations easier and have a database level transaction support for multi-module database operations.
Having such an `ICatalogDbContext` interface allows us to decouple our repositories (and other classes) from the concrete `CatalogDbContext` class. This provides flexibility to the final application to merge multiple `DbContext`s into a single `DbContext` to manage database migrations easier and have a database level transaction support for multi-module database operations. We will do it later in this tutorial.
### Configure the Table Mapping
The DDD module template is designed to be flexible so that your module can have a separate physical database or store its tables inside another database, like the main database of your application. To make that possible, it configures the database mapping in an extension method (`ConfigureProducts`) called inside the `OnModelCreating` method above. Find that extension method (in the `ProductsDbContextModelCreatingExtensions` class) and change its content as the following code block:
The **Standard Module** template is designed to be flexible so that your module can have a separate physical database or store its tables inside another database (typically in the main database of your application). To make that possible, it configures the database mapping in an extension method (`ConfigureCatalog()`) called inside the `OnModelCreating` method above. Find that extension method (in the `CatalogDbContextModelCreatingExtensions` class) and change its content as the following code block:
//Always call this method to setup base entity properties
b.ConfigureByConvention();
@ -137,28 +139,15 @@ public static class ProductsDbContextModelCreatingExtensions
}
````
First, we are setting the database table name with the `ToTable` method. `ProductsDbProperties.DbTablePrefix` defines a constant that is added as a prefix to all database table names of this module. If you see the `ProductsDbProperties` class (in the `ModularCrm.Products.Domain` project), `DbTablePrefix` value is `Products`. In that case, the table name for the `Product` entity will be `ProductsProducts`. That is unnecessary for such a simple module; we can remove that prefix. So, you can change the `ProductsDbProperties` class with the following content to set an empty string to the `DbTablePrefix` property:
````csharp
namespace ModularCrm.Products;
public static class ProductsDbProperties
{
public static string DbTablePrefix { get; set; } = "";
public static string? DbSchema { get; set; } = null;
public const string ConnectionStringName = "Products";
}
````
First, you are setting the database table name with the `ToTable` method. `CatalogDbProperties.DbTablePrefix` defines a constant that is added as a prefix to all database table names of this module. If you see the `CatalogDbProperties` class, `DbTablePrefix` value is `Catalog`. In that case, the table name for the `Product` entity will be `CatalogProducts`. You can change the `CatalogDbProperties` class if you are not happy with the default table prefix, or set a schema for your tables.
You can set a `DbSchema` to collect a module's tables under a separate schema (if your DBMS supports it) or use a `DbTablePrefix` as a prefix for all module table names. We won't set any of them for this tutorial.
At that point, build the `ModularCrm.Products` .NET solution in your IDE (or ABP Studio UI). We will switch to the main application's .NET solution.
At that point, build the `ModularCrm.Catalog` .NET solution in your IDE (or on the ABP Studio UI). Then, switch to the main application's .NET solution.
### Configuring the Main Application Database
We changed the Entity Framework Core configuration. The next step should be adding a new code-first database migration and updating the database so the new Products table is created on the database.
You changed the Entity Framework Core configuration. The next step should be adding a new code-first database migration and updating the database so the new `CatalogProducts` table is created on the database.
We are not managing the database migrations in the module. Instead, the main application decides which DBMS (Database Management System) to use and how to share physical database(s) among modules. We will store all the modules' data in a single physical database to simplify this tutorial.
You are not managing the database migrations in the module. Instead, the main application decides which DBMS (Database Management System) to use and how to share physical database(s) among modules. We will store all the modules' data in a single physical database in this tutorial.
Open the `ModularCrm` module (which is the main application) in your IDE:
@ -168,48 +157,40 @@ Open the `ModularCrmDbContext` class under the `ModularCrm` project's `Data` fol
builder.ConfigureProducts(); //NEW: CALL THE EXTENSION METHOD
}
````
**(3)** Finally, ensure that the `ConfigureCatalog()` extension method is called inside the `OnModelCreating` method (this should be already done because you set the _Setup as a modular solution_ option in the _Modularity_ step while creating the initial solution).
In this way, `ModularCrmDbContext` can be used by the products module over the `IProductsDbContext` interface. This part is only needed once for a module. Next time, you can add a new database migration, as explained in the next section.
In this way, `ModularCrmDbContext` can be used by the catalog module over the `ICatalogDbContext` interface. This part is only needed once for a module. Next time, you can directly add a new database migration, as explained in the next section.
#### Add a Database Migration
Now, we can add a new database migration. You can use Entity Framework Core's `Add-Migration` (or `dotnet ef migrations add`) terminal command, but we will use ABP Studio's shortcut UI in this tutorial.
You can use Entity Framework Core's `Add-Migration` (or `dotnet ef migrations add`) terminal command, but we will use ABP Studio's shortcut UI in this tutorial.
Ensure that the solution has built. You can right-click the `ModularCrm` (under the `main` folder) on ABP Studio *Solution Runner* and select the *Dotnet CLI* -> *Graph Build* command.
@ -225,28 +206,28 @@ Once you click the *OK* button, a new database migration class is added to the `
Now, we will create an [application service](../../framework/architecture/domain-driven-design/application-services.md) to perform some use cases related to products.
Now, you can create an [application service](../../framework/architecture/domain-driven-design/application-services.md) to perform some use cases related to products.
### Defining the Application Service Contract
Return to your IDE (e.g. Visual Studio), open the `ModularCrm.Products` module's .NET solution and create an `IProductAppService` interface under the `ModularCrm.Products.Application.Contracts` project:
Return to your IDE (e.g. Visual Studio), open the `ModularCrm.Catalog` module's .NET solution and create an `IProductAppService` interface under the `ModularCrm.Catalog.Contracts` project:
````csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace ModularCrm.Products;
namespace ModularCrm.Catalog;
public interface IProductAppService : IApplicationService
{
@ -255,18 +236,18 @@ public interface IProductAppService : IApplicationService
}
````
We are defining application service interfaces and [data transfer objects](../../framework/architecture/domain-driven-design/data-transfer-objects.md) in the `Application.Contracts` project. That way, we can share those contracts with clients without sharing the actual implementation class.
We are defining application service interfaces and [data transfer objects](../../framework/architecture/domain-driven-design/data-transfer-objects.md) in the `ModularCrm.Catalog.Contracts` project. That way, you can share those contracts with clients without sharing the actual implementation classes.
### Defining Data Transfer Objects
The `GetListAsync` and `CreateAsync` methods use the `ProductDto` and `ProductCreationDto` classes, which have not been defined yet. So, we need to define them.
The `GetListAsync` and `CreateAsync` methods use the `ProductDto` and `ProductCreationDto` classes, which have not been defined yet. So, you need to define them.
Create a `ProductCreationDto` class under the `ModularCrm.Products.Application.Contracts` project:
Create a `ProductCreationDto` class under the `ModularCrm.Catalog.Contracts` project:
````csharp
using System.ComponentModel.DataAnnotations;
namespace ModularCrm.Products;
namespace ModularCrm.Catalog;
public class ProductCreationDto
{
@ -279,29 +260,28 @@ public class ProductCreationDto
}
````
And create a `ProductDto` class under the `ModularCrm.Products.Application.Contracts` project:
And create a `ProductDto` class under the `ModularCrm.Catalog.Contracts` project:
````csharp
using System;
namespace ModularCrm.Products
namespace ModularCrm.Catalog;
public class ProductDto
{
public class ProductDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public int StockCount { get; set; }
}
public Guid Id { get; set; }
public string Name { get; set; }
public int StockCount { get; set; }
}
````
The new files under the `ModularCrm.Products.Application.Contracts` project are shown below:
The new files under the `ModularCrm.Catalog.Contracts` project are shown below:
@ -339,20 +319,20 @@ public class ProductAppService : ProductsAppService, IProductAppService
}
````
Notice that `ProductAppService` class implements the `IProductAppService` and also inherits from the `ProductsAppService` class. Do not be confused about the naming (`ProductAppService` and `ProductsAppService`). The `ProductsAppService` is a base class. It makes a few configurations for [localization](../../framework/fundamentals/localization.md) and [object mapping](../../framework/infrastructure/object-to-object-mapping.md) (you can see in the `ModularCrm.Products.Application` project). You can inherit all of your application services from that base class. This way, you can define some common properties and methods to share among all your application services. You can rename the base class if you feel that you may be confused later.
Notice that `ProductAppService` class implements the `IProductAppService` and also inherits from the `CatalogAppService` class. The `CatalogAppService` is a base class and it makes a few configurations for [localization](../../framework/fundamentals/localization.md) and [object mapping](../../framework/infrastructure/object-to-object-mapping.md) (you can see in the same `ModularCrm.Catalog` project). You can inherit all of your application services from that base class. This way, you can define some common properties and methods to share among all your application services. You can rename the base class if you feel that you may be confused later.
#### Object Mapping
`ProductAppService.GetListAsync` method uses the `ObjectMapper` service to convert `Product` entities to `ProductDto` objects. The mapping should be configured. Open the `ProductsApplicationAutoMapperProfile` class in the `ModularCrm.Products.Application` project and change it to the following code block:
`ProductAppService.GetListAsync` method uses the `ObjectMapper` service to convert `Product` entities to `ProductDto` objects. The mapping should be configured. Open the `CatalogAutoMapperProfile` class in the `ModularCrm.Catalog` project and change it to the following code block:
````csharp
using AutoMapper;
namespace ModularCrm.Products;
namespace ModularCrm.Catalog;
public class ProductsApplicationAutoMapperProfile : Profile
public class CatalogAutoMapperProfile : Profile
{
public ProductsApplicationAutoMapperProfile()
public CatalogAutoMapperProfile()
{
CreateMap<Product,ProductDto>();
}
@ -363,56 +343,49 @@ We've added the `CreateMap<Product, ProductDto>();` line to define the mapping.
### Exposing Application Services as HTTP API Controllers
For this application, we don't need to create HTTP API endpoints for the products module. But it is good to understand how to do it when you need it. You have two options;
* You can create a regular ASP.NET Core Controller class in the `ModularCrm.Products.HttpApi` project, inject `IProductAppService` and use it to create wrapper methods. We will do this later while we create the Ordering module.
* Alternatively, you can use the ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature to expose your application services as API controllers by conventions. We will do it here.
Open the `ModularCrmModule` class in the main application's solution (the `ModularCrm` solution), find the `PreConfigureServices` method and add the following lines inside that method:
> This application doesn't need to expose any functionality as HTTP API, because all the module integration and communication will be done in the same process as a natural aspect of a monolith modular application. However, in this section, we will create HTTP APIs because;
>
> 1. We will use these HTTP API endpoints in development to create some example data.
> 2. To know how to do it when you need it.
>
> So, follow the instructions in this section and expose the product application service as an HTTP API endpoint.
To create HTTP API endpoints for the catalog module, you have two options:
This will tell the ASP.NET Core to explore the given assembly to discover controllers.
* You can create a regular ASP.NET Core Controller class in the `ModularCrm.Catalog` project, inject `IProductAppService` and create wrapper methods for each public method of the product application service. You will do this later while you create the Ordering module. (Also, you can check the `SampleController` class under the **Samples** folder in the `ModularCrm.Catalog` project for an example)
* Alternatively, you can use the ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature to expose your application services as API controllers by conventions. We will do it here.
Then open the `ConfigureAutoApiControllers` method of the same class and add a second `ConventionalControllers.Create` call as shown in the following code block:
Open the `CatalogModule` class in the Catalog module's .NET solution (the `ModularCrm.Catalog` .NET solution, the `ModularCrm.Catalog` .NET project) in your IDE, find the `ConfigureServices` method and add the following code block into that method:
This will tell the ABP framework to create API controllers for the application services in the assembly.
This will tell the ABP framework to create API controllers for the application services in the `ModularCrm.Catalog`assembly.
> We made these configurations in the main application's solution since there is no project in the product module's solution that references ASP.NET Core MVC packages and uses the product module's application layer. If you add a reference of `ModularCrm.Products.Application` to `ModularCrm.Products.HttpApi`, then you can move these configurations to the `ModularCrm.Products.HttpApi` project.
Now, ABP will automatically expose the application services defined in the `ModularCrm.Products.Application` project as API controllers. The next section will use these API controllers to create some example products.
Now, ABP will automatically expose the application services defined in the `ModularCrm.Catalog` project as API controllers. The next section will use these API controllers to create some example products.
### Creating Example Products
This section will create a few example products using the [Swagger UI](../../framework/api-development/swagger.md). Thus, we will have some sample products to show on the UI.
This section will create a few example products using the [Swagger UI](../../framework/api-development/swagger.md). Thus, you will have some sample products to show on the UI.
Now, right-click the `ModularCrm` under the `main` folder in the Solution Explorer panel and select the *Dotnet CLI* -> *Graph Build* command. This will ensure that the product module and the main application are built and ready to run.
Now, right-click the `ModularCrm` under the `main` folder in the Solution Explorer panel and select the *Dotnet CLI* -> *Graph Build* command. This will ensure that the catalog module and the main application are built and ready to run.
After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm` application runs, we can right-click it and select the *Browse* command to open the user interface.
After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm` application runs, you can right-click it and select the *Browse* command to open the user interface.
Once you see the user interface of the web application, type `/swagger` at the end of the URL to open the Swagger UI. If you scroll down, you should see the `Products` API:
Once you see the user interface of the web application, type `/swagger` at the end of the URL to open the Swagger UI. If you scroll down, you should see the `Catalog` API:
Expand the `/api/products/product` API and click the *Try it out* button as shown in the following figure:
> **Note:** If you have a swagger error on the UI, then you can open the `SampleAppService` class in the `ModularCrm.Catalog` project and add `[RemoteService(false)]` attribute to the `SampleAppService` class. With this attribute, the `SampleAppService` class will not be exposed as a remote service automatically but since there is a `SampleController` class in the `ModularCrm.Catalog` project, the `Catalog` API will be exposed as a remote service.
Expand the `POST /api/catalog/product` API and click the *Try it out* button as shown in the following figure:
We've some entities in the database; we can show them on the user interface now.
You've some entities in the database and now you can show them on the user interface.
## Creating the User Interface
In this section, we will create a very simple user interface to demonstrate how to build UI in the products module and make it work in the main application.
In this section, you will create a very simple user interface to demonstrate how to build UI in the catalog module and make it work in the main application.
As a first step, you can stop the application on ABP Studio's Solution Runner if it is currently running.
Open the `ModularCrm.Products` .NET solution in your IDE, and find the `Pages/Products/Index.cshtml` file under the `ModularCrm.Products.Web` project:
Replace the `Index.cshtml.cs` file with the following content:
@ -442,9 +417,9 @@ Replace the `Index.cshtml.cs` file with the following content:
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ModularCrm.Products.Web.Pages.Products;
namespace ModularCrm.Catalog.UI.Pages.Catalog;
public class IndexModel : ProductsPageModel
public class IndexModel : CatalogPageModel
{
public List<ProductDto> Products { get; set; }
@ -462,15 +437,15 @@ public class IndexModel : ProductsPageModel
}
````
Here, we simply use the `IProductAppService` to get a list of all products and assign the result to the `Products` property. We can use it in the `Index.cshtml` file to show a simple list of products on the UI:
Here, you simply use the `IProductAppService` to get a list of all products and assign the result to the `Products` property. You can use it in the `Index.cshtml` file to show a simple list of products on the UI:
As you can see, developing a UI page in a modular ABP application is pretty straightforward. We kept the UI very simple to focus on modularity. To learn how to build complex application UIs, please check the [Book Store Tutorial](../book-store/index.md).
## Final Notes
Some of the projects in the product module's .NET solution (`ModularCrm.Products`) are not necessary for most of the cases. They are available to support different scenarios. You can delete them from your module (and remove the dependencies on the main application) if you want:
## Summary
* `ModularCrm.Products.HttpApi`: This project aims to define regular HTTP API controllers. If you will always use ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature (like we did in this tutorial), you can delete that project.
* `ModularCrm.Products.HttpApi.Client`: That project is generally shared with 3rd-party applications, so they can easily consume your HTTP API endpoints. In a modular monolith application, you typically don't need it.
* `ModularCrm.Products.HttpApi.Installer`: That project is used to discover and install a multi-projects module (like the product module) when you deploy it to a package management system (like NuGet). If you use the module with local project references (like we did here), you can delete that project.
* You can also delete the test projects (there are 4 of them in the solution) if you don't prefer to write unit/integration tests in the module's solution (Legal warning: it is recommended to write tests 😊)
In this part of the tutorial, you've built the functionality inside the _Catalog_ module, which was created in the [previous part](part-02.md). In the next part, you will create a new _Ordering_ module and install it into the main application.
@ -18,9 +18,11 @@ In this part, you will build a new module for placing orders and install it in t
## Creating a Standard Module
In this part, we have used the *DDD Module* template for the Product module and will use the *Standard Module* template for the Ordering module.
In the first part of this tutorial, you created the `ModularCrm` solution with selecting the _Setup as a modular solution_ option and adding a module named `ModularCrm.Catalog` using the *Standard Module* template.
Right-click the `modules` folder on the *Solution Explorer* panel, and select the *Add* -> *New Module* -> *Standard Module* command:
Now, you will create a second module for the `ModularCrm` solution through ABP Studio's *Solution Explorer*. This new module, called `ModularCrm.Ordering`, will handle all order related functionality in the application.
To add a new module, right-click the `modules` folder on the *Solution Explorer* panel, and select the *Add* -> *New Module* -> *Standard Module* command:
Similar to DDD module creation, you can choose the type of UI you want to support in your module or select *No UI* if you don't need a user interface. In this example, we'll select the *MVC* option and click *Next*. One difference is that, for a standard module, you can only choose one UI type.
You can choose the type of UI you want to support in your module or select *No UI* if you don't need a user interface. In this example, we'll select the *MVC* option and click *Next*.
The same limitation applies to the database selection. You can only choose one database provider for a standard module. Select the *Entity Framework Core* option and click *Next*.
In this screen, select the *Entity Framework Core* option and click *Next*.
Since we've created a standard module, it doesn't have multiple layers like the DDD module. If you open the `modules/modularcrm.ordering` in your file system, you can see the initial files:
You can include or not include unit tests for the new module here. We are unchecking the *Include Tests* option this time to show a different structure for this example. Click the *Create* button to create the module.
Here is the final solution structure after adding the `ModularCrm.Ordering` module:
Because only a single UI package can be chosen, the UI type doesn’t matter. This is why the package name is changed to *ModularCrm.Ordering.UI*. Additionally, there are no *Domain*, *EntityFrameworkCore*, or *Http* layers like in the DDD module. We're going to use the `ModularCrm.Ordering` package for the domain business logic. You can open `ModularCrm.Ordering.sln` in your favorite IDE (e.g. Visual Studio):
In this section, we will install the `ModularCrm.Ordering` module in the main application so it can be part of the system.
In this section, you will install the `ModularCrm.Ordering` module in the main application so it can be part of the monolith application.
> Before the installation, please ensure the web application is not running.
@ -68,8 +64,10 @@ That command opens the *Import Module* dialog:
Select the `ModularCrm.Ordering` module and check the *Install this module* option as shown in the preceding figure. When you click the OK button, a new dialog is shown to select the packages to install:
Select the `ModularCrm.Ordering` and `ModularCrm.Ordering.UI` packages from the left area and ensure the `ModularCrm` package from the middle area was checked as shown in the preceding figure. Finally, click _OK_.
Select the `ModuleCrm.Ordering` and `ModularCrm.Ordering.UI` packages from the left area and the `ModularCrm` package from the middle area as shown in the preceding figure. Finally, click *OK*.
## Summary
In this part of the tutorial, we've created a standard module. This allows you to create modules or applications with a different structure. In the next part, we will add functionality to the Ordering module.
In this part of the tutorial, you've created a new module and installed it into the main solution. In the [next part](part-05), you will add functionality to the new Ordering module.
In the previous part, we created Ordering module and installed it into the main application. However, the Ordering module has no functionality now. In this part, we will create an `Order` entity and add functionality to create and list the orders.
In the [previous part](part-04), you created Ordering module and installed it into the main application. However, the Ordering module has no functionality yet. In this part, you will create an `Order` entity and add functionality to create and list the orders.
## Creating an `Order` Entity
@ -24,32 +24,30 @@ Open the `ModularCrm.Ordering` .NET solution in your IDE.
### Adding an `Order` Class
Create an `Order` class to the `ModularCrm.Ordering` project (open an `Entities` folder and place the `Order.cs` into that folder):
Create an `Order` class to the `ModularCrm.Ordering` project:
````csharp
using System;
using ModularCrm.Ordering.Enums;
using Volo.Abp.Domain.Entities.Auditing;
namespace ModularCrm.Ordering.Entities
namespace ModularCrm.Ordering;
public class Order : CreationAuditedAggregateRoot<Guid>
{
public class Order : CreationAuditedAggregateRoot<Guid>
{
public Guid ProductId { get; set; }
public string CustomerName { get; set; }
public OrderState State { get; set; }
}
public Guid ProductId { get; set; }
public string CustomerName { get; set; } = null!;
public OrderState State { get; set; }
}
````
We allow users to place only a single product within an order. The `Order` entity would be much more complex in a real-world application. However, the complexity of the `Order` entity doesn't affect modularity, so we keep it simple to focus on modularity in this tutorial. We are inheriting from the [`CreationAuditedAggregateRoot` class](../../framework/architecture/domain-driven-design/entities.md) since I want to know when an order has been created and who has created it.
We allow users to place only a single product within an order. The `Order` entity would be much more complex in a real-world application. However, the complexity of the `Order` entity doesn't affect modularity. So, we keep it simple to focus on modularity in this tutorial. We are inheriting from the [`CreationAuditedAggregateRoot` class](../../framework/architecture/domain-driven-design/entities.md) since I want to know when an order has been created and who has created it.
### Adding an `OrderState` Enumeration
We used an `OrderState` enumeration that has not yet been defined. Open an `Enums` folder in the `ModularCrm.Ordering.Contracts` project and create an `OrderState.cs` file inside it:
We used an `OrderState` enumeration that has not yet been defined. Create a `OrderState.cs` file inside the `ModularCrm.Ordering.Contracts` project and define the following Enum:
````csharp
namespace ModularCrm.Ordering.Enums;
namespace ModularCrm.Ordering;
public enum OrderState : byte
{
@ -61,17 +59,17 @@ public enum OrderState : byte
The final structure of the Ordering module should be similar to the following figure in your IDE:
The `Order` entity has been created. Now, we need to configure the database mapping for that entity. We will first define the database table mapping, create a database migration and update the database.
The `Order` entity has been created. Now, you need to configure the database mapping for that entity. You will first define the database table mapping, create a database migration and update the database.
### Defining the Database Mappings
Entity Framework Core requires defining a `DbContext` class as the main object for the database mapping. We want to use the main application's `DbContext` object. That way, we can control the database migrations at a single point, ensure database transactions on multi-module operations, and establish relations between database tables of different modules. However, the Ordering module can not use the main application's `DbContext` object because it doesn't depend on the main application, and we don't want to establish such a dependency.
Entity Framework Core requires defining a `DbContext` class as the main object for the database mapping. We want to use the main application's `DbContext` object. That way, you can control the database migrations at a single point, ensure database transactions on multi-module operations, and establish relations between database tables of different modules. However, the Ordering module can not use the main application's `DbContext` object because it doesn't depend on the main application, and you don't want to establish such a dependency.
As a solution, we will use `DbContext` interface in the Ordering module which is then implemented by the main module's `DbContext`.
As a solution, you will use `DbContext` interface in the Ordering module which is then implemented by the main module's `DbContext`.
Open your IDE, in `Data` folder under the `ModularCrm.Ordering` project, and edit `IOrderingDbContext` interface as shown:
@ -90,7 +88,7 @@ public interface IOrderingDbContext : IEfCoreDbContext
}
````
Afterwards, create *Orders*`DbSet` for the `OrderingDbContext` class in the `Data` folder under the `ModularCrm.Ordering` project.
Afterwards, create *Orders*`DbSet` for the `OrderingDbContext` class in the `Data` folder under the `ModularCrm.Ordering` project:
````csharp
using Microsoft.EntityFrameworkCore;
@ -119,10 +117,9 @@ public class OrderingDbContext : AbpDbContext<OrderingDbContext>, IOrderingDbCon
}
````
You can inject and use the `IOrderingDbContext` in the Ordering module. However, you will not usually directly use that interface. Instead, you will use ABP's [repositories](../../framework/architecture/domain-driven-design/repositories.md), which internally uses that interface.
We can inject and use the `IOrderingDbContext` in the Ordering module. However, we will not usually directly use that interface. Instead, we will use ABP's [repositories](../../framework/architecture/domain-driven-design/repositories.md), which internally uses that interface.
It is best to configure the database table mapping for the `Order` entity in the Ordering module. We will use the `OrderingDbContextModelCreatingExtensions` in the same `Data` folder:
It is best to configure the database table mapping for the `Order` entity in the Ordering module. You will use the `OrderingDbContextModelCreatingExtensions` in the same `Data` folder:
````csharp
using Microsoft.EntityFrameworkCore;
@ -180,7 +177,7 @@ public class ModularCrmDbContext :
}
````
**(3)** Finally, call the `ConfigureOrdering()` extension method inside the `OnModelCreating` method after other `Configure...` module calls:
**(3)** Finally, call the `ConfigureOrdering()` extension method inside the `OnModelCreating` method after other `Configure...` module calls (this should already be done from ABP Studio):
In this way, the Ordering module can use 'ModularCrmDbContext' over the `IProductsDbContext` interface. This part is only needed once for a module. Next time, you can add a new database migration, as explained in the next section.
In this way, the Ordering module can use 'ModularCrmDbContext' over the `IOrderingDbContext` interface. This part is only needed once for a module. Next time, you can add a new database migration, as explained in the next section.
#### Add a Database Migration
Now, we can add a new database migration. You can use Entity Framework Core's `Add-Migration` (or `dotnet ef migrations add`) terminal command, but in this tutorial, we will use ABP Studio's shortcut UI.
Now, you can add a new database migration. You can use Entity Framework Core's `Add-Migration` (or `dotnet ef migrations add`) terminal command, but in this tutorial, you will use ABP Studio's shortcut UI.
Ensure that the solution has built. You can right-click the `ModularCrm` (under the `main` folder) on ABP Studio *Solution Runner* and select the *Dotnet CLI* -> *Graph Build* command.
@ -204,34 +201,36 @@ Right-click the `ModularCrm` package and select the *EF Core CLI* -> *Add Migrat
The *Add Migration* command opens a new dialog to get a migration name:
`Ordering` prefix is added to all table names of the Ordering module. If you want to change or remove it, see the `OrderingDbProperties` class in the Ordering module's .NET solution.
## Creating the Application Service
We will create an application service to manage the `Order` entities.
You will create an application service to manage the `Order` entities.
### Defining the Application Service Contract
We're gonna create the `IOrderAppService` interface under the `ModularCrm.Ordering.Contracts` project. Return to your IDE, open the `ModularCrm.Ordering` module's .NET solution and create an `IOrderAppService` interface under the `Services` folder for`ModularCrm.Ordering.Contracts` project:
You're gonna create the `IOrderAppService` interface under the `ModularCrm.Ordering.Contracts` project. Return to your IDE, open the `ModularCrm.Ordering` module's .NET solution and create an `IOrderAppService` interface in the`ModularCrm.Ordering.Contracts` project:
````csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace ModularCrm.Ordering.Services;
namespace ModularCrm.Ordering;
public interface IOrderAppService : IApplicationService
{
@ -242,7 +241,7 @@ public interface IOrderAppService : IApplicationService
### Defining Data Transfer Objects
The `GetListAsync` and `CreateAsync` methods will use data transfer objects (DTOs) to communicate with the client. We will create two DTO classes for that purpose.
The `GetListAsync` and `CreateAsync` methods will use data transfer objects (DTOs) to communicate with the client. You will create two DTO classes for that purpose.
Create a `OrderCreationDto` class under the `ModularCrm.Ordering.Contracts` project:
@ -250,13 +249,13 @@ Create a `OrderCreationDto` class under the `ModularCrm.Ordering.Contracts` proj
using System;
using System.ComponentModel.DataAnnotations;
namespace ModularCrm.Ordering.Contracts.Services;
namespace ModularCrm.Ordering;
public class OrderCreationDto
{
[Required]
[StringLength(150)]
public string CustomerName { get; set; }
public string CustomerName { get; set; } = null!;
[Required]
public Guid ProductId { get; set; }
@ -267,14 +266,13 @@ Create a `OrderDto` class under the `ModularCrm.Ordering.Contracts` project:
````csharp
using System;
using ModularCrm.Ordering.Enums;
namespace ModularCrm.Ordering.Services;
namespace ModularCrm.Ordering;
public class OrderDto
{
public Guid Id { get; set; }
public string CustomerName { get; set; }
public string CustomerName { get; set; } = null!;
public Guid ProductId { get; set; }
public OrderState State { get; set; }
}
@ -282,16 +280,14 @@ public class OrderDto
The new files under the `ModularCrm.Ordering.Contracts` project should be like the following figure:
Now we should configure the *AutoMapper* object to map the `Order` entity to the `OrderDto` object. We will use the `OrderingAutoMapperProfile` under the `ModularCrm.Ordering` project:
First we configure the *AutoMapper* to map the `Order` entity to the `OrderDto` object, because we will need it later. Open the `OrderingAutoMapperProfile` under the `ModularCrm.Ordering` project:
````csharp
using AutoMapper;
using ModularCrm.Ordering.Entities;
using ModularCrm.Ordering.Services;
namespace ModularCrm.Ordering;
@ -304,21 +300,19 @@ public class OrderingAutoMapperProfile : Profile
}
````
Now, we can implement the `IOrderAppService` interface. Create an `OrderAppService` class under the `Services` folder of the `ModularCrm.Ordering` project:
Now, you can implement the `IOrderAppService` interface. Create an `OrderAppService` class under the `ModularCrm.Ordering` project:
````csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ModularCrm.Ordering.Enums;
using ModularCrm.Ordering.Entities;
using Volo.Abp.Domain.Repositories;
namespace ModularCrm.Ordering.Services;
public class OrderAppService : OrderingAppService, IOrderAppService
public OrderAppService(IRepository<Order,Guid> orderRepository)
{
@ -347,50 +341,48 @@ public class OrderAppService : OrderingAppService, IOrderAppService
### Exposing Application Services as HTTP API Controllers
After implementing the application service, now we need to create HTTP API endpoints for the ordering module. For that purpose, open the `ModularCrmModule` class in the main application's solution (the `ModularCrm` solution), find the `ConfigureAutoApiControllers` method and add the following lines inside that method:
After implementing the application service, we can create HTTP API endpoints for the ordering module using the ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature. For that purpose, open the `OrderingModule` class in the Ordering module's .NET solution (the `ModularCrm.Ordering` solution), find the `ConfigureServices` method and add the following lines inside that method:
This will tell the ABP framework to create API controllers for the application services in the `ModularCrm.Ordering` assembly.
### Creating Example Orders
This section will create a few example orders using the [Swagger UI](../../framework/api-development/swagger.md). Thus, we will have some sample orders to show on the UI.
This section will create a few example orders using the [Swagger UI](../../framework/api-development/swagger.md). Thus, you will have some sample orders to show on the UI.
Now, right-click the `ModularCrm` under the `main` folder in the Solution Explorer panel and select the *Dotnet CLI* -> *Graph Build* command. This will ensure that the order module and the main application are built and ready to run.
After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm` application runs, we can right-click it and select the *Browse* command to open the user interface.
After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm` application runs, you can right-click it and select the *Browse* command to open the user interface.
Once you see the user interface of the web application, type `/swagger` at the end of the URL to open the Swagger UI. If you scroll down, you should see the `Order` API:
Once you see the user interface of the web application, type `/swagger` at the end of the URL to open the Swagger UI. If you scroll down, you should see the `Orders` API:
> **Note:** If you have a swagger error on the UI, then you can open the `SampleAppService` class in the `ModularCrm.Ordering` project and add `[RemoteService(false)]` attribute to the `SampleAppService` class. With this attribute, the `SampleAppService` class will not be exposed as a remote service automatically but since there is a `SampleController` class in the `ModularCrm.Catalog` project, the `Catalog` API will be exposed as a remote service.
Expand the `/api/orders/order` API and click the *Try it out* button. Then, create a few orders by filling in the request body and clicking the *Execute* button:
Expand the `POST /api/ordering/order` API and click the *Try it out* button. Then, create a few orders by filling in the request body and clicking the *Execute* button:
In this section, you will create a very simple user interface to demonstrate how to build UI in the catalog module and make it work in the main application.
As a first step, you can stop the application on ABP Studio's Solution Runner if it is currently running.
### Creating the Orders Page
Replace the `Index.cshtml.cs` content in the `Pages/Ordering` folder of the `ModularCrm.Ordering.UI` project with the following code block:
@ -399,30 +391,28 @@ Replace the `Index.cshtml.cs` content in the `Pages/Ordering` folder of the `Mod
public IndexModel(IOrderAppService orderAppService)
{
_orderAppService = orderAppService;
}
public IndexModel(IOrderAppService orderAppService)
{
_orderAppService = orderAppService;
}
public async Task OnGetAsync()
{
Orders = await _orderAppService.GetListAsync();
}
public async Task OnGetAsync()
{
Orders = await _orderAppService.GetListAsync();
}
}
````
Here, we are injecting `IOrderAppService` to query `Order` entities from the database to show on the page. Open the `Index.cshtml` file and replace the content with the following code block:
Here, you are injecting `IOrderAppService` to query `Order` entities from the database to show on the page. Open the `Index.cshtml` file and replace the content with the following code block:
````html
@page
@ -446,7 +436,7 @@ Here, we are injecting `IOrderAppService` to query `Order` entities from the dat
</abp-card>
````
This page shows a list of orders on the UI. We haven't created a UI to create new orders, and we will not do it to keep this tutorial simple. If you want to learn how to create advanced UIs with ABP, please follow the [Book Store tutorial](../book-store/index.md).
This page shows a list of orders on the UI. You haven't created a UI to create new orders, and we will not do it to keep this tutorial simple. If you want to learn how to create advanced UIs with ABP, please follow the [Book Store tutorial](../book-store/index.md).
### Editing the Menu Item
@ -487,26 +477,22 @@ public class OrderingMenuContributor : IMenuContributor
````
`OrderingMenuContributor` implements the `IMenuContributor` interface, which forces us to implement the `ConfigureMenuAsync` method. In that method, we can manipulate the menu items (add new menu items, remove existing menu items or change the properties of existing menu items). The `ConfigureMenuAsync` method is executed whenever the menu is rendered on the UI, so you can dynamically decide how to manipulate the menu items.
`OrderingMenuContributor` implements the `IMenuContributor` interface, which forces us to implement the `ConfigureMenuAsync` method. In that method, you can manipulate the menu items (add new menu items, remove existing menu items or change the properties of existing menu items). The `ConfigureMenuAsync` method is executed whenever the menu is rendered on the UI, so you can dynamically decide how to manipulate the menu items.
> You can check the [menu documentation](../../framework/ui/mvc-razor-pages/navigation-menu.md) to learn more about manipulating menu items.
### Building the Application
Now, we will run the application to see the result. Please stop the application if it is already running. Then open the *Solution Runner* panel, right-click the `ModularCrm` application, and select the *Build* -> *Graph Build* command:
Now, you will run the application to see the result. Please stop the application if it is already running. Then open the *Solution Runner* panel, right-click the `ModularCrm` application, and select the *Build* -> *Graph Build* command:
We've performed a graph build since we've made a change on a module, and more than building the main application is needed. *Graph Build* command also builds the depended modules if necessary. Alternatively, you could build the Ordering module first (on ABP Studio or your IDE). This approach can be faster if you have too many modules and you make a change in one of the modules. Now you can run the application by right-clicking the `ModularCrm` application and selecting the *Start* command.
Great! We can see the list of orders. However, there is a problem:
You've performed a graph build since you've made a change on a module, and more than building the main application is needed. *Graph Build* command also builds the depended modules if necessary. Alternatively, you could build the Ordering module first (on ABP Studio or your IDE). This approach can be faster if you have too many modules and you make a change in one of the modules. Now you can run the application by right-clicking the `ModularCrm` application and selecting the *Start* command.
1. We see Product's GUID ID instead of its name. This is because the Ordering module has no integration with the Products module and doesn't have access to Product module's database to perform a JOIN query.
We will solve this problem in the [next part](part-06.md).
Great! We can see the list of orders. However, there is a problem: We see Product's GUID ID instead of its name. This is because the Ordering module has no integration with the Catalog module and doesn't have access to Product module's database to perform a JOIN query. We will solve this problem in the [next part](part-06.md).
## Summary
In this part of the *Modular CRM* tutorial, we've built the functionality inside the Ordering module we created in the [previous part](part-04.md). In the next part, we will work on establishing communication between the Orders module and the Products module.
In this part of the *Modular CRM* tutorial, you've built the functionality inside the Ordering module you created in the [previous part](part-04.md). In the [next part](part-06.md), you will work on establishing communication between the Orders module and the Catalog module.
In the previous parts, we created two modules: the Products module to store and manage products and the Orders module to accept orders. However, these modules were completely independent from each other. Only the main application brought them together to execute in the same application, but these modules don't communicate with each other.
You have created two modules so far: the **Catalog** module to store and manage products and the **Ordering** module to accept orders. However, these modules were completely independent from each other. The main application brought them together to execute in the same application, but these modules don't communicate with each other.
In the next three parts, you will learn to implement three patterns for integrating these modules:
In this part and next two pars, you will learn to implement three common patterns for integrating these modules:
1. The Order module will make a request to the Products module to get product information when needed.
2. The Product module will listen to events from the Orders module, so it can decrease a product's stock count when an order is placed.
3. Finally, we will execute a database query that includes product and order data.
1. The Order module will make a request to the Catalog module to get product information when needed.
2. The Product module will listen to events from the Ordering module, so it can decrease a product's stock count when an order is placed.
3. Finally, you will execute a database query that includes product and order data.
Let's begin from the first one: The Integration Services.
@ -28,27 +28,27 @@ Let's begin from the first one: The Integration Services.
Remember from the [previous part](part-05.md), the Orders page shows product's identities instead of their names:
That is because the Orders module has no access to the product data, so it can not perform a JOIN query to get the names of products from the `Products` table. That is a natural result of the modular design. However, we also don't want to show a product's identity on the UI, which is not a good user experience.
That is because the Ordering module has no access to the product data, so it can not perform a JOIN query to get the names of products from the `Products` table. That is a natural result of the modular design. However, you also don't want to show a product's GUID identity on the UI, which is not a good user experience.
As a solution to that problem, the Orders module may ask product names to the Product module using an [integration service](../../framework/api-development/integration-services.md). Integration service concept in ABP is designed for request/response style inter-module (in modular applications) and inter-microservice (in distributed systems) communication.
As a solution to that problem, the Ordering module may ask product names to the Catalog module using an [integration service](../../framework/api-development/integration-services.md). Integration service concept in ABP is designed for request/response style inter-module (in modular applications) and inter-microservice (in distributed systems) communication.
> When you implement integration services for inter-module communication, you can easily convert them to REST API calls if you convert your solution to a microservice system and convert your modules to services later.
## Creating a Products Integration Service
The first step is to create an integration service in the Products module, so other modules can consume it.
The first step is to create an integration service in the Catalog module, so other modules can consume it.
We will define an interface in the `ModularCrm.Products.Application.Contracts` package and implement it in the `ModularCrm.Products.Application` package.
You will define an interface in the `ModularCrm.Catalog.Contracts` package and implement it in the `ModularCrm.Catalog` package.
### Defining the `IProductIntegrationService` Interface
Open the `ModularCrm.Products` .NET solution in your IDE, find the `ModularCrm.Products.Application.Contracts` project, create an `Integration` folder inside inside of that project and finally create an interface named `IProductIntegrationService` into that folder. The final folder structure should be like that:
Open the `ModularCrm.Catalog` .NET solution in your IDE, find the `ModularCrm.Catalog.Contracts` project, create an `Integration` folder inside inside of that project and finally create an interface named `IProductIntegrationService` into that folder. The final folder structure should be like that:
(Creating an`Integration` folder is not required, but it can be a good practice)
Creating an`Integration` folder is not required, but it can be a good practice to isolate integration-related code from the business logic of your module.
Open the `IProductIntegrationService.cs` file and replace it's content with the following code block:
@ -56,16 +56,16 @@ Open the `IProductIntegrationService.cs` file and replace it's content with the
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ModularCrm.Catalog;
using Volo.Abp;
using Volo.Abp.Application.Services;
namespace ModularCrm.Products.Integration
namespace ModularCrm.Products.Integration;
[IntegrationService]
public interface IProductIntegrationService : IApplicationService
{
[IntegrationService]
public interface IProductIntegrationService : IApplicationService
### Implementing the `ProductIntegrationService` Class
We've defined the integration service interface. Now, we can implement it in the `ModularCrm.Products.Application` project. Create an `Integration` folder and then create a `ProductIntegrationService` class in that folder. The final folder structure should be like this:
We've defined the integration service interface. Now, you can implement it in the `ModularCrm.Catalog` project. Create an `Integration` folder and then create a `ProductIntegrationService` class in that folder. The final folder structure should be like this:
The implementation is pretty simple. Just using a [repository](../../framework/architecture/domain-driven-design/repositories.md) to query `Product` [entities](../../framework/architecture/domain-driven-design/entities.md).
> Here, we directly used `List<T>` classes, but instead, you could wrap inputs and outputs into [DTOs](../../framework/architecture/domain-driven-design/data-transfer-objects.md). In that way, it can be possible to add new properties to these DTOs without changing the signature of your integration service method (and without introducing breaking changes for your client modules).
> Here, you directly used `List<T>` classes, but instead, you could wrap inputs and outputs into [DTOs](../../framework/architecture/domain-driven-design/data-transfer-objects.md). In that way, it can be possible to add new properties to these DTOs without changing the signature of your integration service method (and without introducing breaking changes for your client modules).
## Consuming the Products Integration Service
The Product Integration Service is ready for the other modules to use. In this section, we will use it in the Ordering module to convert product IDs to product names.
The Product Integration Service is ready for the other modules to use. In this section, you will use it in the Ordering module to convert product IDs to product names.
### Adding a Reference to the `ModularCrm.Products.Application.Contracts` Package
### Adding a Reference of the `ModularCrm.Catalog.Contracts` Package
Open the ABP Studio UI and stop the application if it is already running. Then open the *Solution Explorer* in ABP Studio, right-click the `ModularCrm.Ordering` package and select the *Add Package Reference* command:
ABP Studio adds the package reference and arranges the [module](../../framework/architecture/modularity/basics.md) dependency.
> Instead of directly adding such a package reference, it can be best to import the module first (right-click the `ModularCrm.Ordering` module, select the _Import Module_ command and import the `ModularCrm.Products` module), then install the package reference. In that way, it would be easy to see and keep track of inter-module dependencies.
> Instead of directly adding such a package reference, it can be possible to import the module first (right-click the `ModularCrm.Ordering` module, select the _Import Module_ command and import the `ModularCrm.Catalog` module), then add the package references. ABP automatically import module when you add a package reference from a local module, but for other sources you may need to do it manually.
### Using the Products Integration Service
Now, we can inject and use `IProductIntegrationService` in the Ordering module codebase.
Now, you can inject and use `IProductIntegrationService` in the Ordering module codebase.
Open the `OrderAppService` class (the `OrderAppService.cs` file under the `Services` folder of the `ModularCrm.Ordering` project of the `ModularCrm.Ordering` .NET solution) and change its content as like the following code block:
Open the `OrderAppService` class of the `ModularCrm.Ordering` project of the `ModularCrm.Ordering` .NET solution and change its content as like the following code block:
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ModularCrm.Ordering.Enums;
using ModularCrm.Ordering.Entities;
using ModularCrm.Products.Integration;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace ModularCrm.Ordering.Services;
namespace ModularCrm.Ordering;
public class OrderAppService : ApplicationService, IOrderAppService
@ -207,21 +205,20 @@ public class OrderAppService : ApplicationService, IOrderAppService
}
````
And also, open the `OrderDto` class (the `OrderDto.cs` file under the `Services` folder of the `ModularCrm.Ordering.Contracts` project of the `ModularCrm.Ordering` .NET solution) and add a `ProductName` property to it:
And also, open the `OrderDto` class of the `ModularCrm.Ordering.Contracts` project of the `ModularCrm.Ordering` .NET solution and add a `ProductName` property to it:
````csharp
using System;
using ModularCrm.Ordering.Enums;
namespace ModularCrm.Ordering.Services;
namespace ModularCrm.Ordering;
public class OrderDto
{
public Guid Id { get; set; }
public string CustomerName { get; set; }
public string CustomerName { get; set; } = null!;
public Guid ProductId { get; set; }
public string ProductName { get; set; } // New property
public OrderState State { get; set; }
public string ProductName { get; set; } = null!; // New Property
}
````
@ -229,8 +226,6 @@ Lastly, open the `OrderingAutoMapperProfile` class (the `OrderingAutoMapperProfi
````csharp
using AutoMapper;
using ModularCrm.Ordering.Services;
using ModularCrm.Ordering.Entities;
using Volo.Abp.AutoMapper;
namespace ModularCrm.Ordering;
@ -248,11 +243,11 @@ public class OrderingApplicationAutoMapperProfile : Profile
Let's see what we've changed:
* We've added a `ProductName` property to the `OrderDto` class to store the product name.
* Injecting the `IProductIntegrationService` interface so we can use it to request products.
* Injecting the `IProductIntegrationService` interface so you can use it to request products.
* In the `GetListAsync` method;
* First getting the orders from the ordering module's database just like done before.
* Next, we are preparing a unique list of product IDs since the `GetProductsByIdsAsync` method requests it.
* Then we are calling the `IProductIntegrationService.GetProductsByIdsAsync` method to get a `List<ProductDto>` object.
* Next, you are preparing a unique list of product IDs since the `GetProductsByIdsAsync` method requests it.
* Then you are calling the `IProductIntegrationService.GetProductsByIdsAsync` method to get a `List<ProductDto>` object.
* In the last line, we are converting the product list to a dictionary, where the key is `Guid Id` and the value is `string Name`. That way, we can easily find a product's name with its ID.
* Finally, we are mapping the orders to `OrderDto` objects and setting the product name by looking up the product ID in the dictionary.
@ -286,8 +281,10 @@ That's all. Now, you can graph build the main application and run it in ABP Stud
As you can see, we can see the product names instead of product IDs.
In the way explained in this section, you can easily create integration services for your modules and consume these integration services in any other module.
> **Design Tip**
>
> It is suggested that you keep that type of communication to a minimum and not couple your modules with each other. It can make your solution complicated and may also decrease your system performance. When you need to do it, think about performance and try to make some optimizations. For example, if the Ordering module frequently needs product data, you can use a kind of [cache layer](../../framework/fundamentals/caching.md), so it doesn't make frequent requests to the Products module. Especially if you consider converting your system to a microservice solution in the future, too many direct integration API calls can be a performance bottleneck.
> It is suggested that you keep that type of communication to a minimum and not couple your modules with each other. It can make your solution complicated and may also decrease your system performance. When you need to do it, think about performance and try to make some optimizations. For example, if the Ordering module frequently needs product data, you can use a kind of [cache layer](../../framework/fundamentals/caching.md), so it doesn't make frequent requests to the Catalog module. Especially if you consider converting your system to a microservice solution in the future, too many direct integration API calls can be a performance bottleneck.
## Conclusion
In the way explained in this part of this tutorial, you can easily create integration services for your modules and consume these integration services in any other module. In the [next part](part-07.md), we will explore event based messaging between the modules.
Another common approach to communicating between modules is messaging. By publishing and handling messages, a module can perform an operation when an event happens in another module.
## Understanding the Event Bus Types
ABP provides two types of event buses for loosely coupled communication:
* [Local Event Bus](../../framework/infrastructure/event-bus/local/index.md) is suitable for in-process messaging. Since in a modular monolith, both of publisher and subscriber are in the same process, they can communicate in-process, without needing an external message broker.
@ -29,7 +31,7 @@ We will use the distributed event bus since we will use messaging (events) betwe
## Publishing an Event
In the example scenario, we want to publish an event when a new order is placed. The Ordering module will publish the event since it knows when a new order is placed. The Products module will subscribe to that event and get notified when a new order is placed. This will decrease the stock count of the product related to the new order. The scenario is pretty simple; let's implement it.
In the example scenario, we want to publish an event when a new order is placed. The Ordering module will publish the event since it knows when a new order is placed. The Catalog module will subscribe to that event and get notified when a new order is placed. This will decrease the stock count of the product related to the new order. The scenario is pretty simple; let's implement it.
### Defining the Event Class
@ -42,30 +44,27 @@ We've placed the `OrderPlacedEto` class inside the `ModularCrm.Ordering.Contract
````csharp
using System;
namespace ModularCrm.Ordering.Events
namespace ModularCrm.Ordering.Events;
public class OrderPlacedEto
{
public class OrderPlacedEto
{
public string CustomerName { get; set; }
public Guid ProductId { get; set; }
}
public string CustomerName { get; set; } = null!;
public Guid ProductId { get; set; }
}
````
`OrderPlacedEto` is very simple. It is a plain C# class used to transfer data related to the event (*ETO* is an acronym for *Event Transfer Object*, a suggested naming convention but not required). You can add more properties if needed, but for this tutorial, it is more than enough.
`OrderPlacedEto` is very simple. It is a plain C# class used to transfer data related to the event (*ETO* is an acronym for *Event Transfer Object*, a suggested naming convention by the ABP team, but not technically required). You can add more properties if needed, but for this tutorial, that is more than enough.
### Using the `IDistributedEventBus` Service
The `IDistributedEventBus` service publishes events to the event bus. Until this point, the Ordering module has no functionality to create new orders. Let's change that and place an order, for that purpose open the `ModularCrm.Ordering` module's .NET solution, and update the `OrderAppService` as follows:
The `IDistributedEventBus` service publishes events to the event bus. Open the `ModularCrm.Ordering` module's .NET solution, and update the `OrderAppService` as follows:
@ -132,17 +131,17 @@ public class OrderAppService : OrderingAppService, IOrderAppService
}
````
The `OrderAppService.CreateAsync` method creates a new `Order` entity, saves it to the database and finally publishes an `OrderPlacedEto` event.
We've changed the `CreateAsync` method. Now it creates a new `Order` entity, saves it to the database and finally publishes an `OrderPlacedEto` event.
## Subscribing to an Event
This section will subscribe to the `OrderPlacedEto` event in the Products module and decrease the related product's stock count once a new order is placed.
This section will subscribe to the `OrderPlacedEto` event in the Catalog module and decrease the related product's stock count once a new order is placed.
### Adding a Reference to the `ModularCrm.Ordering.Contracts` Package
### Adding a Reference of the `ModularCrm.Ordering.Contracts` Package
Since the `OrderPlacedEto` class is in the `ModularCrm.Ordering.Contracts` project, we must add that package's reference to the Products module. This time, we will use the *Import Module* feature of ABP Studio (as an alternative to the approach we used in the *Adding a Reference to the `ModularCrm.Products.Application.Contracts` Package* section of the [previous part](part-06.md)).
Since the `OrderPlacedEto` class is in the `ModularCrm.Ordering.Contracts` project, we must add that package's reference to the Catalog module. This time, we will use the *Import Module* feature of ABP Studio (as an alternative to the approach we used in the *Adding a Reference to the `ModularCrm.Catalog.Contracts` Package* section of the [previous part](part-06.md)).
Open the ABP Studio UI and stop the application if it is already running. Then open the *Solution Explorer* in ABP Studio, right-click the `ModularCrm.Products` module and select the *Import Module* command:
Open the ABP Studio UI and stop the application if it is already running. Then open the *Solution Explorer* in ABP Studio, right-click the `ModularCrm.Catalog` module and select the *Import Module* command:
Here, select the `ModularCrm.Ordering.Contracts` package on the left side (because we want to add that package reference) and `ModularCrm.Products.Domain` package on the middle area (because we want to add the package reference to that project). We installed it on the [domain layer](../../framework/architecture/domain-driven-design/domain-layer.md) of the Products module since we will create our event handler in that layer. Click the OK button to finish the installation operation.
Here, select the `ModularCrm.Ordering.Contracts` package on the left side (because we want to add that package reference) and `ModularCrm.Catalog` package on the middle area (because we want to add the package reference to that project). Also, select the `ModularCrm.Ordering` package on the right side, and unselect all packages on the middle area (we don't need the implementation or any other packages). Then, click the OK button to finish the installation operation.
You can check the ABP Studio's *Solution Explorer* panel to see the module import and the project reference (dependency).
Now, it is possible to use the `OrderPlacedEto` class inside the Product module's domain layer since it has the `ModularCrm.Ordering.Contracts` package reference.
Now, it is possible to use the `OrderPlacedEto` class inside the Catalog module since it has the `ModularCrm.Ordering.Contracts` package reference.
Open the Product module's .NET solution in your IDE, locate the `ModularCrm.Products.Domain` project, and create a new `Orders` folder and an `OrderEventHandler` class inside that folder. The final folder structure should be like this:
Open the Catalog module's .NET solution in your IDE, locate the `ModularCrm.Catalog` project, and create a new `Orders` folder and an `OrderEventHandler` class inside that folder. The final folder structure should be like this:
We inject the product repository and update the stock count in the event handler method (`HandleEventAsync`). That's it.
### Testing the Order Creation
## Testing the Order Creation
To keep this tutorial more focused, we will not create a UI for creating an order. You can easily create a form to create an order on your user interface. In this section, we will test it just using the Swagger UI.
@ -233,7 +232,7 @@ Find the *Orders* API, click the *Try it out* button, enter a sample value the t
}
````
> **IMPORTANT:** Here, you should type a valid Product Id from the Products table of your database!
> **IMPORTANT:** Here, you should type a valid product Id from the *CatalogProducts* table of your database!
Once you press the *Execute* button, a new order is created. At that point, you can check the `/Orders` page to see if the new order is shown on the UI, and check the `/Products` page to see if the related product's stock count has decreased.
@ -242,3 +241,7 @@ Here are sample screenshots from the Products and Orders pages:
We placed a new order for Product C. As a result, Product C's stock count has decreased from 55 to 54 and a new line is added to the Orders page.
## Conclusion
In this part, we've used ABP's distributed event bus to perform loosely coupled messaging between the modules. In the [next part](part-08.md), we will execute a database query that includes product and order data as an alternative way of integrating modules' data.
@ -14,60 +14,40 @@ In this part, you will learn how to perform a database-level JOIN operation on t
## The Problem
One essential purpose of modularity is to create modules that hide (encapsulate) their internal data and implementation details from the other modules. These modules communicate with each other through well-defined [integration services](../../framework/api-development/integration-services.md) and [events](framework/infrastructure/event-bus/distributed). In that way, you can independently develop and change module implementations (even modules' database structures) from each other as long as you don't break these inter-module integration points.
One essential purpose of modularity is to create modules that hide (encapsulate) their internal data and implementation details from the other modules. These modules communicate with each other through well-defined [integration services](../../framework/api-development/integration-services.md) and [events](framework/infrastructure/event-bus/distributed). In that way, you can develop and change module implementations (even modules' database structures) independently from each other as long as you don't introduce break changes on these module integration points.
In a non-modular application, accessing the related data is easy. You could write a LINQ expression that joins `Orders` and `Products` database tables to get the data with a single database query. It would be easier to implement and execute with a good performance.
On the other hand, it becomes harder to perform operations or get reports requiring access to multiple modules' internal data in a modular system. Remember the *[Implementing Integration Services](part-06.md)* part; We couldn't access the product data inside the Ordering module (`IOrderingDbContext` only defines a `DbSet<Order>`), so we needed to create an integration service just to get names of products. This approach is harder to implement and less performant (yet it is acceptable if you don't show too many orders on the UI or properly implement a caching layer). Still, it gives freedom to the Products module about its internal database or application logic changes. For example, you can decide to move product data to another physical database or even to another database management system (DBMS) without affecting the other modules.
On the other hand, it becomes harder to perform operations or get reports requiring access to multiple modules' internal data in a modular system. Remember the *[Implementing Integration Services](part-06.md)* part; We couldn't access the product data inside the Ordering module (`IOrderingDbContext` only defines a `DbSet<Order>`), so we needed to create an integration service just to get names of products with a list of IDs. This approach is harder to implement and less performant (yet it is acceptable if you don't show too many orders on the UI or properly implement a caching layer). Still, it gives freedom to the Catalog module about its internal database or application logic changes. For example, you can decide to move product data to another physical database or even to another database management system (DBMS) without affecting the other modules.
## A Solution Option
If you want to perform a single database query that spans database tables of multiple modules in a modular system, you still have some options. One option can be creating a reporting module with access to all entities (or database tables). However, when you do that, you accept the following limitations:
* When you change a module's database structure, you should also update your reporting code. That is reasonable, but all module developers should let you know in such a case.
* You can not change the DBMS of a module easily. For example, performing such a JOIN operation would be impossible if you decide to use MongoDB for your Products module while the Ordering module still uses SQL Server. Moving the Products module to another SQL Server database in another physical server can also break your reporting logic.
* You can not change the DBMS of a module easily. For example, performing such a JOIN operation would be impossible if you decide to use MongoDB for your Catalog module while the Ordering module still uses SQL Server. Moving the Catalog module to another SQL Server database in another physical server can also break your reporting logic.
If these are not problems for you, or if you can handle them when they become problems, you can create reporting modules or aggregator modules that work with multiple modules' data.
In the next section, we will use the main application's codebase to implement such a JOIN operation to keep the tutorial short. However, you already learned how to create new modules, so you can create a new module and develop your JOIN logic inside that new module if you want.
In the next section, we will use the main application's codebase to implement such a JOIN operation to keep the tutorial short. However, you already learned how to create new modules, so you can create a new reporting module and develop your JOIN logic inside that new module if you want.
## The Implementation
In this section, we will create an application service in the main application's .NET solution. That application service will perform a LINQ operation on the `Product` and `Order` entities.
### Defining the Reporting Service Interface
We will define the `IOrderReportingAppService` interface in the main application's .NET solution.
As the first step, we should reference the `ModularCrm.Ordering.Contracts` package (of the `ModularCrm.Ordering` module) since we will reuse the `OrderState` enum defined in that package.
Open the ABP Studio's *Solution Explorer* panel, right-click the `ModularCrm` package and select the *Add Package Reference* command:
The package reference has been added, and we can now use the types in the `ModularCrm.Ordering.Contracts` package.
#### Defining the `IOrderReportingAppService` Interface
### Defining the `IOrderReportingAppService` Interface
Open the main `ModularCrm` .NET solution in your IDE, create an `Orders` folder under the `Services` folder and add an `IOrderReportingAppService` interface. Here is the definition of that interface:
````csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using ModularCrm.Orders;
using Volo.Abp.Application.Services;
namespace ModularCrm.Orders
namespace ModularCrm.Services.Orders;
public interface IOrderReportingAppService : IApplicationService
{
public interface IOrderReportingAppService : IApplicationService
{
Task<List<OrderReportDto>> GetLatestOrders();
}
Task<List<OrderReportDto>> GetLatestOrders();
}
````
@ -75,25 +55,24 @@ We have a single method, `GetLatestOrders`, that will return a list of the lates
````csharp
using System;
using ModularCrm.Ordering.Contracts.Enums;
using ModularCrm.Ordering;
namespace ModularCrm.Orders;
namespace ModularCrm.Orders
public class OrderReportDto
{
public class OrderReportDto
{
// Order data
public Guid OrderId { get; set; }
public string CustomerName { get; set; }
public OrderState State { get; set; }
// Product data
public Guid ProductId { get; set; }
public string ProductName { get; set; }
}
// Order data
public Guid OrderId { get; set; }
public string CustomerName { get; set; } = null!;
public OrderState State { get; set; }
// Product data
public Guid ProductId { get; set; }
public string ProductName { get; set; } = null!;
}
````
`OrderReportDto` contains data from both the `Order` and `Product` entities. We could use the `OrderState` since we have a reference to the package that defines that enum.
`OrderReportDto` contains data from both the `Order` and `Product` entities.
After adding these files, the final folder structure should be like this:
@ -106,52 +85,49 @@ Create a class named `OrderReportingAppService` under the `Services/Orders` fold
Open the `OrderReportingAppService.cs` file and change its content by the following code block:
public async Task<List<OrderReportDto>> GetLatestOrders()
{
var orders = await _orderRepository.GetQueryableAsync();
var products = await _productRepository.GetQueryableAsync();
var latestOrders = (from order in orders
join product in products on order.ProductId equals product.Id
orderby order.CreationTime descending
select new OrderReportDto
{
OrderId = order.Id,
CustomerName = order.CustomerName,
State = order.State,
ProductId = product.Id,
ProductName = product.Name
})
.Take(10)
.ToList();
return latestOrders;
}
var orders = await _orderRepository.GetQueryableAsync();
var products = await _productRepository.GetQueryableAsync();
var latestOrders = (from order in orders
join product in products on order.ProductId equals product.Id
orderby order.CreationTime descending
select new OrderReportDto
{
OrderId = order.Id,
CustomerName = order.CustomerName,
State = order.State,
ProductId = product.Id,
ProductName = product.Name
})
.Take(10)
.ToList();
return latestOrders;
}
}
````
@ -168,11 +144,11 @@ That's all. In that way, you can execute JOIN queries that use data from multipl
We haven't created a UI to show list of the latest orders using `OrderReportingAppService`. However, we can use the Swagger UI again to test it.
Open the ABP Studio UI, stop the application if it is running, build and run it again. Once the application starts, browse it, then add `/swagger` to the end of the URL to open the Swagger UI:
Open the ABP Studio UI, stop the application if it is running, build and run it again. Once the application starts, browse it, then add `/swagger` to the end of the URL to open the Swagger UI. Here, find the `OrderReporting` API and execute it as shown below:
Here, find the `OrderReporting` API and execute it as shown above. You should get the order objects with product names.
You should get the order objects with product names.
Alternatively, you can visit the `/api/app/order-reporting/latest-orders` URL to directly execute the HTTP API on the browser (you should write the full URL, like `https://localhost:44303/api/app/order-reporting/latest-orders` - port can be different for your case)
@ -188,7 +164,7 @@ Now, you know the fundamental principles and mechanics of building sophisticated
## Download the Source Code
You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCrm).
You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCrm-Standard).