This section covers advanced use cases and configuration options for Facet.
You can create multiple facets from the same source type:
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public DateTime DateOfBirth { get; set; }
public string Password { get; set; }
public decimal Salary { get; set; }
public string Department { get; set; }
}
// Public profile (exclude sensitive data)
[Facet(typeof(User), nameof(User.Password), nameof(User.Salary))]
public partial class UserPublicDto { }
// Contact information only (include specific fields)
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email)])]
public partial class UserContactDto { }
// Summary for lists (include minimal data)
[Facet(typeof(User), Include = [nameof(User.Id), nameof(User.FirstName), nameof(User.LastName)])]
public partial class UserSummaryDto { }
// HR view (exclude password but include salary)
[Facet(typeof(User), nameof(User.Password))]
public partial class UserHRDto { }Since Facet v6, a single target class can carry multiple [Facet] attributes, each pointing to a different source type. The generator emits a single partial class containing:
- A union of all mapped properties (deduplicated by name; first-occurrence wins).
- Per-source constructors and
FromSourcefactory method overloads (naturally overloaded by parameter type — no naming conflict). - Per-source projection expressions named
ProjectionFrom{SourceTypeName}to avoid static-property collisions. - Per-source
ToSourcemethods namedTo{SourceTypeName}()to avoid method-signature conflicts; the deprecatedBackToalias is not generated for multi-source facets. - Shared artefacts (parameterless constructor, copy constructor, equality) are generated once from the primary (first) attribute.
A common scenario in domain-driven or layered architectures is a "drop-down" or "summary" DTO that can be populated from multiple different source types — an EF Core entity and a domain DTO both containing the same logical data:
// Two different source representations of the same concept
public partial class UnitEntity : ModifiedByBaseEntity { /* Id, Name, ... */ }
public partial class UnitDto : FacetsModifiedByBaseDto { /* Id, Name, ... */ }
// One unified "display" target that can accept both
[Facet(typeof(UnitEntity), Include = [nameof(UnitEntity.Name)])]
[Facet(typeof(UnitDto), Include = [nameof(UnitDto.Name)])]
public partial class UnitDropDownDto;For the example above Facet generates a single UnitDropDownDto class with:
// Constructors (overloaded by source type — no ambiguity)
var a = new UnitDropDownDto(unitEntity);
var b = new UnitDropDownDto(unitDto);
// Factory methods (overloaded)
var c = UnitDropDownDto.FromSource(unitEntity);
var d = UnitDropDownDto.FromSource(unitDto);
// Per-source LINQ projections
IQueryable<UnitDropDownDto> q1 = dbContext.Units
.Select(UnitDropDownDto.ProjectionFromUnitEntity);
IQueryable<UnitDropDownDto> q2 = unitDtos.AsQueryable()
.Select(UnitDropDownDto.ProjectionFromUnitDto);When source types share properties (e.g. both have Id and Name) the property is generated once (first-definition wins). Exclusive properties from each source type are also included:
public class EntityA { public int Id { get; set; } public string Name { get; set; } public string EntityAOnly { get; set; } }
public class EntityB { public int Id { get; set; } public string Name { get; set; } public string EntityBOnly { get; set; } }
[Facet(typeof(EntityA))]
[Facet(typeof(EntityB))]
public partial class UnionDto;
// Generated properties: Id, Name, EntityAOnly, EntityBOnlyWhen GenerateToSource = true is specified on an attribute, a To{SourceTypeName}() method is generated for that source type:
[Facet(typeof(UnitEntity), Include = [nameof(UnitEntity.Name)], GenerateToSource = true)]
[Facet(typeof(UnitDto), Include = [nameof(UnitDto.Name)])]
public partial class UnitDropDownDto;
// Generated:
// public UnitEntity ToUnitEntity() { ... }
// (no ToUnitDto because GenerateToSource was not set on the second attribute)| Behaviour | Detail |
|---|---|
| Projection names | Always ProjectionFrom{SourceSimpleName} for multi-source targets |
| ToSource names | Always To{SourceSimpleName}() for multi-source targets — no BackTo() alias |
| Single-source behaviour | Unchanged: Projection, ToSource(), and BackTo() are still generated with the original names |
| Member deduplication | Properties with the same name across multiple sources are generated once; the type from the first mapping is used |
| Configuration | Configuration, BeforeMap, AfterMap, and ToSourceConfiguration are each read independently per attribute |
Use the Include pattern when you want facets with only specific properties:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public bool IsAvailable { get; set; }
public DateTime CreatedAt { get; set; }
public string InternalNotes { get; set; }
public decimal Cost { get; set; }
public string SKU { get; set; }
}
// API response with only customer-facing data
[Facet(typeof(Product), Include = [nameof(Product.Id), nameof(Product.Name), nameof(Product.Description), nameof(Product.Price), nameof(Product.IsAvailable)])]
public partial record ProductApiDto;
// Search results with minimal data
[Facet(typeof(Product), Include = [nameof(Product.Id), nameof(Product.Name), nameof(Product.Price)])]
public partial record ProductSearchDto;
// Internal admin view with cost data
[Facet(typeof(Product), Include = [nameof(Product.Id), nameof(Product.Name), nameof(Product.Price), nameof(Product.Cost), nameof(Product.SKU), nameof(Product.InternalNotes)])]
public partial class ProductAdminDto;Use the Exclude pattern when you want most properties but need to hide specific ones:
// Exclude only sensitive information
[Facet(typeof(User), nameof(User.Password))]
public partial class UserDto { }
// Exclude multiple sensitive fields
[Facet(typeof(Employee), nameof(Employee.Salary), nameof(Employee.SSN))]
public partial class EmployeePublicDto { }public class LegacyEntity
{
public int Id;
public string Name;
public DateTime CreatedDate;
public string Status { get; set; }
public string Notes { get; set; }
}
// Include specific fields and properties
[Facet(typeof(LegacyEntity), Include = [nameof(LegacyEntity.Name), nameof(LegacyEntity.Status)], IncludeFields = true)]
public partial class LegacyEntityDto;
// Only include properties (fields ignored even if listed)
[Facet(typeof(LegacyEntity), Include = [nameof(LegacyEntity.Status), nameof(LegacyEntity.Notes), nameof(LegacyEntity.Name)], IncludeFields = false)]
public partial class LegacyEntityPropsOnlyDto;Facet supports automatic mapping of nested objects through the NestedFacets parameter. This eliminates the need to manually declare nested properties and handle their mapping.
// Source entities with nested structure
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public Address HeadquartersAddress { get; set; }
}
// Facet DTOs with automatic nested mapping
[Facet(typeof(Address))]
public partial record AddressDto;
[Facet(typeof(Company), NestedFacets = [typeof(AddressDto)])]
public partial record CompanyDto;
// Usage
var company = new Company
{
Name = "Acme Corp",
HeadquartersAddress = new Address { City = "San Francisco" }
};
var companyDto = new CompanyDto(company);
// companyDto.HeadquartersAddress is automatically an AddressDtopublic class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PasswordHash { get; set; }
public decimal Salary { get; set; }
public Company Company { get; set; }
public Address HomeAddress { get; set; }
}
public class Department
{
public int Id { get; set; }
public string Name { get; set; }
public Company Company { get; set; }
public Employee Manager { get; set; }
}
// Employee DTO with multiple nested facets
[Facet(typeof(Employee), exclude: ["PasswordHash", "Salary"],
NestedFacets = [typeof(CompanyDto), typeof(AddressDto)])]
public partial record EmployeeDto;
// Department DTO with deeply nested structure
[Facet(typeof(Department), NestedFacets = [typeof(CompanyDto), typeof(EmployeeDto)])]
public partial record DepartmentDto;
// Usage - handles 3+ levels of nesting automatically
var department = new Department
{
Name = "Engineering",
Company = new Company
{
Name = "Tech Corp",
HeadquartersAddress = new Address { City = "Seattle" }
},
Manager = new Employee
{
FirstName = "Jane",
Company = new Company
{
Name = "Tech Corp",
HeadquartersAddress = new Address { City = "Seattle" }
},
HomeAddress = new Address { City = "Bellevue" }
}
};
var departmentDto = new DepartmentDto(department);
// departmentDto.Manager.Company.HeadquartersAddress.City == "Seattle"Automatic Type Detection:
- The generator inspects each nested facet's source type
- Properties in the parent source that match nested facet source types are automatically replaced
- For example, if
CompanyDtofacets fromCompany, anyCompanyproperty becomesCompanyDto
Generated Code:
// For: [Facet(typeof(Company), NestedFacets = [typeof(AddressDto)])]
public partial record CompanyDto
{
public int Id { get; init; }
public string Name { get; init; }
public AddressDto HeadquartersAddress { get; init; } // Automatically becomes AddressDto
public CompanyDto(Company source)
: this(source.Id, source.Name, new AddressDto(source.HeadquartersAddress)) // Automatic nesting
{ }
public Company ToSource()
{
return new Company
{
Id = this.Id,
Name = this.Name,
HeadquartersAddress = this.HeadquartersAddress.ToSource() // Automatic reverse mapping
};
}
}// Works seamlessly with Entity Framework Core
var companies = await dbContext.Companies
.Where(c => c.IsActive)
.Select(CompanyDto.Projection)
.ToListAsync();
// The generated projection handles nested types automatically:
// c => new CompanyDto(c.Id, c.Name, new AddressDto(c.HeadquartersAddress))- No Manual Property Declarations: Don't redeclare nested properties
- Automatic Constructor Mapping: Nested constructors are called automatically
- ToSource Support: Reverse mapping works for nested structures
- EF Core Compatible: Projections work in database queries
- Multi-Level Support: Handle 3+ levels of nesting
Facet fully supports nested facets within collections, automatically mapping List<T>, ICollection<T>, T[], and other collection types to their corresponding nested facet types.
// Source entities
public class OrderItem
{
public int Id { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class Order
{
public int Id { get; set; }
public string OrderNumber { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderItem> Items { get; set; } // Collection of nested objects
}
// Facet DTOs
[Facet(typeof(OrderItem))]
public partial record OrderItemDto;
[Facet(typeof(Order), NestedFacets = [typeof(OrderItemDto)])]
public partial record OrderDto;
// Usage
var order = new Order
{
Id = 1,
OrderNumber = "ORD-2025-001",
OrderDate = DateTime.Now,
Items = new List<OrderItem>
{
new() { Id = 1, ProductName = "Laptop", Price = 1200.00m, Quantity = 1 },
new() { Id = 2, ProductName = "Mouse", Price = 25.00m, Quantity = 2 }
}
};
var orderDto = new OrderDto(order);
// orderDto.Items is List<OrderItemDto>
// Each OrderItem is automatically mapped to OrderItemDtoFacet automatically handles all standard .NET collection types through interface-based detection. This means any custom collection type implementing standard interfaces is automatically supported!
public class Project
{
// Standard mutable collections
public List<Task> Tasks { get; set; } // List<T>
public ICollection<Team> Teams { get; set; } // ICollection<T>
public IList<Milestone> Milestones { get; set; } // IList<T>
public IEnumerable<Comment> Comments { get; set; } // IEnumerable<T>
public Employee[] Employees { get; set; } // T[] (arrays)
public Collection<Note> Notes { get; set; } // Collection<T>
// Read-only collections
public IReadOnlyList<Tag> Tags { get; set; } // IReadOnlyList<T>
public IReadOnlyCollection<Label> Labels { get; set; } // IReadOnlyCollection<T>
// System.Collections.Immutable support
public ImmutableList<Feature> Features { get; set; } // ImmutableList<T>
public ImmutableArray<Version> Versions { get; set; } // ImmutableArray<T>
public ImmutableHashSet<Category> Categories { get; set; } // ImmutableHashSet<T>
public ImmutableSortedSet<Priority> Priorities { get; set; } // ImmutableSortedSet<T>
public ImmutableQueue<Event> EventQueue { get; set; } // ImmutableQueue<T>
public ImmutableStack<Change> ChangeStack { get; set; } // ImmutableStack<T>
public IImmutableList<Requirement> Requirements { get; set; } // IImmutableList<T>
public IImmutableSet<Constraint> Constraints { get; set; } // IImmutableSet<T>
}
[Facet(typeof(Project), NestedFacets = [typeof(TaskDto), typeof(TeamDto), /* ... */])]
public partial record ProjectDto;
// All collections automatically map to their corresponding DTO collection types:
// - List<Task> → List<TaskDto>
// - ICollection<Team> → ICollection<TeamDto>
// - Employee[] → EmployeeDto[]
// - IReadOnlyList<Tag> → IReadOnlyList<TagDto>
// - ImmutableList<Feature> → ImmutableList<FeatureDto>
// - ImmutableArray<Version> → ImmutableArray<VersionDto>Facet uses interface-based detection, which means any custom collection type that implements standard collection interfaces is automatically supported—no manual configuration needed!
// Define a custom collection type
public class CustomReadOnlyList<T> : IReadOnlyList<T>
{
private readonly List<T> _items;
public CustomReadOnlyList(IEnumerable<T> items) => _items = new List<T>(items);
public T this[int index] => _items[index];
public int Count => _items.Count;
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
}
// Use it in your entity
public class Gallery
{
public int Id { get; set; }
public string Name { get; set; }
// Custom collection type - automatically supported!
public CustomReadOnlyList<Exhibit> Exhibits { get; set; }
}
[Facet(typeof(Gallery), NestedFacets = [typeof(ExhibitDto)])]
public partial record GalleryDto;
// Usage - works automatically via interface detection!
var gallery = new Gallery
{
Exhibits = new CustomReadOnlyList<Exhibit>(exhibitList)
};
var dto = new GalleryDto(gallery);
// dto.Exhibits is IReadOnlyList<ExhibitDto>How It Works:
- Exact Type Preservation: Known concrete types (List, ImmutableList, etc.) are preserved exactly
- Interface Fallback: Unknown types are detected via their implemented interfaces
- Automatic Support: Any type implementing IReadOnlyList, IList, ICollection, or IEnumerable works automatically
Supported Detection Interfaces:
IReadOnlyList<T>(most specific read-only)IReadOnlyCollection<T>IList<T>(most specific mutable)ICollection<T>IEnumerable<T>(least specific fallback)
This makes Facet extensible to any collection library
The generator creates efficient LINQ-based transformations:
// Generated constructor
public OrderDto(Order source)
{
Id = source.Id;
OrderNumber = source.OrderNumber;
OrderDate = source.OrderDate;
// Uses LINQ Select to map each element
Items = source.Items.Select(x => new OrderItemDto(x)).ToList();
}
// Generated ToSource method
public Order ToSource()
{
return new Order
{
Id = this.Id,
OrderNumber = this.OrderNumber,
OrderDate = this.OrderDate,
// Maps each DTO back to entity
Items = this.Items.Select(x => x.ToSource()).ToList()
};
}Collections can be nested at any depth:
public class OrderItem
{
public int Id { get; set; }
public string ProductName { get; set; }
public List<OrderItemOption> Options { get; set; } // Nested collection
}
public class Order
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; } // Collection of objects with collections
}
[Facet(typeof(OrderItemOption))]
public partial record OrderItemOptionDto;
[Facet(typeof(OrderItem), NestedFacets = [typeof(OrderItemOptionDto)])]
public partial record OrderItemDto;
[Facet(typeof(Order), NestedFacets = [typeof(OrderItemDto)])]
public partial record OrderDto;
// Usage
var order = new Order
{
Items = new List<OrderItem>
{
new()
{
ProductName = "Laptop",
Options = new List<OrderItemOption>
{
new() { Name = "Extended Warranty" },
new() { Name = "Gift Wrap" }
}
}
}
};
var dto = new OrderDto(order);
// dto.Items[0].Options is List<OrderItemOptionDto>You can have both collection and single nested facets in the same entity:
public class Order
{
public int Id { get; set; }
public Address ShippingAddress { get; set; } // Single nested object
public Address BillingAddress { get; set; } // Another single nested object
public List<OrderItem> Items { get; set; } // Collection of nested objects
}
[Facet(typeof(Address))]
public partial record AddressDto;
[Facet(typeof(OrderItem))]
public partial record OrderItemDto;
[Facet(typeof(Order), NestedFacets = [typeof(AddressDto), typeof(OrderItemDto)])]
public partial record OrderDto;
// Generated OrderDto will have:
// - AddressDto ShippingAddress
// - AddressDto BillingAddress
// - List<OrderItemDto> ItemsCollections work seamlessly with Entity Framework Core queries:
// Efficient database projection
var orders = await dbContext.Orders
.Include(o => o.Items) // Include related data
.Where(o => o.OrderDate >= DateTime.Today.AddDays(-30))
.Select(OrderDto.Projection)
.ToListAsync();
// The generated Projection property handles collections automatically:
// source => new OrderDto
// {
// Id = source.Id,
// OrderNumber = source.OrderNumber,
// Items = source.Items.Select(x => new OrderItemDto(x)).ToList()
// }Empty collections are handled gracefully:
var order = new Order
{
Id = 1,
OrderNumber = "EMPTY",
Items = new List<OrderItem>() // Empty collection
};
var dto = new OrderDto(order);
// dto.Items is an empty List<OrderItemDto>, not nullThe original collection type is preserved during mapping:
public class Team
{
public Employee[] Members { get; set; } // Array
}
[Facet(typeof(Employee))]
public partial record EmployeeDto;
[Facet(typeof(Team), NestedFacets = [typeof(EmployeeDto)])]
public partial record TeamDto;
// Generated TeamDto has:
// public EmployeeDto[] Members { get; init; } // Stays as array
var team = new Team { Members = new[] { employee1, employee2 } };
var dto = new TeamDto(team);
// dto.Members is EmployeeDto[] (array type preserved)// Domain entities
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class OrderItem
{
public int Id { get; set; }
public Product Product { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class Order
{
public int Id { get; set; }
public string OrderNumber { get; set; }
public DateTime OrderDate { get; set; }
public Customer Customer { get; set; }
public List<OrderItem> Items { get; set; }
public decimal TotalAmount { get; set; }
}
// DTOs with nested facets
[Facet(typeof(Product))]
public partial record ProductDto;
[Facet(typeof(OrderItem), NestedFacets = [typeof(ProductDto)])]
public partial record OrderItemDto;
[Facet(typeof(Customer))]
public partial record CustomerDto;
[Facet(typeof(Order), NestedFacets = [typeof(CustomerDto), typeof(OrderItemDto)])]
public partial record OrderDto;
// Usage in API
[HttpGet("orders/{id}")]
public async Task<ActionResult<OrderDto>> GetOrder(int id)
{
var order = await dbContext.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null) return NotFound();
// Automatic nested and collection mapping
return new OrderDto(order);
}-
Define facets in dependency order: Define child facets before parent facets
[Facet(typeof(OrderItem))] // Define child first public partial record OrderItemDto; [Facet(typeof(Order), NestedFacets = [typeof(OrderItemDto)])] // Then parent public partial record OrderDto;
-
Use collections for one-to-many relationships: Perfect for Entity Framework navigation properties
public class Order { public List<OrderItem> Items { get; set; } // One-to-many }
-
Consider performance with large collections: Be mindful when mapping large collections in memory
// For very large collections, consider pagination var recentOrders = dbContext.Orders .OrderByDescending(o => o.OrderDate) .Take(50) // Limit collection size .Select(OrderDto.Projection) .ToListAsync();
-
Handle null collections: Initialize collections to avoid null reference exceptions
public class Order { public List<OrderItem> Items { get; set; } = new(); // Initialize }
- Automatic Collection Mapping: No manual LINQ Select calls needed
- Type Safety: Compiler-verified collection element types
- Bidirectional Support: Both forward and reverse (
ToSource()) mapping - EF Core Optimized: Works efficiently with database projections
- Preserves Collection Types: Lists stay lists, arrays stay arrays
- Multi-Level Support: Unlimited nesting depth for collections
Include mode works seamlessly with inheritance:
public class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public string CreatedBy { get; set; }
}
public class Product : BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
// Include properties from both base and derived class
[Facet(typeof(Product), Include = [nameof(Product.Id), nameof(Product.Name), nameof(Product.Price)])]
public partial class ProductSummaryDto;
// Include only derived class properties
[Facet(typeof(Product), Include = [nameof(Product.Name), nameof(Product.Category)])]
public partial class ProductInfoDto;Both include and exclude work with nested classes:
public class OuterClass
{
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName)])]
public partial class NestedUserDto { }
}You can combine Include mode with custom mapping:
public class UserIncludeMapper : IFacetMapConfiguration<User, UserFormattedDto>
{
public static void Map(User source, UserFormattedDto target)
{
target.DisplayName = $"{source.FirstName} {source.LastName}".ToUpper();
}
}
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName)], Configuration = typeof(UserIncludeMapper))]
public partial class UserFormattedDto
{
public string DisplayName { get; set; } = string.Empty;
}The NullableProperties parameter makes all non-nullable properties nullable in the generated facet, which is extremely useful for query DTOs and partial update scenarios.
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int CategoryId { get; set; }
public bool IsAvailable { get; set; }
public DateTime CreatedAt { get; set; }
}
// All properties become nullable for flexible querying
[Facet(typeof(Product), "CreatedAt", NullableProperties = true, GenerateToSource = false)]
public partial class ProductQueryDto;
// Usage: Only specify the fields you want to filter on
var query = new ProductQueryDto
{
Name = "Widget", // Filter by name
Price = 50.00m, // Filter by price
IsAvailable = true // Filter by availability
// Id, CategoryId remain null (not part of filter)
};
// Use in LINQ queries
var results = products.Where(p =>
(query.Name == null || p.Name.Contains(query.Name)) &&
(query.Price == null || p.Price == query.Price) &&
(query.IsAvailable == null || p.IsAvailable == query.IsAvailable)
).ToList();// Create a patch model where only non-null fields are updated
[Facet(typeof(User), "Id", "CreatedAt", NullableProperties = true, GenerateToSource = false)]
public partial class UserPatchDto;
// Usage: Only update specific fields
var patch = new UserPatchDto
{
Email = "newemail@example.com", // Update email
IsActive = false // Update active status
// Other properties remain null (won't be updated)
};
// Apply the patch
void ApplyPatch(User user, UserPatchDto patch)
{
if (patch.FirstName != null) user.FirstName = patch.FirstName;
if (patch.LastName != null) user.LastName = patch.LastName;
if (patch.Email != null) user.Email = patch.Email;
if (patch.IsActive != null) user.IsActive = patch.IsActive.Value;
// ... etc
}- Value Types: Become nullable (
int→int?,bool→bool?,DateTime→DateTime?, enums →EnumType?) - Reference Types: Remain reference types but are marked as nullable (
string→string) - Already Nullable Types: Stay nullable (
DateTime?remainsDateTime?)
-
Disable GenerateToSource: When using
NullableProperties = true, setGenerateToSource = falsesince mapping nullable properties back to non-nullable source properties is not logically sound. -
Constructor Behavior: The generated constructor will still map from source to nullable properties correctly.
-
Comparison with GenerateDtos Query: This provides the same functionality as the Query DTOs in
GenerateDtos, but gives you more control with theFacetattribute.
// Similar to GenerateDtos Query DTO
[Facet(typeof(Product), NullableProperties = true, GenerateToSource = false)]
public partial record ProductQueryRecord;The ConvertEnumsTo property converts all enum properties in the source type to string or int in the generated facet, which is useful for API responses, serialization, and database storage.
public enum OrderStatus { Draft, Submitted, Processing, Completed, Cancelled }
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public OrderStatus Status { get; set; }
public decimal Total { get; set; }
}
[Facet(typeof(Order), ConvertEnumsTo = typeof(string), GenerateToSource = true)]
public partial class OrderDto;
// Usage
var order = new Order { Id = 1, CustomerName = "Alice", Status = OrderStatus.Processing, Total = 99.99m };
var dto = new OrderDto(order);
dto.Status // "Processing" (string, not OrderStatus)
// Round-trip
var entity = dto.ToSource();
entity.Status // OrderStatus.Processing[Facet(typeof(Order), ConvertEnumsTo = typeof(int), GenerateToSource = true)]
public partial class OrderIntDto;
var dto = new OrderIntDto(order);
dto.Status // 2 (int value of OrderStatus.Processing)Nullable enum properties preserve their nullability after conversion:
public class Entity
{
public int Id { get; set; }
public OrderStatus? Status { get; set; } // Nullable
public OrderStatus Priority { get; set; } // Non-nullable
}
[Facet(typeof(Entity), ConvertEnumsTo = typeof(string))]
public partial class EntityStringDto;
// Status: string (null when source is null)
// Priority: string
[Facet(typeof(Entity), ConvertEnumsTo = typeof(int))]
public partial class EntityIntDto;
// Status: int? (nullable)
// Priority: int[Facet(typeof(Order), ConvertEnumsTo = typeof(string), NullableProperties = true, GenerateToSource = false)]
public partial class OrderQueryDto;
// All properties nullable + enums as strings - perfect for filter DTOsEnum conversions are included in the generated Projection expression and translate correctly to SQL:
var results = await dbContext.Orders
.Where(o => o.Status == OrderStatus.Completed)
.Select(OrderDto.Projection)
.ToListAsync();
// Status column returned as string in the DTO- All enums are converted: The setting applies to every enum property. For mixed behavior, use separate facets or custom configurations.
- Non-enum properties are unaffected: Only enum-typed properties are converted.
- Supported target types: Only
typeof(string)andtypeof(int)are supported.
See Enum Conversion for the full reference.
// Controller uses different facets for different endpoints
[ApiController]
public class UsersController : ControllerBase
{
[HttpGet]
public List<UserSummaryDto> GetUsers()
{
return users.SelectFacets<User, UserSummaryDto>().ToList();
}
[HttpGet("{id}")]
public UserDetailDto GetUser(int id)
{
return user.ToFacet<User, UserDetailDto>();
}
[HttpPost]
public IActionResult CreateUser(UserCreateDto dto)
{
var user = dto.ToSource();
// Save user...
}
}
// Different DTOs for different use cases
[Facet(typeof(User), Include = [nameof(User.Id), nameof(User.FirstName), nameof(User.LastName)])]
public partial record UserSummaryDto;
[Facet(typeof(User), nameof(User.Password))] // Exclude password but include everything else
public partial class UserDetailDto;
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email), nameof(User.Department)])]
public partial class UserCreateDto;Include mode works perfectly with modern C# records:
public record ModernUser
{
public required string Id { get; init; }
public required string FirstName { get; init; }
public required string LastName { get; init; }
public string? Email { get; set; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public string? Bio { get; set; }
}
// Generate record with only specific properties
[Facet(typeof(ModernUser), Include = [nameof(ModernUser.FirstName), nameof(ModernUser.LastName), nameof(ModernUser.Email)])]
public partial record ModernUserContactRecord;
// Include with init-only preservation
[Facet(typeof(ModernUser),
Include = [nameof(ModernUser.Id), nameof(ModernUser.FirstName), nameof(ModernUser.LastName)],
PreserveInitOnlyProperties = true)]
public partial record ModernUserImmutableRecord;- Include mode: Generates smaller facets, which can improve serialization performance and reduce memory usage
- Exclude mode: Better when you need most properties from the source type
// Include mode - generates minimal code
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.Email)])]
public partial class UserMinimalDto;
// Generated: only FirstName and Email properties
// Exclude mode - generates more code
[Facet(typeof(User), nameof(User.Password))]
public partial class UserFullDto;
// Generated: all properties except PasswordWhen using Include mode, the ToSource() method generates source objects with default values for non-included properties:
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName), nameof(User.Email)])]
public partial class UserContactDto;
var dto = new UserContactDto();
var sourceUser = dto.ToSource();
// sourceUser.FirstName = dto.FirstName (copied)
// sourceUser.LastName = dto.LastName (copied)
// sourceUser.Email = dto.Email (copied)
// sourceUser.Id = 0 (default for int)
// sourceUser.Password = string.Empty (default for string)
// sourceUser.IsActive = false (default for bool)The CopyAttributes parameter enables automatic copying of attributes from source type members to the generated facet. This is particularly useful for preserving validation attributes, display metadata, and JSON serialization settings in DTOs.
public class CreateUserRequest
{
[Required(ErrorMessage = "First name is required")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be 2-50 characters")]
public string FirstName { get; set; } = string.Empty;
[Required]
[EmailAddress(ErrorMessage = "Invalid email address")]
public string Email { get; set; } = string.Empty;
[Range(18, 120, ErrorMessage = "Age must be between 18 and 120")]
public int Age { get; set; }
[Phone(ErrorMessage = "Invalid phone number")]
public string? PhoneNumber { get; set; }
[StringLength(500)]
public string? Bio { get; set; }
}
// Generate DTO with all validation attributes copied
[Facet(typeof(CreateUserRequest), CopyAttributes = true)]
public partial class CreateUserDto;The generated CreateUserDto will include all the validation attributes:
public partial class CreateUserDto
{
[Required(ErrorMessage = "First name is required")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be 2-50 characters")]
public string FirstName { get; set; }
[Required]
[EmailAddress(ErrorMessage = "Invalid email address")]
public string Email { get; set; }
[Range(18, 120, ErrorMessage = "Age must be between 18 and 120")]
public int Age { get; set; }
[Phone(ErrorMessage = "Invalid phone number")]
public string? PhoneNumber { get; set; }
[StringLength(500)]
public string? Bio { get; set; }
}Attributes are only copied for properties that are included in the facet:
public class User
{
public int Id { get; set; }
[Required]
[StringLength(50)]
public string FirstName { get; set; } = string.Empty;
[Required]
[StringLength(50)]
public string LastName { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 8)]
public string Password { get; set; } = string.Empty;
}
// Exclude password - its attributes won't be copied
[Facet(typeof(User), "Password", CopyAttributes = true)]
public partial class UserDto;
// UserDto has Required, StringLength, EmailAddress on included properties
// No attributes from Password property
// Include only specific properties - only those get attributes
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.Email)], CopyAttributes = true)]
public partial class UserContactDto;
// UserContactDto only has attributes for FirstName and EmailAttribute copying works seamlessly with nested facets:
public class Address
{
[Required]
[StringLength(100)]
public string Street { get; set; } = string.Empty;
[Required]
[StringLength(50)]
public string City { get; set; } = string.Empty;
[Required]
[RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Invalid ZIP code")]
public string ZipCode { get; set; } = string.Empty;
}
public class Order
{
[Required]
[StringLength(20)]
public string OrderNumber { get; set; } = string.Empty;
[Range(0.01, 1000000)]
public decimal TotalAmount { get; set; }
public Address ShippingAddress { get; set; } = null!;
public string? InternalNotes { get; set; }
}
// Both facets copy their respective attributes
[Facet(typeof(Address), CopyAttributes = true)]
public partial class AddressDto;
[Facet(typeof(Order), "InternalNotes", CopyAttributes = true, NestedFacets = [typeof(AddressDto)])]
public partial class OrderDto;
// Usage - validation works on both parent and nested objects
var orderDto = new OrderDto
{
OrderNumber = "ORD-12345",
TotalAmount = 99.99m,
ShippingAddress = new AddressDto
{
Street = "123 Main St",
City = "Springfield",
ZipCode = "12345"
}
};
// ASP.NET Core will validate all attributes including nested ones
var validationContext = new ValidationContext(orderDto);
var validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(orderDto, validationContext, validationResults, validateAllProperties: true);Attribute copying works with more than just validation attributes:
public class Product
{
[Display(Name = "Product Name", Description = "The name of the product")]
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[Display(Name = "Unit Price")]
[DisplayFormat(DataFormatString = "{0:C}", ApplyFormatInEditMode = true)]
[Range(0.01, 10000)]
public decimal Price { get; set; }
[JsonPropertyName("product_sku")]
[Required]
public string Sku { get; set; } = string.Empty;
[Display(Name = "Available")]
public bool IsAvailable { get; set; }
}
[Facet(typeof(Product), CopyAttributes = true)]
public partial class ProductDto;
// ProductDto has all Display, DisplayFormat, JsonPropertyName, Required, StringLength, and Range attributesThe attribute copier automatically excludes:
- Compiler-generated attributes (e.g.,
System.Runtime.CompilerServices.*) - The base ValidationAttribute class (only derived validation attributes are copied)
- Attributes not valid for the target member based on
AttributeUsage
// Example: These attributes would NOT be copied
[CompilerGenerated] // Excluded: compiler-generated
[ValidationAttribute] // Excluded: base class itself
[Table("Users")] // Excluded if AttributeUsage doesn't allow properties
public class User { ... }A common use case is ensuring DTOs have the same validation as domain models:
// Domain model with validation
public class RegisterUserCommand
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3)]
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "Username can only contain letters, numbers, and underscores")]
public string Username { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 8)]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*$", ErrorMessage = "Password must contain uppercase, lowercase, and number")]
public string Password { get; set; } = string.Empty;
}
// API request DTO with copied validation
[Facet(typeof(RegisterUserCommand), CopyAttributes = true)]
public partial class RegisterUserRequest;
// API controller automatically validates using copied attributes
[ApiController]
public class AuthController : ControllerBase
{
[HttpPost("register")]
public IActionResult Register([FromBody] RegisterUserRequest request)
{
// ModelState.IsValid uses the copied validation attributes
if (!ModelState.IsValid)
return BadRequest(ModelState);
var command = new RegisterUserCommand
{
Username = request.Username,
Email = request.Email,
Password = request.Password
};
// Process registration...
return Ok();
}
}You can add additional attributes to the partial class:
public class User
{
[Required]
[StringLength(50)]
public string FirstName { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
}
[Facet(typeof(User), CopyAttributes = true)]
public partial class UserDto
{
// Add custom property with its own attributes
[Required]
[MinLength(10)]
public string CustomField { get; set; } = string.Empty;
}
// Generated properties will have copied attributes
// Custom properties keep their own attributesUse CopyAttributes = true when:
- Creating API request/response DTOs that need the same validation as domain models
- Building form models for UI frameworks
- Ensuring consistency between entity and DTO validation
- Preserving display metadata for data grids and forms
- Maintaining JSON serialization settings across layers
Don't use it when:
- DTOs need different validation rules than the source
- You want to avoid tight coupling between domain and API models
- Source types have ORM or infrastructure-specific attributes
- API Responses: Create focused DTOs for API endpoints
- Search Results: Include only essential data for search listings
- Mobile Apps: Minimize data transfer with targeted DTOs
- Microservices: Create service-specific views of shared models
- Security: Hide sensitive fields while keeping everything else
- Legacy Code: Maintain existing patterns and behavior
- Large Models: When you need most properties from complex entities
// Descriptive names for include-based DTOs
[Facet(typeof(User), Include = [nameof(User.FirstName), nameof(User.LastName)])]
public partial class UserNameOnlyDto; // Clear about what's included
[Facet(typeof(Product), Include = [nameof(Product.Id), nameof(Product.Name), nameof(Product.Price)])]
public partial class ProductListItemDto; // Indicates usage context
// Traditional names for exclude-based DTOs
[Facet(typeof(User), nameof(User.Password))]
public partial class UserDto; // General DTO name when excluding few fieldsSee Expression Mapping for advanced query scenarios and Custom Mapping for complex transformation logic.