Integrating the Modules: Joining the Products and Orders Data

In this part, you will learn how you can make database-level JOIN operation on data of multiple modules, when their data located in the same physical database.

The Problem

One of the essential purposes of modularity is to create modules those hide (encapsulate) their internal data and implementation details from the other modules. They communicate each other through well-defined integration services and events. 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.

In a non-modular application, accessing the related data is easy. You could just 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 executed with a good performance.

On the other hand, it becomes harder to perform operations or get reports those requires to access data of multiple modules in a modular system. Remember the Implementing Integration Services 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 services 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 if you properly implement a caching layer), but 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 effecting 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 have still some options. One option can be creating a reporting module that has access to all of the entities (or database tables). However, when you do that, you accept the following limitations:

  • When you make changes in a module's database structure, you should also update your reporting code. That is reasonable, but you should be informed by all module developers in such a case.
  • You can not change DBMS of a module easily. For example, if you decide to use MongoDB for your Products module while the Ordering module still uses SQL Server, performing such a JOIN operation would not be possible. Even moving Products module to another SQL Server database in another physical server can break your reporting logic.

If these are not problems for you, or you can handle when they become problems, you can create reporting modules or aggregator modules that works 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.

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 ModularCrm.Application.Contracts project of the main application's .NET solution.

Adding ModularCrm.Ordering.Contracts Package Reference

As the first step, we should to add a reference of the ModularCrm.Ordering.Contracts package (of the ModularCrm.Ordering module) since we will reuse the OrderState enum which is defined in that package.

Open the ABP Studio's Solution Explorer panel, right-click the ModularCrm.Application.Contracts package and select the Add Package Reference command:

abp-studio-add-package-reference-5

Select the Imported modules tab, find and check the ModularCrm.Ordering.Contracts package and click the OK button:

abp-studio-add-package-reference-dialog-4

The package reference is added and now we can use the types in the ModularCrm.Ordering.Contracts package.

Defining the IOrderReportingAppService Interface

Open the main ModularCrm .NET solution in your IDE, find the ModularCrm.Application.Contracts project, create an Orders folder and add an IOrderReportingAppService interface inside it. Here the definition of that interface:

using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;

namespace ModularCrm.Orders
{
    public interface IOrderReportingAppService : IApplicationService
    {
        Task<List<OrderReportDto>> GetLatestOrders();
    }
}

We have a single method, GetLatestOrders, that will return a list of the latest orders. We should also define the OrderReportDto class that is returned by that method. Create the following class in the same Orders folder:

using System;
using ModularCrm.Ordering.Contracts.Enums;

namespace ModularCrm.Orders
{
    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; }
    }
}

OrderReportDto contains data from both of Order and Product entities. We could use the OrderState since we have a reference to the package which defines that enum.

After adding these files, the final folder structure should be like that:

visual-studio-order-reporting-app-service

Implementing the OrderReportingAppService Class

Create an Orders folder inside of the ModularCrm.Application project and add a class named OrderReportingAppService inside it. The final folder structure should be like that:

visual-studio-order-reporting-app-service-impl

Open the OrderReportingAppService.cs file and change its content by the following code block:

using ModularCrm.Ordering.Entities;
using ModularCrm.Products;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;

namespace ModularCrm.Orders
{
    public class OrderReportingAppService :
    	ModularCrmAppService,
        IOrderReportingAppService
    {
        private readonly IRepository<Order, Guid> _orderRepository;
        private readonly IRepository<Product, Guid> _productRepository;

        public OrderReportingAppService(
            IRepository<Order, Guid> orderRepository,
            IRepository<Product, Guid> productRepository)
        {
            _orderRepository = orderRepository;
            _productRepository = productRepository;
        }

        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;
        }
    }
}

Let's explain that class;

  • It inject repository services for Order and Product entities. We can access all entities of all modules from the main application codebase.
  • In the GetLatestOrders method, we are getting IQueryable objects for the entities, so we can create LINQ expressions.
  • Then we are executing a LINQ expression with join keyword, so we can be able to execute a single query that uses multiple tables.

That's all. In that way, you can execute JOIN queries that uses multiple modules' data. However, if you write majority of your application code into the main application and perform operations on multiple modules, then your system won't be so modular. So, here we show it is technically possible. Please use at your own risk.

Testing the Reporting Service

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:

abp-studio-swagger-list-orders

Here, find the OrderReporting API and execute it as shown above. 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:44358/api/app/order-reporting/latest-orders - port can be different for your case)

Summary

In the last three parts of this tutorial, you have learned three ways of integrating your application modules:

  1. You can use the integration services to make request/response style communication between your modules.
  2. You can publish events from a module and subscribe to these events from other modules.
  3. You can write your code into the main application, so you can access to all entities (and related data) of all modules. Instead of writing it into the main application code, you can also create some aggregation or reporting modules that can access more than one module entities.

Now, you know the fundamental principles and mechanics to build sophisticated modular monolith application with ABP.

Download the Source Code

You can download the completed sample solution here.

See Also

See the following sections for additional resources.

The Book Store Tutorial

In this tutorial, we intentionally kept the application logic very simple and didn't build a usable user interface for the modules. Also, didn't implement authorization and localization for the modules. This was to keep your focus on modularity. If you want to learn how to build real-world user interfaces with ABP, you can check the Book Store tutorial. All the principles and approaches explained there are already possible with a modular system too.

ABP Reusable Application Modules

ABP is designed as modular from the first day. The ABP team has created tens of production-ready and reusable application modules. You can investigate these modules (some of them are already free and open source) to see real-world modules.

When you create a new ABP solution, some of these modules are already comes as installed into your application (as NuGet and NPM packages). So, your initial ABP application is already a modular application from day one.

Guide: Module Development Best Practices & Conventions

ABP team has created the reusable application modules based on some strict principles and rules to make them reusable as much as possible in different scenarios, including modular monolith applications and microservice systems.

These application modules are designed so that they are generic, extensible, doesn't depend on each other, reusable in any web application, provides multiple UI and database options, etc. In a monolith modular application, you mostly don't have such requirements. But, you can still check the best practices guides that is prepared by the ABP team and followed while implementing these application modules.


Contributors


Last updated: September 19, 2024 Edit this page on GitHub

Was this page helpful?

Please make a selection.

To help us improve, please share your reason for the negative feedback in the field below.

Please enter a note.

Thank you for your valuable feedback!

Please note that although we cannot respond to feedback, our team will use your comments to improve the experience.

In this document
Community Talks

ABP Studio: The Missing Tool for .NET Developers

15 Aug, 17:00
Online
Watch the Event
Mastering ABP Framework Book
Mastering ABP Framework

This book will help you gain a complete understanding of the framework and modern web application development techniques.

Learn More