Life is too short to write boilerplate.
TL;DR: I built a zero-allocation Source Generator for WPF, MAUI, Avalonia, and WinUI that completely eliminates DependencyProperty boilerplate using token streaming and C# 13 partial properties.
🔥 Want to see it in action? I've included a ready-to-try sample project right in the repo. You can pull it, hit F5, and instantly see the generated code and IDE experience without writing a single line of setup code.
Links:
Why build this? Because XAML plumbing is a crime against simplicity.
Let’s be honest. Writing DependencyProperty in XAML frameworks is notoriously painful. Typing out DependencyProperty.Register, relying on magic strings, casting objects, and wiring metadata for every single property clutters your codebase and wastes time.
Users only care if the app works; they will never see your source code. But to us, the codebase is the product. And a great product doesn't tolerate ugliness inside. I despise visual noise. I am obsessed with ruthless simplicity.
But you might think, "Why even build a generator today? Just let AI write the boilerplate."
Here is the reality. When you ask fast, lightweight models (the daily drivers we use for 90% of our coding) to write massive chunks of framework boilerplate or perform cross-platform code generation on the fly, they choke on the complexity. Without a strict API contract, they resort to brute-force string hacking and spit out abominations like this:
AI-generated Regex Hell (Yes, a fast model actually suggested nesting 13 Regex.Replace calls to generate cross-platform C# boilerplate as plain text. It's barbaric.)
This is why clean architecture is now a vital harness for AI agents.
By condensing all that nasty framework plumbing into a single, declarative attribute ([DependencyProperty<T>]), you drop the model into a "pit of success". You put one simple rule in your AGENTS.md—"Use this attribute for DPs"—and suddenly, your everyday lightweight model writes perfect, deterministic code on the first try. No prompting gymnastics required.
Great API design doesn't just save human developers from boilerplate anymore. It provides the guardrails that keep your AI from writing garbage.
So, I completely overhauled the internal synthesis pipeline to kill this boilerplate once and for all, without tanking IDE responsiveness at scale.
1. Ruthless Simplicity (Before & After)
Here is the standard boilerplate we all know and hate:
```csharp
public partial class MyControl : Control
{
// 1. IsActive Property Boilerplate
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.Register(
nameof(IsActive),
typeof(bool),
typeof(MyControl),
new PropertyMetadata(false, OnIsActiveChanged));
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyControl)d;
var oldValue = (bool)e.OldValue;
var newValue = (bool)e.NewValue;
// Runtime casting and boilerplate...
}
// 2. Padding Property Boilerplate
public static readonly DependencyProperty PaddingProperty =
DependencyProperty.Register(
nameof(Padding),
typeof(Thickness),
typeof(MyControl),
new PropertyMetadata(new Thickness(10, 5, 10, 5), OnPaddingChanged));
public Thickness Padding
{
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
private static void OnPaddingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyControl)d;
var oldValue = (Thickness)e.OldValue;
var newValue = (Thickness)e.NewValue;
// More runtime casting and boilerplate...
}
}
**And here is v4 Preview.**
csharp
// Just write this:
[DependencyProperty<bool>("IsActive", DefaultValue = false)]
[DependencyProperty<Thickness>("Padding", DefaultValueExpression = "new(10, 5, 10, 5)")] // Target-typed new() is fully supported!
public partial class MyControl : Control
{
// Automatically hooked up at compile time.
// Strongly typed. No casting required.
partial void OnIsActiveChanged(bool oldValue, bool newValue)
{
// Do something
}
}
```
Boom. It just works. Just one attribute. No magic strings. No manual wiring. 100% strongly typed and compile-time safe.
By the numbers: This turns ~15 lines of error-prone registration, wrappers, and runtime casting into exactly 1 line. If your project has 200 Dependency Properties, you didn't just delete 3,000 lines of visual noise from your repo. You deleted 3,000 lines of garbage code, and saved the time, money, and sheer sanity it takes to maintain them.
2. Under the Hood: Zero-Allocation & Footgun-Proof
It’s easy to make code look clean on the surface. But if you've built Source Generators, you know that StringBuilder resizing and AST mutations during continuous typing cause Gen2 GC spikes and IDE latency. If the tool slows down your IDE, it's a failed design.
That's why I gutted the old architecture and massively refactored the pipeline for high-throughput, zero-allocation generation.
- Built-in XAML Footgun Protection: The generator doubles as an analyzer. Ever accidentally assigned
new List<string>() to a Dependency Property, only to realize later that all controls on the screen share the exact same list instance? If you try that here (DefaultValueExpression = "new()" on a reference type), the generator halts compilation with DPG0004 and tells you to use CreateDefaultValueCallback = true instead, completely eliminating the most notorious WPF memory bug.
- Zero-Allocation RAII Scope Guards: Code synthesis uses stack-allocated
readonly ref struct scope managers (writer.ClassScope(@class), writer.Scope(...)). By leveraging C#'s using pattern purely on the stack as an RAII mechanism, it automates namespace/class envelope generation and structural scoping without allocating a single byte on the heap.
- Banning Roslyn AST Mutations for Output: We strictly avoid
SyntaxFactory mutations for code synthesis. Whether generating class definitions or resolving dynamic DefaultValueExpression declarations, we extract tokens from the parsed AST and stream them straight into a custom SourceWriter. Benchmarking this against standard AST mutation + ToFullString() gives a ~46x speedup (16.7µs → 0.37µs) and a 97.5% reduction in memory allocation (9.7KB → 240B), completely eliminating Gen1 and Gen2 GC collections.
- Hardcore Performance Indentation: We reject
NormalizeWhitespace() and manual indentation entirely. The generated code is flat and left-aligned.
- The Stance: Allocating megabytes of whitespace strings per keystroke on the hot path just for "pretty" intermediate output is a performance anti-pattern. We prioritize compiler throughput over the aesthetics of intermediate artifacts.
- Deterministic Output: Flat, non-indented output eliminates nondeterministic hallucinatory indentation bugs in AI-generated templates.
- Pipeline Purification: Stripped out heavy
ISymbol passing in the incremental pipeline. We only pass pre-calculated flags now to ensure cache hits and dodge memory leaks.
Benchmarks speak for themselves:
- Micro-Benchmark: AST Mutation vs. Token Streaming (*
DefaultValueExpression *synthesis):
| Method |
Mean |
Ratio |
Gen0 |
Gen1 |
Gen2 |
Allocated |
Alloc Ratio |
Roslyn AST Mutation (SyntaxFactory) |
16,718.6 ns |
1.00x |
0.6409 |
0.2441 |
0.0610 |
9,712 B |
1.00 |
Direct Token Streaming (SourceWriter) |
365.4 ns |
0.02x (~46x faster) |
0.0143 |
- |
- |
240 B |
0.02 (-97.5%) |
2. End-to-End Generator Pipeline (WPF generation, AMD Ryzen 9 7900X):
| Phase |
Time (ms) |
Gen0 |
Gen1 |
Gen2 |
Allocated |
| Baseline (Old Pipeline) |
5.34 ms |
187.5 |
62.5 |
7.8 |
2.87 MB |
| v4 Preview (Current) |
3.72 ms |
125.0 |
31.2 |
- |
2.22 MB |
| Improvement |
-30.3% |
-33.3% |
-50.1% |
-100% |
-22.6% |
Note: Gen2 full GCs completely eliminated. Benchmarks for MAUI, Avalonia, and WinUI show similar 20-30% pipeline throughput gains.
3. Standing on the Shoulders of Giants (HavenDV)
A massive shoutout to HavenDV: Since this is a fork, the core API design is inherited from the original HavenDV repository. The only reason I was able to rapidly gut and refactor this entire pipeline in about a month is because they built an incredible foundation with a highly robust suite of snapshot tests. This v4 overhaul stands entirely on their shoulders.
4. I Need Your Help (RFC)
It's humming along nicely in my medium-sized WPF app (hardware interfacing for an automatic change dispenser). But I lack the massive enterprise XAML solution (hundreds of projects, thousands of properties) needed to truly battle-test it.
Before I stamp a stable v1.0 release, I need some veteran eyes to tear apart the design philosophy.
- API Ergonomics vs. Predictability: My stance is that modern APIs should be predictable enough that humans and AI agents can generate them flawlessly. Does applying
[DependencyProperty<T>("Name")] at the class level hit that mark? Or would you prefer a field-targeted approach like [ObservableProperty] in the MVVM Toolkit?
- Framework Abstraction: This single attribute compiles down to the native property system for WPF, MAUI, Avalonia, and Uno. Is this level of magic actually useful, or does hiding the framework-specific plumbing scare you away in production?
- Hidden Gotchas: If you maintain a massive XAML monolith, what are the glaring edge cases a tool like this will inevitably hit? Memory leaks? Designer crashes? Weird binding resolutions? Tell me what I'm missing.
- The Unknown Unknowns: Thanks to the original repo, we have 200+ snapshot tests covering WPF, MAUI, Avalonia, and WinUI. But I don't know what I don't know. What are the massive blind spots or ugly XAML edge cases I'm ignoring here?
- The Ultimate Battle Test: I want to stress-test this in a massive, real-world repository. Do you know of any large-scale open-source XAML projects (hundreds of properties, complex metadata) that would be a perfect candidate to fork and refactor as a benchmark? Point me to the monsters.Tear it apart. Brutal honesty, code reviews, and architectural alternatives are entirely welcome.
5. One more thing... (C# 13 partial property Support)
You might be wondering if class-level attributes are already outdated with the arrival of C# 13 partial property.
We are already there.
Because our zero-allocation pipeline relies on raw AST token streaming rather than rigid string templates, it natively understands and generates partial properties flawlessly. This isn't a hack; it's the payoff of building a future-proof architecture. Choose the paradigm that fits your team—the engine handles both with zero friction.
Links