# Facet: a C# source generator that turns one domain model into several DTO shapes

> Facet generates DTO types, constructors, LINQ projections and reverse mappings from a single domain model at compile time, with attributes for renaming, conditional mapping, flattening and CRUD sets. It ships as several NuGet packages, one per capability level, and supports .NET 8, 9 and 10.

**Tim-Maes/Facet** — Generate DTOs, mappings, constructors and LINQ projections from your domain models.

- Repository: https://github.com/Tim-Maes/Facet
- Stars: 1,209 · Forks: 55
- Language: C#
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/tim-maes-facet

## One model, several views of it, generated from a single source

Facet is a C# source generator that removes DTO boilerplate. You declare what you want, and it generates the type, the constructor, the LINQ projection and the reverse mapping, all at compile time and, in the README's phrasing, with zero runtime overhead.

The mental model the project offers is a gem with many facets, where a facet is one part of a subject that has many parts. The examples given are the three cases that push teams to write DTOs at all: a public API needs a facet without sensitive data, an admin endpoint needs a different facet with additional fields, and database queries need efficient projections. Instead of hand writing each of those shapes, you generate them from one source of truth.

That framing is what distinguishes Facet from a mapper library. A mapper takes two types you already declared and copies values between them. Facet derives the second type from the first, so the shape of the API contract is a decision expressed as an attribute on the model rather than as a second class that has to be kept in sync by hand.

The repository is structured as a solution, `Facet.sln`, with `src/`, `test/` and `docs/` directories, a `Directory.Build.props` and a `Directory.Packages.props` for centralised build settings, and a NuGet configuration. There is also a separate `src/Facet.Dashboard` project, linked from the README as the Facet Dashboard, plus a `CHANGELOG.md` and community files including `GOVERNANCE.md`, `SECURITY.md` and `SUPPORT.md`.

## Five packages, and the split tells you what each feature needs

Installation is a set of `dotnet add package` commands, and the way they are grouped tells you something about the design. The core package carries the generator itself:

```bash
dotnet add package Facet
```

The LINQ helpers are a separate package:

```bash
dotnet add package Facet.Extensions
```

Entity Framework Core support is another:

```bash
dotnet add package Facet.Extensions.EFCore
```

Advanced EF Core custom mappers with dependency injection support are their own package:

```bash
dotnet add package Facet.Extensions.EFCore.Mapping
```

And the expression transformation utilities are separate again:

```bash
dotnet add package Facet.Mapping.Expressions
```

The split is a deliberate isolation of dependencies. A project that only wants generated DTOs and projections does not pull in EF Core, and a project that only wants the expression helpers does not pull in the generator. The README also notes that the project works with any LINQ provider through `Facet.Extensions`.

The supported frameworks are .NET 8, .NET 9 and .NET 10, which is the constraint to check first: the generator is a Roslyn source generator, so it has to run inside the compiler of the SDK you build with.

## What the generator emits for a single facet

The code generation feature list is precise. DTOs can be generated as classes, records, structs, or record structs, so the output can match the conventions of the codebase it lands in. The constructor, a static factory, and a LINQ projection expression are all generated, and nested objects and collections are mapped automatically, so an entity with an address and a list of projects produces a projection that walks the graph without hand written copy code.

Two preservation features are easy to miss and expensive to lose. XML documentation is preserved, so a public API surface generated from a model keeps its doc comments, and data validation attributes are preserved as well, which is what lets a generated DTO still carry the validation rules declared on the source model.

Beyond the basic mapping, the generated surface can be shaped. A copy constructor can be generated for cloning and MVVM scenarios, and value-based equality can be generated for class DTOs, producing `Equals`, `GetHashCode`, `==` and `!=`, so two DTOs with the same values compare equal instead of comparing by reference. A `SetAccessor` option forces either `{ get; init; }` or `{ get; set; }` on all generated properties, which is how you produce an immutable variant and a mutable variant from the same model.

The `docs/07_WhatIsBeingGenerated.md` page in the documentation is the reference for the exact output, and it is the page to read before you decide anything else, because the whole value of the project is in what it emits.

## The attributes are where the interesting decisions live

Most of the customization surface is expressed as attributes on the source model, and the list is long enough that it is worth picking out the ones that change behaviour rather than shape.

`[MapFrom]` gives declarative property renaming with optional reverse mapping and expression support, so a domain property can be exposed under a different name without a hand written setter. `[MapWhen]` performs conditional mapping based on runtime values, and the README stresses that it works in SQL projections, which matters because a condition that only works in memory is useless in a query path. Before and after hooks let you inject validation, defaults or computed values around the automatic mapping, and custom mapping configurations can be synchronous or asynchronous and resolved from dependency injection, which is the escape hatch for mappings that need I/O.

`[Flatten]` collapses nested object graphs into top-level properties, and `[Wrapper]` provides reference-based delegation for facades, ViewModels and decorators, which is a different shape from copying values: the wrapper holds a reference and forwards to it.

`[ConvertEnumsTo]` converts all enums to `string` or `int` with full round-trip support, and `CollectionTargetType` remaps source collection types globally per facet, for example turning a `Collection<T>` into a `List<T>`, with a per-property override through `[MapFrom(..., AsCollection = typeof(List<>))]`.

Individual properties can be included or excluded with simple attribute arguments, and inheritance is supported: the generator traverses base classes and suppresses duplicates when facets inherit base types.

## GenerateDtos produces a whole CRUD set from one declaration

The `[GenerateDtos]` attribute is the highest-leverage feature in the list. It auto-generates a full CRUD DTO set, covering Create, Update, Response, Query, Upsert and Patch shapes, from the domain model.

This is the feature that changes how much code a feature branch touches. A typical endpoint needs a request DTO, a response DTO and often a patch shape for partial updates, and each of those is a class with a constructor and a set of properties that mirrors an entity. Generating the family at once means the six types stay consistent with the model by construction.

Multi-source mapping extends the same idea. A single target class can carry multiple `[Facet]` attributes, each mapping from a different source type, and the generator then produces per-source constructors, projections named `ProjectionFrom{Source}`, and reverse-mapping methods named `To{Source}()`. The naming convention in the README is worth noting, because it tells you how the generated API will look in your code before you generate it.

There is also a safety net in the list called source signature tracking, which produces a compile-time warning when a source entity's structure changes. That converts a silent mapping break into a build error, which matters in a codebase where the DTOs are generated and therefore invisible in a diff.

## Entity Framework Core support and expression transformation

The integration section claims full Entity Framework Core support with automatic navigation loading and no `.Include()` required. The second half of that claim is the interesting one. Navigation loading is normally a per-query decision, and getting it wrong is the classic cause of an N+1 query or of a serialization loop. If the generated projections resolve the navigation properties themselves, the query and the projection become one operation.

Expression transformation is the related feature: it remaps predicates and selectors from entity types to their projections, so a `Where` clause written against an entity still works after the data has been projected into a facet. That is a subtle piece of work, since the expression tree has to be rewritten rather than re-evaluated, and it is what makes the projections usable in a query provider rather than only in memory.

Async variants with cancellation token support are listed, as are custom async mappers with dependency injection for mappings that require I/O. The runtime cost claim is that there is no reflection and everything is generated at compile time, which is the standard argument for a source generator over a dynamic mapper: the mapping is a constructor call, so it is inlined by the JIT and the type safety is enforced by the compiler.

Whether that holds for your query shapes is a question for your own measurements, since the README does not publish benchmarks.

## Project-wide defaults, output locations and a dashboard to try it

Three documentation pages in the repository are about controlling the generator rather than using it, and they are the ones that decide whether the tool fits an existing codebase.

Global configuration lets you override the default attribute settings project-wide through MSBuild properties, which is the answer to the problem of the same configuration repeated on every model in a solution. The second page covers where the generated files are written, which matters for source control: generated code that lands in your tree becomes diff noise unless the team agrees to commit it or ignore it.

The third is the Facet Dashboard, a separate project under `src/Facet.Dashboard` that the README links as a tool of its own. It sits next to the other supporting files in the repository root, including `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `GOVERNANCE.md`, `SECURITY.md`, `SUPPORT.md` and a `be-nice.md`, and the presence of separate `V5_README.md` and `V6_README.md` files records that the project has already had a major version transition with different guidance for each line.

The release history is steady: v6.6.15 was published on 2026-10-15, v6.6.14 on 2026-10-05 and v6.6.13 on 2026-09-29, and the last push was on 2026-10-15. The project is under the MIT licence, with the text in `LICENSE.txt`.

## Where Facet is the wrong tool

The first wrong case is a mapping problem that is genuinely irregular. Facet assumes your DTO is a derivative of your model. Where the API contract is a different shape entirely, where fields are computed from several sources, or where the mapping depends on request context, the attributes and hooks accumulate until the configuration is more work than the hand written DTOs would have been. The `[MapWhen]` and before and after hooks exist for this, and their existence is also the warning.

The second is a project that cannot move to a recent .NET SDK. The generator is a Roslyn source generator and the supported targets are .NET 8, 9 and 10, so a team on an older LTS has to upgrade the toolchain to adopt it.

The third is a team that wants a mapper rather than a generator. If the goal is to map between two types you already own, with runtime configuration and per-call behaviour, a source generator is the wrong shape, since everything is decided at compile time by attributes.

The fourth is a large existing codebase with hand written DTOs. Facet does not describe a migration path for that in the visible documentation, and mixing generated and hand written DTOs for the same entity produces two sources of truth for the same contract, which is the situation the project is meant to remove. The v5 and v6 readme files also suggest the attribute surface changes between major versions, so an upgrade is a migration, not a version bump.

## Conclusion

Adopt Facet if you maintain a .NET service whose domain entities and API contracts have drifted apart and you want the DTO layer generated rather than hand written, particularly if you use Entity Framework Core and want projections that do not need manual `.Include()` calls. Do not adopt it if your mapping needs are irregular enough that generated constructors would fight your design, or if you are still on the v5 line and cannot absorb the v6 changes, since the repository keeps separate V5 and V6 readme files. Before you install, look at `docs/07_WhatIsBeingGenerated.md` to see exactly what the generator emits, and add the `Facet.Extensions.EFCore` package only if you actually use EF Core.

## FAQ

### What does the Facet C# source generator produce from a domain model?

It generates the DTO type as a class, record, struct or record struct, plus a constructor, a static factory and a LINQ projection expression, with nested objects and collections mapped automatically. It can also generate reverse mappings, copy constructors and value-based equality.

### Which NuGet packages does Facet need?

The core generator is the `Facet` package, the LINQ helpers are in `Facet.Extensions`, EF Core support is in `Facet.Extensions.EFCore`, advanced EF Core custom mappers with DI are in `Facet.Extensions.EFCore.Mapping`, and the expression transformation utilities are in `Facet.Mapping.Expressions`.

### Does Facet support Entity Framework Core without Include calls?

The README claims full Entity Framework Core support with automatic navigation loading and no `.Include()` required, and it also provides expression transformation that remaps predicates and selectors from entity types to their projections.

### Which .NET versions does Facet target?

The README states support for .NET 8, .NET 9 and .NET 10, and the project ships a CI workflow badge, a test workflow badge and a release workflow badge alongside the package on NuGet.

## Sources

- [Issues](https://github.com/Tim-Maes/Facet/issues)
- [License: MIT](https://github.com/Tim-Maes/Facet/blob/master/LICENSE)
- [README](https://github.com/Tim-Maes/Facet/blob/master/README.md)
- [Releases](https://github.com/Tim-Maes/Facet/releases)
- [Tim-Maes/Facet on GitHub](https://github.com/Tim-Maes/Facet)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tim-maes-facet
