⚠️
Disclaimer: Facet is a fairly new library, and some things referenced in this article may be inaccurate because Facet might have had updates or changes since this was written. Please refer to the official documentation for the most current information.

Facets in .NET

Facet - "One part of an object, situation, or subject that has many parts."

This blogpost presents a comprehensive analysis of Facet, a C# source generator designed to address the proliferation of boilerplate code in modern .NET applications through compile-time projection generation. I address the theoretical foundations of facetting as a software engineering concept, analyze the implementation architecture, and demonstrate how to leverage this approach in real-world applications.

The article covers advanced scenarios including asynchronous mapping with dependency injection, Entity Framework Core integration patterns, and expression tree transformation for LINQ compatibility.

1. Introduction

In contemporary .NET development, the Data Transfer Object (DTO) pattern has become ubiquitous for creating boundaries between application layers, API contracts, and external integrations. However, this pattern often leads to significant code duplication, maintenance overhead, and potential for mapping errors.

Facet addresses these challenges through compile-time code generation, implementing what we term "facetting" - the process of creating lightweight, focused projections of richer domain models. This approach eliminates boilerplate while maintaining strong typing, compile-time safety, and zero runtime overhead.

1.1 Quick Start: See It In Action

Before diving into the theory, let's see what Facet looks like in practice. Here are common scenarios you'll encounter:

Basic Projection - Exclude Mode

// 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; }
    public DateTime CreatedAt { get; set; }
}

// Exclude sensitive properties
[Facet(typeof(User), exclude: nameof(User.PasswordHash))]
public partial class UserDto { }

// Usage
var userDto = user.ToFacet<User, UserDto>();
var users = dbContext.Users
    .SelectFacet<User, UserDto>()
    .ToListAsync();

Focused Projection - Include Mode

// Only include specific properties for a contact card
[Facet(typeof(User), Include = new[] { "FirstName", "LastName", "Email" })]
public partial class UserContactDto { }

// Only include name for a simple dropdown
[Facet(typeof(User), Include = new[] { "Id", "FirstName", "LastName" })]
public partial class UserNameDto { }

// Usage
var contactInfo = user.ToFacet<User, UserContactDto>();
var dropdownItems = await dbContext.Users
    .SelectFacet<User, UserNameDto>()
    .ToListAsync();

Bidirectional Mapping

// Create a DTO
[Facet(typeof(User), Include = new[] { "FirstName", "LastName", "Email" })]
public partial class UpdateUserDto { }

// Map to DTO
var dto = user.ToFacet<User, UpdateUserDto>();

// Map back to entity (with smart defaults for missing properties)
var updatedUser = dto.BackTo<UpdateUserDto, User>();

// Or update existing entity efficiently
user.UpdateFromFacet(dto, dbContext);

Custom Computed Properties

// Define custom mapping logic
public class UserMapper : IFacetMapConfiguration<User, UserSummaryDto>
{
    public static void Map(User source, UserSummaryDto target)
    {
        target.FullName = $"{source.FirstName} {source.LastName}";
        target.MemberSince = $"Member since {source.CreatedAt:MMMM yyyy}";
    }
}

// Apply the mapper
[Facet(typeof(User), Configuration = typeof(UserMapper))]
public partial class UserSummaryDto
{
    public string FullName { get; set; }
    public string MemberSince { get; set; }
}

// Usage
var summary = user.ToFacet<User, UserSummaryDto>();

Auto-Generate CRUD DTOs

// Generate all standard CRUD DTOs at once
[GenerateDtos(Types = DtoTypes.All)]
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public DateTime CreatedAt { get; set; }
}

// Auto-generates:
// - CreateProductRequest (excludes Id, CreatedAt)
// - UpdateProductRequest (includes Id, excludes CreatedAt)
// - ProductResponse (includes everything)
// - ProductQuery (all properties nullable for filtering)
// - UpsertProductRequest (includes Id for create/update)

These examples demonstrate the core patterns you'll use daily with Facet. Now let's explore the theory and architecture behind these capabilities.

1.2 Objectives

This post aims to:

  • Establish the theoretical foundation of facetting as a software engineering practice
  • Analyze the implementation architecture of compile-time projection generation
  • Evaluate integration patterns with modern .NET frameworks
  • Demonstrate practical usage in real-world scenarios

1.3 Scope and Limitations

This article focuses on .NET 8+ implementations with C# 12+ language features. While the concepts are broadly applicable, specific implementation details are tailored to the Microsoft .NET ecosystem.

2. Problem Analysis

2.1 The Projection Proliferation Problem

Modern applications exhibit a characteristic pattern we term "projection proliferation" - the exponential growth of mapping code as application complexity increases. Consider a typical e-commerce system where a Product entity requires different projections for:

  • API responses: Public properties excluding internal metadata
  • Search indexes: Denormalized data optimized for full-text search
  • Administrative interfaces: Complete entity data including audit trails
  • Mobile applications: Bandwidth-optimized minimal datasets
  • External integrations: Schema-compliant data structures
  • Caching layers: Serialization-optimized representations

2.2 Traditional Mapping Approaches

2.2.1 Manual Mapping

public class ProductSummaryDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    // ... properties
}

public static class ProductMapper
{
    public static ProductSummaryDto ToSummaryDto(Product product)
    {
        return new ProductSummaryDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            // ... property assignments
        };
    }
}

Analysis: While providing maximum control, manual mapping scales poorly. Each new projection requires complete implementation and maintenance. Error-prone property assignments lead to runtime bugs that could be prevented at compile time.

2.3 Maintenance Overhead Analysis

In a typical enterprise application with 50 domain entities requiring an average of 4 projections each, traditional approaches result in:

  • Manual mapping: 200 DTO classes + 200 mapper classes = 400 files to maintain
  • Facet: 200 DTO declarations (partial classes) = 200 files, minimal maintenance

3. Theoretical Foundation

3.1 Facetting as a Design Pattern

Facetting represents a formalization of the projection pattern, drawing inspiration from both the Adapter and Facade patterns while maintaining strong compile-time guarantees. The concept is rooted in three fundamental principles:

3.1.1 Selective Exposure

A facet exposes only the properties relevant to a specific context, creating a focused view of a larger model. This principle supports the Interface Segregation Principle by ensuring consumers only depend on the data they actually need.

3.1.2 Compile-Time Generation

Unlike runtime mapping solutions, facetting occurs entirely at compile time through source generators. This approach provides several advantages:

  • Zero runtime performance overhead
  • Full IntelliSense support for generated code
  • Compile-time error detection for mapping issues
  • Debugger support for generated mapping logic

3.1.3 Type Safety Preservation

Facets maintain complete type safety including nullable reference type annotations, generic constraints, and custom attributes. This ensures that the compiler can provide the same level of safety guarantees as manually written code.

3.2 Mathematical Model

We can model facetting as a function F that takes a source type S and a specification σ to produce a target type T:

F: (S, σ) → T

where:
- S is the source type with properties {p₁, p₂, ..., pₙ}
- σ is the specification defining included/excluded properties
- T is the generated target type with properties {q₁, q₂, ..., qₘ}
- m ≤ n (target has equal or fewer properties than source)

The mapping function M between instances follows:

M: S → T
M(s) = t where t.qᵢ = s.pⱼ for all valid property mappings

3.3 Complexity Analysis

The time complexity of facet generation is O(n) where n is the number of properties in the source type. The space complexity is O(m) where m is the number of properties in the target type. This linear relationship ensures scalability as model complexity grows.

4. Implementation Architecture

4.1 Source Generator Pipeline

The Facet source generator implements the IIncrementalGenerator interface to leverage Roslyn's incremental compilation capabilities. The pipeline consists of four main stages:

4.1.1 Attribute Discovery

// Stage 1: Discover types annotated with [Facet] attributes
var facetTargets = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: static (s, _) => IsFacetCandidate(s),
        transform: static (ctx, _) => GetFacetTarget(ctx))
    .Where(static m => m is not null);

4.1.2 Semantic Analysis

// Stage 2: Analyze semantic model for type information
var semanticModels = facetTargets
    .Combine(context.CompilationProvider)
    .Select(static (x, _) => AnalyzeSemantics(x.Left, x.Right));

4.1.3 Code Generation Model Building

// Stage 3: Build generation models
var generationModels = semanticModels
    .Select(static (x, _) => BuildGenerationModel(x))
    .Where(static m => m.IsValid);

4.1.4 Source Code Emission

// Stage 4: Generate source code
generationModels.RegisterSourceOutput(context, 
    static (ctx, model) => EmitSourceCode(ctx, model));

4.2 Type System Integration

Facet integrates deeply with the C# type system to support modern language features:

4.2.1 Nullable Reference Types

// Source type with nullable annotations
public class User
{
    public string Name { get; set; } = string.Empty;
    public string? Email { get; set; }
    public DateTime? LastLoginAt { get; set; }
}

// Generated facet preserves nullability
[Facet(typeof(User))]
public partial class UserDto;

4.2.2 Generic Type Support

// Generic source types are fully supported
public class Repository<T> where T : class
{
    public IEnumerable<T> Items { get; set; }
    public int Count { get; set; }
}

[Facet(typeof(Repository<>))]
public partial class RepositoryDto<T> where T : class
{
    // Generated with proper generic constraints
}

5. Source Generator Internals

5.1 Incremental Generation Strategy

Facet leverages Roslyn's incremental generator architecture to minimize compilation overhead. The implementation uses a multi-stage pipeline that caches intermediate results and only regenerates code when dependencies change.

5.1.1 Change Detection

// Efficient change detection using content-based caching
public void Initialize(IncrementalGeneratorInitializationContext context)
{
    var facetDeclarations = context.SyntaxProvider
        .CreateSyntaxProvider(
            predicate: static (node, _) => IsFacetDeclaration(node),
            transform: static (ctx, ct) => ExtractFacetInfo(ctx, ct))
        .Where(static x => x is not null);

    // Combine with compilation to access semantic model
    var compilationAndFacets = context.CompilationProvider
        .Combine(facetDeclarations.Collect());

    context.RegisterSourceOutput(compilationAndFacets, 
        static (ctx, source) => GenerateFacetCode(ctx, source));
}

5.2 Symbol Analysis

The generator performs comprehensive symbol analysis to extract type information while preserving all metadata:

5.2.1 Property Analysis

private static FacetMember AnalyzeProperty(IPropertySymbol property)
{
    return new FacetMember(
        Name: property.Name,
        TypeName: GetFullTypeName(property.Type),
        Kind: FacetMemberKind.Property,
        IsInitOnly: property.SetMethod?.IsInitOnly == true,
        IsRequired: property.IsRequired,
        IsNullable: property.Type.CanBeReferencedByName && 
                   property.NullableAnnotation == NullableAnnotation.Annotated,
        XmlDocumentation: ExtractDocumentation(property),
        Attributes: ExtractAttributes(property)
    );
}

5.2.2 Generic Type Handling

private static string GetFullTypeName(ITypeSymbol type)
{
    return type switch
    {
        INamedTypeSymbol namedType when namedType.IsGenericType =>
            $"{namedType.Name}<{string.Join(", ", namedType.TypeArguments.Select(GetFullTypeName))}>",
        IArrayTypeSymbol arrayType =>
            $"{GetFullTypeName(arrayType.ElementType)}[]",
        _ => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
    };
}

5.3 Code Generation Templates

Facet uses template-based code generation with optimized string building to minimize memory allocations during compilation:

5.3.1 Constructor Generation

private static void GenerateConstructor(StringBuilder sb, FacetModel model)
{
    sb.AppendLine($"    public {model.Name}({model.SourceTypeFullName} source)");
    
    if (model.Kind is FacetKind.Record or FacetKind.RecordStruct && 
        !model.HasExistingPrimaryConstructor)
    {
        // Positional record constructor
        var parameters = string.Join(", ", 
            model.Members.Select(m => $"source.{m.Name}"));
        sb.AppendLine($"        : this({parameters})");
    }
    else
    {
        // Property assignment constructor
        sb.AppendLine("    {");
        foreach (var member in model.Members)
        {
            if (member.NeedsCustomMapping)
            {
                sb.AppendLine($"        // {member.Name} handled by custom mapper");
            }
            else
            {
                sb.AppendLine($"        this.{member.Name} = source.{member.Name};");
            }
        }
        
        if (!string.IsNullOrEmpty(model.ConfigurationTypeName))
        {
            sb.AppendLine($"        {model.ConfigurationTypeName}.Map(source, this);");
        }
        
        sb.AppendLine("    }");
    }
}

5.4 LINQ Expression Generation

For database integration, Facet generates optimized LINQ expressions that can be translated to SQL:

private static void GenerateProjectionExpression(StringBuilder sb, FacetModel model)
{
    sb.AppendLine($"    public static Expression<Func<{model.SourceTypeFullName}, {model.Name}>> Projection =>");
    
    if (model.HasCustomMapping)
    {
        // Complex projections require materialization first
        sb.AppendLine($"        source => {model.ConfigurationTypeName}.Map(source, null);");
    }
    else
    {
        // Simple projections can be translated to SQL
        sb.AppendLine($"        source => new {model.Name}(source);");
    }
}

5.5 Error Handling and Diagnostics

Comprehensive error reporting helps developers identify and resolve configuration issues at compile time:

private static void ReportDiagnostics(SourceProductionContext context, FacetModel model)
{
    // Check for missing source properties
    foreach (var excludedProperty in model.ExcludedProperties)
    {
        if (!model.SourceProperties.Contains(excludedProperty))
        {
            var descriptor = new DiagnosticDescriptor(
                "FACET001",
                "Excluded property not found",
                $"Property '{excludedProperty}' specified in exclude list was not found on source type '{model.SourceTypeName}'",
                "Facet",
                DiagnosticSeverity.Warning,
                isEnabledByDefault: true);
                
            context.ReportDiagnostic(Diagnostic.Create(descriptor, model.Location));
        }
    }
}

6. Mapping Strategies

6.1 Simple Property Mapping

The most basic form of facetting involves direct property copying with optional exclusions:

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
    public DateTime CreatedAt { get; set; }
    public bool IsActive { get; set; }
}

[Facet(typeof(User), exclude: new[] { nameof(User.PasswordHash) })]
public partial class UserDto
{
    // All properties except PasswordHash are generated
}

6.2 Custom Synchronous Mapping

For computed properties and transformation logic, Facet supports custom mappers:

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.ToLowerInvariant();
        target.AccountAge = DateTime.UtcNow - source.CreatedAt;
        
        // Conditional logic
        target.StatusText = source.IsActive ? "Active" : "Inactive";
        
        // Format transformations
        target.CreatedAtFormatted = source.CreatedAt.ToString("MMM dd, yyyy");
    }
}

[Facet(typeof(User), Configuration = typeof(UserMapper))]
public partial class UserDto
{
    public string FullName { get; set; } = string.Empty;
    public string DisplayEmail { get; set; } = string.Empty;
    public TimeSpan AccountAge { get; set; }
    public string StatusText { get; set; } = string.Empty;
    public string CreatedAtFormatted { get; set; } = string.Empty;
}

6.3 Asynchronous Mapping with Dependencies

For complex scenarios requiring external data sources, Facet supports asynchronous mapping with dependency injection:

6.3.1 Service Configuration

// Dependency injection setup
services.AddScoped<IUserProfileService, UserProfileService>();
services.AddScoped<IReputationService, ReputationService>();
services.AddFacetMapping(); // Registers mapping services

6.3.2 Async Mapper Implementation

public class UserAsyncMapper : IFacetMapConfigurationAsync<User, UserDto>
{
    public static async Task MapAsync(
        User source, 
        UserDto target, 
        IServiceProvider services,
        CancellationToken cancellationToken = default)
    {
        var profileService = services.GetRequiredService<IUserProfileService>();
        var reputationService = services.GetRequiredService<IReputationService>();
        
        // Parallel async operations for optimal performance
        var tasks = new[]
        {
            LoadProfilePictureAsync(source.Id, profileService, cancellationToken),
            CalculateReputationAsync(source.Email, reputationService, cancellationToken),
            LoadPreferencesAsync(source.Id, profileService, cancellationToken)
        };
        
        var results = await Task.WhenAll(tasks);
        
        target.ProfilePictureUrl = results[0];
        target.ReputationScore = (decimal)results[1];
        target.Preferences = (UserPreferences)results[2];
    }
    
    private static async Task<string> LoadProfilePictureAsync(
        int userId, 
        IUserProfileService service, 
        CancellationToken cancellationToken)
    {
        var profile = await service.GetProfileAsync(userId, cancellationToken);
        return profile?.ProfilePictureUrl ?? "/images/default-avatar.png";
    }
}

6.4 Hybrid Mapping Strategy

For optimal balance, Facet supports hybrid mapping that combines synchronous and asynchronous operations:

public class UserHybridMapper : IFacetMapConfigurationHybrid<User, UserDto>
{
    // Fast synchronous operations
    public static void Map(User source, UserDto target)
    {
        target.FullName = $"{source.FirstName} {source.LastName}";
        target.DisplayEmail = source.Email.ToLowerInvariant();
        target.AccountAge = DateTime.UtcNow - source.CreatedAt;
        target.IsRecent = source.CreatedAt > DateTime.UtcNow.AddDays(-30);
    }

    // Expensive asynchronous operations
    public static async Task MapAsync(
        User source, 
        UserDto target, 
        IServiceProvider services,
        CancellationToken cancellationToken = default)
    {
        var externalService = services.GetRequiredService<IExternalDataService>();
        
        // Only perform expensive operations if needed
        if (target.IsRecent)
        {
            target.ExternalData = await externalService
                .GetDataAsync(source.Id, cancellationToken);
        }
    }
}

6.5 Collection Mapping

For collections, Facet provides convenient extension methods:

6.5.1 Parallel Processing

// Sequential mapping (default)
var userDtos = await users.ToFacetsAsync<UserDto, UserAsyncMapper>(serviceProvider);

// Parallel mapping with controlled concurrency
var userDtosParallel = await users.ToFacetsParallelAsync<UserDto, UserAsyncMapper>(
    serviceProvider,
    maxDegreeOfParallelism: Environment.ProcessorCount,
    cancellationToken: cancellationToken);

// Batch processing for database-intensive operations
var userDtosBatched = await users.ToFacetsBatchAsync<UserDto, UserAsyncMapper>(
    serviceProvider,
    batchSize: 50,
    cancellationToken: cancellationToken);

6.5.2 Memory-Efficient Streaming

// For very large collections, use streaming
await foreach (var userDto in users.ToFacetsStreamAsync<UserDto, UserAsyncMapper>(
    serviceProvider, cancellationToken))
{
    // Process each item as it's mapped
    await ProcessUserDto(userDto);
}

7. Advanced Scenarios

7.1 Nested Type Mapping

Facet supports complex object graphs with nested type transformations:

public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public Customer Customer { get; set; }
    public List<OrderItem> Items { get; set; }
    public Address ShippingAddress { get; set; }
}

public class OrderMapper : IFacetMapConfiguration<Order, OrderDto>
{
    public static void Map(Order source, OrderDto target)
    {
        // Transform nested objects
        target.CustomerInfo = source.Customer.ToFacet<CustomerDto>();
        
        // Transform collections
        target.Items = source.Items
            .Select(item => item.ToFacet<OrderItemDto>())
            .ToList();
            
        // Transform with custom logic
        target.ShippingAddress = source.ShippingAddress?.ToFacet<AddressDto>() 
                                 ?? new AddressDto { Type = "Unknown" };
                                 
        // Computed properties
        target.TotalAmount = source.Items.Sum(i => i.Price * i.Quantity);
        target.ItemCount = source.Items.Count;
    }
}

7.2 Conditional Mapping

Dynamic property inclusion based on runtime conditions:

public class ConditionalUserMapper : IFacetMapConfiguration<User, UserDto>
{
    public static void Map(User source, UserDto target)
    {
        // Include sensitive data only for admin users
        if (IsAdmin(source))
        {
            target.InternalNotes = source.InternalNotes;
            target.LastPasswordChange = source.LastPasswordChange;
        }
        
        // Include premium features for premium users
        if (source.SubscriptionType == SubscriptionType.Premium)
        {
            target.PremiumFeatures = LoadPremiumFeatures(source.Id);
        }
        
        // Localized content based on user preferences
        target.LocalizedContent = GetLocalizedContent(
            source.PreferredLanguage, 
            source.Region);
    }
    
    private static bool IsAdmin(User user) => 
        user.Roles.Any(r => r.Name == "Administrator");
}

7.3 Polymorphic Type Handling

Support for inheritance hierarchies and polymorphic scenarios:

public abstract class PaymentMethod
{
    public int Id { get; set; }
    public string Type { get; set; }
    public bool IsActive { get; set; }
}

public class CreditCard : PaymentMethod
{
    public string LastFourDigits { get; set; }
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}

public class PayPalAccount : PaymentMethod
{
    public string Email { get; set; }
    public bool IsVerified { get; set; }
}

public class PaymentMethodMapper : IFacetMapConfiguration<PaymentMethod, PaymentMethodDto>
{
    public static void Map(PaymentMethod source, PaymentMethodDto target)
    {
        target.TypeSpecificData = source switch
        {
            CreditCard cc => new
            {
                LastFour = cc.LastFourDigits,
                Expiry = $"{cc.ExpiryMonth}/{cc.ExpiryYear}"
            },
            PayPalAccount pp => new
            {
                Email = pp.Email,
                Verified = pp.IsVerified
            },
            _ => new { Type = "Unknown" }
        };
    }
}

7.4 Expression Tree Transformation

Advanced LINQ integration with expression tree transformation for filtering and sorting:

// Original predicate on domain entity
Expression<Func<User, bool>> domainPredicate = u => u.IsActive && u.Email.Contains("@company.com");

// Transform to work with DTO
Expression<Func<UserDto, bool>> dtoPredicate = domainPredicate.Transform<User, UserDto>();

// Use with projected collections
var filteredDtos = await dbContext.Users
    .SelectFacet<UserDto>()
    .Where(dtoPredicate)
    .ToListAsync();

7.5 Validation Integration

Integration with validation frameworks for automatic constraint propagation:

public class User
{
    [Required]
    [MaxLength(100)]
    public string FirstName { get; set; }
    
    [EmailAddress]
    public string Email { get; set; }
    
    [Range(18, 120)]
    public int Age { get; set; }
}

[Facet(typeof(User), PreserveValidationAttributes = true)]
public partial class UserDto
{
    // Validation attributes are automatically copied
    // [Required, MaxLength(100)] public string FirstName { get; set; }
    // [EmailAddress] public string Email { get; set; }
    // [Range(18, 120)] public int Age { get; set; }
}

8. Integration Patterns

8.1 Entity Framework Core Integration

Facet provides comprehensive Entity Framework Core integration through the Facet.Extensions.EFCore package:

8.1.1 Query Projections

// Basic projection
var userDtos = await dbContext.Users
    .Where(u => u.IsActive)
    .SelectFacet<UserDto>()
    .ToListAsync();

// Projection with includes
var orderDtos = await dbContext.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
    .SelectFacet<OrderDto>()
    .ToListAsync();

// Projection with custom filtering
var recentUserDtos = await dbContext.Users
    .Where(u => u.CreatedAt > DateTime.UtcNow.AddDays(-30))
    .SelectFacet<UserDto>()
    .OrderBy(dto => dto.LastName)
    .ToListAsync();

8.1.2 Update Operations

// Efficient updates using facets
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(int id, UserUpdateDto dto)
{
    var user = await dbContext.Users.FindAsync(id);
    if (user == null) return NotFound();
    
    // Only modified properties are tracked for changes
    user.UpdateFromFacet(dto, dbContext);
    
    await dbContext.SaveChangesAsync();
    return NoContent();
}

// Generated UpdateFromFacet method ensures optimal SQL
// UPDATE Users SET FirstName = @p0, Email = @p1 
// WHERE Id = @p2 -- Only changed properties

8.1.3 Bulk Operations

// Bulk insert with facets
var userDtos = GetUserDtosFromApi();
var users = userDtos.Select(dto => dto.ToEntity<User>()).ToList();

dbContext.Users.AddRange(users);
await dbContext.SaveChangesAsync();

// Bulk update with optimized change tracking
var existingUsers = await dbContext.Users
    .Where(u => userIds.Contains(u.Id))
    .ToListAsync();

foreach (var user in existingUsers)
{
    var dto = userDtos.First(d => d.Id == user.Id);
    user.UpdateFromFacet(dto, dbContext, trackChanges: false);
}

await dbContext.SaveChangesAsync();

8.2 ASP.NET Core API Integration

Seamless integration with ASP.NET Core controllers and minimal APIs:

8.2.1 Controller Integration

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly IFacetMapper _mapper;

    public UsersController(AppDbContext context, IFacetMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    [HttpGet]
    public async Task<ActionResult<List<UserDto>>> GetUsers(
        [FromQuery] int page = 1, 
        [FromQuery] int pageSize = 20)
    {
        var users = await _context.Users
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .SelectFacet<UserDto>()
            .ToListAsync();

        return Ok(users);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<UserDetailDto>> GetUser(int id)
    {
        var user = await _context.Users
            .Include(u => u.Profile)
            .FirstOrDefaultAsync(u => u.Id == id);

        if (user == null) return NotFound();

        // Async mapping with services
        var dto = await user.ToFacetAsync<UserDetailDto, UserDetailMapper>(_mapper);
        return Ok(dto);
    }

    [HttpPost]
    public async Task<ActionResult<UserDto>> CreateUser(CreateUserDto dto)
    {
        var user = dto.ToEntity<User>();
        
        _context.Users.Add(user);
        await _context.SaveChangesAsync();

        var responseDto = user.ToFacet<UserDto>();
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, responseDto);
    }
}

8.2.2 Minimal API Integration

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFacetMapping();

var app = builder.Build();

app.MapGet("/users", async (AppDbContext db) =>
    await db.Users.SelectFacet<UserDto>().ToListAsync());

app.MapGet("/users/{id}", async (int id, AppDbContext db) =>
{
    var user = await db.Users.FindAsync(id);
    return user != null ? Results.Ok(user.ToFacet<UserDto>()) : Results.NotFound();
});

app.MapPost("/users", async (CreateUserDto dto, AppDbContext db) =>
{
    var user = dto.ToEntity<User>();
    db.Users.Add(user);
    await db.SaveChangesAsync();
    
    return Results.Created($"/users/{user.Id}", user.ToFacet<UserDto>());
});

8.3 Dependency Injection Configuration

Comprehensive dependency injection setup for all Facet features:

public void ConfigureServices(IServiceCollection services)
{
    // Core facet services
    services.AddFacetMapping();
    
    // Async mapping with scoped services
    services.AddFacetMappingAsync(options =>
    {
        options.DefaultParallelism = Environment.ProcessorCount;
        options.EnableBatchProcessing = true;
        options.DefaultTimeout = TimeSpan.FromSeconds(30);
    });
    
    // EF Core integration
    services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(connectionString));
    services.AddFacetEFCore<AppDbContext>();
    
    // Expression transformation
    services.AddFacetExpressions();
}

9. Conclusion

9.1 Summary of Key Points

This analysis has demonstrated that Facet represents a significant advancement in .NET mapping and projection technology. Through compile-time code generation, it addresses fundamental limitations of existing solutions while providing superior developer experience.

Key Takeaways:

  • Compile-Time Generation: Facet achieves zero runtime overhead while eliminating boilerplate code
  • Type Safety: Compile-time guarantees eliminate entire categories of runtime errors
  • Integration: Seamless integration with modern .NET frameworks and patterns
  • Flexibility: Support for synchronous, asynchronous, and hybrid mapping strategies
  • Maintainability: Significant reduction in maintenance overhead compared to manual approaches

9.2 Architectural Implications

The adoption of facetting as a design pattern has broader implications for software architecture:

9.2.1 Microservices Architecture

Facet's efficient projection capabilities support microservices patterns by enabling fine-grained data contracts without performance penalties. The compile-time generation ensures that service boundaries remain clean and efficient.

9.2.2 Domain-Driven Design

The clear separation between domain models and their projections reinforces DDD principles. Facets serve as anti-corruption layers, protecting domain integrity while enabling diverse presentation needs.

9.2.3 Clean Architecture

Facet supports Clean Architecture by facilitating efficient boundary crossing between layers. The generated mappers provide the necessary abstraction without violating dependency inversion principles.

9.3 Recommendations for Adoption

9.3.1 Immediate Adoption Scenarios

Teams should consider immediate Facet adoption for:

  • New .NET 8+ projects with significant DTO requirements
  • Entity Framework Core heavy applications
  • APIs with multiple client types requiring different data shapes
  • Projects prioritizing maintainability and compile-time safety

9.3.2 Gradual Migration Strategy

For existing applications, a gradual migration approach is recommended:

  1. Pilot Phase: Implement Facet for new features and high-traffic endpoints
  2. Feature Completion: Gradually expand Facet usage as features are enhanced
  3. Legacy Replacement: Replace remaining manual mapping as technical debt allows

9.4 Final Thoughts

Facet demonstrates that modern development tools can achieve both developer productivity and optimal compile-time safety. Through careful design and leveraging of platform capabilities, it provides a blueprint for future innovations in the .NET ecosystem.

The techniques explored in this analysis - compile-time generation, incremental compilation, type-safe projections, and async mapping patterns - represent best practices that extend beyond Facet itself. As software systems continue to grow in complexity, tools like Facet become increasingly essential for maintaining developer productivity while meeting demanding requirements.

Getting Started

To start using Facet in your projects:

  • Visit the GitHub repository for documentation and examples
  • Install the NuGet package: dotnet add package Facet
  • Check out the sample projects for real-world usage patterns
  • Join the community discussions for support and feature requests