This is a theoretical question.
Sample problem: Let's assume I have modules/domains named Customers/Appointments/Finances. The Customers module is supposed to work independently of the others, with its own UI, entities, DTOs, DbContext. The Appointments and Finances modules have a dependency on the Customers module as their entities are connected to the Customers.
Let's say I have the need to conditionally show some extra information in the Customers list (grid), which can be obtained from the Appointments (next appointment date) and the Finances (last payment date) modules. This information should be shown conditionally when the modules for Appointments and/or Finances are setup and have registered their services. Otherwise, this information should not be shown.
Current way of thinking: I understand that the Customers module can gather injected services that add extra information to the customer list and have the Appointments and Finances modules implement such services. However, in order to prevent one call to the database per customer in the list per module providing extra information, I need a way to perform a join along with the customer query or use the customer query in the other modules to perform the join there (making it 3 queries).
To do one query with joins (using EF Core), I injected the entity information from each module to the Customer module's DbContext (to perform the joins between entities), I created a model that holds the Customer entity and an IEnumerable<object> of joined information, modified the getList query to project the customer list to that model and passed the query to multiple injected services that added their joins and placed each joined entity into that IEnumerable<object> of the container model.
To pass that from the Application layer to the UI layer, I would need to create an equivalent DTO (instead of the CustomerDTO, a CustomerWithExtraInfoDTO).
Generalized problem: However, at this point, I began thinking that a DTO with a list of unknown objects attached to it kind of defeats the purpose of a known data transfer interface. Also, while I succeeded in injecting services that perform a simple join with the main entity, what if I need those services to filter the customer list or connect the extra information through other logic?
So, the main problem is how to ensure that the Customers module can stay pure without being contaminated with entities or logic from subsequently developed modules, while still being extensible enough to have other modules inject logic and information (in an efficient way when it comes to queries)?
I am already searching for best practices in revelant docs (such as DDD books), but I thought I'd ask here as well believing that this issue must be common enough and maybe the ABP framework provides something for such purposes.
1 Answer(s)
-
0
To achieve dynamic, efficient extension of your Customers module with extra information from other modules (like Appointments and Finances) in ABP, while maintaining modular boundaries and performance, you should consider the following best practices:
Modular Data Isolation & Integration:
- Each module (Customers, Appointments, Finances) should own its own data and DbContext.
- Direct joins across DbContexts are not supported by EF Core and are discouraged in modular architectures for decoupling reasons.
- Instead, use integration services or events to communicate between modules.
Extending the Customer List with Extra Info:
- The recommended approach is to use a ViewModel composition pattern at the application or UI layer, not at the entity or DbContext level.
- Each module can provide a service that contributes extra data for a given set of customer IDs.
- The Customers module exposes an extension point (e.g., an interface like ICustomerExtraInfoProvider).
- Appointments and Finances modules implement this interface and register their providers conditionally.
Efficient Data Fetching:
- When building the customer list, first fetch the customers in a single query.
- Then, for each registered ICustomerExtraInfoProvider, call a batch method (e.g., GetExtraInfoForCustomersAsync(IEnumerable<Guid> customerIds)) to fetch all needed extra info in one query per module.
- Compose the final DTO (e.g., CustomerWithExtraInfoDto) by merging the base customer data with the extra info from each provider.
DTO Design:
- Use a dictionary or a well-defined extension object in your DTO to hold extra info, e.g.:
public class CustomerWithExtraInfoDto : CustomerDto { public Dictionary<string, object> ExtraInfo { get; set; } }
- Each provider can add its data under a unique key (e.g., "NextAppointmentDate", "LastPaymentDate").
Conditional UI Display:
- The UI can check for the presence of specific keys in ExtraInfo to decide what to display.
- This keeps the Customers module agnostic of the specifics of other modules.
Filtering and Advanced Logic:
- If extra info providers need to filter the customer list, you can allow them to return a filtered set of customer IDs, and intersect these with the base list.
- For more complex scenarios, consider a two-phase approach: providers can first filter, then contribute extra info.
ABP Guidance:
- ABP recommends using integration services and view model composition for such cross-module data needs, especially in modular and microservice architectures.
- See the following for more details and patterns:
This approach keeps your modules decoupled, supports dynamic discovery of extra info providers, and ensures efficient data access.
Sources:
- How to Use Multiple DbContexts in a Single Query Execution
- Integrating the Modules: Joining the Products and Orders Data
- ABP Support Q&A: ViewModel Composition
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.