Upside-Down Inheritance: Discovering CRTP in C#
You hesitate at the threshold. The tunnel curves inward—smooth, deliberate—but there’s something wrong in the air. Not a wall, but a tension. A subtle force that seems to repel the thought of entry. You pause, eyes adjusting, and see nothing. No danger. No tricks. Just stone. Still, you feel it in your chest: this place shouldn’t exist.
And yet, the passage feel familiar—inviting, even. You embrace it, the tension, unable to resist the curiosity: as you step forward, the pressure vanishes. Light flares along the walls. Symbols begin to rise—faint engravings of this very moment, of you walking into what felt so anomalous at first. Suddenly you understand: this is exactly where you were meant to end up.
This is a story about finding a pattern by accident—and realizing, only after it worked, that it had a name. The Curiously Recurring Template Pattern, or CRTP, is a structure often associated with C++—but this isn’t a C++ article. It’s about C#, and the quiet discovery of something deeper than syntax.
You won’t need to know templates, or even inheritance, to follow along. All you need is a question: what if a type could remember what kind of thing it is—not by behavior, but by shape?
This article walks through how CRTP emerged from within a symbolic math system I was building in C#. It shows how the shape of recursion became possible, how the pattern worked despite the language, and why constraints sometimes reveal the best designs. Along the way, it might even teach you enough to use CRTP in your own projects—whether or not you call it that.
If you’re here for the practical side—what CRTP looks like in C#, how to use it, and why it might help your own project—you can jump straight to 2. The Pattern I Didn’t Know I Was Using. It walks through real examples from my project Vellum and explains the mechanics step by step. But if you’re in the mood to follow the thread from first principles—welcome. Let's unfold it together.
1. The Shape I Needed
It began as a question of structure. I had two kinds of terms: individual terms and expressions. Both needed to be the same type: an expression should logically be able to go wherever an atomic term can (whether inside another expression, or as a factor or exponent—they should always be treated the same way). But they couldn’t just be any term: I needed specificity. An algebraic term shouldn’t end up inside a numeric expression, and vice versa.
That’s what made it tricky. Not just because expressions needed to contain other terms—but because they needed to contain only the right kind of terms.
A generic Expression<T>
was needed, to impose type specificity on what terms an expression could contain:
public class Expression<T> where T : Term
{
public List<T> Terms { get; }
}
But, since C# doesn't allow inheriting from a generic type (i.e. Expression<T> : T
, which is not permitted), there wasn't an obvious way to make expressions nest without losing the type boundary given by T
. Sure, Expression<T>
could just derive from Term
, but then expressions couldn't fit inside the Terms
list, which expects terms of type T
specifically. So the question became: how do you express this constraint in the type system? How do you make Expression<T>
and T
coexist within a single list?
Interfaces helped, but not much. I tried tagging each subclass with IComposableTerm<T>
, a kind of compatibility marker:
public interface IComposableTerm<T> { }
public class Expression<T> : IComposableTerm<T> where T : Term
{
public List<IComposableTerm<T>> Terms { get; }
}
public class NumericTerm : Term, IComposableTerm<NumericTerm> { }
public class AlgebraicTerm : Term, IComposableTerm<AlgebraicTerm> { }
That let expressions and terms share a base—but at the cost of redundancy. Every term had to declare both its identity and its compatibility separately. And worse, the interface had no behavior. I couldn’t rely on it for anything but classification.
What I needed was a shape. A single type that could do both: define what a term was, and declare what kind of other terms it could live beside. That’s when the recursion came in. What if the base class already knew the shape of its most compatible form? What if instead of just Term
, Term<T>
could be used, meaning “a term that is compatible with other T
s”?
What emerged looked strange—almost suspicious:
public abstract class Term<T> where T : Term<T> { }
public class NumericTerm : Term<NumericTerm> { }
At first glance, it felt like a loop: T
is a Term<T>
, which is itself a Term<T>
. Wouldn’t that lead to infinite expansion—Term<Term<Term<...>>>
? Surely the compiler would complain—or explode—but no: it accepted it. C# treated the generic constraint not as an instruction to expand, but as a boundary it should enforce. As long as T
eventually resolved to a concrete type that followed the rule—such as NumericTerm
—the structure held.
The moment I paired it with an expression, the shape fell into place:
public class Expression<T> : Term<T> where T : Term<T>
{
public List<Term<T>> Terms { get; }
}
Now both NumericTerm
and Expression<NumericTerm>
were Term<NumericTerm>
. They could sit side-by-side in the same list. No casting. No interface tags. No loss of specificity. It felt wrong. Backwards, upside-down even. But it didn’t break.
I hadn’t discovered a new trick. I’d walked straight into an old one—a pattern used in C++, one I hadn’t even heard of yet, called the Curiously Recurring Template Pattern.
2. The Pattern I Didn’t Know I Was Using
What I’d stumbled into was an old pattern—well known in C++, less so in C#. It’s called the Curiously Recurring Template Pattern, or CRTP. It looks like this in C++:
template <class T>
class Base {
void doSomething() {
static_cast<T*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
void implementation() { ... }
};
It allows compile-time polymorphism: the base class can “reach down” into the derived class using T
, without relying on virtual methods. In C#, though, this kind of casting isn’t allowed in the same way—and many of CRTP’s classic use cases (like static polymorphism or polymorphic cloning) don’t translate directly.
But something else does carry over: type specificity. That was the missing piece in my design. By writing:
public abstract class Term<T> where T : Term<T> { }
public class NumericTerm : Term<NumericTerm> { }
I wasn’t gaining dynamic behavior. I wasn’t trying to reach into the derived class. I was doing something simpler: tagging a type as being compatible with itself. Saying: this Term
belongs to the NumericTerm
ecosystem. That was all. And that was enough: it meant I could now write:
public class Expression<T> : Term<T> where T : Term<T>
{
public List<Term<T>> Terms { get; }
}
Now, both NumericTerm
and Expression<NumericTerm>
satisfy Term<NumericTerm>
. I can create expressions like this:
Expression<NumericTerm> expr = new(
new NumericTerm(2),
new NumericTerm(3)
);
Or even nest them:
Expression<NumericTerm> nested = new(
new NumericExpression(new NumericTerm(1), new NumericTerm(4)),
new NumericTerm(3)
);
The compiler enforces compatibility. A Term<AlgebraicTerm>
won’t be allowed inside an Expression<NumericTerm>
. No runtime checks, no casts. Just clean, recursive typing.
Why Not Interfaces?
You might be wondering: couldn’t you just use an interface? Something like:
public interface IComposableTerm<T> where T : Term { }
public class NumericTerm : Term, IComposableTerm<NumericTerm> { }
public class Expression<T> : Term, IComposableTerm<T> where T : Term { }
That works—but as mentioned in the first part of the article, it introduces redundancy. Each term now has to implement both Term
and IComposableTerm<T>
. Worse, the interface doesn’t give you any behavior—just a tag. You still need to cast or wrap to do anything with the values inside.
With CRTP-style tagging, the generic isn’t just a marker—it’s part of the type. You can define shared behavior inside Term<T>
, override methods in the derived classes, and still keep your types organized by compatibility.
But Isn’t This… Recursive?
Yes, and that’s what makes this design weird-looking, but surprisingly powerful: when you declare Term<T> where T : Term<T>
, it seems like you're creating an infinite type chain: Term<Term<Term<...>>>
. But you’re not. The compiler doesn’t try to expand the generic—it just enforces that T
must itself be a Term<T>
. It’s a boundary, not a recursion.
You can’t rely on T
being anything specific within the base class. That’s one of the main differences between CRTP in C# and its more traditional form in C++. In C++, CRTP often enables static polymorphism: the base class uses T
to call into the derived class using casts like static_cast<T*>(this)
, allowing methods in the base to invoke behavior defined in the derived class—without needing virtual methods or runtime dispatch.
In C#, that kind of casting and behavior isn’t allowed. You can’t cast this
to T
and invoke members on it. You can’t override static methods, and you don’t get compile-time method resolution based on T
. But that’s not what this structure is for. Here, Term<T>
doesn’t rely on T
to access behavior. It relies on T
to define type compatibility. When T : Term<T>
, the base class isn’t trying to act like the derived one—it’s simply enforcing that all terms in a given ecosystem share a consistent shape. This lets the rest of the system (like Expression<T>
) use that structure to ensure safety: a List<Term<NumericTerm>>
will happily accept both NumericTerm
and Expression<NumericTerm>
, but reject anything else.
The point isn’t to reach into the derived class. It's to create a type-level echo—a recursive pattern that doesn’t execute, but aligns.
3. The Uneasy Fit
CRTP in C# works. But it doesn’t feel like it should.
The first time I wrote Term<T> where T : Term<T>
, I assumed I’d hit a compiler error. It looked like a loop, a snake eating its own tail. And even when it compiled, it still felt wrong—like I was doing something the language only accidentally allowed.
Part of the unease comes from how C# handles generics. They're not covariant in classes. Which means:
- A
NumericTerm
is aTerm<NumericTerm>
- An
Expression<NumericTerm>
is also aTerm<NumericTerm>
- But
Term<NumericTerm>
is not aNumericTerm
, and never will be
This creates a subtle asymmetry: once you go down the tree—from NumericTerm
to Term<NumericTerm>
—you can't go back up. The generic parameter tells you what shape something claims to be compatible with, but not what it actually is. That makes it easy to lose track of what the generic is doing—and even easier to question whether you needed it in the first place.
I couldn’t climb back up the type tree. The generic told me what something claimed to be, but not what it was. It kept catching me off guard. More than once, I found myself tempted to delete the <T>
altogether. Just use a plain abstract Term
base, keep things loosely typed, and manage compatibility through runtime checks. But every time I tried that, the cracks started to show. I’d no longer be able to restrict expressions to a specific domain. I’d need to cast constantly. And I’d have no shared base to lean on when implementing operations like Add()
or Evaluate()
.
The generic wasn’t just there for the compiler. It was there for the design. I needed to use it for the system to make sense. It became the thing that kept everything aligned: every term knew what kind of terms it belonged with. Every expression could enforce that boundary. And even though C# couldn’t let the base reach down into the derived type, it could still hold the shape of the structure as a whole.
And once you stop expecting it to behave like C++—once you embrace the idea that T
is for alignment, not inheritance—it all starts to feel natural, even elegant.
4. The Possibility Beyond the Constraint
The more this pattern settled into place, the more I began to wonder—what else could it have done, if the language had allowed it?
C# let me define the shape, using generics to declare compatibility, and to hold structure in place. But it stopped short of letting that structure act. I couldn’t use T
to construct new objects, calling a shared method in the base Term<T>
, and instead had to implement a new deep copying method in each deriving class. I couldn’t make static overrides, or propagate behavior through the shape. The pattern looked recursive, but it couldn’t reach inward or outward. It was a surface echo. Almost like a map, marking the structure, but staying still.
But what if that limitation was lifted?
Reading more about CRTP in C++ made me realize how much more potential thits pattern has. There, the base class can invoke methods on T
. It can call T::static_func()
or static_cast<T*>(this)->implementation()
. You can build cloneable interfaces without any virtual methods. You can chain method calls across an inheritance tree without losing type identity.
In C#, every time I needed one of those behaviors, I had to write it explicitly in the derived class. Copying, reconstructing: always delegating by hand. It was clean, but it was manual. I sculpted every piece with intention—but always with the sense that I was working against the grain.
And I started to wonder what it might feel like to build the same kind of system in C++—to take this design, this shape I’d coaxed out of constraint, and let it breathe in a language that welcomes recursion and metaprogramming. Not to escape C#, but to try a different kind of freedom.
Because C# gives me clarity, stability. It forces me to justify every layer, to confront every edge case. But this project made me wonder: what if I could just explore? Compose without friction, follow a pattern not because it’s legal—but because it’s possible. And that’s a different kind of thrill.
Lately, I’ve been looking toward C++. Not as a replacement, but as a horizon—somewhere the rules bend differently, where structure can emerge even before it's fully understood. I don’t know exactly what I’ll build there, or what the language will teach me. But I think there’s something worth chasing: that sense of unbound composition, where shape leads and meaning follows. I might write about it—what it’s like for a C# dev to approach a new system—fundamentally different, more flexible, unfamiliar. Not with answers, but with questions.
The Lantern Moment: The Type That Knew Its Shape
What made it click wasn’t cleverness. It wasn’t even the pattern. It was the feeling that everything was finally where it belonged.
I’d been trying to hold too many things at once: specificity, nesting, compatibility, clarity. And this one shift—declaring a term with its own type as a parameter—suddenly made the structure hold. Expressions became terms, without sacrificing specificity. The system stopped needing exceptions or casting or catch-all abstractions. It just…fit. Not because it solved the whole system, but because it shaped it. Because once that one detail was defined, everything else found its alignment.
Turns out, it wasn’t about tricks. It was just... about trust. That if a term knows its kind, the system will know what to do.
Even in a language like C#, where recursion feels foreign, where inheritance is cautious, this shape still worked. Not loudly, not dynamically—quietly, behind everything. A type that knew its own kind. And maybe that’s all structure really is—just the memory of what belongs together.
The Pattern That Waited
Some patterns arrive fully formed. Others wait—half-buried, woven into the structure before you have the words to name them. This one didn’t start as a design pattern. It started as a feeling: something resisting, just at the threshold. Like the system didn’t want to let me in.
I think that’s what I’m chasing now, in every project, every design. Not novelty, not optimization: just the quiet satisfaction of when something fits.
Like stepping into a place that shouldn’t exist—and finding your own footsteps already carved into the floor.