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:
- Pilot Phase: Implement Facet for new features and high-traffic endpoints
- Feature Completion: Gradually expand Facet usage as features are enhanced
- 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
Comments