HomeNikola Knezevic

In this article

Banner

Mapping with Facets in .NET

26 Mar 2026
7 min

Sponsor Newsletter

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:

bash
Install-Package Facet

After installation, you can start defining your facets using attributes.

Let’s say you have a simple entity:

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

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

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

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

bash
Install-Package Facet.Extensions

Facets also come with a copy constructor which can be used as an alternative to the ToFacet method.

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

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

csharp
[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:

csharp
var products = await dbContext.Products
    .Select(ProductFacets.ProductResponse.Projection)
    .ToListAsync();

You can even simplify it further using the built-in extensions:

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

bash
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 Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.