I don't like auto mappers in C#, but this one deserves a look
I’m going to be honest, I don’t like auto mappers. They hide too much logic, make debugging harder, and usually add another layer of magic you don’t need. But every once in a while, something shows up that actually piques my interest, like Facet, a compile-time mapper that, in my opinion, has some nicer ways of handling mapping scenarios, and also how it deals with EF Core projections.
I took it for a spin, tried some things out, and benchmarked it against AutoMapper and Mapster.
Quick disclaimer ⚠️
The author of Facet, Tim Maes, has pointed out that the library isn’t actually meant to be a traditional mapper:“At its core Facet is not a mapper library but a source generator for facetted models. This project was initially created as an answer to a Reddit question (someone was looking for a library to source generate redacted models of existing models) and it evolved into a library that also provides the mapping. It is/was not meant to be the next best mapper."
The core concept of Facet
Facet is a new open-source library that creates so called ‘facets’ of your models. The core idea of the library is that a model can take up multiple forms. You start from one model, and have the ability to define one or more facets this model might transform into.
To put it more into practice, let’s say we have the following model:
public class Book
{
public int Id { get; init; }
public required string Title { get; init; }
public required string Author { get; init; }
public decimal Price { get; init; }
}
We can define a facet like this:
[Facet(typeof(Domain.Book))]
public partial class BookDto;
This will generate out an implementation of the BookDto, which it will map to behind the scenes. In the end, you can map it like this:
var newBookDto = newBook.ToFacet<Book, BookDto>();
But now let’s say we don’t only have a normal BookDto, but also DiscountedBooksDto we would like to map to. In that case, we just add another Facet:
[Facet(typeof(Book), Configuration = typeof(DiscountedBookMap))]
public partial class DiscountedBookDto
{
public decimal DiscountedPrice { get; set; }
}
public sealed class DiscountedBookMap : IFacetMapConfiguration<Book, DiscountedBookDto>
{
public static void Map(Book s, DiscountedBookDto t)
=> t.DiscountedPrice = s.Price * 0.90m;
}
And this is the real beauty of Facet 💎. It allows you to define multiple forms of a single model. This becomes especially useful when dealing with large objects, say one with 200 properties, that are displayed across several parts of your UI. Instead of exposing the full model each time, you can create separate facets tailored to each use case. For example, if your UI shows that object in five different contexts, each requiring only a subset of the data, you can simply define five corresponding facets.
Of course, libraries like AutoMapper also do this, but personally I find this to be less elegant. AutoMapper relies heavily on runtime reflection and convention-based mapping, which means that much of the configuration happens dynamically and is often implicit.
Another big advantage is that Facet encourages strongly typed and modular transformations. Each facet represents a specific ‘view’ or ‘projection’ of your model, and because it’s generated at compile time, you avoid runtime mapping errors or missing members.
EF Core projections
In Entity Framework Core, we can project something to another object by using the Select LINQ method. For mapping our books to the book DTO’s, that could look something like this:
var books = await db.Books
.Select(x => new BookDto
{
Id = x.Id,
Title = x.Title,
Author = x.Author,
Price = x.Price
})
.ToListAsync();
One of the nice features that Facet has is the ability to easily project something. When the BookDto class is generated, it comes with a Projection method:
public static Expression<Func<global::Domain.Book, DiscountedBookDto>> Projection =>
source => new DiscountedBookDto(source);
This gives back an expression tree, which is compatible with the Select LINQ method from the IQueryable interface. Now, we have the ability to do this:
var books = await db.Books
.Select(BookDto.Projection)
.ToListAsync();
That’s pretty elegant if you ask me. ✨
Both AutoMapper and Mapster have very similar capabilities for tackling this problem, I just like the syntax of Facet more. ☝️
About performance
Now I hear you asking: “What about performance? 🤔”. This also got my interested. So I made small benchmark, comparing AutoMapper, Mapster, and of course Facet:
[MemoryDiagnoser]
public class MapperBenchmarks
{
private readonly IMapper _autoMapper;
private readonly TypeAdapterConfig _mapsterConfig;
private readonly List<Book> _books;
public MapperBenchmarks()
{
AppDbContext db = new();
db.Seed();
_books = db.Books.ToList();
var cfg = new MapperConfiguration(c => c.CreateMap<Book, BookDto>(), new NullLoggerFactory());
_autoMapper = cfg.CreateMapper();
_mapsterConfig = TypeAdapterConfig.GlobalSettings;
_mapsterConfig.NewConfig<Book, BookDto>();
}
[Benchmark]
public List<BookDto> AutoMapper_Map() => _books.Select(_autoMapper.Map<BookDto>).ToList();
[Benchmark]
public List<BookDto> Mapster_Map() => _books.Adapt<List<BookDto>>(_mapsterConfig);
[Benchmark]
public List<BookDto> Facet_Map() => _books.SelectFacets<Book, BookDto>().ToList();
}
The benchmark will map the books that we get from our database to a BookDto, there are three books in the database. These were the results after running the benchmarks:

And as you can see, Facet actually performs pretty good! The only quirky thing is that if you use SelectFacets, you also have to specify the source type Book, which could normally be inferred from the caller type.
You also have the option to use
SelectFacetswithout specifying the source type, but this has impact on performance.
So, if you are working on a project where the same object takes many shapes, then Facet might just be your new favorite tool. And once you try it, you’ll probably end up a little facet-nated too. 💎