Skip to main content

Facet by Tim Maes

NuGet / site data

Nuget GitHub last commit GitHub Repo stars

Details

Info

info

Name: Facet

Generate lean DTOs, slim views, or faceted projections of your models with a single attribute.

Author: Tim Maes

NuGet: https://www.nuget.org/packages/Facet/

You can find more details at https://github.com/Tim-Maes/Facet/

Source: https://github.com/Tim-Maes/Facet/

Author

note

Tim Maes Alt text

Original Readme

note
Facet logo

"One part of a subject, situation, object that has many parts."


NuGet Downloads GitHub CI CD


Facet is a C# source generator that lets you define lightweight projections (DTOs, API models, etc.) directly from your domain models, without writing boilerplate.

It generates partial classes, records, structs, or record structs with constructors, optional LINQ projections, and even supports custom mappings, all at compile time, with zero runtime cost.

💎 What is Facetting?

Facetting is the process of defining focused views of a larger model at compile time.

Instead of manually writing separate DTOs, mappers, and projections, Facet allows you to declare what you want to keep, and generates everything else.

You can think of it like carving out a specific facet of a gem:

  • The part you care about
  • Leaving the rest behind.

Why Facetting?

  • Reduce duplication across DTOs, projections, and ViewModels
  • Maintain strong typing with no runtime cost
  • Stay DRY (Don't Repeat Yourself) without sacrificing performance
  • Works seamlessly with LINQ providers like Entity Framework

📋 Documentation

Key Features

  • Generate classes, records, structs, or record structs from existing types
  • Exclude fields/properties you don't want (create a Facetted view of your model)
  • Include/redact public fields
  • Auto-generate constructors for fast mapping
  • LINQ projection expressions
  • Full mapping support with custom mapping configurations
  • Auto-generate complete CRUD DTO sets with [GenerateDtos]
  • Expression transformation and mapping utilities for reusing business logic across entities and DTOs
  • Preserves member and type XML documentation

🌎 The Facet Ecosystem

Facet is modular and consists of several NuGet packages:

  • Facet: The core source generator. Generates DTOs, projections, and mapping code.

  • Facet.Extensions: Provider-agnostic extension methods for mapping and projecting (works with any LINQ provider, no EF Core dependency).

  • Facet.Mapping: Advanced static mapping configuration support with async capabilities and dependency injection for complex mapping scenarios.

  • Facet.Mapping.Expressions: Expression tree transformation utilities for transforming predicates, selectors, and business logic between source entities and their Facet projections.

  • Facet.Extensions.EFCore: Async extension methods for Entity Framework Core (requires EF Core 6+).

🚀 Quick start

Install the NuGet Package

dotnet add package Facet

For LINQ helpers:

dotnet add package Facet.Extensions

For EF Core support:

dotnet add package Facet.Extensions.EFCore

For expression transformation utilities:

dotnet add package Facet.Mapping.Expressions

Basic Projection

[Facet(typeof(User))]
public partial class UserFacet \{ }

// Auto-generates constructor, properties, and LINQ projection
var user = user.ToFacet<UserFacet>();
var user = user.ToFacet<User, UserFacet>(); //Much faster

var users = users.SelectFacets<UserFacet>();
var users = users.SelectFacets<User, UserFacet>(); //Much faster

Property Exclusion & Field Inclusion

// Exclude sensitive properties
string[] excludeFields = \{ "Password", "Email" };

[Facet(typeof(User), exclude: excludeFields)]
public partial class UserWithoutEmail \{ }

// Include public fields
[Facet(typeof(Entity), IncludeFields = true)]
public partial class EntityDto \{ }

Different Type Kinds

// Generate as record (immutable by default)
[Facet(typeof(Product))]
public partial record ProductDto;

// Generate as struct (value type)
[Facet(typeof(Point))]
public partial struct PointDto;

// Generate as record struct (immutable value type)
[Facet(typeof(Coordinates))]
public partial record struct CoordinatesDto; // Preserves required/init-only

Custom Sync Mapping

public class UserMapper : IFacetMapConfiguration<User, UserDto>
{
public static void Map(User source, UserDto target)
{
target.FullName = $"{source.FirstName} {source.LastName}";
target.Age = CalculateAge(source.DateOfBirth);
}
}

[Facet(typeof(User), Configuration = typeof(UserMapper))]
public partial class UserDto
{
public string FullName \{ get; set; }
public int Age \{ get; set; }
}

Async Mapping for I/O Operations

public class UserAsyncMapper : IFacetMapConfigurationAsync<User, UserDto>
{
public static async Task MapAsync(User source, UserDto target, CancellationToken cancellationToken = default)
{
// Async database lookup
target.ProfilePicture = await GetProfilePictureAsync(source.Id, cancellationToken);

// Async API call
target.ReputationScore = await CalculateReputationAsync(source.Email, cancellationToken);
}
}

// Usage
var userDto = await user.ToFacetAsync<User, UserDto, UserAsyncMapper>();
var userDtos = await users.ToFacetsParallelAsync<User, UserDto, UserAsyncMapper>();

Async Mapping with Dependency Injection

public class UserAsyncMapperWithDI : IFacetMapConfigurationAsyncInstance<User, UserDto>
{
private readonly IProfilePictureService _profileService;
private readonly IReputationService _reputationService;

public UserAsyncMapperWithDI(IProfilePictureService profileService, IReputationService reputationService)
{
_profileService = profileService;
_reputationService = reputationService;
}

public async Task MapAsync(User source, UserDto target, CancellationToken cancellationToken = default)
{
// Use injected services
target.ProfilePicture = await _profileService.GetProfilePictureAsync(source.Id, cancellationToken);
target.ReputationScore = await _reputationService.CalculateReputationAsync(source.Email, cancellationToken);
}
}

// Usage with DI
var mapper = new UserAsyncMapperWithDI(profileService, reputationService);
var userDto = await user.ToFacetAsync(mapper);
var userDtos = await users.ToFacetsParallelAsync(mapper);

EF Core Integration

Forward Mapping (Entity -> Facet)

// Async projection directly in EF Core queries
var userDtos = await dbContext.Users
.Where(u => u.IsActive)
.ToFacetsAsync<UserDto>();

// LINQ projection for complex queries
var results = await dbContext.Products
.Where(p => p.IsAvailable)
.SelectFacet<ProductDto>()
.OrderBy(dto => dto.Name)
.ToListAsync();

Reverse Mapping (Facet -> Entity)

[Facet(typeof(User)]
public partial class UpdateUserDto \{ }

[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(int id, UpdateUserDto dto)
{
var user = await context.Users.FindAsync(id);
if (user == null) return NotFound();

// Only updates properties that mutated
user.UpdateFromFacet(dto, context);

await context.SaveChangesAsync();
return NoContent();
}

// With change tracking for auditing
var result = user.UpdateFromFacetWithChanges(dto, context);
if (result.HasChanges)
{
logger.LogInformation("User {UserId} updated. Changed: {Properties}",
user.Id, string.Join(", ", result.ChangedProperties));
}

Automatic CRUD DTO Generation

Generate standard Create, Update, Response, Query, and Upsert DTOs automatically:

// Generate all standard CRUD DTOs
[GenerateDtos(Types = DtoTypes.All, OutputType = OutputType.Record)]
public class User
{
public int Id \{ get; set; }
public string FirstName \{ get; set; }
public string LastName \{ get; set; }
public string Email \{ get; set; }
public DateTime CreatedAt \{ get; set; }
}

// Auto-generates:
// - CreateUserRequest (excludes Id)
// - UpdateUserRequest (includes Id)
// - UserResponse (includes all)
// - UserQuery (all properties nullable)
// - UpsertUserRequest (includes Id, for create/update operations)

Entities with Smart Exclusions

[GenerateAuditableDtos(
Types = DtoTypes.Create | DtoTypes.Update | DtoTypes.Response,
OutputType = OutputType.Record,
ExcludeProperties = new[] \{ "Password" })]
public class Product
{
public int Id \{ get; set; }
public string Name \{ get; set; }
public string Password \{ get; set; \} // Excluded
public DateTime CreatedAt \{ get; set; \} // Auto-excluded (audit)
public string CreatedBy \{ get; set; \} // Auto-excluded (audit)
}

// Auto-excludes audit fields: CreatedAt, UpdatedAt, CreatedBy, UpdatedBy

Multiple Configurations for Fine-Grained Control

// Different exclusions for different DTO types
[GenerateDtos(Types = DtoTypes.Response, ExcludeProperties = new[] \{ "Password", "InternalNotes" })]
[GenerateDtos(Types = DtoTypes.Upsert, ExcludeProperties = new[] \{ "Password" })]
public class Schedule
{
public int Id \{ get; set; }
public string Name \{ get; set; }
public string Password \{ get; set; \} // Excluded from both
public string InternalNotes \{ get; set; \} // Only excluded from Response
}

// Generates:
// - ScheduleResponse (excludes Password, InternalNotes)
// - UpsertScheduleRequest (excludes Password, includes InternalNotes)

Perfect for RESTful APIs

[HttpPost]
public async Task<ActionResult<ScheduleResponse>> CreateSchedule(CreateScheduleRequest request)
{
var schedule = new Schedule
{
Name = request.Name,
// Map other properties;;;
};

context.Schedules.Add(schedule);
await context.SaveChangesAsync();
return schedule.ToFacet<ScheduleResponse>();
}

[HttpPut("{id}")]
public async Task<ActionResult<ScheduleResponse>> UpsertSchedule(int id, UpsertScheduleRequest body)
{
var schedule = context.GetScheduleById(id);
if (schedule == null) return NotFound();

// Ensure the body ID matches the route ID
body = body with \{ Id = id };

schedule.UpdateFromFacet(body, context);
await context.SaveChangesAsync();
return schedule.ToFacet<ScheduleResponse>();
}

📈 Performance Benchmarks

Facet delivers competitive performance across different mapping scenarios. Here's how it compares to popular alternatives:

Single Mapping

LibraryMean TimeMemory AllocatedPerformance vs Facet
Facet15.93 ns136 BBaseline
Mapperly15.09 ns128 B5% faster, 6% less memory
Mapster21.90 ns128 B38% slower, 6% less memory

Collection Mapping (10 items)

LibraryMean TimeMemory AllocatedPerformance vs Facet
Mapster192.55 ns1,416 B10% faster, 10% less memory
Facet207.32 ns1,568 BBaseline
Mapperly222.50 ns1,552 B7% slower, 1% less memory

For this benchmark we used the <TSource, TTarget> methods.

Insights:

  • Single mapping: All three libraries perform similarly with sub-nanosecond differences
  • Collection mapping: Mapster has a slight edge for bulk operations, while Facet and Mapperly are very close
  • Memory efficiency: All libraries are within ~10% of each other for memory allocation
  • Compile-time generation: Both Facet and Mapperly benefit from zero-runtime-cost source generation

About

note

Custom generation and mapper

How to use

Example (source csproj, source files)

This is the CSharp Project that references Facet

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Facet" Version="2.7.0" />
</ItemGroup>




</Project>

Generated Files

Those are taken from $(BaseIntermediateOutputPath)\GX

// <auto-generated>
// This code was generated by the Facet source generator.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>

using System;
using System.Linq.Expressions;

namespace mapperDemo;
public partial struct PersonDTO
{
public int ID \{ get; set; }
public string FirstName \{ get; set; }
public string LastName \{ get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="PersonDTO"/> class from the specified <see cref="global::Person"/>.
/// </summary>
/// <param name="source">The source <see cref="global::Person"/> object to copy data from.</param>
public PersonDTO(global::Person source)
{
this.ID = source.ID;
this.FirstName = source.FirstName;
this.LastName = source.LastName;
}

/// <summary>
/// Initializes a new instance of the <see cref="PersonDTO"/> class with default values.
/// </summary>
/// <remarks>
/// This constructor is useful for unit testing, object initialization, and scenarios
/// where you need to create an empty instance and populate properties later.
/// </remarks>
public PersonDTO()
{
}

/// <summary>
/// Gets the projection expression for converting <see cref="global::Person"/> to <see cref="PersonDTO"/>.
/// Use this for LINQ and Entity Framework query projections.
/// </summary>
/// <value>An expression tree that can be used in LINQ queries for efficient database projections.</value>
/// <example>
/// <code>
/// var dtos = context.global::Persons
/// .Where(x => x.IsActive)
/// .Select(PersonDTO.Projection)
/// .ToList();
/// </code>
/// </example>
public static Expression<Func<global::Person, PersonDTO>> Projection =>
source => new PersonDTO(source);
}

Useful

Download Example (.NET C#)

Share Facet

https://ignatandrei.github.io/RSCG_Examples/v2/docs/Facet

Category "Mapper" has the following generators:

1 AutoDTO

2 AutoGen

3 DynamicsMapper

4 Facet

5 LightweightObjectMapper

6 MagicMap

7 mapperly

8 MapTo

9 NextGenMapper

See category

Mapper