One part of an object, situation, or subject that has many parts.
In modern .NET development, we constantly find ourselves writing the same boilerplate code: DTOs for APIs, ViewModels for UI, projection classes for database queries, and endless mapping logic between them. There is a way to eliminate 90% of this repetitive work while maintaining strong typing, improving performance, and supporting advanced scenarios like async operations and dependency injection.
Introducing Facet - a C# source generator that significantly revises how we handle projections and mapping in .NET applications.

The Problem: Projection Proliferation
Consider a typical e-commerce application. You have a Product
entity with dozens of properties, but different parts of your application need different views of this data:
- API responses: Need public properties but exclude internal fields
- Search results: Need only name, price, and thumbnail
- Admin panels: Need everything including audit fields
- Email templates: Need formatted strings and computed properties
- Mobile apps: Need lightweight versions with different property names
Traditionally, this means:
- Writing separate DTO classes for each scenario
- Creating mapping methods or AutoMapper profiles
- Maintaining LINQ projection expressions for database efficiency
- Handling async operations for computed properties
- Managing dependencies for complex mapping logic
This can result in hundreds of lines of repetitive, error-prone boilerplate code that you need to manually maintain and test.
Facetting
Facetting is the process of carving out focused, lightweight views of richer models at compile time. Think of it like a diamond cutter carefully selecting which facets to polish and display, leaving others hidden.
Instead of manually writing DTOs, mappers, and projections, you simply declare what you want to keep - and Facet generates everything else. The beauty is that this happens entirely at compile time, meaning zero runtime overhead and full IntelliSense support.
What makes Facetting special?
⚡ Zero runtime cost
Everything is generated at compile time. No reflection, no IL emit, no performance penalties. Your projections run as fast as hand-written code because they are hand-written code - just not by you.
🔒 Strongly typed & nullable-aware
Full C# compile-time safety with complete nullability contract support. If your source property is nullable, your facet property will be too.
🎯 Multiple target types
Generate classes
, records
, structs
, or record structs
depending on your needs. Perfect for modern C# development patterns.
? Advanced Async mapping
Support for complex async operations like database lookups, API calls, and file I/O during mapping - with full dependency injection support.
🧩 Modular architecture
Four focused NuGet packages that work together:
- Facet: Core source generator
- Facet.Extensions: Provider-agnostic mapping helpers
- Facet.Mapping: Advanced async mapping with dependency injection
- Facet.Extensions.EFCore: Entity Framework Core integration
Quick Start
1. Install the package
dotnet add package Facet
dotnet add package Facet.Extensions
dotnet add package Facet.Extensions.EFCore # For EF Core integration
2. Define your 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 PasswordHash { get; set; } // Sensitive data
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public bool IsEmailVerified { get; set; }
}
3. Create your first Facet
using Facet;
// Simple DTO excluding sensitive data
[Facet(typeof(User), exclude: new[] { nameof(User.PasswordHash) })]
public partial class UserDto;
This generates:
- A
UserDto(User source)
constructor - All properties from
User
exceptPasswordHash
- A static
Expression<Func<User, UserDto>> Projection
for LINQ
4. Use your generated Facet
using Facet.Extensions;
// Constructor mapping
var user = GetUserFromDatabase();
var dto = new UserDto(user);
// Or even simpler
var dto = user.ToFacet<UserDto>();
// LINQ projection for database queries
var userDtos = await dbContext.Users
.Where(u => u.IsEmailVerified)
.SelectFacet<UserDto>()
.ToListAsync();
Advanced Scenarios
Different Output Types
// Immutable record for API responses
[Facet(typeof(User), Kind = FacetKind.Record)]
public partial record UserRecord;
// Value type for performance-critical scenarios
[Facet(typeof(User), Kind = FacetKind.Struct)]
public partial struct UserStruct;
// Modern immutable value type
[Facet(typeof(User), Kind = FacetKind.RecordStruct)]
public partial record struct UserRecordStruct;
Custom synchronous mapping
using Facet.Mapping;
public class UserMapper : IFacetMapConfiguration<User, UserDto>
{
public static void Map(User source, UserDto target)
{
// Computed properties
target.FullName = $"{source.FirstName} {source.LastName}";
target.DisplayEmail = source.Email.ToLower();
target.AccountAge = DateTime.UtcNow - source.CreatedAt;
// Conditional logic
target.IsActive = source.LastLoginAt.HasValue &&
source.LastLoginAt > DateTime.UtcNow.AddDays(-30);
}
}
[Facet(typeof(User), Configuration = typeof(UserMapper))]
public partial class UserDto
{
public string FullName { get; set; }
public string DisplayEmail { get; set; }
public TimeSpan AccountAge { get; set; }
public bool IsActive { get; set; }
}
💡 Pro Tip
Use async mapping for expensive operations like database lookups, API calls, or file I/O. Facet supports full dependency injection for these scenarios.
Entity Framework Core: Beyond Simple Projections
Facet's EF Core integration goes far beyond basic SELECT projections. It supports full bidirectional mapping for update scenarios:
Forward Mapping (Database ? DTO)
using Facet.Extensions.EFCore;
// High-performance async projections
var userDtos = await dbContext.Users
.Where(u => u.IsEmailVerified)
.ToFacetsAsync<UserDto>(cancellationToken);
// Single record with null safety
var userDto = await dbContext.Users
.Where(u => u.Id == userId)
.FirstFacetAsync<UserDto>(cancellationToken);
Reverse Mapping (DTO ? Database)
Perfect for update operations - only modified properties are marked as changed:
[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 actually changed
user.UpdateFromFacet(dto, context);
await context.SaveChangesAsync();
return NoContent();
}
Performance & Best Practices
Collection Processing Guidelines
// Small collections (< 100 items) - sequential processing
var results = await users.ToFacetsAsync(mapper);
// Large collections - parallel processing
var results = await users.ToFacetsParallelAsync(mapper, maxDegreeOfParallelism: Environment.ProcessorCount);
// Database-heavy operations - limit concurrency to avoid overwhelming the database
var results = await users.ToFacetsParallelAsync(mapper, maxDegreeOfParallelism: 2);
Real-World Example: E-Commerce Product API
Let's see how all these features work together in a real-world scenario:
// Domain model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public string ImagePath { get; set; }
public int StockQuantity { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
}
// Search results facet - minimal data for list views
[Facet(typeof(Product), exclude: new[] { nameof(Product.Description), nameof(Product.CreatedAt) })]
public partial record ProductSearchResult;
// API Controller using all facet types
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet("search")]
public async Task<List<ProductSearchResult>> Search([FromQuery] string query)
{
return await _context.Products
.Where(p => p.IsActive && p.Name.Contains(query))
.ToFacetsAsync<ProductSearchResult>();
}
}
Why Choose Facet?
? Eliminate Boilerplate
Reduce 90% of repetitive DTO and mapping code across your solution. Focus on business logic, not plumbing.
? Zero Runtime Overhead
All code generation happens at compile time. Your projections are as fast as hand-written code.
? Modern C# Support
Full support for records, record structs, nullable reference types, and init-only properties.
? Async-First Design
Built-in support for async operations with dependency injection, perfect for modern microservice architectures.
? EF Core Integration
Seamless integration with Entity Framework Core for both query projections and update operations.
? Gradual Adoption
Start simple with basic projections and gradually add complexity as needed. All features are opt-in.
? IntelliSense Support
Full IDE support with autocomplete, refactoring, and debugging because the generated code is just C#.
Getting Started Today
Ready to eliminate boilerplate and supercharge your .NET applications? Here's your action plan:
- Install Facet:
dotnet add package Facet
- Start simple: Create your first facet with just the
[Facet]
attribute - Add extensions: Install
Facet.Extensions
for mapping helpers - Integrate with EF Core: Add
Facet.Extensions.EFCore
for database projections - Go advanced: Explore async mapping with
Facet.Mapping
when you need it
Comments