Generate DTOs, mappings, constructors and LINQ projections from your domain models.
Generate DTOs, mappings, constructors and LINQ projections from your domain models.
Facet is a C# source generator that eliminates DTO boilerplate. Declare what you want, and Facet generates the type, constructor, LINQ projection, and reverse mapping, all at compile time with zero runtime overhead.
Think of your domain model as a gem with many facets! Different views for different purposes:
Instead of manually creating each facet, Facet auto-generates them from a single source of truth. Generates constructors, projections, reverse mappings, patch, etc...
[MapFrom] - declarative property renaming with optional reverse mapping and expression support[MapWhen] - conditional mapping based on runtime values, works in SQL projectionsConvertEnumsTo - convert all enums to string or int with full round-trip supportCollectionTargetType - remap source collection types (e.g. Collection) to List or any target collection type globally per facet; per-property via [MapFrom(..., AsCollection = typeof(List<>))]GenerateCopyConstructor - generate a copy constructor for cloning and MVVM scenariosGenerateEquality - generate value-based Equals, GetHashCode, ==, != for class DTOsSetAccessor - force { get; init; } or { get; set; } on all generated properties for immutable / mutable variants[Facet] attributes, each mapping from a different source type; produces per-source constructors, projections (ProjectionFrom{Source}), and reverse-mapping methods (To{Source}())[Flatten] - collapse nested object graphs into top-level properties[Wrapper] - reference-based delegation for facades, ViewModels, and decorators[GenerateDtos] - auto-generate full CRUD DTO sets (Create, Update, Response, Query, Upsert, Patch).Include() requiredFacet.ExtensionsInstallation
dotnet add package Facet
For LINQ helpers:
dotnet add package Facet.Extensions
For EF Core support:
dotnet add package Facet.Extensions.EFCore
For advanced EF Core custom mappers (with DI support):
dotnet add package Facet.Extensions.EFCore.Mapping
For expression transformation utilities:
dotnet add package Facet.Mapping.Expressions
Define Facets
…
Create focused facets for different scenarios:
…
Basic Projection of Facets
[Facet(typeof(User))]
public partial class UserFacet { }
// Map your source to facet
var userFacet = user.ToFacet();
var userFacet = user.ToFacet(); //Much faster
// Map back to source
var user = userFacet.ToSource();
var user = userFacet.ToSource(); //Much faster
// Patch only changed properties back to source
user.ApplyFacet(userFacet);
user.ApplyFacet(userFacet); // Much faster
// Patch with change tracking
bool hasChanges = userFacet.ApplyFacetWithChanges(userFacet);
// LINQ queries
var users = users.SelectFacets();
var users = users.SelectFacets(); //Much faster
Custom Sync Mapping
public class UserMapper : IFacetMapConfiguration
{
public static void Map(User source, UserDto target)
{
target.FullName = $"{source.FirstName} {source.LastName}";
target.Age = CalculateAge(source.DateOfBirth);
}
}
[Facet(typeof(User), Configuration = typeof(UserMapper))]
public partial class UserDto
{
public string FullName { get; set; }
public int Age { get; set; }
}
Custom Reverse Mapping with ToSourceConfiguration
When GenerateToSource = true, Facet generates a ToSource() method that maps the DTO back to the source entity. Use ToSourceConfiguration to hook in custom logic for properties that need special treatment on the reverse path, for example, serialising a parsed object back to a JSON column.
…
The Map method in ToSourceConfiguration is called after all auto-mapped properties are copied, so you only need to handle the properties that require custom logic.
Both configs can live in the same class by implementing both interfaces.
Property Mapping with [MapFrom]
Rename properties declaratively without custom mapping configurations:
…
…
| Use Case | MapFrom | Custom Config |
|---|---|---|
| Simple property rename | :white_check_mark: Best choice | Overkill |
| Multiple renames | :white_check_mark: Best choice | Overkill |
| Computed values (expressions) | :white_check_mark: Supported | Alternative |
| Async operations | :x: | :white_check_mark: Required |
| Complex transformations | :x: | :white_check_mark: Required |
Note: MapFrom and custom configurations can be combined. Auto-generated mappings (including MapFrom) are applied first, then the custom mapper is called.
Conditional Mapping with [MapWhen]
Map properties only when specific conditions are met. Perfect for status-dependent fields, null checks, or role-based data exposure:
…
[Facet(typeof(Order))]
public partial class SecureOrderDto
{
// Both conditions must be true
[MapWhen("IsActive")]
[MapWhen("Status == OrderStatus.Completed")]
public DateTime? CompletedAt { get; set; }
}
[MapWhen("IsActive")][MapWhen("Status == OrderStatus.Completed")][MapWhen("Status != OrderStatus.Cancelled")][MapWhen("Email != null")][MapWhen("Age >= 18")][MapWhen("!IsDeleted")]var orders = await dbContext.Orders
.Where(o => o.IsActive)
.SelectFacet() // Conditions included in SQL
.ToListAsync();
Before/After Mapping Hooks
Run custom logic before and/or after the automatic property mapping. Perfect for validation, setting defaults, and computing derived values:
…
Use IFacetMapHooksConfiguration for both before and after logic in one class:
public class UserMappingHooks : IFacetMapHooksConfiguration
{
public static void BeforeMap(User source, UserDto target)
{
target.MappedAt = DateTime.UtcNow;
}
public static void AfterMap(User source, UserDto target)
{
target.FullName = $"{target.FirstName} {target.LastName}";
}
}
[Facet(typeof(User),
BeforeMapConfiguration = typeof(UserMappingHooks),
AfterMapConfiguration = typeof(UserMappingHooks))]
public partial class UserDto { }
public class UserEnrichmentHook : IFacetAfterMapConfigurationAsyncInstance
{
private readonly IProfileService _profileService;
public UserEnrichmentHook(IProfileService profileService)
{
_profileService = profileService;
}
public async Task AfterMapAsync(User source, UserDto target, CancellationToken ct = default)
{
target.ProfileUrl = await _profileService.GetProfileUrlAsync(source.Id, ct);
}
}
| Hook | When Called | Use Case |
|---|---|---|
| BeforeMap | Before properties copied | Validation, defaults, timestamps |
| AfterMap | After properties copied | Computed values, transformations |
| Configuration (Map) | After mapping | Simple computed properties |
Execution order: BeforeMap → Property Mapping → Configuration.Map → AfterMap
Enum Conversion with ConvertEnumsTo
Automatically convert all enum properties in the source type to string or int in the generated facet. Perfect for API DTOs, serialization, and frontend consumption:
public enum UserStatus { Active, Inactive, Pending, Suspended }
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public UserStatus Status { get; set; }
public string Email { get; set; }
}
// Convert enums to strings (for JSON APIs)
[Facet(typeof(User), ConvertEnumsTo = typeof(string), GenerateToSource = true)]
public partial class UserStringDto;
// Convert enums to integers (for compact storage)
[Facet(typeof(User), ConvertEnumsTo = typeof(int), GenerateToSource = true)]
public partial class UserIntDto;
var user = new User { Id = 1, Name = "John", Status = UserStatus.Active, Email = "[email protected]" };
// String conversion
var stringDto = new UserStringDto(user);
stringDto.Status // "Active" (string)
// Int conversion
var intDto = new UserIntDto(user);
intDto.Status // 0 (int)
// Round-trip back to entity
var entity = stringDto.ToSource();
entity.Status // UserStatus.Active (enum)
// Works with LINQ / EF Core projections
var dtos = await dbContext.Users
.Select(UserStringDto.Projection)
.ToListAsync();
Nullable enum properties preserve nullability:
public class Entity
{
public UserStatus? Status { get; set; } // Nullable enum
}
[Facet(typeof(Entity), ConvertEnumsTo = typeof(string))]
public partial class EntityDto;
// Status becomes string (null when source is null)
[Facet(typeof(Entity), ConvertEnumsTo = typeof(int))]
public partial class EntityIntDto;
// Status becomes int? (nullable)
[Facet(typeof(User), ConvertEnumsTo = typeof(string), NullableProperties = true)]
public partial class UserQueryDto;
// All properties nullable + enums converted to string
Copy Constructor and Value Equality
Generate a copy constructor for clon
No open issues yet, or sync has not completed.