Reduce Boilerplate with .NET Source Generators for DTO‑to‑Entity Mapping
Learn how a .NET Source Generator can automate DTO‑to‑entity mapping, cut boilerplate, and keep your ASP.NET Core controllers focused on real work.
09 Dec 2025, 02:28 UTC

The problem: repetitive DTO‑to‑entity mapping
In many ASP.NET Core applications controllers receive data‑transfer objects (DTOs) that must be copied into entity models before persisting, and the reverse operation is needed for responses. Writing this mapping by hand leads to dozens of nearly identical lines, easy‑to‑miss typos, and maintenance friction when a property is added or renamed.
Thesis: a compile‑time source generator can eliminate the boilerplate while keeping full type safety
By marking DTO and entity pairs with a custom attribute, a .NET Source Generator inspects the compilation, creates extension methods such as MapToEntity and MapToDto, and emits them into the assembly during the build. The generated code is ordinary C#; you call it like any other method, and the compiler can still detect mismatches.
How the generator works
The generator implements ISourceGenerator. During compilation it:
- Receives the
Compilationobject. - Searches for types annotated with the generator’s attribute (e.g.,
[GenerateMapper]). - For each matched DTO/entity pair, it produces a partial class containing two static extension methods that copy each public property with the same name and compatible type.
- The generated source is added to the compilation; the final assembly contains the mapper methods.
Because the code is produced at compile time, there is no runtime reflection overhead.
Setting up the generator in an ASP.NET Core project
Assuming you have an ASP.NET Core Web API targeting .NET 6 or later:
- Add the source‑generator NuGet package (replace
YourMapperGeneratorwith the actual package id):
Run this command in the project directory; it requires read/write access to the project file and restores the package from the configured feeds.dotnet add package YourMapperGenerator - Reference the generator as an analyzer so it participates in the build:
(If you used the<ItemGroup> <PackageReference Include="YourMapperGenerator" Version="1.0.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup>dotnet add packagecommand with the--prereleaseflag or a version that includes the analyzer, the SDK may add this automatically.) - Add the attribute definition to your codebase (usually shipped with the package). For example:
The attribute can be placed on either the DTO, the entity, or both; the generator will pair them by matching names.using YourMapperGenerator; [GenerateMapper] public class UserDto { public string Name { get; set; } public int Age { get; set; } } [GenerateMapper] public class User { public string Name { get; set; } public int Age { get; set; } } - Build the project:
After a successful build, look underdotnet buildobj/Debug/net6.0/generated/(or the corresponding Release folder) for a file named something likeUserMapper.g.cs. Opening it should reveal methods similar to:
If the file is missing, check the build output for generator‑related warnings or errors.public static User MapToEntity(this UserDto dto) => new User { Name = dto.Name, Age = dto.Age }; public static UserDto MapToDto(this User entity) => new UserDto { Name = entity.Name, Age = entity.Entity.Age };
Worked example: using the generated mapper in a controller
With the generator in place, a controller can stay focused on concerns like validation and routing:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserRepository _repo;
public UsersController(IUserRepository repo) => _repo = repo;
[HttpPost]
public async Task<ActionResult> Create([FromBody] UserDto dto)
{
var entity = dto.MapToEntity(); // generated extension method
await _repo.AddAsync(entity);
return CreatedAtAction(nameof(GetById), new { id = entity.Id }, entity.MapToDto());
}
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> GetById(int id)
{
var entity = await _repo.GetByIdAsync(id);
if (entity == null) return NotFound();
return Ok(entity.MapToDto());
}
}
Notice that no manual mapping code appears; the compiler guarantees that if you rename Name to FullName in one type but not the other, the generated mapper will fail to compile, alerting you to the mismatch.
Trade‑offs and limitations
- Assembly size: The generated methods increase the IL size proportionally to the number of mapped properties. For most DTOs this impact is negligible, but very large objects could add noticeable weight.
- Debugging experience: Stepping through a mapping operation will show the generated source. If you need to set a breakpoint inside the mapper, you must do so in the generated file (which is regenerated each build). To avoid losing custom logic, place any special conversion in a separate partial class and call it from the generated method.
- Complex mapping: The generator described in the research brief copies properties with identical names and compatible types. It does not handle flattening, nested object transformation, or custom formatting; those cases still require manual code or a different approach.
- SDK version: Source generators require .NET 5 SDK or later. Projects targeting older frameworks must upgrade to benefit.
Practical verification steps
- After building, confirm the generated file exists:
ls obj/Debug/net6.0/generated/*.g.cs(or the equivalentdircommand on Windows). - Open the generated file and verify that the method signatures match the DTO/entity pair.
- Write a unit test that invokes the mapper and asserts property equality:
Run[Fact] public void UserDtoMapsToUserCorrectly() { var dto = new UserDto { Name = "Ada", Age = 30 }; var entity = dto.MapToEntity(); Assert.Equal("Ada", entity.Name); Assert.Equal(30, entity.Age); }dotnet testto ensure the test passes. - Introduce a deliberate mismatch (e.g., change the DTO property to
FullName) and verify that the build fails with a clear CS1061‑style error indicating the missing member.
Actionable closing
If your ASP.NET Core codebase spends more time writing mapping boilerplate than implementing business logic, try adding a source‑generator package that creates MapToEntity and MapToDto extensions. Start with a single DTO/entity pair, confirm the generated file appears after dotnet build, and replace the hand‑written mapper with the generated method. Keep an eye on the generated file’s location, remember that manual edits there will be lost, and reserve complex transformations for separate partial classes. This approach gives you compile‑time safety, zero runtime overhead, and a cleaner controller layer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.