Clean Assert Wrappers
When working in C#, you often use debugging APIs like Debug.Assert() or Debug.Fail(). But if you call them directly across the whole project, it quickly becomes inconvenient. That's why many developers create a wrapper function and use it globally.
For example:
DBG_CHECK(x != null, "x is null!");
Why bother with a wrapper?
- If you import
DBG_CHECK()globally viausing static, typing becomes easier. - Since the name is in ALL CAPS, it stands out in the code. → It's immediately obvious that this is a debug-only utility, not business logic, so you can skim past it easily.
- You can also sneak in extra enforcement logic inside the wrapper.
In other words, wrapping repeated Debug.Assert() calls makes them cleaner, easier to use, and more visible.
Why use [Conditional("DEBUG")]
This wrapper should, of course, only run in DEBUG mode. In C#, attaching the [Conditional("DEBUG")] attribute tells the compiler to remove the call entirely in release builds.
[Conditional("DEBUG")]
public static void DBG_CHECK(bool condition, string? message = null)
{
if (!condition)
{
Debug.Fail(message);
}
}
- In release builds, not even IL code remains.
- You no longer need to wrap everything in
#if DEBUG ... #endif. - From a performance and security perspective, you must never leave asserts in release builds.
The annoying part: stack trace
The problem is that when Debug.Assert() hits inside the wrapper, the top of the stack is always your DBG_CHECK() wrapper function.
For example:
void Foo()
{
Bar(null);
}
void Bar(object? arg)
{
DBG_CHECK(arg != null, "arg is null");
}
If arg is null, the stack looks like this:
DBG_CHECK()
Bar()
Foo()
What you really care about is Bar(), but the debugger always stops inside DBG_CHECK(). So you have to "Step Out" every time to get to the actual caller. Pretty annoying.
The fix: [DebuggerHidden]
This is where [DebuggerHidden] comes to the rescue.
[Conditional("DEBUG")]
[DebuggerHidden]
public static void DBG_CHECK(bool condition, string? message = null)
{
if (!condition)
{
Debug.Fail(message);
}
}
DebuggerHiddentells the debugger to hide this function's frame.- If a break happens inside it, the debugger jumps directly to the caller.
- Works flawlessly in Visual Studio
Example in action
In the Bar() example above, when DBG_CHECK() fails, the debugger now shows:
Bar()
Foo()
The DBG_CHECK() frame is gone, so you can start debugging exactly at the caller site.
Extra details
DebuggerHiddenis supported from .NET Framework 2.0 onward, including .NET Core and .NET 5–9.- Verified in Visual Studio 2019 and 2022. (JetBrains Rider shows similar behavior but may vary slightly.)
- Caveat: you cannot set breakpoints inside a method marked
[DebuggerHidden].
But since this is just an assert wrapper, that's not a problem.
Wrap-up
- Wrapping
Debug.Assert()with something likeDBG_CHECK()makes it easier to type, more visible, and cleaner. [Conditional("DEBUG")]ensures it never appears in release builds.- The downside is that the wrapper clutters the call stack, but adding
[DebuggerHidden]fixes that by shifting focus to the caller. - In Visual Studio, the experience is clean and seamless.
👉 Bottom line: When writing assert wrappers, the Conditional("DEBUG") + DebuggerHidden combo is basically a must-have.