Mapping is a topic that keeps coming up over and over again.
Some people use libraries for mapping, some do manual mapping but the need for DTOs and mapping is always present.
In this blog post, we will cover a package that could change the way we've been mapping so far.
Facet
Facet is a package that provides a source-generated, type-safe, and expressive way to map between objects with a different approach in mind.
The key concept behind Facet is the idea that an entity can have multiple forms depending on context.
Instead of thinking purely in terms of mapping one type to another, Facet allows you to define alternate views (facets) of your object.
Each facet can expose only the data you need, exclude sensitive fields, or even transform data along the way.
Originally it was created to be a library to source generate redacted models of existing models and it evolved into a library that also provides the mapping.
Getting started with Facet
To get started with Facet, you need to install the NuGet package. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:
Install-Package Facet
After installation, you can start defining your facets using attributes.
Let’s say you have a simple entity:
public class Product
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
Traditionally, you would create DTOs like this:
public sealed class ProductResponse
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
After that, you would have to use some mapping package and create config or, if you’re doing manual mapping you would have to write methods or extension methods that will handle it.
With Facet it looks a bit different:
public partial class ProductFacets
{
[Facet(typeof(Product), exclude: [nameof(Product.CreatedAt), nameof(Product.ModifiedAt)])]
public partial class ProductResponse;
}
With Facet it's enough to create a partial class that will contain the partial classes of views or facets of our Product.
If you want additional properties, just extend the partial class for those props.
Based on the attributes above, Facet will generate properties using source generators and mapping for Product.
Since it's source generated, it means that everything is resolved at compile time, no runtime costs.
Configuring In-Memory
You can use Facet to map objects in memory just as easily as with any other mapper:
var product = await dbContext.Products.FirstOrDefaultAsync(x => x.Id == id);
var productDto = product?.ToFacet<ProductFacets.ProductResponse>();
Console.WriteLine($"{productDto.Name} ({productDto.Price} $)");
This will map the matching fields automatically, excluding those you marked out (like CreatedAt in the example above).
NOTE: To use extension methods for mapping/projection, you need to install the following package:
Install-Package Facet.Extensions
Facets also come with a copy constructor which can be used as an alternative to the ToFacet method.
var product = await dbContext.Products.FirstOrDefaultAsync(x => x.Id == id);
var productDto = product is null
? null
: new ProductFacets.ProductResponse(product);
Console.WriteLine($"{productDto.Name} ({productDto.Price} $)");
Configuring Custom Mapping
Facet also allows custom mapping configurations for advanced scenarios.
You can define a mapping configuration class to adjust how data flows from source to target:
public class ProductMapConfiguration : IFacetMapConfiguration<Product, ProductFacets.ProductResponse>
{
public static void Map(Product source, ProductFacets.ProductResponse target)
{
target.Id = source.Id;
target.Name = $"{source.Name} ({source.Price} $)";
target.Description = source.Description;
target.Price = source.Price;
}
}
Then, reference the configuration in your facet definition:
[Facet(typeof(Product), Configuration = typeof(ProductMapConfiguration))]
public partial class ProductResponse;
If you have additional fields, the mapping configuration is necessary for automatic property mapping. This gives you full control while keeping mapping logic organized and testable.
EF Core Examples
One of Facet’s standout features is its integration with Entity Framework Core.
You can use the generated projection expressions directly in your LINQ queries:
var products = await dbContext.Products
.Select(ProductFacets.ProductResponse.Projection)
.ToListAsync();
You can even simplify it further using the built-in extensions:
var products = await dbContext.Products
.ToFacetsAsync<ProductResponse>()
This automatically applies the generated projection and returns a list of your DTOs.
NOTE: To use EF Core extensions, you need to install the following package:
Install-Package Facet.Extensions.EFCore
To view the list of available extensions, you can check the documentation: Facet Extension Methods
Benchmarks
Lastly, probably the least important but also the most interesting topic, how fast is it?
This package performs okay but it's far from using its full potential:
Even though it uses source generators, there's room for further optimization.
However, is this really a significant difference? It isn’t.
Conclusion
Facet is a powerful tool that can help you improve your mapping code.
It's source generated, so it's fast and it's easy to use. It's also easy to test and to integrate with other libraries.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
