Facet Attribute Reference
May 23, 2026 · View on GitHub
The [Facet] attribute is used to declare a new projection (facet) type based on an existing source type.
Usage
Exclude Mode (Default)
[Facet(typeof(SourceType), exclude: "Property1", "Property2")]
public partial class MyFacet { }
Include Mode (New)
[Facet(typeof(SourceType), Include = [nameof(SourceType.Property1), nameof(SourceType.Property2)])]
public partial class MyFacet { }
Parameters
| 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). |
ChainToParameterlessConstructor | bool | Chain generated constructors to the user-defined parameterless constructor using : this() (default: false). See Constructor Chaining below. |
Configuration | Type? | Custom mapping config type for forward mapping (Entity > DTO). See Custom Mapping. |
ToSourceConfiguration | Type? | Custom mapping config type for reverse mapping (DTO > Entity), called inside ToSource(). Requires GenerateToSource = true. See Reverse Mapping. |
GenerateProjection | bool | Generate a static LINQ projection (default: true). |
GenerateToSource | bool | Generates ToSource() (creates a new source instance) and ApplyToSource(source) (mutates an existing source instance) methods. Default: false. ApplyToSource is not generated for positional-record source types. |
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. |
CopyDocs | bool | Copy XML documentation comments from source type members to generated facet members (default: true). See XML Documentation 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: 10). Set to 0 for unlimited (not recommended). See Circular Reference Protection below. |
MaxDepthToSource | int | Maximum depth for ToSource() reverse mapping, independent of MaxDepth. Default is 0 (no limit, same as existing behavior). Set to a positive value to limit how many levels ToSource() maps back. See MaxDepthToSource 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. |
ConvertEnumsTo | Type? | When set, all enum properties are converted to the specified type (typeof(string) or typeof(int)) in the generated facet. Default is null (enums retain their original types). See Enum Conversion. |
CollectionTargetType | Type? | Overrides the collection type used for all mapped collection properties on this facet. Use an open generic type such as typeof(List<>) to remap source collections (e.g. Collection<T> from EF Core entities) to a different target type. See Collection Type Mapping below. |
GenerateCopyConstructor | bool | Generate a copy constructor that accepts another instance of the same facet type and copies all member values (default: false). See Copy Constructor below. |
GenerateEquality | bool | Generate value-based equality members (Equals, GetHashCode, ==, !=) and implement IEquatable<T> (default: false). Ignored for records. See Equality Generation below. |
SetAccessor | PropertySetAccessor | Override the set accessor emitted on all generated properties. Preserve (default) keeps the source accessor; Set forces { get; set; }; Init forces { get; init; }. See Set Accessor Override below. |
Include vs Exclude
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 the
Includearray
Include Mode Behavior
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
Examples
Basic Include Usage
// Only include FirstName, LastName, and Email
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email)])]
public partial class UserContactDto;
Single Property Include
// Only include the Name property
[Facet(typeof(Product), Include = [nameof(Product.Name)])]
public partial class ProductNameDto;
Include with Custom Properties
// 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 with Fields
// Include fields as well as properties
[Facet(typeof(EntityWithFields), Include = [nameof(EntityWithFields.Name), nameof(EntityWithFields.Age)], IncludeFields = true)]
public partial class EntityDto;
Include with Records
// Generate a record type with only specific properties
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName)])]
public partial record UserNameRecord;
Traditional Exclude Usage
// Exclude sensitive properties (original behavior)
[Facet(typeof(User), exclude: nameof(User.Password))]
public partial record UserDto;
Nullable Properties for Query Models
// 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.
Nested Facets for Composing DTOs
// 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 reconstructed
How 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
When to Use Include vs Exclude
Use Include when:
- 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
Use Exclude when:
- 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
Use NullableProperties when:
- 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
Constructor Chaining
The ChainToParameterlessConstructor parameter allows the generated constructor to chain to your user-defined parameterless constructor using : this(). This ensures any custom initialization logic in your constructor runs before property mapping.
Usage
public class ModelType
{
public int MaxValue { get; set; }
public string Name { get; set; } = string.Empty;
}
[Facet(typeof(ModelType), GenerateParameterlessConstructor = false, ChainToParameterlessConstructor = true)]
public partial class MyDto
{
public int Value { get; set; }
public bool Initialized { get; set; }
public MyDto()
{
// Custom initialization logic that runs before mapping
Value = 100;
Initialized = true;
}
}
// Usage
var source = new ModelType { MaxValue = 42, Name = "Test" };
var dto = new MyDto(source);
// dto.Value == 100 (from parameterless constructor)
// dto.Initialized == true (from parameterless constructor)
// dto.MaxValue == 42 (from source mapping)
// dto.Name == "Test" (from source mapping)
Generated Code
With ChainToParameterlessConstructor = true, the generated constructor chains to your parameterless constructor:
public MyDto(ModelType source) : this() // <-- Chains to your constructor
{
this.MaxValue = source.MaxValue;
this.Name = source.Name;
}
When to Use
- When you have initialization logic in your parameterless constructor that needs to run during mapping
- When you need to set default values that aren't simply copied from the source
- When you have computed or derived properties that need initial values
Note: Set GenerateParameterlessConstructor = false to prevent the generator from creating its own parameterless constructor, which would conflict with yours.
Attribute Copying
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.
Usage
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; }
}
What Gets Copied
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
Attribute Parameters
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.
With Nested Facets
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.
When to Use CopyAttributes
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
Default Behavior
By default, CopyAttributes = false, meaning no attributes are copied. This maintains backward compatibility and gives you explicit control over when attributes should be copied.
XML Documentation Copying
The CopyDocs parameter allows you to copy XML documentation comments from the source type's members to the generated facet members. This is particularly useful when creating DTOs for API models that use tools like OpenAPI/Swagger, which can extract documentation from XML comments.
Usage
public class User
{
public int Id { get; set; }
/// <summary>
/// The user's first name.
/// </summary>
[Required]
[StringLength(50)]
public string FirstName { get; set; } = string.Empty;
/// <summary>
/// The user's email address. Must be a valid email format.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
/// <summary>
/// The user's age in years.
/// </summary>
[Range(0, 150)]
public int Age { get; set; }
public string Password { get; set; } = string.Empty;
}
[Facet(typeof(User), nameof(User.Password), CopyDocs = true)]
public partial class UserDto;
The generated UserDto will include all the XML documentation comments:
public partial class UserDto
{
public int Id { get; set; }
/// <summary>
/// The user's first name.
/// </summary>
public string FirstName { get; set; }
/// <summary>
/// The user's email address. Must be a valid email format.
/// </summary>
public string Email { get; set; }
/// <summary>
/// The user's age in years.
/// </summary>
public int Age { get; set; }
}
Combining CopyDocs and CopyAttributes
CopyDocs and CopyAttributes are independent options that can be used together:
// Copy both attributes and XML documentation
[Facet(typeof(User), nameof(User.Password), CopyAttributes = true, CopyDocs = true)]
public partial class UserDto;
// Copy only attributes
[Facet(typeof(User), nameof(User.Password), CopyAttributes = true)]
public partial class UserDto;
// Copy only documentation
[Facet(typeof(User), nameof(User.Password), CopyDocs = true)]
public partial class UserDto;
When to Use CopyDocs
Use CopyDocs = true when:
- Creating API DTOs for OpenAPI/Swagger documentation
- Building DTOs that expose the same semantics as the domain models
- Maintaining consistent documentation across layers
- Generating client SDKs from your API that need inline documentation
- Using XML documentation for UI tooltips or help text
Don't use it when:
- You want different documentation for your DTOs
- Source model documentation includes internal implementation details
- You prefer to define DTO-specific documentation
Default Behavior
By default, CopyDocs = true, meaning XML documentation is automatically copied from source types to DTOs. This is the most useful default for API DTOs and OpenAPI/Swagger scenarios. Set CopyDocs = false to disable copying if you want different documentation on your DTOs.
Circular Reference Protection
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:
MaxDepth
Controls how many levels deep nested facets can be instantiated. This prevents infinite recursion during both code generation and runtime.
Default: 10 (handles most real-world scenarios including deep non-circular nesting)
// 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)
- Properties that would exceed MaxDepth are set to
null(with default MaxDepth = 10, this covers most real-world scenarios)
ApplyToSource
When GenerateToSource = true, Facet generates both ToSource() and ApplyToSource(). While ToSource() creates a new source instance, ApplyToSource() writes the mapped properties back onto an existing source instance without allocating a new one.
[Facet(typeof(User), "Password", "CreatedAt", GenerateToSource = true)]
public partial class UserDto { }
// Load an existing entity from the database
var entity = await dbContext.Users.FindAsync(id);
// Apply DTO changes onto it - EF Core change tracking stays intact
dto.ApplyToSource(entity);
await dbContext.SaveChangesAsync();
This is the recommended pattern for update operations because the original entity reference is retained, which means ORM change tracking and audit hooks continue to work correctly.
Limitations:
- Not generated when the source type uses a positional constructor (e.g.
record User(string Id, string Name)), because positional record properties are init-only and cannot be set after construction. UseToSource()and re-attach the new instance instead. - Only properties included in the facet and marked as reversible are written. All other source properties are left untouched.
MaxDepthToSource
Controls how many levels deep ToSource() maps nested objects back to the source entity. This is independent of MaxDepth, which only controls the forward source-to-DTO direction.
Default: 0 (no limit - preserves existing behavior)
Use case: Backends with separation of duties often only want the top-level entity updated via ToSource(), without cascading saves into child entities. Setting MaxDepthToSource = 1 maps only the root object and leaves all nested entity references as null (or empty collections).
// ToFacet (source -> DTO) goes up to 5 levels deep.
// ToSource (DTO -> entity) only maps the top-level object.
[Facet(typeof(Order), GenerateToSource = true,
MaxDepth = 5, MaxDepthToSource = 1,
NestedFacets = [typeof(LineItemDto)])]
public partial class OrderDto;
How MaxDepthToSource Works:
When MaxDepthToSource > 0, Facet generates two overloads of ToSource():
public ToSource()- the public entry point, callsToSource(0)internal ToSource(int __depth)- carries the depth counter through nested calls
At the root level __depth = 0, so children at depth 0 are still mapped (the guard is __depth < MaxDepthToSource). Once the limit is reached, nested entity references become null and collections become empty.
// Only the root Order entity properties are set; LineItems is empty.
[Facet(typeof(Order), GenerateToSource = true,
MaxDepthToSource = 1,
NestedFacets = [typeof(LineItemDto)])]
public partial class OrderDto;
var entity = dto.ToSource(); // entity.LineItems is []
// Both Order and its LineItems are mapped; LineItem.Product is null.
[Facet(typeof(Order), GenerateToSource = true,
MaxDepthToSource = 2,
NestedFacets = [typeof(LineItemDto)])]
public partial class OrderDto;
MaxDepthToSource only applies to facets that have GenerateToSource = true. Child facets that do not set MaxDepthToSource are called via their normal ToSource() and are not depth-limited.
PreserveReferences
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)
Best Practices
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;
Performance Considerations
- 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
Common Scenarios
| 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 |
Troubleshooting
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
MapWhen Attribute
The [MapWhen] attribute enables conditional property mapping based on source values.
Basic Usage
[Facet(typeof(Order))]
public partial class OrderDto
{
[MapWhen("Status == OrderStatus.Completed")]
public DateTime? CompletedAt { get; set; }
}
Parameters
| 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) |
Supported Conditions
- Boolean:
[MapWhen("IsActive")] - Equality:
[MapWhen("Status == OrderStatus.Completed")] - Null checks:
[MapWhen("Email != null")] - Comparisons:
[MapWhen("Age >= 18")] - Negation:
[MapWhen("!IsDeleted")]
Multiple Conditions
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.
Enum Conversion
The ConvertEnumsTo property converts all enum properties to string or int in the generated facet.
Basic Usage
// Convert enums to strings (for API responses)
[Facet(typeof(User), ConvertEnumsTo = typeof(string))]
public partial class UserDto;
// Convert enums to integers (for storage)
[Facet(typeof(User), ConvertEnumsTo = typeof(int))]
public partial class UserDto;
With Reverse Mapping
[Facet(typeof(User), ConvertEnumsTo = typeof(string), GenerateToSource = true)]
public partial class UserDto;
var dto = new UserDto(user);
dto.Status // "Active" (string)
var entity = dto.ToSource();
entity.Status // UserStatus.Active (enum)
Nullable Enums
Nullable enum properties preserve their nullability after conversion:
UserStatus??string?(null when source is null)UserStatus??int?(nullable int)
See Enum Conversion for full documentation.
Copy Constructor
The GenerateCopyConstructor property generates a constructor that accepts another instance of the same facet type and copies all member values. This is useful for MVVM scenarios, cloning DTOs, or creating independent copies.
Basic Usage
[Facet(typeof(User), GenerateCopyConstructor = true)]
public partial class UserDto;
// Usage
var original = new UserDto(user);
var copy = new UserDto(original); // Copy constructor
// Modify the copy without affecting the original
copy.FirstName = "Changed";
original.FirstName; // Still "John"
Generated Code
public partial class UserDto
{
/// <summary>
/// Initializes a new instance by copying all member values from another instance.
/// </summary>
public UserDto(UserDto other)
{
if (other is null) throw new ArgumentNullException(nameof(other));
this.Id = other.Id;
this.FirstName = other.FirstName;
this.LastName = other.LastName;
this.Email = other.Email;
}
}
When to Use
- MVVM ViewModels: Clone a view model for editing while preserving the original for cancel/revert
- DTO Cloning: Create independent copies of DTOs for caching or comparison
- Undo/Redo: Snapshot DTO state before modifications
- Inheritance Scenarios: Use with base class inheritance where you need to copy facet properties to derived types
Equality Generation
The GenerateEquality property generates value-based equality members for class and struct facets. This includes Equals(T), Equals(object), GetHashCode(), and the == / != operators. The generated type also implements IEquatable<T>.
Note: This option is ignored for record types, which already have built-in value-based equality from the C# language.
Basic Usage
[Facet(typeof(User), GenerateEquality = true)]
public partial class UserDto;
// Value-based comparison
var dto1 = new UserDto(user);
var dto2 = new UserDto(user);
dto1.Equals(dto2); // true
dto1 == dto2; // true
dto1.GetHashCode() == dto2.GetHashCode(); // true
// Works in collections
var set = new HashSet<UserDto> { dto1 };
set.Contains(dto2); // true, same values
Combining with Copy Constructor
[Facet(typeof(User), GenerateCopyConstructor = true, GenerateEquality = true)]
public partial class UserDto;
var original = new UserDto(user);
var copy = new UserDto(original);
original == copy; // true , same values, different instances
Generated Code
public partial class UserDto : System.IEquatable<UserDto>
{
public bool Equals(UserDto? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return this.Id == other.Id
&& EqualityComparer<string>.Default.Equals(this.FirstName, other.FirstName)
&& EqualityComparer<string>.Default.Equals(this.LastName, other.LastName);
}
public override bool Equals(object? obj) => obj is UserDto other && Equals(other);
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 31 + Id.GetHashCode();
hash = hash * 31 + (FirstName?.GetHashCode() ?? 0);
hash = hash * 31 + (LastName?.GetHashCode() ?? 0);
return hash;
}
}
public static bool operator ==(UserDto? left, UserDto? right) { ... }
public static bool operator !=(UserDto? left, UserDto? right) => !(left == right);
}
When to Use
- Class-based DTOs that need value comparison without converting to records
- Change detection: Compare DTOs to check if data has been modified
- Caching: Use DTOs as dictionary keys or in hash sets
- Testing: Assert DTO equality in unit tests
When NOT to Use
- Records: Records already have value-based equality �
GenerateEqualityis automatically ignored - Reference equality needed: If you need identity-based comparison, don't enable this
Collection Type Mapping
By default, Facet preserves the source collection type when mapping nested facet collections. For example, if an EF Core entity uses Collection<T>, the generated facet property will also use Collection<T>. Use CollectionTargetType to override this for all collection properties on a facet.
Supported Types
Any open generic collection type is accepted:
CollectionTargetType | Generated property type | Materialized via |
|---|---|---|
typeof(List<>) | List<TFacet> | .ToList() |
typeof(IList<>) | IList<TFacet> | .ToList() |
typeof(ICollection<>) | ICollection<TFacet> | .ToList() |
typeof(IEnumerable<>) | IEnumerable<TFacet> | lazy (no materialization) |
typeof(IReadOnlyList<>) | IReadOnlyList<TFacet> | .ToList() |
typeof(IReadOnlyCollection<>) | IReadOnlyCollection<TFacet> | .ToList() |
typeof(Collection<>) | Collection<TFacet> | new Collection<>(…) |
Facet-Level Override
Remap all Collection<T> (or any source collection type) to List<T>:
public class UnitEntity
{
public int Id { get; set; }
public Collection<UnitItemEntity> Items { get; set; } = new();
}
[Facet(typeof(UnitEntity),
NestedFacets = [typeof(UnitItemDto)],
CollectionTargetType = typeof(List<>))]
public partial class UnitDto { }
// Generated: public List<UnitItemDto> Items { get; set; }
Per-Property Override with [MapFrom]
Use AsCollection on [MapFrom] to override a single property:
[Facet(typeof(UnitEntity), NestedFacets = [typeof(UnitItemDto)])]
public partial class UnitDto
{
[MapFrom(nameof(UnitEntity.Items), AsCollection = typeof(List<>))]
public List<UnitItemDto> Items { get; set; } = new();
}
Round-Trip Support
When GenerateToSource = true, ToSource() restores the original source collection type even when the facet uses a different type:
[Facet(typeof(UnitEntity),
NestedFacets = [typeof(UnitItemDto)],
CollectionTargetType = typeof(List<>),
GenerateToSource = true)]
public partial class UnitDto { }
// facet.Items is List<UnitItemDto>
// facet.ToSource().Items is Collection<UnitItemEntity> , original source type restored
Set Accessor Override
The SetAccessor parameter controls which set accessor is emitted on every generated property, overriding the default behaviour.
| Value | Generated accessor | When to use |
|---|---|---|
PropertySetAccessor.Preserve (default) | Same as the source property | Usual DTO/projection work |
PropertySetAccessor.Set | { get; set; } | Force mutability even when source uses init |
PropertySetAccessor.Init | { get; init; } | Immutable DTOs — all properties settable only during construction |
Basic Usage
// All generated properties use { get; init; }
[Facet(typeof(Foo), SetAccessor = PropertySetAccessor.Init)]
public partial class ImmutableFoo;
// All generated properties use { get; set; }
[Facet(typeof(Foo), SetAccessor = PropertySetAccessor.Set)]
public partial class MutableFoo;
Builder Pattern Example
SetAccessor is ideal for a mutable builder → immutable read model workflow:
public class Order
{
public int Id { get; set; }
public string Reference { get; set; } = string.Empty;
public decimal Total { get; set; }
}
// Mutable version — build up the object in multiple steps
[Facet(typeof(Order))]
public partial class OrderBuilder;
// Immutable version, locked down after construction
[Facet(typeof(Order), SetAccessor = PropertySetAccessor.Init)]
public partial class ImmutableOrder;
// Build
var builder = new OrderBuilder();
builder.Reference = "ORD-001";
builder.Total = 99.99m;
// Freeze into an immutable snapshot
var order = new ImmutableOrder(builder.ToSource());
// order.Reference = "x"; // <- compile error: init-only
Generated Code
// Source
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
// SetAccessor = PropertySetAccessor.Init
public partial class ImmutableFoo
{
public int Id { get; init; }
public string Name { get; init; } = default!;
}
Interaction with PreserveInitOnlyProperties
SetAccessor takes full precedence:
SetAccessor = Init> all properties getinit, regardless of source orPreserveInitOnlyPropertiesSetAccessor = Set> all properties getset, even ifPreserveInitOnlyProperties = trueSetAccessor = Preserve(default) > falls back to the normalPreserveInitOnlyPropertieslogic
See Custom Mapping for advanced scenarios.