
⚠️ This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the 🤖 in C# are 100% mine.
Hi!
So, I’ve been away from any scenario that involves and needs data mapping, until I get to OpenClawNet. So I got to the point that I need to map my entities to DTOs?
I read this a lot >> runtime reflection doesn’t play nice with AOT and trimming. 😅
So I SQUAD myselft and built:
A Roslyn source-generator library that creates all your mapping code at compile time. No reflection. No dynamic IL. Just clean, generated C# that works everywhere — including NativeAOT and trimmed apps.
Note: this is a WIP, so I expect to learn more during my usage. If you find any issue or have any ideas, please drop an issue on the repo, or even better > a PR with some cool fixes / updates!
Let me show you how it works. ⬇️
📦 Install
Two packages — the core attributes and the generator:
dotnet add package ElBruno.AotMapperdotnet add package ElBruno.AotMapper.Generator
That’s it. The generator runs during build and creates extension methods for you automatically.
🚀 The Simplest Mapping — One Attribute, Done
Let’s start with the basics. You have a Product entity and you need a DTO:
using ElBruno.AotMapper;public class Product{ public int Id { get; set; } public string Name { get; set; } = string.Empty; public decimal Price { get; set; } public ProductCategory Category { get; set; } public List<string> Tags { get; set; } = new();}public enum ProductCategory { Library, Tool, Framework }[MapFrom(typeof(Product))]public partial record ProductDto(int Id, string Name, decimal Price, string Category, List<string> Tags);
See that [MapFrom] attribute? That’s the magic. The generator sees it, matches properties by name, and creates a .ToProductDto() extension method for you.
Now you use it like this:
var product = new Product{ Id = 42, Name = "ElBruno.AotMapper", Price = 0.0m, Category = ProductCategory.Library, Tags = new List<string> { "aot", "mapper", "dotnet" }};var dto = product.ToProductDto();Console.WriteLine($"Product: {dto.Name} (#{dto.Id})");Console.WriteLine($" Category: {dto.Category}");Console.WriteLine($" Tags: {string.Join(", ", dto.Tags)}");
I forgot how fun this was > no reflection, no runtime overhead, AOT safe (I think, still checking stuff here) ✅
🔀 Remapping Properties with [MapProperty]
What if your source and destination properties don’t have the same name? Easy — use [MapProperty]:
using ElBruno.AotMapper;public class Customer{ public int Id { get; set; } public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public CustomerTier Tier { get; set; }}public enum CustomerTier { Standard, Premium, Enterprise }[MapFrom(typeof(Customer))][MapProperty(nameof(Customer.FirstName), nameof(CustomerDto.Name))]public partial record CustomerDto(int Id, string Name, string Email, string Tier);
Here FirstName on the source gets mapped to Name on the DTO. The generator handles it at build time — zero surprises at runtime.
🌐 Minimal API — Full Example
Here’s where it gets fun. Let’s use AotMapper in an ASP.NET Core Minimal API.
First, add the ASP.NET Core integration:
dotnet add package ElBruno.AotMapper.AspNetCore
Then wire it up:
using MinimalApiSample.Models;using ElBruno.AotMapper.AspNetCore;var builder = WebApplication.CreateBuilder(args);builder.Services.AddAotMapper();var app = builder.Build();var customers = new List<Customer>{ new() { Id = 1, FirstName = "Bruno", LastName = "Capuano", Email = "bruno@example.com", Tier = CustomerTier.Enterprise }, new() { Id = 2, FirstName = "Jane", LastName = "Smith", Email = "jane@example.com", Tier = CustomerTier.Premium }};app.MapGet("/customers", () => customers.Select(c => c.ToCustomerDto()));app.MapGet("/customers/{id}", (int id) =>{ var customer = customers.FirstOrDefault(c => c.Id == id); return customer is null ? Results.NotFound() : Results.Ok(customer.ToCustomerDto());});app.Run();
All the magic in the endpoing handlers with > c.ToCustomerDto() (that’s the generated extension method)
🗄️ EF Core — In-Memory Mapping and SQL Projections
If you’re using Entity Framework, there’s a package for that, and I have some samples in the repo (I think I can do more here … I didn’t have time yet for this)
🧩 More Attributes You Should Know
The library has a few more tricks:
| Attribute | What it does |
|---|---|
[MapFrom(typeof(T))] | Map from a source type to this DTO |
[MapTo(typeof(T))] | Map to a destination type from this source |
[MapProperty("src", "dest")] | Remap a property with a different name |
[MapIgnore] | Skip a property during mapping |
[MapConverter(typeof(T))] | Use a custom converter for a property |
And for custom converters, you implement IMapConverter<TSource, TDestination>:
public class UpperCaseConverter : IMapConverter<string, string>{ public string Convert(string source) => source.ToUpperInvariant();}
🏗️ How It Works Under the Hood
The flow is simple:
- You add
[MapFrom](or[MapTo]) to your DTO - During build, the Roslyn incremental source generator kicks in
- It matches source and destination properties by name and type
- It generates strongly-typed extension methods (like
.ToProductDto()) - If something doesn’t match, you get a compile-time error — not a runtime surprise
That’s the whole point: move the errors to build time, move the mapping to generated code, and keep the runtime clean.
📂 Samples
The repo includes three complete samples you can run right now:
- 🔧 NativeAotSample — Basic mapping in a NativeAOT console app
- 🌐 MinimalApiSample — ASP.NET Core Minimal API with DI
- 🗄️ EfProjectionSample — EF Core with in-memory and SQL projections
Resources
- 📦 NuGet: ElBruno.AotMapper
- 🐙 GitHub: github.com/elbruno/ElBruno.AotMapper
- 📖 Docs: Quick Start | Supported Mappings | EF Integration
- 📜 License: MIT
Happy coding!
Greetings
El Bruno
More posts in my blog ElBruno.com.
More info in https://beacons.ai/elbruno
Leave a comment