Debug.Assert Everyone!


Every developer knows that unit testing improves the quality of the code. We also profit from static code analysis and use tools such as SonarQube in our build pipelines. Yet, I still find that many developers are not aware of a much older way of checking the validity of the code: assertions. In this post, I will present you the benefits of using assertions, as well as some configuration tips for .NET applications. We will also learn how .NET and Windows support them.

What are assertions and when should I use them

An assertion declares that a certain predicate (true-false expression) must be true at a specific time in a program. When an assertion evaluates to false, an assertion failure occurs, which usually leads to a program crash. We normally use assertions in debug builds and handle assertion exceptions either in a debugger or some special log (we will focus on the configuration later). In .NET there are two ways of using assertions: Debug.Assert or Trace.Assert. The definition of the first method looks as follows:

[System.Diagnostics.Conditional("DEBUG")]  
public static void Assert(bool condition, string message) {
    TraceInternal.Assert(condition, message);
}

As you can see it will appear in the resulting IL only if we define the DEBUG compilation symbol, which we usually do solely for debug builds. Trace.Assert, on the other hand, uses the TRACE compilation symbol and by default will not be stripped by the compiler in the release builds. I prefer using the Debug.Assert method and have no assertions in the binaries pushed to production.

Let’s have a look at some scenarios in which we may use assertions. I will use snippets of code from the CoreCLR repository.

Validating internal method arguments

Assertions are an excellent way to perform validation of internal/private method arguments. We should place them at the beginning of the method, so any person planning to use our method will immediately see what are its expectations, for example:

private static char GetHexValue(int i)
{
    Debug.Assert(i >= 0 && i < 16, "i is out of range.");
    if (i < 10) {
        return (char)(i + '0');
    }
    return (char)(i - 10 + 'A');
}

or

internal static int MakeHRFromErrorCode(int errorCode)
{
    Debug.Assert((0xFFFF0000 & errorCode) == 0, "This is an HRESULT, not an error code!");
    return unchecked(((int)0x80070000) | errorCode);
}

Often true-false expressions are just enough, but for more complicated scenarios we might consider using the Debug.Assert(bool condition, string message) variant (as in the samples above), where we can explain our requirement.

We must not use assertions to validate public API method parameters. Firstly, the assertions will disappear in the release build. Secondly, our API clients expect exceptions of some specific types. If you still want to use assertions in public API methods, you should use both exceptions and assertions to validate arguments, for example:

public User FindUser(string login) {
    if (string.IsNullOrEmpty(login)) {
        Debug.Assert(false, "Login must not be null or empty"); 
        // or equivalent: Debug.Fail("Login must not be null or empty");
        throw new ArgumentException("Login must not be null or empty.");
    }
}

Validating execution context

To see an example of logic validation using assertions we will analyze the Length property and the AssertInvariants method of the StringBuilder class. Notice, how assertions (highlighted) validate the context at various stages of the method execution. They reflect assumptions of a developer who wrote the code and at the same time help us better understand the logic of the code:

/// <summary>
/// Gets or sets the length of this builder.
/// </summary>
public int Length
{
    get
    {
        return m_ChunkOffset + m_ChunkLength;
    }
    set
    {
        //If the new length is less than 0 or greater than our Maximum capacity, bail.
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NegativeLength);
        }
        if (value > MaxCapacity)
        {
            throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_SmallCapacity);
        }
        int originalCapacity = Capacity;
        if (value == 0 && m_ChunkPrevious == null)
        {
            m_ChunkLength = 0;
            m_ChunkOffset = 0;
            Debug.Assert(Capacity >= originalCapacity);
            return;
        }
        int delta = value - Length;
        if (delta > 0)
        {
            // Pad ourselves with null characters.
            Append('\0', delta);
        }
        else
        {
            StringBuilder chunk = FindChunkForIndex(value);
            if (chunk != this)
            {
                // We crossed a chunk boundary when reducing the Length. We must replace this middle-chunk with a new larger chunk,
                // to ensure the original capacity is preserved.
                int newLen = originalCapacity - chunk.m_ChunkOffset;
                char[] newArray = new char[newLen];
                Debug.Assert(newLen > chunk.m_ChunkChars.Length, "The new chunk should be larger than the one it is replacing.");
                Array.Copy(chunk.m_ChunkChars, 0, newArray, 0, chunk.m_ChunkLength);
                m_ChunkChars = newArray;
                m_ChunkPrevious = chunk.m_ChunkPrevious;
                m_ChunkOffset = chunk.m_ChunkOffset;
            }
            m_ChunkLength = value - chunk.m_ChunkOffset;
            AssertInvariants();
        }
        Debug.Assert(Capacity >= originalCapacity);
    }
}

[System.Diagnostics.Conditional("DEBUG")]
private void AssertInvariants()
{
    Debug.Assert(m_ChunkOffset + m_ChunkChars.Length >= m_ChunkOffset, "The length of the string is greater than int.MaxValue.");

    StringBuilder currentBlock = this;
    int maxCapacity = this.m_MaxCapacity;
    for (;;)
    {
        // All blocks have the same max capacity.
        Debug.Assert(currentBlock.m_MaxCapacity == maxCapacity);
        Debug.Assert(currentBlock.m_ChunkChars != null);

        Debug.Assert(currentBlock.m_ChunkLength <= currentBlock.m_ChunkChars.Length);
        Debug.Assert(currentBlock.m_ChunkLength >= 0);
        Debug.Assert(currentBlock.m_ChunkOffset >= 0);

        StringBuilder prevBlock = currentBlock.m_ChunkPrevious;
        if (prevBlock == null)
        {
            Debug.Assert(currentBlock.m_ChunkOffset == 0);
            break;
        }
        // There are no gaps in the blocks.
        Debug.Assert(currentBlock.m_ChunkOffset == prevBlock.m_ChunkOffset + prevBlock.m_ChunkLength);
        currentBlock = prevBlock;
    }
}

There are a lot of other places in .NET Core source where you can find assertions in use. It could be an interesting exercise to look for them and learn how .NET developers use them, especially when you have doubts about using assertions in your code.

Assert implementation details

We have covered some sample scenarios where we can use assertions so it is time to go a little bit further and learn how .NET and Windows support assertions. In a default configuration when assertion fails you will see this nice dialog:

assert-msg

It is the DefaultTraceListener‘s Fail, or more exactly AssertWrapper’s ShowMessageBoxAssert method that displays this MessageBox. The title of the window describes the options you have. If you press Retry, the application will call the Debugger.Break method, which emits a software interrupt (int 0x3) transferring the execution to the KiBreakpointTrap method in the kernel and later KiExceptionDispatch. The latter is a method which also handles a “normal” exception dispatching and is a part of the Structured Exception Handling (SEH) mechanism provided by Windows. So you may think of an assert failure as a special type of an unhandled exception. Starting from Vista there is another software interrupt (int 0x2c) created especially for assertions, but I haven’t found a way to call it from .NET without using pinvoking. There is no much difference though in a way how the system handles them. So when you hit Retry, Windows will check if there is any default debugger configured in the AeDebug key in the registry. And if there is, it will start and attach to your application, stopping in a place where the assert failure occurred. If there is no debugger in the AeDebug key, Windows Error Reporting will take a chance at trying to resolve the problem, which will probably result in a new report sent to Microsoft 🙂

Handling assert output in .NET applications

As you may guess, the MessageBox display is not always the desired behavior for a failing assertion. For processes running in the Session 0 (e.g., Windows Services or ASP.NET web applications hosted on IIS) such a MessageBox will completely block the application without giving us an option for interaction (there is no Desktop in Session 0). Another example is automatic tests, which may also hang infinitely. To remedy these problems we have a flag in the application configuration file to disable the assert UI and redirect the logs to some file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.diagnostics>
    <assert assertuienabled="false" logfilename="C:\logs\assert.log" />
  </system.diagnostics>
</configuration>

If we do not set the logfilename attribute, the assertion messages will appear only on the debug output. Another way to disable the assertion MessageBox is to remove the DefaultTraceListener (which you should normally do for the release builds) from the listeners collection:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.diagnostics>
    <trace>
      <listeners>
        <remove name="Default" />
      </listeners>
    </trace>
  </system.diagnostics>
</configuration>

The unfortunate side-effect would be that assertions failures will not be reported. Thus, if you are going to remove the DefaultTraceListener, add your custom implementation of a Fail method. You may then log the errors your way or call Debugger.Break immediately.

When it comes to handling failed assertions, I am a big fan of creating dumps when an unhandled exception occurs in the application. I usually install procdump as my default system exception handler. You may also use Visual Studio or WinDbg. Keep in mind though that this won’t work for processes running in Session 0. As an alternative to procdump, especially on some server machines, you may consider configuring Windows Error Reporting.

One thought on “Debug.Assert Everyone!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.