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. Therer 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.

Facet logo

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:

  1. Writing separate DTO classes for each scenario
  2. Creating mapping methods or AutoMapper profiles
  3. Maintaining LINQ projection expressions for database efficiency
  4. Handling async operations for computed properties
  5. 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.

Facet on GitHub & Facet on NuGet

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 except PasswordHash
  • 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; }
}

Async mapping with Dependency Injection

This is where Facet really shines (pun intended). Need to load related data from databases, call external APIs, or perform file operations during mapping? No problem:

public interface IProfilePictureService
{
    Task<string> GetProfilePictureAsync(int userId, CancellationToken cancellationToken = default);
}

public interface IReputationService  
{
    Task<decimal> CalculateReputationAsync(string email, CancellationToken cancellationToken = default);
}

public class UserAsyncMapper : IFacetMapConfigurationAsyncInstance<User, EnrichedUserDto>
{
    private readonly IProfilePictureService _profileService;
    private readonly IReputationService _reputationService;
    private readonly ILogger<UserAsyncMapper> _logger;

    public UserAsyncMapper(
        IProfilePictureService profileService,
        IReputationService reputationService,
        ILogger<UserAsyncMapper> logger)
    {
        _profileService = profileService;
        _reputationService = reputationService;
        _logger = logger;
    }

    public async Task MapAsync(User source, EnrichedUserDto target, CancellationToken cancellationToken = default)
    {
        try
        {
            // Async database lookup
            target.ProfilePictureUrl = await _profileService.GetProfilePictureAsync(source.Id, cancellationToken);
            
            // Async API call
            target.ReputationScore = await _reputationService.CalculateReputationAsync(source.Email, cancellationToken);
            
            // Computed properties
            target.FullName = $"{source.FirstName} {source.LastName}";
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to enrich user data for {UserId}", source.Id);
            // Set defaults on error
            target.ProfilePictureUrl = "/images/default-avatar.png";
            target.ReputationScore = 0m;
        }
    }
}

[Facet(typeof(User))]
public partial class EnrichedUserDto 
{
    public string FullName { get; set; }
    public string ProfilePictureUrl { get; set; }
    public decimal ReputationScore { get; set; }
}

Using Async Mappers with Dependency Injection

// Register in your DI container
services.AddScoped<IProfilePictureService, ProfilePictureService>();
services.AddScoped<IReputationService, ReputationService>();
services.AddScoped<UserAsyncMapper>();

// Use in your controllers
public class UserController : ControllerBase
{
    private readonly UserAsyncMapper _userMapper;
    
    public UserController(UserAsyncMapper userMapper)
    {
        _userMapper = userMapper;
    }
    
    [HttpGet("{id}")]
    public async Task<EnrichedUserDto> GetUser(int id)
    {
        var user = await dbContext.Users.FindAsync(id);
        
        // Single instance async mapping with DI
        return await user.ToFacetAsync(_userMapper);
    }
    
    [HttpGet]
    public async Task<List<EnrichedUserDto>> GetUsers()
    {
        var users = await dbContext.Users.ToListAsync();
        
        // Collection async mapping with DI (parallel processing)
        return await users.ToFacetsParallelAsync(_userMapper, maxDegreeOfParallelism: 4);
    }
}

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

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

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

Hybrid Mapping for Optimal Performance

Combine fast synchronous operations with expensive async operations:

public class UserHybridMapper : IFacetMapConfigurationHybridInstance<User, UserDto>
{
    private readonly IExternalService _externalService;
    
    public UserHybridMapper(IExternalService externalService)
    {
        _externalService = externalService;
    }

    // Fast synchronous operations first
    public void Map(User source, UserDto target)
    {
        target.FullName = $"{source.FirstName} {source.LastName}";
        target.DisplayEmail = source.Email.ToLower();
        target.AgeCategory = CalculateAgeCategory(source.BirthDate);
    }

    // Expensive async operations only when needed
    public async Task MapAsync(User source, UserDto target, CancellationToken cancellationToken = default)
    {
        target.ExternalData = await _externalService.GetDataAsync(source.Id, cancellationToken);
    }
}

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;

// Detailed view with computed properties and external data
public class ProductDetailMapper : IFacetMapConfigurationAsyncInstance<Product, ProductDetailDto>
{
    private readonly ICategoryService _categoryService;
    private readonly IImageService _imageService;
    private readonly IReviewService _reviewService;

    public ProductDetailMapper(ICategoryService categoryService, IImageService imageService, IReviewService reviewService)
    {
        _categoryService = categoryService;
        _imageService = imageService;
        _reviewService = reviewService;
    }

    public async Task MapAsync(Product source, ProductDetailDto target, CancellationToken cancellationToken = default)
    {
        // Parallel async operations for better performance
        var categoryTask = _categoryService.GetCategoryNameAsync(source.CategoryId, cancellationToken);
        var imageUrlTask = _imageService.GetOptimizedImageUrlAsync(source.ImagePath, cancellationToken);
        var avgRatingTask = _reviewService.GetAverageRatingAsync(source.Id, cancellationToken);

        await Task.WhenAll(categoryTask, imageUrlTask, avgRatingTask);

        target.CategoryName = await categoryTask;
        target.OptimizedImageUrl = await imageUrlTask;
        target.AverageRating = await avgRatingTask;
        target.IsInStock = source.StockQuantity > 0;
        target.FormattedPrice = source.Price.ToString("C");
    }
}

[Facet(typeof(Product), Configuration = typeof(ProductDetailMapper))]
public partial class ProductDetailDto 
{
    public string CategoryName { get; set; }
    public string OptimizedImageUrl { get; set; }
    public decimal AverageRating { get; set; }
    public bool IsInStock { get; set; }
    public string FormattedPrice { get; set; }
}

// API Controller using all facet types
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly ProductDetailMapper _detailMapper;

    public ProductsController(AppDbContext context, ProductDetailMapper detailMapper)
    {
        _context = context;
        _detailMapper = detailMapper;
    }

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

    [HttpGet("{id}")]
    public async Task<ProductDetailDto> GetProduct(int id)
    {
        var product = await _context.Products.FindAsync(id);
        return await product.ToFacetAsync(_detailMapper);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProduct(int id, UpdateProductDto dto)
    {
        var product = await _context.Products.FindAsync(id);
        if (product == null) return NotFound();

        var result = product.UpdateFromFacetWithChanges(dto, _context);
        if (result.HasChanges)
        {
            await _context.SaveChangesAsync();
            return Ok(new { changedProperties = result.ChangedProperties });
        }

        return Ok(new { message = "No changes detected" });
    }
}

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:

  1. Install Facet: dotnet add package Facet
  2. Start simple: Create your first facet with just the [Facet] attribute
  3. Add extensions: Install Facet.Extensions for mapping helpers
  4. Integrate with EF Core: Add Facet.Extensions.EFCore for database projections
  5. Go advanced: Explore async mapping with Facet.Mapping when you need it

Additional Resources

Have questions, suggestions, or want to contribute? The Facet project welcomes community involvement. Star the repository, open an issue, or submit a pull reques, every contribution makes the project better!

Comments