The [Facet] attribute is used to declare a new projection (facet) type based on an existing source type.
[Facet(typeof(SourceType), exclude: "Property1", "Property2")]
public partial class MyFacet { }[Facet(typeof(SourceType), Include = [nameof(SourceType.Property1), nameof(SourceType.Property2)])]
public partial class MyFacet { }| Parameter | Type | Description |
|---|---|---|
sourceType |
Type |
The type to project from (required). |
exclude |
string[] |
Names of properties/fields to exclude from the generated type (optional). |
Include |
string[] |
Names of properties/fields to include in the generated type (optional). Mutually exclusive with exclude. |
NestedFacets |
Type[]? |
Array of nested facet types to automatically map nested objects (default: null). |
IncludeFields |
bool |
Include public fields from the source type (default: false for include mode, false for exclude mode). |
GenerateConstructor |
bool |
Generate a constructor that copies values from the source (default: true). |
GenerateParameterlessConstructor |
bool |
Generate a parameterless constructor for testing and initialization (default: true). |
Configuration |
Type? |
Custom mapping config type (see Custom Mapping). |
GenerateProjection |
bool |
Generate a static LINQ projection (default: true). |
GenerateToSource |
bool |
Generate a method to map back from facet to source type (default: false). |
PreserveInitOnlyProperties |
bool |
Preserve init-only modifiers from source properties (default: true for records). |
PreserveRequiredProperties |
bool |
Preserve required modifiers from source properties (default: true for records). |
NullableProperties |
bool |
Make all properties nullable in the generated facet (default: false). |
CopyAttributes |
bool |
Copy attributes from source type members to generated facet members (default: false). See Attribute Copying below. |
UseFullName |
bool |
Use full type name in generated file names to avoid collisions (default: false). |
MaxDepth |
int |
Maximum depth for nested facet recursion to prevent stack overflow (default: 3). Set to 0 for unlimited (not recommended). See Circular Reference Protection below. |
PreserveReferences |
bool |
Enable runtime circular reference detection using object tracking (default: true). See Circular Reference Protection below. |
SourceSignature |
string? |
Hash signature to track source entity changes. Emits FAC022 warning when source structure changes. See Source Signature Change Tracking. |
The Include and Exclude parameters are mutually exclusive:
- Exclude Mode: Include all properties except those listed in
exclude(default behavior) - Include Mode: Only include properties listed in
Includearray
When using Include mode:
- Only the properties specified in the
Includearray are copied to the facet IncludeFieldsdefaults tofalse(disabled by default for include mode)- All other properties from the source type are excluded
- Works with inheritance - you can include properties from base classes
// Only include FirstName, LastName, and Email
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email)])]
public partial class UserContactDto;// Only include the Name property
[Facet(typeof(Product), Include = [nameof(Product.Name)])]
public partial class ProductNameDto;// Include specific properties and add custom ones
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName)])]
public partial class UserSummaryDto
{
public string FullName { get; set; } = string.Empty; // Custom property
}// Include fields as well as properties
[Facet(typeof(EntityWithFields), Include = [nameof(EntityWithFields.Name), nameof(EntityWithFields.Age)], IncludeFields = true)]
public partial class EntityDto;// Generate a record type with only specific properties
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName)])]
public partial record UserNameRecord;// Exclude sensitive properties (original behavior)
[Facet(typeof(User), exclude: nameof(User.Password))]
public partial record UserDto;// Make all properties nullable for query/filter scenarios
[Facet(typeof(Product), nameof(Product.InternalNotes), NullableProperties = true, GenerateToSource = false)]
public partial class ProductQueryDto;
// Usage: All fields are optional for filtering
var query = new ProductQueryDto
{
Name = "Widget",
Price = 50.00m
// Other fields remain null
};Note: When using NullableProperties = true, it's recommended to set GenerateToSource = false since mapping nullable properties back to non-nullable source properties is not logically sound.
// Define facets for nested types
[Facet(typeof(Address))]
public partial record AddressDto;
[Facet(typeof(Company), NestedFacets = [typeof(AddressDto)])]
public partial record CompanyDto;
[Facet(typeof(Employee),
exclude: [nameof(Employee.PasswordHash), nameof(Employee.Salary)],
NestedFacets = [typeof(CompanyDto), typeof(AddressDto)])]
public partial record EmployeeDto;
// Usage - automatically handles nested mapping
var employee = new Employee
{
FirstName = "John",
Company = new Company
{
Name = "Acme Corp",
HeadquartersAddress = new Address { City = "San Francisco" }
},
HomeAddress = new Address { City = "Oakland" }
};
var employeeDto = new EmployeeDto(employee);
// employeeDto.Company is CompanyDto
// employeeDto.Company.HeadquartersAddress is AddressDto
// employeeDto.HomeAddress is AddressDto
// ToSource also handles nested types automatically
var mappedEmployee = employeeDto.ToSource();
// All nested objects are properly reconstructedHow NestedFacets Works:
- The generator automatically detects which properties in your source type match the source types of the nested facets
- For each match, it replaces the property type with the nested facet type
- Constructors automatically call
new NestedFacetType(source.Property)for nested properties - Projections work seamlessly for EF Core queries through constructor chaining
- ToSource methods call
.ToSource()on nested facets to reconstruct the original type hierarchy
Benefits:
- No manual property declarations for nested types
- Automatic mapping in constructors, projections, and ToSource methods
- Works with multiple levels of nesting
- Supports multiple nested facets on the same parent type
- You want a facet with only a few specific properties from a large source type
- Creating focused DTOs (e.g., summary views, contact info only)
- Building API response models that should only expose certain fields
- Creating search result DTOs with minimal data
- You want most properties but need to hide a few sensitive ones
- The majority of the source type should be included in the facet
- Following the original Facet pattern for backward compatibility
- Creating query/filter DTOs where all search criteria are optional
- Building patch/update models where only changed fields are provided
- Implementing flexible API request models that support partial data
- Generating DTOs similar to the Query DTOs in
GenerateDtos
Important considerations:
- Value types (int, bool, DateTime, enums) become nullable (int?, bool?, etc.)
- Reference types (string, objects) remain reference types but are marked nullable
- Disable
GenerateToSourceto avoid mapping issues from nullable to non-nullable types
The CopyAttributes parameter allows you to copy attributes from the source type's members to the generated facet members. This is particularly useful for preserving data validation attributes when creating DTOs for API models.
public class User
{
public int Id { get; set; }
[Required]
[StringLength(50)]
public string FirstName { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Range(0, 150)]
public int Age { get; set; }
public string Password { get; set; } = string.Empty;
}
[Facet(typeof(User), nameof(User.Password), CopyAttributes = true)]
public partial class UserDto;The generated UserDto will include all the validation attributes:
public partial class UserDto
{
public int Id { get; set; }
[Required]
[StringLength(50)]
public string FirstName { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Range(0, 150)]
public int Age { get; set; }
}The attribute copying feature intelligently filters attributes to copy only those that make sense on the target:
Commonly copied attributes include:
- Data validation attributes:
Required,StringLength,Range,EmailAddress,Phone,Url,RegularExpression,CreditCard, etc. - Display attributes:
Display,DisplayName,Description - JSON serialization attributes:
JsonPropertyName,JsonIgnore, etc. - Custom validation attributes that inherit from
ValidationAttribute
Automatically excluded attributes:
- Internal compiler-generated attributes (e.g.,
System.Runtime.CompilerServices.*) - The base
ValidationAttributeclass itself (only derived validation attributes are copied) - Attributes that are not valid for the target member type based on
AttributeUsage
All attribute parameters are preserved with correct C# syntax:
public class Product
{
[Required]
[StringLength(100, MinimumLength = 3, ErrorMessage = "Name must be 3-100 characters")]
public string Name { get; set; } = string.Empty;
[Range(0.01, 10000.00)]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]{3}-\d{4}$", ErrorMessage = "Invalid SKU format")]
public string Sku { get; set; } = string.Empty;
}
[Facet(typeof(Product), CopyAttributes = true)]
public partial class ProductDto;All parameters including named parameters, string literals with escape sequences, and numeric values are correctly preserved.
CopyAttributes works seamlessly with NestedFacets:
[Facet(typeof(Address), CopyAttributes = true)]
public partial class AddressDto;
[Facet(typeof(Order), nameof(Order.InternalNotes), CopyAttributes = true, NestedFacets = [typeof(AddressDto)])]
public partial class OrderDto;Both the parent and nested facets will have their attributes copied from their respective source types.
Use CopyAttributes = true when:
- Creating API request/response DTOs that need validation
- Building DTOs for ASP.NET Core model validation
- Preserving display metadata for UI frameworks
- Maintaining JSON serialization attributes
- You want consistent validation between your domain models and DTOs
Don't use it when:
- You want different validation rules for your DTOs
- Your source types have attributes specific to their domain concerns (e.g., ORM mapping attributes)
- You prefer to define validation attributes directly on the facet
By default, CopyAttributes = false, meaning no attributes are copied. This maintains backward compatibility and gives you explicit control over when attributes should be copied.
When working with nested facets, circular references in your object graph can cause stack overflow exceptions and IDE crashes. The Facet library provides two complementary features to prevent this:
Controls how many levels deep nested facets can be instantiated. This prevents infinite recursion during both code generation and runtime.
Default: 3 (recommended for most scenarios)
// Handles: Order -> LineItems -> Product -> Category
[Facet(typeof(Order), NestedFacets = [typeof(LineItemDto)])]
public partial record OrderDto;
// For deeper nesting, increase MaxDepth
[Facet(typeof(Organization), MaxDepth = 5, NestedFacets = [typeof(DepartmentDto)])]
public partial record OrganizationDto;
// To disable depth limiting (use with caution!)
[Facet(typeof(SimpleType), MaxDepth = 0)]
public partial record SimpleTypeDto;How MaxDepth Works:
- Level 0: Root object (e.g., Order)
- Level 1: First level nested objects (e.g., LineItems)
- Level 2: Second level nested objects (e.g., Product)
- Level 3: Third level nested objects (e.g., Category) - stops here with default MaxDepth = 3
- Properties that would exceed MaxDepth are set to
null
Enables runtime tracking of object instances to detect when the same object is being processed multiple times. This prevents circular references where objects reference each other.
Default: true (recommended for safety)
// Enable circular reference detection (default)
[Facet(typeof(Author), PreserveReferences = true, NestedFacets = [typeof(BookDto)])]
public partial record AuthorDto;
[Facet(typeof(Book), PreserveReferences = true, NestedFacets = [typeof(AuthorDto)])]
public partial record BookDto;
// Disable for maximum performance (only if you're certain no circular refs exist)
[Facet(typeof(FlatDto), PreserveReferences = false)]
public partial record FlatDto;How PreserveReferences Works:
- Uses a
HashSet<object>with reference equality to track processed objects - When creating nested facets, checks if the source object was already processed
- Returns
nullfor already-processed objects to break circular references - Filters out duplicates from collections using
.Where(x => x != null)
For circular references (e.g., Author <> Book, Employee <> Manager):
[Facet(typeof(Author), MaxDepth = 2, PreserveReferences = true,
NestedFacets = [typeof(BookDto)])]
public partial record AuthorDto;
[Facet(typeof(Book), MaxDepth = 2, PreserveReferences = true,
NestedFacets = [typeof(AuthorDto)])]
public partial record BookDto;For self-referencing types (e.g., Employee -> Manager -> Manager):
[Facet(typeof(Employee), MaxDepth = 5, PreserveReferences = true,
NestedFacets = [typeof(EmployeeDto)])]
public partial record EmployeeDto;For simple hierarchies with no circular references:
// Can reduce overhead if certain no circular refs
[Facet(typeof(Category), MaxDepth = 10, PreserveReferences = false,
NestedFacets = [typeof(CategoryDto)])]
public partial record CategoryDto;For flat DTOs with no nested facets:
// Can disable both for maximum performance
[Facet(typeof(Product), MaxDepth = 0, PreserveReferences = false)]
public partial record ProductDto;- MaxDepth: Negligible overhead - just depth counter checks
- PreserveReferences: Minimal overhead - HashSet reference lookups (typically < 1% performance impact)
- Both features are safe to leave enabled by default
- Only disable if you have profiled your application and identified these as bottlenecks
| Scenario | MaxDepth | PreserveReferences | Example |
|---|---|---|---|
| Flat DTO (no nesting) | 0 | false | Simple user profile |
| Simple parent-child | 2 | false | Order -> Customer |
| Multi-level hierarchy | 3-5 | false | Order -> LineItem -> Product -> Category |
| Circular references | 2-3 | true | Author <> Book, Post <> Comments |
| Self-referencing | 3-5 | true | Employee tree, Category tree |
| Complex object graphs | 3-5 | true | Any complex domain model |
Stack overflow during code generation:
- Increase
MaxDepth, the source generator is hitting infinite recursion - Ensure
MaxDepth > 0when usingPreserveReferences = true
Stack overflow at runtime:
- Enable
PreserveReferences = true - Increase
MaxDepthif your legitimate nesting depth exceeds the current value
Missing nested data:
- Check if your nesting depth exceeds
MaxDepth - Verify
PreserveReferencesisn't filtering out valid references
The [MapWhen] attribute enables conditional property mapping based on source values.
[Facet(typeof(Order))]
public partial class OrderDto
{
[MapWhen("Status == OrderStatus.Completed")]
public DateTime? CompletedAt { get; set; }
}| Parameter | Type | Description |
|---|---|---|
Condition |
string |
The condition expression to evaluate (required) |
Default |
object? |
Custom default value when condition is false |
IncludeInProjection |
bool |
Include condition in Projection expression (default: true) |
- Boolean:
[MapWhen("IsActive")] - Equality:
[MapWhen("Status == OrderStatus.Completed")] - Null checks:
[MapWhen("Email != null")] - Comparisons:
[MapWhen("Age >= 18")] - Negation:
[MapWhen("!IsDeleted")]
Multiple attributes are combined with AND logic:
[MapWhen("IsActive")]
[MapWhen("Status == OrderStatus.Completed")]
public DateTime? CompletedAt { get; set; }See MapWhen Conditional Mapping for full documentation.
See Custom Mapping for advanced scenarios.