⚠️ 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:

👉 ElBruno.AotMapper

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.AotMapper
dotnet 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:

AttributeWhat 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:

  1. You add [MapFrom] (or [MapTo]) to your DTO
  2. During build, the Roslyn incremental source generator kicks in
  3. It matches source and destination properties by name and type
  4. It generates strongly-typed extension methods (like .ToProductDto())
  5. 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:


Resources

Happy coding!

Greetings

El Bruno

More posts in my blog ElBruno.com.

More info in https://beacons.ai/elbruno


Leave a comment

Discover more from El Bruno

Subscribe now to keep reading and get access to the full archive.

Continue reading