Boldog.Mixins
2.1.0
dotnet add package Boldog.Mixins --version 2.1.0
NuGet\Install-Package Boldog.Mixins -Version 2.1.0
<PackageReference Include="Boldog.Mixins" Version="2.1.0" />
<PackageVersion Include="Boldog.Mixins" Version="2.1.0" />
<PackageReference Include="Boldog.Mixins" />
paket add Boldog.Mixins --version 2.1.0
#r "nuget: Boldog.Mixins, 2.1.0"
#:package Boldog.Mixins@2.1.0
#addin nuget:?package=Boldog.Mixins&version=2.1.0
#tool nuget:?package=Boldog.Mixins&version=2.1.0
Boldog.Mixins
Write a C# source generator without writing a source generator.
Boldog.Mixins is a Roslyn incremental source generator that expands .mixin template files into
real members on your partial types. Templates get a reflection-like, compile-time view of the
type they're applied to — so the code they generate adapts to whatever it lands on.
.NET Standard 2.0 analyzer · works in any C# 11+ project · MIT
The problem
You hit the same C# pattern for the third time this month — value equality, a ToString, an
interface delegated to a field, a mapper, a builder, a state machine. Your options:
- Copy-paste it. Fast today; drifts tomorrow. Add a property and equality silently goes stale.
- Reach for runtime reflection. Slow, allocates, fails at run time instead of compile time, and the trimmer hates it.
- Write a Roslyn incremental source generator. The right tool — and a multi-day project.
IIncrementalGenerator, syntax providers, value-equatable models so the cache doesn't thrash, a separate analyzer assembly, packaging, tests. All to stamp out members you could have described in a dozen lines.
The gap is real: the thing you want is "a generator," but the cost of authoring one is wildly out of proportion to the pattern you're trying to capture.
The solution
Write the pattern once, as a .mixin file. Boldog.Mixins turns it into an attribute. Apply
the attribute to any partial type, and the template expands onto it as additional partial
members — at build time, with no runtime cost.
The part that replaces a hand-written generator: inside the template you have mixin.Target,
a reflection-like model of the decorated type — its properties, fields, methods, constructors,
events, attributes, base type, interfaces, generic parameters, nested types — walkable to any
depth, with generic type arguments substituted, all resolved by the compiler. Your template reads
the shape of the type and generates code to fit it.
That's the whole pitch: the power of a custom source generator, at the cost of writing the code you wanted in the first place.
30-second example
Create a file named AutoToString.mixin:
public override string ToString()
{
var parts = new System.Collections.Generic.List<string>();
{{~ for $p in mixin.Target.Properties ~}}
{{~ if $p.AccessSpecifier == "public" && !$p.IsStatic && $p.HasGetter ~}}
parts.Add("{{ $p.Name }} = " + {{ $p.Name }});
{{~ end ~}}
{{~ end ~}}
return "{{ mixin.Target.Name }} { " + string.Join(", ", parts) + " }";
}
The filename becomes an attribute. Apply it:
[AutoToString]
public partial class Order
{
public int Id { get; set; }
public string Customer { get; set; } = "";
public decimal Total { get; set; }
}
new Order { Id = 7, Customer = "Acme", Total = 42m }.ToString()
// => "Order { Id = 7, Customer = Acme, Total = 42 }"
Add a property and rebuild — it shows up in ToString automatically, because the template reads
the type's actual shape. No reflection, no copy-paste, no drift. You just wrote a source
generator without writing a source generator.
Why you'll like it
- No Roslyn boilerplate. No
IIncrementalGenerator, no syntax providers, no equality models, no separate analyzer project. A template file and an attribute. - Zero runtime cost. Everything resolves at compile time into ordinary partial members. No reflection, no startup scan, trimmer- and AOT-friendly.
- Type-aware, not text-blind. Templates branch on the target's real shape — kind, modifiers, members, nullability, attributes — and stay correct as the type evolves.
- Library-safe by default. Generated attributes are
internal, so mixins you define stay private to your assembly; the mixed-in members on yourpublictypes remain visible. - Edit-and-go. Change a
.mixinfile and generated code updates live — no rebuild ritual. - Honest about cost. When a template does something the incremental analyzer can't bound, it
emits an
infodiagnostic (MIXIN0501) telling you exactly what, and how to avoid it. Output is always correct; only build incrementality is at stake.
Features
- One template, every type kind.
class,struct,record,record struct,interface— including nested types at any depth. The generator emits the matchingpartialwrapper. - Compile-time introspection (
mixin.Target). A reflection-like graph:Properties,Fields,Methods,Constructors,Events,BaseType,Interfaces,NestedTypes,GenericParameters,Attributes— each with rich, value-accurate metadata. - Walk to any depth, with substitution. Descend into a property's type, its members' types,
and so on. In-compilation types resolve full member graphs (enums and delegates included);
Box<int>resolvesBox<T>'s members withTmapped toint, through containers, arrays, and nullability. - Attribute-driven generation. Read
[Attribute(...)]arguments off the target — including walkabletypeof(...)references and enum values that round-trip back to compilable C# via theto_csharp_literalfilter. - Parameters & generics.
@parameterflows attribute arguments into the template;Name{T}.mixinproduces a generic attribute whose type arguments become template variables. - Scriban templating. Full Scriban syntax, plus mixin-specific
helpers (
mixin.Target,to_csharp_literal). - Directives for the generated output:
@namespace,@using,@parameter,@attribute,@inherits,@implements,@nullable.
Installation
Reference the package and mark it as an analyzer:
<PackageReference Include="Boldog.Mixins"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
That's it. The package's .props file automatically globs **/*.mixin under your project into
AdditionalFiles — drop a .mixin file in and start using its attribute. (To scope the glob
yourself, see the reference.)
Real-world examples
The repo ships 74 complete, build-verified samples in
samples/Boldog.Mixins.Samples — every one compiles on each
build and runs in the sample app. They re-create battle-tested mixin practice from across the
language landscape (Ruby modules, Scala stackable traits, C++ CRTP, Rust derives, Lombok, Swift
protocol synthesis, the GoF catalog, and the C# ecosystem's own Refit/Moq/StronglyTypedId). A
few:
Implement an interface by delegating to a field — the textbook mixin. The template walks the
interface's members and forwards every one, ref/out and generic methods included
(sample 06):
[DelegateToFields]
public partial class AuditFacade(IAuditLog log) : IAuditLog
{
readonly IAuditLog _log = log; // every IAuditLog member is generated, forwarding to _log
}
Map an entity to a DTO by walking a typeof(...) argument and matching properties by name and
type — extra properties on either side are simply skipped
(sample 07):
[AutoMapper]
[MapSource(typeof(PersonEntity))]
public partial class PersonDto { /* Id, FullName, Email — PersonDto.From(entity) is generated */ }
Turn an attribute list into control flow. Repeated [Transition] attributes are the
transition table; their enum arguments round-trip into a generated switch
(sample 37):
[StateMachine]
[Transition(DocumentStatus.Draft, DocumentStatus.Review)]
[Transition(DocumentStatus.Review, DocumentStatus.Published)]
public partial class DocumentWorkflow { public DocumentStatus Status { get; private set; } }
Generate a Refit-style HTTP client from an annotated interface — the most composite walk:
typeof argument → interface methods → their route attributes → Task<T> unwrap
(sample 50).
The full catalog with descriptions is in docs/samples.md.
Architecture & philosophy
- Compile-time, not run-time. A mixin is ordinary generated C#. There is nothing to discover or dispatch at run time — it's as if you'd hand-written the members.
- Correctness first, incrementality made visible. The generator statically bounds what each
template's deep walk can read, so unrelated edits don't re-render targets. When a construct
can't be bounded, it falls back conservatively and tells you (
MIXIN0501) — output is always correct; the diagnostic just makes the build cost auditable and avoidable. - Symbol-free caching. The per-type pipeline embeds external type identity at materialization time rather than holding Roslyn symbols, keeping cached models small and value-equatable. (The one documented edge — a swapped reference under an otherwise-untouched session — is noted in docs/known-issues.md.)
- Generic-attribute native. Generic mixins use C# 11 generic attributes; type arguments are first-class template variables.
- No magic cascade. Mixins don't emit other mixins (Roslyn generators see only user source) — a deliberate, predictable boundary rather than a hidden one.
How it compares
| Approach | Authoring cost | Runtime cost | Adapts to the type | Compile-time safety |
|---|---|---|---|---|
| Copy-paste / snippets | Low (then drifts) | None | No | Manual |
| Runtime reflection | Low | High (slow, allocates, AOT-hostile) | Yes | No (fails at run time) |
| T4 templates | Medium | None | Limited (no semantic model) | Weak |
| Hand-written Roslyn generator | High (days) | None | Yes | Yes |
| Boldog.Mixins | Low (a template file) | None | Yes (mixin.Target) |
Yes |
Boldog.Mixins occupies the slot the table makes obvious: the compile-time safety and type-awareness of a hand-written generator, at roughly the authoring cost of a snippet.
Documentation
- Reference — the complete
manual: directives, the full
mixin.Targetmember model, file naming, generics, behavior notes, diagnostics, and limitations. - Template authoring guide
— writing templates: graph-walking idioms, rendering values into C#, and the incremental-build
(
MIXIN0501) rules. - Introspection reference
— the member-by-member object model behind
mixin.Target, generated from the generator's own sources. - Samples — all 74 compiled samples, indexed.
- Bundling mixins in a library
— packaging
.mixinfiles into a reusable NuGet library for downstream consumers. - Known issues — documented limitations and design record.
License
MIT.
Acknowledgements
Boldog.Mixins is built on Scriban by Alexandre Mutel
(@xoofx) — the templating engine that powers .mixin files. Scriban
is licensed under the BSD-2-Clause license; its
copyright notice and license text are bundled with the package in THIRD-PARTY-NOTICES.txt.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
- Bundles THIRD-PARTY-NOTICES.txt carrying Scriban's BSD-2-Clause copyright and license text (required when redistributing its binary), and credits Scriban in the README.
- Upgrades Scriban to 7.2.4 (verified against the template analyzer's AST soundness pin).
- Adds a documentation guide for bundling mixins in a reusable library.
Full changelog: https://gitlab.com/Boldog/Mixins/-/blob/main/CHANGELOG.md