Facet 5.3.0

dotnet add package Facet --version 5.3.0
                    
NuGet\Install-Package Facet -Version 5.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Facet" Version="5.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Facet" Version="5.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Facet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Facet --version 5.3.0
                    
#r "nuget: Facet, 5.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Facet@5.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Facet&version=5.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Facet&version=5.3.0
                    
Install as a Cake Tool

<div align="center"> <img src="https://raw.githubusercontent.com/Tim-Maes/Facet/master/assets/Facet.png" alt="Facet logo" width="400"> </div>

<div align="center"> "One part of a subject, situation, object that has many parts." </div>

<br>

<div align="center">

CI Test CD NuGet Downloads GitHub Discord

</div>


Facet is a C# source generator that automatically creates DTOs, mappings, and LINQ projections from your domain models at compile time, eliminating boilerplate with zero runtime cost.

💎 What is a Facet?

Think of your domain model as a gem with many facets! Different views for different purposes:

  • Public APIs need a facet without sensitive data
  • Admin endpoints need a different facet with additional fields
  • Database queries need efficient projections

Instead of manually creating each facet, Facet auto-generates them from a single source of truth.

📋 Documentation

⭐ Features

Click on a section to expand/collapse

<details> <summary>Code Generation</summary>

  • Generate DTOs as classes, records, structs, or record structs
  • Constructors & LINQ projection expressions
  • Handle complex nested objects & collections automatically
  • Preserve XML documentation

</details>

<details> <summary>Configuration & customization</summary>

  • Include/exclude pattern with simple attributes
  • Copy data validation attributes
  • Reverse & custom mapping configurations (sync & async)
  • Patch/update source with change tracking
  • Expression transformation utilities for business logic reuse
  • Property mapping with [MapFrom] for declarative property renaming
  • Conditional mapping with [MapWhen] for status-dependent fields
  • Before/After mapping hooks for validation, defaults, and computed values

</details>

<details> <summary>Additional Features</summary>

  • Flatten nested objects into top-level properties
  • Wrapper pattern for reference-based delegation (facades, decorators, ViewModels)
  • Auto-generate CRUD DTOs (Create, Update, Response, Query, Upsert, Patch)
  • Source signature tracking for detecting breaking changes when source entities change
  • Inheritance support for source types and facet base classes

</details>

<details> <summary>Integration</summary>

  • Full Entity Framework Core support with automatic navigation loading
  • Works with any LINQ provider (via Facet.Extensions)
  • Expression tree transformation for predicates & selectors
  • Zero runtime cost and no reflection, everything happens at compile time
  • Supports .NET 8, .NET 9, and .NET 10 (LTS)

</details>

🚀 Quick Start

<details> <summary>Installation</summary>

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 advanced EF Core custom mappers (with DI support):

dotnet add package Facet.Extensions.EFCore.Mapping

For expression transformation utilities:

dotnet add package Facet.Mapping.Expressions

</details> <details> <summary>Define Facets</summary>

// Example domain models:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }
    public DateTime DateOfBirth { get; set; }
    public decimal Salary { get; set; }
    public string Department { get; set; }
    public bool IsActive { get; set; }
    public Address HomeAddress { get; set; }
    public Company Employer { get; set; }
    public List<Project> Projects { get; set; }
    public DateTime CreatedAt { get; set; }
    public string InternalNotes { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Headquarters { get; set; }
}

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime StartDate { get; set; }
}

Create focused facets for different scenarios:

  // 1. Public API - Exclude all sensitive data
  [Facet(typeof(User),
      exclude: [nameof(User.PasswordHash), nameof(User.Salary), nameof(User.InternalNotes)])]
  public partial record UserPublicDto;

  // 2. Contact Information - Include only specific properties
  [Facet(typeof(User),
      Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email), nameof(User.Department)])]
  public partial record UserContactDto;

  // 3. Query/Filter DTO - Make all properties nullable
  [Facet(typeof(User),
      Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email), nameof(User.Department), nameof(User.IsActive)],
      NullableProperties = true,
      GenerateToSource = false)]
  public partial record UserFilterDto;

  // 4. Validation-Aware DTO - Copy data annotations
  [Facet(typeof(User),
      Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email)],
      CopyAttributes = true)]
  public partial record UserRegistrationDto;

  // 5. Nested Objects - Single nested facet
  [Facet(typeof(Address))]
  public partial record AddressDto;

  [Facet(typeof(User),
      Include = [nameof(User.Id), nameof(User.FirstName), nameof(User.LastName), nameof(User.HomeAddress)],
      NestedFacets = [typeof(AddressDto)])]
  public partial record UserWithAddressDto;
  // Address -> AddressDto automatically
  // Type-safe nested mapping

  // 6. Complex Nested - Multiple nested facets
  [Facet(typeof(Company), NestedFacets = [typeof(AddressDto)])]
  public partial record CompanyDto;

  [Facet(typeof(User),
      exclude: [nameof(User.PasswordHash), nameof(User.Salary), nameof(User.InternalNotes)],
      NestedFacets = [typeof(AddressDto), typeof(CompanyDto)])]
  public partial record UserDetailDto;
  // Multi-level nesting supported

  // 7. Collections - Automatic collection mapping
  [Facet(typeof(Project))]
  public partial record ProjectDto;

  [Facet(typeof(User),
      Include = [nameof(User.Id), nameof(User.FirstName), nameof(User.LastName), nameof(User.Projects)],
      NestedFacets = [typeof(ProjectDto)])]
  public partial record UserWithProjectsDto;
  // List<Project> -> List<ProjectDto> automatically!
  // Arrays, ICollection<T>, IEnumerable<T> all supported

  // 8. Everything Combined
  [Facet(typeof(User),
      exclude: [nameof(User.PasswordHash), nameof(User.Salary), nameof(User.InternalNotes)],
      NestedFacets = [typeof(AddressDto), typeof(CompanyDto), typeof(ProjectDto)],
      CopyAttributes = true)]
  public partial record UserCompleteDto;
  // Excludes sensitive fields
  // Maps nested Address and Company objects
  // Maps Projects collection (List<Project> -> List<ProjectDto>)
  // Copies validation attributes
  // Ready for production APIs

</details>

<details> <summary>Basic Projection of Facets</summary>

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

// Map your source to facet
var userFacet = user.ToFacet<UserFacet>();
var userFacet = user.ToFacet<User, UserFacet>(); //Much faster

// Map back to source
var user = userFacet.ToSource<User>();
var user = userFacet.ToSource<UserFacet, User>(); //Much faster

// Patch only changed properties back to source
user.ApplyFacet(userFacet);
user.ApplyFacet<User, UserFacet>(userFacet); // Much faster

// Patch with change tracking
bool hasChanges = userFacet.ApplyFacetWithChanges<user, userDto>(userFacet);

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

</details>

<details> <summary>Custom Sync Mapping</summary>

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; }
}

</details>

<details> <summary>Property Mapping with [MapFrom]</summary>

Rename properties declaratively without custom mapping configurations:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

[Facet(typeof(User), GenerateToSource = true)]
public partial class UserDto
{
    // Type-safe property rename with reverse mapping
    [MapFrom(nameof(User.FirstName), Reversible = true)]
    public string Name { get; set; } = string.Empty;

    // Rename multiple properties
    [MapFrom(nameof(User.LastName), Reversible = true)]
    public string FamilyName { get; set; } = string.Empty;

    // Computed expression (not reversible)
    [MapFrom("FirstName + \" \" + LastName")]
    public string FullName { get; set; } = string.Empty;
}

// Usage
var user = new User { Id = 1, FirstName = "John", LastName = "Doe", Email = "john@example.com" };
var dto = new UserDto(user);
// dto.Name = "John" (mapped from FirstName)
// dto.FamilyName = "Doe" (mapped from LastName)
// dto.FullName = "John Doe" (computed expression)

// Reverse mapping works automatically
var entity = dto.ToSource();
// entity.FirstName = "John" (mapped from Name)
// entity.LastName = "Doe" (mapped from FamilyName)

// Projections also work
var dtos = users.SelectFacet<UserDto>().ToList();
Controlling Reversibility and Projection Inclusion
[Facet(typeof(User), GenerateToSource = true)]
public partial class UserDto
{
    // Reversible mapping (included in ToSource) - opt-in
    [MapFrom(nameof(User.FirstName), Reversible = true)]
    public string Name { get; set; } = string.Empty;

    // Default: not reversible (one-way, source → DTO only)
    [MapFrom(nameof(User.LastName))]
    public string DisplayName { get; set; } = string.Empty;

    // Exclude from EF Core projection (for client-side computed values)
    [MapFrom("Name.ToUpper()", IncludeInProjection = false)]
    public string UpperName { get; set; } = string.Empty;
}
When to Use MapFrom vs Custom Configuration
Use Case MapFrom Custom Config
Simple property rename ✅ Best choice Overkill
Multiple renames ✅ Best choice Overkill
Computed values (expressions) ✅ Supported Alternative
Async operations ✅ Required
Complex transformations ✅ Required

Note: MapFrom and custom configurations can be combined. Auto-generated mappings (including MapFrom) are applied first, then the custom mapper is called.

</details>

<details> <summary>Conditional Mapping with [MapWhen]</summary>

Map properties only when specific conditions are met. Perfect for status-dependent fields, null checks, or role-based data exposure:

public class Order
{
    public int Id { get; set; }
    public OrderStatus Status { get; set; }
    public DateTime? CompletedAt { get; set; }
    public string? TrackingNumber { get; set; }
    public bool IsActive { get; set; }
    public string? Email { get; set; }
}

[Facet(typeof(Order))]
public partial class OrderDto
{
    // Only map when status is Completed
    [MapWhen("Status == OrderStatus.Completed")]
    public DateTime? CompletedAt { get; set; }

    // Only map when not cancelled
    [MapWhen("Status != OrderStatus.Cancelled")]
    public string? TrackingNumber { get; set; }

    // Boolean condition
    [MapWhen("IsActive")]
    public string? Email { get; set; }
}

// Usage
var order = new Order
{
    Id = 1,
    Status = OrderStatus.Completed,
    CompletedAt = DateTime.Now,
    IsActive = true,
    Email = "user@example.com"
};

var dto = new OrderDto(order);
// dto.CompletedAt = DateTime.Now (condition true)
// dto.Email = "user@example.com" (IsActive is true)

var pendingOrder = new Order { Status = OrderStatus.Pending, Email = "test@example.com" };
var pendingDto = new OrderDto(pendingOrder);
// pendingDto.CompletedAt = null (condition false)
Multiple Conditions (AND Logic)
[Facet(typeof(Order))]
public partial class SecureOrderDto
{
    // Both conditions must be true
    [MapWhen("IsActive")]
    [MapWhen("Status == OrderStatus.Completed")]
    public DateTime? CompletedAt { get; set; }
}
Supported Conditions
  • Boolean: [MapWhen("IsActive")]
  • Equality: [MapWhen("Status == OrderStatus.Completed")]
  • Inequality: [MapWhen("Status != OrderStatus.Cancelled")]
  • Null checks: [MapWhen("Email != null")]
  • Comparisons: [MapWhen("Age >= 18")]
  • Negation: [MapWhen("!IsDeleted")]
Works with EF Core Projections
var orders = await dbContext.Orders
    .Where(o => o.IsActive)
    .SelectFacet<OrderDto>()  // Conditions included in SQL
    .ToListAsync();

</details>

<details> <summary>Before/After Mapping Hooks</summary>

Run custom logic before and/or after the automatic property mapping. Perfect for validation, setting defaults, and computing derived values:

using Facet.Mapping;

// BeforeMap - runs BEFORE properties are copied
public class UserBeforeMapConfig : IFacetBeforeMapConfiguration<User, UserDto>
{
    public static void BeforeMap(User source, UserDto target)
    {
        // Validate input
        if (string.IsNullOrEmpty(source.Email))
            throw new ValidationException("Email is required");
        
        // Set defaults on target
        target.MappedAt = DateTime.UtcNow;
    }
}

// AfterMap - runs AFTER properties are copied
public class UserAfterMapConfig : IFacetAfterMapConfiguration<User, UserDto>
{
    public static void AfterMap(User source, UserDto target)
    {
        // Compute derived values
        target.FullName = $"{target.FirstName} {target.LastName}";
        target.Age = CalculateAge(source.DateOfBirth);
    }
}

// Apply hooks via attribute
[Facet(typeof(User), 
    BeforeMapConfiguration = typeof(UserBeforeMapConfig),
    AfterMapConfiguration = typeof(UserAfterMapConfig))]
public partial class UserDto
{
    public DateTime MappedAt { get; set; }
    public string FullName { get; set; } = string.Empty;
    public int Age { get; set; }
}
Combined Hooks

Use IFacetMapHooksConfiguration for both before and after logic in one class:

public class UserMappingHooks : IFacetMapHooksConfiguration<User, UserDto>
{
    public static void BeforeMap(User source, UserDto target)
    {
        target.MappedAt = DateTime.UtcNow;
    }
    
    public static void AfterMap(User source, UserDto target)
    {
        target.FullName = $"{target.FirstName} {target.LastName}";
    }
}

[Facet(typeof(User), 
    BeforeMapConfiguration = typeof(UserMappingHooks),
    AfterMapConfiguration = typeof(UserMappingHooks))]
public partial class UserDto { }
Async Hooks with Dependency Injection
public class UserEnrichmentHook : IFacetAfterMapConfigurationAsyncInstance<User, UserDto>
{
    private readonly IProfileService _profileService;
    
    public UserEnrichmentHook(IProfileService profileService)
    {
        _profileService = profileService;
    }
    
    public async Task AfterMapAsync(User source, UserDto target, CancellationToken ct = default)
    {
        target.ProfileUrl = await _profileService.GetProfileUrlAsync(source.Id, ct);
    }
}
When to Use Each Hook
Hook When Called Use Case
BeforeMap Before properties copied Validation, defaults, timestamps
AfterMap After properties copied Computed values, transformations
Configuration (Map) After mapping Simple computed properties

Execution order: BeforeMap → Property Mapping → Configuration.Map → AfterMap

</details>

<details> <summary>Inheritance Mapping</summary>

Facet fully supports inheritance hierarchies. Properties from base classes are automatically included:

// Base domain model
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }  // Sensitive
}

// Derived domain model
public class Employee : User
{
    public string Department { get; set; }
    public decimal Salary { get; set; }  // Sensitive
}

// Facet for Employee - includes User properties automatically
[Facet(typeof(Employee), "Password", "Salary")]
public partial class EmployeeDto;

// Generated properties:
// From User: Id, FirstName, Email
// From Employee: Department
// Excluded: Password, Salary
Facets with Base Classes

Your facet types can also inherit from base classes. Facet won't duplicate inherited properties:

// Shared base facet
public abstract class BaseFacet
{
    public int Id { get; set; }
    public bool IsActive { get; set; }
}

// Facet inherits from base - Id and IsActive come from base
[Facet(typeof(Product), "InternalCode")]
public partial class ProductDto : BaseFacet
{
    // Generated: Name, Description, Price
    // Inherited from BaseFacet: Id, IsActive (NOT duplicated)
}
Generic Base Classes
public class BaseEntity<TKey>
{
    public TKey Id { get; set; }
}

public class Category : BaseEntity<uint>
{
    public string Name { get; set; }
}

// Exclude Id from generic base
[Facet(typeof(Category), "Id")]
public partial class UpdateCategoryDto;
// Result: Name only (Id excluded)

</details>

<details> <summary>Async Mapping for I/O Operations</summary>

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>();

</details>

<details> <summary>Async Mapping with Dependency Injection</summary>

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);

</details>

<details> <summary>EF Core Integration</summary>

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();
Automatic Navigation Property Loading (No .Include() Required!)
// Define nested facets
[Facet(typeof(Address))]
public partial record AddressDto;

[Facet(typeof(Company), NestedFacets = [typeof(AddressDto)])]
public partial record CompanyDto;

// Navigation properties are automatically loaded - no .Include() needed!
var companies = await dbContext.Companies
    .Where(c => c.IsActive)
    .SelectFacet<CompanyDto>()
    .ToListAsync();

// The HeadquartersAddress navigation property is automatically included!
// EF Core analyzes the projection expression and generates the necessary JOINs

// This also works with collections:
[Facet(typeof(OrderItem))]
public partial record OrderItemDto;

[Facet(typeof(Order), NestedFacets = [typeof(OrderItemDto), typeof(AddressDto)])]
public partial record OrderDto;

var orders = await dbContext.Orders
    .SelectFacet<OrderDto>()  // Automatically includes Items collection and ShippingAddress!
    .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));
}
Advanced: Custom Mappers with EF Core (Facet.Extensions.EFCore.Mapping)

For complex mappings that cannot be expressed as SQL projections (e.g., external service calls, complex type conversions), use the advanced mapping package:

// Install: dotnet add package Facet.Extensions.EFCore.Mapping
using Facet.Extensions.EFCore.Mapping;

// Example: Converting separate X, Y properties into a Vector2 type
[Facet(typeof(User), exclude: [nameof(User.X), nameof(User.Y)])]
public partial class UserDto
{
    public Vector2 Position { get; set; }
}

// Static mapper
public class UserMapper : IFacetMapConfigurationAsync<User, UserDto>
{
    public static async Task MapAsync(User source, UserDto target, CancellationToken cancellationToken = default)
    {
        target.Position = new Vector2(source.X, source.Y);
    }
}

// Usage with EF Core queries
var users = await dbContext.Users
    .Where(u => u.IsActive)
    .ToFacetsAsync<User, UserDto, UserMapper>();

// Or with dependency injection
public class UserMapper : IFacetMapConfigurationAsyncInstance<User, UserDto>
{
    private readonly ILocationService _locationService;

    public UserMapper(ILocationService locationService)
    {
        _locationService = locationService;
    }

    public async Task MapAsync(User source, UserDto target, CancellationToken cancellationToken = default)
    {
        target.Position = new Vector2(source.X, source.Y);
        target.Location = await _locationService.GetLocationAsync(source.LocationId);
    }
}

// Usage with DI
var users = await dbContext.Users
    .Where(u => u.IsActive)
    .ToFacetsAsync<User, UserDto>(userMapper);

Note: Custom mapper methods materialize the query first (execute SQL), then apply your custom logic. All matching properties are auto-mapped first.

</details>

<details> <summary>Automatic CRUD DTO Generation with [GenerateDtos]</summary>

Generate standard Create, Update, Response, Query, Upsert, and Patch 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 = [nameof(Product.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 = [nameof(Schedule.Password), nameof(Schedule.InternalNotes)])]
[GenerateDtos(Types = DtoTypes.Upsert, ExcludeProperties = [nameof(Schedule.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)

</details>

<details> <summary>Flatten nested objects with [Flatten]</summary>

Flatten nested object hierarchies into top-level properties automatically - perfect for API responses, reports, and denormalized views:

// Domain models with nested structure
public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
    public Country Country { get; set; }
}

public class Country
{
    public string Name { get; set; }
    public string Code { get; set; }
}

public class ContactInfo
{
    public string Email { get; set; }
    public string Phone { get; set; }
}

// Automatically flatten all nested properties
[Flatten(typeof(Person))]
public partial class PersonFlatDto
{
    // Auto-generates:
    // public int Id { get; set; }
    // public string FirstName { get; set; }
    // public string LastName { get; set; }
    // public string AddressStreet { get; set; }
    // public string AddressCity { get; set; }
    // public string AddressZipCode { get; set; }
    // public string AddressCountryName { get; set; }
    // public string AddressCountryCode { get; set; }
    // public string ContactInfoEmail { get; set; }
    // public string ContactInfoPhone { get; set; }
}

// Usage with constructor
var person = new Person
{
    FirstName = "John",
    Address = new Address
    {
        Street = "123 Main St",
        City = "Springfield",
        Country = new Country { Name = "USA", Code = "US" }
    },
    ContactInfo = new ContactInfo { Email = "john@example.com" }
};

var dto = new PersonFlatDto(person);
// dto.AddressStreet = "123 Main St"
// dto.AddressCountryName = "USA"

// Usage with Entity Framework projection
var flatDtos = await dbContext.People
    .Where(p => p.IsActive)
    .Select(PersonFlatDto.Projection)
    .ToListAsync();
Controlling Depth and Exclusions
// Limit flattening depth
[Flatten(typeof(Person), MaxDepth = 2)]
public partial class PersonFlatDepth2Dto
{
    // Includes Address.Street and Address.City
    // Does NOT include Address.Country.* (beyond depth 2)
}

// Exclude specific paths
[Flatten(typeof(Person), nameof(Person.ContactInfo))]
public partial class PersonFlatWithoutContactDto
{
    // All properties except ContactInfo.*
}

[Flatten(typeof(Person), $"{nameof(Person.Address)}.{nameof(Address.Country)}")]
public partial class PersonFlatWithoutCountryDto
{
    // Includes Address.Street, Address.City
    // Excludes Address.Country.*
}
Naming Strategies
// Prefix strategy (default): AddressStreet, AddressCity
[Flatten(typeof(Person), NamingStrategy = FlattenNamingStrategy.Prefix)]
public partial class PersonFlatPrefixDto { }

// Leaf-only strategy: Street, City (may cause collisions)
[Flatten(typeof(Person), NamingStrategy = FlattenNamingStrategy.LeafOnly)]
public partial class PersonFlatLeafDto { }

</details>

<details> <summary>Reference-based Wrappers with [Wrapper]</summary>

Generate wrapper classes that delegate to a source object instead of copying values. Unlike [Facet] which creates independent copies, wrappers maintain a reference to the source, so changes propagate:

// Domain model
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }  // Sensitive!
    public decimal Salary { get; set; }   // Sensitive!
}

// Hide sensitive properties with a facade
[Wrapper(typeof(User), nameof(User.Password), nameof(User.Salary))]
public partial class PublicUserWrapper { }

// Usage - changes propagate to source!
var user = new User { Id = 1, FirstName = "John", Password = "secret" };
var wrapper = new PublicUserWrapper(user);

wrapper.FirstName = "Jane";
Console.WriteLine(user.FirstName);  // "Jane" - source is modified!

// Sensitive properties not accessible
// wrapper.Password;  // Compile error
// wrapper.Salary;    // Compile error
Read-Only Wrappers (Immutable Facades)
// Prevent modifications with ReadOnly mode
[Wrapper(typeof(Product), ReadOnly = true)]
public partial class ReadOnlyProductView { }

var product = new Product { Name = "Laptop", Price = 1299.99m };
var view = new ReadOnlyProductView(product);

// Can read
Console.WriteLine(view.Name);

// Cannot write (compile error CS0200)
// view.Name = "Desktop";  // Property is read-only

// Still reflects source changes
product.Name = "Desktop";
Console.WriteLine(view.Name);  // "Desktop"
Use Cases
  • Facade Pattern: Hide sensitive/internal properties from API consumers
  • ViewModel Pattern: Expose domain model subset to UI with live binding
  • Decorator Pattern: Add behavior without modifying domain models
  • Memory Efficiency: Avoid duplicating large object graphs
  • Read-only Views: Immutable facades
Wrapper vs Facet
Aspect Facet (Value Copy) Wrapper (Reference)
Data Storage Independent copy Reference to source
Memory Duplicates data No duplication
Changes Independent Synchronized to source
Use Case DTOs, EF projections Facades, ViewModels

</details>

<details> <summary>Source Signature Tracking for Breaking Change Detection</summary>

Track changes to your source entities and get compile-time warnings when the structure changes. This helps prevent unintended API breaking changes when your EF Core models are modified.

Basic Usage
// Without tracking (default behavior)
[Facet(typeof(User))]
public partial class UserDto;

// With change detection - add a SourceSignature
[Facet(typeof(User), SourceSignature = "a1b2c3d4")]
public partial class UserDto;
How It Works
  1. The signature is an 8-character hash based on property names and types
  2. When the source entity changes (properties added/removed/renamed), the hash changes
  3. A compile-time warning (FAC022) alerts you to review and acknowledge the change
Example Warning

When someone adds a new property to the User entity:

warning FAC022: Source entity 'User' structure has changed.
Update SourceSignature to 'e5f6g7h8' to acknowledge this change.
IDE Code Fix

In Visual Studio or Rider, click the lightbulb to automatically update the signature:

"Update SourceSignature to 'e5f6g7h8'"

Benefits
Benefit Description
Prevents data exposure New sensitive fields don't silently appear in API responses
Catches breaking changes Removed fields are detected before runtime errors
Explicit acknowledgment Forces review of changes before they reach production
Opt-in Only active when you set a SourceSignature
Workflow
  1. During development, use Facet without signatures for flexibility
  2. Before release, add SourceSignature to lock the API contract
  3. When source entities change, the warning reminds you to review
  4. Update the signature to acknowledge intentional changes

</details>

🌎 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, projecting and patch updates (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+).
  • Facet.Extensions.EFCore.Mapping: Advanced custom async mapper support for EF Core queries. Enables complex mappings that cannot be expressed as SQL projections

Facet Dashboard

Use Facet.Dashboard to visualize your Facets!

<img width="1595" height="1311" alt="image" src="https://github.com/user-attachments/assets/af99d28d-d83b-4807-b7c4-8dad192dc9c4" />

Comparison

Feature Facet AutoMapper Mapperly Mapster
Generation Time Compile Runtime Compile Runtime
EF Core Projections ✅ Auto ❌ Manual ⚠️ Manual ⚠️ Manual
Navigation Loading ✅ Auto ❌ Manual ❌ Manual ❌ Manual
Flatten/Wrapper/CRUD ✅ Built-in ⚠️ Limited
Expression Transform
Breaking Detection
Conditional Mapping ✅ MapWhen ⚠️ Custom ⚠️ Custom ⚠️ Custom
Before/After Hooks ✅ Built-in ⚠️ Manual ⚠️ Custom

Facet is the only tool that combines compile-time generation with deep EF Core integration.

Performance Benchmarks

Note that these are perfomed by using the <TSource, TDestination> mapping method overloads wherever possible, as they are significantly faster than the <TDestination> versions.

Simple mapping

Method Mean Ratio Allocated Alloc Ratio
Facet 5.922 ns baseline 40 B
Mapperly 6.227 ns 1.05x slower 40 B 1.00x more
Mapster 13.243 ns 2.24x slower 40 B 1.00x more
AutoMapper 31.459 ns 5.31x slower 40 B 1.00x more

Nested mapping

Method Mean Ratio Allocated Alloc Ratio
Facet 5.497 ns baseline 32 B
Mapperly 9.015 ns 1.64x slower 72 B 2.25x more
Mapster 17.743 ns 3.23x slower 72 B 2.25x more
AutoMapper 36.794 ns 6.69x slower 72 B 2.25x more

💖 Contributors

Facet wouldn't be the same without these awesome contributors. Thank you!

<a href="https://github.com/Tim-Maes/Facet/graphs/contributors"> <img src="https://contrib.rocks/image?repo=Tim-Maes/Facet" /> </a>

💖 Backers & supporters

<a href="https://github.com/pokeparadox"> <img src="https://images.weserv.nl/?url=github.com/pokeparadox.png&w=64&h=64&mask=circle" width="64" height="64" /> </a>

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Facet:

Package Downloads
Facet.Extensions

Provider-agnostic extension methods for Facet.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.3.0 57 1/2/2026
5.2.0 467 12/23/2025
5.2.0-alpha 331 12/18/2025
5.1.12 776 12/14/2025
5.1.11 1,082 12/9/2025
5.1.10 910 12/5/2025
5.1.9 1,033 12/3/2025
5.1.8 1,936 12/1/2025
5.1.7 626 12/1/2025
5.1.6 298 11/28/2025
5.1.5 196 11/28/2025
5.1.4 219 11/28/2025
5.1.3 332 11/28/2025
5.1.2 264 11/27/2025
5.1.1 259 11/27/2025
5.1.0 261 11/27/2025
5.0.4 823 11/26/2025
5.0.3 404 11/26/2025
5.0.2 308 11/26/2025
5.0.1 528 11/25/2025
5.0.0 404 11/24/2025
5.0.0-alpha5 246 11/24/2025
5.0.0-alpha4 234 11/23/2025
5.0.0-alpha3 232 11/22/2025
5.0.0-alpha2 247 11/22/2025
5.0.0-alpha 320 11/21/2025
4.4.3 567 11/21/2025
4.4.2 439 11/21/2025
4.4.1 528 11/20/2025
4.4.1-alpha4 340 11/16/2025
4.4.1-alpha3 327 11/16/2025
4.4.1-alpha2 335 11/16/2025
4.4.1-alpha 333 11/16/2025
4.4.0 956 11/15/2025
4.4.0-alpha 274 11/14/2025
4.3.3.1 314 11/14/2025
4.3.3 313 11/14/2025
4.3.2.1 297 11/14/2025
4.3.2 468 11/13/2025
4.3.1 900 11/12/2025
4.3.0 913 11/8/2025
4.3.0-alpha 199 11/7/2025
3.4.0 225 11/8/2025
3.3.0 375 11/7/2025
3.3.0-alpha.1 158 11/4/2025
3.3.0-alpha 235 11/3/2025
3.2.2 2,938 10/29/2025
3.2.1 1,188 10/27/2025
3.2.1-alpha.1 147 10/27/2025
3.2.1-alpha 229 10/26/2025
3.2.0-alpha 232 10/26/2025
3.1.14 374 10/24/2025
3.1.13 322 10/24/2025
3.1.12 780 10/21/2025
3.1.11 240 10/21/2025
3.1.5 201 10/24/2025
3.1.4 228 10/23/2025
3.1.3 240 10/23/2025
3.1.3-alpha 217 10/22/2025
3.1.2 225 10/21/2025
3.1.2-alpha 212 10/22/2025
3.1.1 241 10/21/2025
3.1.0 327 10/19/2025
3.0.0 203 10/17/2025
2.9.31 504 10/13/2025
2.9.3 396 10/8/2025
2.9.3-alpha 217 10/7/2025
2.9.2 1,497 10/6/2025
2.9.1 1,488 10/3/2025
2.9.0 319 10/1/2025
2.8.2 263 10/1/2025
2.8.1 1,720 9/21/2025
2.8.0 1,255 9/17/2025
2.7.0 807 9/12/2025
2.6.2 557 9/12/2025
2.6.1 454 9/10/2025
2.6.0 298 9/9/2025
2.5.0 892 9/4/2025
2.4.8 482 9/3/2025
2.4.7 591 9/1/2025
2.4.6 215 9/1/2025
2.4.5 294 8/30/2025
2.4.4 577 8/27/2025
2.4.3 265 8/27/2025
2.4.2 259 8/27/2025
2.4.1 271 8/26/2025
2.4.0 284 8/26/2025
2.3.0 1,892 8/20/2025
2.2.0 232 8/20/2025
2.1.0 680 8/18/2025
2.0.1 967 8/5/2025
2.0.0 223 8/4/2025
1.9.3 269 7/4/2025
1.9.1 211 7/3/2025
1.9.0 212 7/3/2025
1.8.0 384 6/4/2025
1.7.0 534 5/6/2025
1.6.0 252 4/27/2025
1.5.0 502 4/26/2025
1.4.0 190 4/25/2025
1.3.0 211 4/24/2025
1.2.0 210 4/24/2025
1.1.1 212 4/23/2025
1.1.0 224 4/23/2025
1.0.2 734 4/23/2025
1.0.1 247 4/23/2025
1.0.0 316 4/23/2025
0.1.0 254 4/22/2025