New ArgumentException and ArgumentOutOfRangeException helpers in .NET 8

01/12/2022
.NET 8C#.NET

Do you remember how .NET 6 introduced the ArgumentNullException.ThrowIfNull guard? And afterward, with .NET 7 we've got this excellent bit: ArgumentException.ThrowIfNullOrEmpty? Guess what, there might come some new handy additions for the upcoming .NET 8 iteration.

So let's see what are those new changes and how they make the code simpler.


As always a small disclaimer in the beginning: We are in the very early stages of .NET 8 and plans can change. The new additions I will highlight here might or might not come. At the moment (1st of December) they are flagged as a potential candidate for .NET8. But it can happen that they will not make the cut at all. I do like to dig into the GitHub tracker of the dotnet team and spread the word.

Also as always, I'll provide the links so you can read further.

If you think these are good additions, make your voice count on the GitHub tracker with a ?? so the team knows what you're missing.


ArgumentException.ThrowIfNullOrWhiteSpace

In .NET 7 we got ArgumentException.ThrowIfNullOrEmpty, which basically throws an ArgumentNullExcepion when a string is null and an ArgumentException when a string was empty. The usage is straightforward:

public void MyFunction(string str)
{
  ArgumentException.ThrowIfNullOrEmpty(str);
}

And as we have string.IsNullOrEmpty, we also want to have a guard like string.IsNullOrWhiteSpace. I originally proposed this a while back, but it didn't seem that it is general enoughso vote for this 😄. Anyway, that topic did get some second wind again and might make it. There are two reasons in favor of that: First, it is nice to have a first-class citizen API for such cases. Often times there is no difference if a string is null, empty, or just filled with blanks. Take your GitHub username for example. That thing can not be just whitespace. The second point is that you can come around with things like str.Trim(), but that creates unnecessary allocations.

public void MyFunction(string str)
{
  // This (<.net8.0)
  if (string.IsNullOrWhiteSpace(str))
    throw new ArgumentException("String can't be null or whitespace", nameof(str));

  // Can become this
  ArgumentException.ThrowIfNullOrWhiteSpace(str);
}

Link: GitHub

ArgumentException.ThrowIfNullOrEmpty

This one is fairly simply explained with a good example. In the "old" world (well, technically the current world, but you get the idea), we have this for checking if an array or list is null or empty:

public void MyFunction(List<Customer> customers)
{
  if (customers is null || customers.Count == 0)
    throw ArgumentException(...);
}

This is a very common pattern when dealing with collections. The proposal would add the following guard:

public void MyFunction(List<Customer> customers)
{
  ArgumentException.ThrowIfNullOrEmpty(customers);

The same semantics as with ThrowIfNullOrEmpty of type string applies here. If the collection is null we get an ArgumentNullException. If the collection is empty we will get an ArgumentException. Right now (1st of December) the proposal would only allow collections, which are not IEnumerable. Why is that? Why can't we use just Any? The reason is many-fold but mainly performance-driven. Any has some overhead in comparison against things like Length (Array) or Count (List). Any can't be inlined as it's using a using block, which translates to try-finally constructs. But worse, just imagine you have a nIQueryable under the hood. You might go to the database without knowing that! That is why (for now) the usage is "constrained" to things like T[], List<T>, IReadOnlyCollection<T>, ... Anyway with those types, you cover the majority of cases.

Link: GitHub


The next part gets interesting. In the last iteration (.NET 7) the team introduced static abstract interfaces and generic math well technically as preview feature it was available in .NET6 as well. With this opportunity, we can get some more guards, which makes now sense.

ThrowIfZero, ThrowIfNegative, ThrowIfNegativeOrZero, ThrowIfGreaterThan

So basically the the generic math approach allows us to define a lot of generic guards. In general we have interfaces like INumberBase, which have properties like Zero or One. Pair this with the ability to compare elements via the IComparable interface and you can do the things described above.

Some examples. Checking if something is negative.


// This will throw an exception
AddMoney(-2m, steven);

public void AddMoney(decimal amount, User toUser)
{
  // decimal derives from INumberBase and ISignedNumber
  ArgumentOutOfRangeException.ThrowIfNegative(amount);
}

Another one, throwing when the value is zero:

public class MyNumber : INumberBase<MyNumber>, IComparable<MyNumber>
{
    // Left out for brevity
}

public void MyFunction(MyNumber number)
{
  // As MyNumber implements INumberBase as well as IComparable
  // we can use the new function
  ArgumentOutOfRangeException.ThrowIfZero(number);
}

Link: GitHub

Conclusion

There you have it—some excellent additions, which might face the light latest November 2023. Again follow the GitHub issues if you want to know more or if you want to raise your voice.

8
An error has occurred. This application may no longer respond until reloaded. Reload x