WHAT WAS THE CONTEXT AND HOW DID WE DISCOVER THE MAPPING ISSUE?
While working on a large-scale financial ERP system modernization, our engineering team was tasked with heavily optimizing the data aggregation layer. The system required us to pull data from separate domains, specifically joining primary financial ledgers with metadata tables. We relied heavily on DOTNET 8 and CSHARP source generators to guarantee high-performance execution without the overhead of runtime reflection. Mapperly was our tool of choice for mapping entities to Data Transfer Objects.
During a recent sprint, we realized that our aggregated API responses were missing critical data. We encountered a situation where a LINQ join was combining two distinct entities, but the resulting DTO only contained data from the first entity. The secondary object properties were completely ignored by the mapper. In production, this type of silent mapping failure can lead to incomplete data being served to client applications or downstream integration services.
For organizations looking to hire software developer talent, handling these specific nuances of compile-time code generation is a key marker of engineering maturity. This challenge inspired the article so other architecture teams can avoid the same pitfall when performing multi-source mapping with source generators.
WHY DID THE ARCHITECTURE REQUIRE JOINING MULTIPLE ENTITIES?
In microservice and modular monolith architectures, data is often normalized across multiple tables. For this use case, we had a primary entity holding the core transaction ID and baseline details and a secondary entity holding enriched classification metadata. To serve the frontend application efficiently, we needed to return a single flattened DTO.
Using Entity Framework and LINQ, the most efficient way to query this data in a single database round trip is by using a join operation, projecting the results into an anonymous type and then mapping that result into the final DTO. The problem surfaced exactly at the boundary between the LINQ projection and the Mapperly execution.
WHAT CAUSED MAPPERLY TO IGNORE THE SUBSEQUENT OBJECT PROPERTIES?
When mapping a DTO from multiple source parameters, engineers intuitively expect the mapper to flatten all provided arguments into the target object. In our implementation, we defined a mapping method that accepted two parameters.
We called this mapping inside a LINQ Select statement after joining the two collections. The mapped result contained the first object mapped correctly, but the properties from the second object were entirely null or default.
The root cause lies in how source generators like Mapperly interpret method signatures. By default, Mapperly expects a single primary source object and a single return type. When you provide multiple parameters to a single map method without explicit instructions, the generator assumes the first parameter is the primary mapping source. It treats subsequent parameters as contextual objects or reference handlers, not as equal contributors to the target DTO. Because there is no explicit instruction to map the secondary parameter into the target object, the generated CSHARP code simply ignores it.
HOW DID WE APPROACH RESOLVING THIS MULTI-SOURCE MAPPING CHALLENGE?
To fix this, we needed to instruct the source generator to map properties from both sources into a single destination instance. We considered three distinct approaches before settling on our final implementation.
COULD WE USE A COMPOSITE WRAPPER OBJECT?
Our first thought was to create a new intermediate class that wrapped both entities. We could map this single composite object to the target DTO. While this works, it requires allocating an extra object per row in the database result set. When processing millions of rows, this unnecessary allocation creates heavy garbage collection pressure, which we wanted to avoid in a high-performance system.
WHAT ABOUT MAPPING FROM AN ANONYMOUS TYPE?
LINQ joins often result in anonymous types. We explored whether Mapperly could map directly from the anonymous type produced by the join. Unfortunately, source generators analyze syntax trees at compile time. Anonymous types present significant challenges for compile-time generators because the exact type name is not deterministic or accessible in the way concrete DTOs are. This approach led to generation errors.
CAN WE LEVERAGE THE MAPPERTARGET ATTRIBUTE FOR TWO-STEP MAPPING?
The most optimal approach natively supported by Mapperly is using the MapperTarget attribute. Instead of expecting the generator to magically merge two sources in one pass, we can explicitly define a workflow. We map the first entity to create the DTO instance and then we apply the second entity to that existing instance. This aligns perfectly with how compile-time mapping trees are built and incurs zero overhead.
WHAT WAS THE FINAL MAPPERLY IMPLEMENTATION FOR LINQ JOINS?
We refactored our Mapperly interface to utilize an orchestration method combined with partial methods utilizing the MapperTarget attribute. Here is the sanitized, generic structure of the technical fix.
[Mapper]
public partial class AggregationMapper
{
public DataTransferObject Map(EntityA primary, EntityB secondary)
{
DataTransferObject result = MapPrimaryEntity(primary);
ApplySecondaryEntity(secondary, result);
return result;
}
private partial DataTransferObject MapPrimaryEntity(EntityA primary);
private partial void ApplySecondaryEntity(EntityB secondary, [MapperTarget] DataTransferObject result);
}
With this setup, the LINQ query works flawlessly.
var mapper = new AggregationMapper();
var resultList = primaryCollection
.Join(secondaryCollection,
p => p.Id,
s => s.PrimaryId,
(p, s) => new { primary = p, secondary = s })
.Select(joined => mapper.Map(joined.primary, joined.secondary))
.ToList();
By explicitly defining how the DTO is created and subsequently populated, Mapperly successfully generates the CSHARP code to map both properties without relying on reflection. This maintains strict type safety and optimal memory performance.
WHAT ARE THE KEY LESSONS FOR ENGINEERING TEAMS?
When organizations decide to hire dotnet developers for enterprise modernization, they expect teams to deeply understand the tools they implement. Relying on source generators requires a mindset shift from traditional reflection-based libraries. Here are the actionable insights from this challenge.
- Source generators are literal: Unlike reflection-based mappers that might guess your intentions at runtime, compile-time generators require explicit instructions for complex object flattening.
- Inspect generated code: When mapping fails silently, always navigate to the source-generated files in your IDE. Reading the generated CSHARP code immediately reveals what the generator omitted.
- Avoid intermediate allocations: Do not create throwaway wrapper classes just to satisfy a mapping library. Utilize target updating techniques to keep garbage collection minimal.
- Validate mapping layers with unit tests: Write deterministic unit tests specifically for your mappers. Ensure that edge cases, like secondary parameters in a LINQ join, are asserted against expected output values.
- Understand tool limitations: When you hire developers for scalable data systems, ensure they understand that advanced LINQ projections and anonymous types do not always play perfectly with compile-time analyzers. Define concrete boundaries between query execution and data mapping.
HOW CAN WE SUMMARIZE THIS MAPPING PITFALL?
Modernization efforts using DOTNET 8 provide incredible performance benefits, particularly when leveraging source generators over runtime reflection. However, these tools require precise configuration when moving beyond simple one-to-one object mappings. By utilizing the MapperTarget attribute, we were able to instruct Mapperly to merge multiple database entities into a single DTO efficiently within a LINQ projection. If your architecture requires dedicated technical expertise, contact us to discuss how we can support your delivery goals.
Social Hashtags
#Mapperly #DotNet #DotNet8 #CSharp #SourceGenerators #LINQ #EntityFramework #EFCore #SoftwareDevelopment #DotNetDeveloper #BackendDevelopment #CleanCode #SoftwareArchitecture #PerformanceOptimization #DTO
Frequently Asked Questions
Mapperly is a source generator that creates mapping code at compile time. This eliminates the runtime performance hit and memory allocations associated with reflection-based mapping tools, making it significantly faster for high-throughput applications.
Anonymous types are generated by the compiler during the build process. Source generators run alongside the compiler. Because the exact type structure of an anonymous type is inferred and lacks a concrete reusable declaration, mapping generators cannot reliably bind to them across different method boundaries.
Yes. When you hire backend developers for scalable data systems from an experienced technology partner, they bring practical knowledge of source generators, memory profiling and advanced LINQ optimization, ensuring that your enterprise architecture avoids common performance bottlenecks.
The MapperTarget attribute tells Mapperly that instead of instantiating a new object, it should apply the properties from the source parameter directly onto the provided existing instance. This is the recommended way to merge multiple objects into one target DTO.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

US SaaS Platform Cut Manual Ops by 70% After Hiring WeblineGlobal’s n8n Automation Pod

















