Intent.DocumentDB.Dtos.AutoMapper
What This Module Does
Extends the base DTO + AutoMapper generation so that DTOs whose field mappings traverse DocumentDB aggregate boundaries are correctly populated. It injects repository lookups and mapping logic for cross-aggregate fields after AutoMapper's standard property mapping has run.
Why You Need It
When a DTO field maps to data located in a different aggregate (e.g. navigating through an association chain that crosses aggregate roots), a simple AutoMapper configuration cannot materialize that data because it requires additional repository queries. This module detects those situations and generates an AfterMap hook with a MappingAction class that performs the necessary loads.
Use it when:
- You have DTOs whose field mapping path includes an aggregational association (collection or nullable end) not directly exposed on the source entity's public properties.
- You need to populate DTO fields from related aggregates using repository access within AutoMapper.
How It Works
During template registration a factory extension (DtoAutoMapperFactoryExtension) runs CrossAggregateMappingConfigurator.Execute(...):
- Locates all DTO templates (
DtoModelTemplate) that are mapped (templateModel.Mapping!= null). - For each DTO, inspects each mapped field's path segments. If any segment is an aggregational association and the source entity does not expose a matching property, the DTO is considered a cross-aggregate mapping candidate.
- Modifies the generated mapping profile (location determined by the Application.Dtos settings: Profile in DTO class or separate profile) to append
AfterMap<MappingAction>()to the mapping chain. - Generates a nested
MappingActionclass that implementsIMappingAction<SourceEntity, Dto>. - Injects required repositories (resolved via
EntityRepositoryInterfaceTemplate) andIMapperinto theMappingActionconstructor. - In
Process(source, destination, context)it:- Computes ordered load instructions for each aggregate path (ensuring parent aggregates load first).
- Uses repository
FindByIdAsync/FindByIdsAsyncto materialize each aggregate based on foreign key attributes. - Throws a
NotFoundExceptionwhen a required (non-nullable) single aggregate cannot be loaded. - Assigns destination DTO properties from loaded aggregates, mapping nested DTO-valued fields using generated
MapTo{Dto}helper functions. - Handles optional and "expression optional" segments with null checks.
- If DTO property setter accessibility is configured as
privateorinit(DTO Settings), property setters for affected fields are elevated tointernalto allow mutation in the AfterMap stage.
Generated Artifacts
- Augmented mapping chain:
AfterMap<MappingAction>(). - Nested
MappingActionclass containing repository fields and aProcessmethod. - Internal setters for certain DTO properties (only when necessary to enable assignment post-map).
Key Implementation Details
- Aggregational association detection: An association end is considered aggregational if the source end is a collection or nullable.
- Load ordering: Aggregates load in ascending path length to respect dependency ordering.
- Foreign key resolution: For each associated aggregate the code searches the target entity for a FK attribute referencing the association's target end; absence raises an exception.
- Optional path handling: Two booleans distinguish optional association vs optional expression segment (
IsOptionalandIsExpressionOptional). Expression optionality currently relies on a simple?presence check in the path expression. - Nested DTO mapping: DTO-type fields invoke generated
MapTo{Dto}orMapTo{Dto}Listhelpers with_mapper. - Error handling: Required single aggregate null -> throws
NotFoundExceptionincluding the attempted key value. - Synchronous waits: Repository async calls are awaited via
.Resultinside the mapping action; consider refactoring for fully async pipelines if necessary.
Configuration & Dependencies
- Depends on base modules:
Intent.Application.Dtos,Intent.Application.Dtos.AutoMapper,Intent.Entities.Repositories.Api. - Respects setting: DTO AutoMapper Profile Location (profile in DTO vs separate profile affects where AfterMap is injected).
- Respects DTO Settings: Property Setter Accessibility (may elevate to internal).
Performance Considerations
- Multiple repository calls per mapped DTO can incur N+1 queries. Group or batch logic may be desirable for large collections – you can customize the generated
MappingActionbecause its body is produced in merge mode. .Resultusage can block; in high-throughput asynchronous contexts consider customizing to use fully async mapping patterns.
Customization Points
- The nested
MappingActionclass can be modified (merge mode) to add caching, batching, or alternative loading strategies. - You can introduce additional safety checks or logging around repository calls.
- Adjust exception messages or replace with domain-specific error handling as needed.
Limitations
- Optional expression detection is heuristic (searches for
?in path expression); complex optional patterns may require manual refinement. - Assumes FK attributes exist; if foreign key modeling is absent generation will throw at build time.
- Does not attempt to minimize duplicate repository loads when multiple DTO fields traverse identical aggregate paths (can be manually optimized).
When Not To Use
- DTOs confined to a single aggregate root (standard AutoMapper from
Intent.Application.Dtos.AutoMappersuffices). - Read models where manual projection queries (e.g., LINQ with joins) are more efficient than per-field repository lookups.
Related Modules
Intent.DocumentDB(aggregate modeling & persistence abstraction)Intent.Application.DtosIntent.Application.Dtos.AutoMapperIntent.Entities.Repositories.Api
Example (Conceptual)
A OrderDetailsDto maps Customer.Email where Customer is a separate aggregate not directly exposed as a navigation property on Order. The module:
- Detects that
Customerassociation segment requires cross-aggregate access. - Adds
AfterMap<MappingAction>(). - In
MappingAction.Processloads theCustomerviaICustomerRepository.FindByIdAsync(order.CustomerId). - Assigns
destination.CustomerEmail = customer.Email.
Extending Further
If you need batching, consider collecting all keys first, performing a single FindByIdsAsync per aggregate, and indexing results for assignment.