3
\$\begingroup\$

I really like ArgumentNullException.ThrowIf(...) and ArgumentException.ThrowIfNullOrEmpty(...) and there are upcoming ones in .NET 8, so until then I created a ThrowHelper on my own. Could you please review it and tell me your opinion? Can something be improved?

enter image description here

public static class Assertions
{
    public static void ThrowIfNull<T>([NotNull] T? value, string? paramName = null, string message = "Parameter cannot be null.") where T : class
    {
        if (value == null)
        {
            ThrowArgumentNullException(paramName, message);
        }
    }

    public static void ThrowIfNullOrEmpty(string value, string? paramName = null, string message = "String cannot be empty.")
    {
        if (string.IsNullOrEmpty(value))
        {
            ThrowArgumentException(paramName, message);
        }
    }

    public static void ThrowIfNullOrEmpty<T>([NotNull] ICollection<T>? collection, string? paramName = null, string message = "Collection must have elements.")
    {
        if (collection == null || !collection.Any())
        {
            ThrowArgumentException(paramName, message);
        }
    }

    public static void ThrowIfNullOrEmpty<T>([NotNull] T[]? array, string? paramName = null, string message = "Array must have elements.")
    {
        if (array == null || array.Length == 0)
        {
            ThrowArgumentException(paramName, message);
        }
    }

    public static void ThrowIfNullOrWhiteSpace(string value, string? paramName = null, string message = "String cannot be empty or whitespace.")
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            ThrowArgumentException(paramName, message);
        }
    }

    public static void ThrowIfOutOfRange<T>(T value, T minValue, T maxValue, string? paramName = null, string message = "Value must be within the specified range.") where T : IComparable<T>
    {
        if (value.CompareTo(minValue) < 0 || value.CompareTo(maxValue) > 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfLessThan<T>(T value, T threshold, string? paramName = null, string message = "Value must be greater than or equal to the threshold.") where T : IComparable<T>
    {
        if (value.CompareTo(threshold) < 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfLessThanOrEqualTo<T>(T value, T threshold, string? paramName = null, string message = "Value must be greater than the threshold.") where T : IComparable<T>
    {
        if (value.CompareTo(threshold) <= 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfGreaterThan<T>(T value, T threshold, string? paramName = null, string message = "Value must be less than the threshold.") where T : IComparable<T>
    {
        if (value.CompareTo(threshold) > 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfGreaterThanOrEqualTo<T>(T value, T threshold, string? paramName = null, string message = "Value must be less than or equal to the threshold.") where T : IComparable<T>
    {
        if (value.CompareTo(threshold) >= 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfEqual<T>(T value, T target, string? paramName = null, string message = "Parameter cannot be equal to its default value.")
    {
        if (EqualityComparer<T>.Default.Equals(value, target))
        {
            ThrowArgumentException(paramName, message);
        }
    }

    [DoesNotReturn]
    private static void ThrowArgumentException(string? paramName, string message) => throw new ArgumentException(message, paramName);

    [DoesNotReturn]
    private static void ThrowArgumentOutOfRangeException(string? paramName, string message) => throw new ArgumentOutOfRangeException(paramName, message);

    [DoesNotReturn]
    private static void ThrowArgumentNullException(string? paramName, string message) => throw new ArgumentNullException(paramName, message);
}

Modifications based on slepic's comment

using System.Diagnostics.CodeAnalysis;

namespace GridBot.Api.Extensions;

public static class Assertions
{
    public static void ThrowIfNull<T>([NotNull] T? argument, string? paramName = null, string message = "Parameter cannot be null.") where T : class
    {
        if (argument == null)
        {
            ThrowArgumentNullException(paramName, message);
        }
    }

    public static void ThrowIfNullOrEmpty(string argument, string? paramName = null, string message = "String cannot be empty.")
    {
        if (string.IsNullOrEmpty(argument))
        {
            ThrowNullOrEmptyException(argument, paramName, message);
        }
    }

    public static void ThrowIfNullOrEmpty<T>([NotNull] ICollection<T>? argument, string? paramName = null, string message = "Collection must have elements.")
    {
        if (argument == null || !argument.Any())
        {
            ThrowArgumentException(paramName, message);
        }
    }

    public static void ThrowIfNullOrEmpty<T>([NotNull] T[]? argument, string? paramName = null, string message = "Array must have elements.")
    {
        if (argument == null || argument.Length == 0)
        {
            ThrowArgumentException(paramName, message);
        }
    }

    public static void ThrowIfNullOrWhiteSpace(string argument, string? paramName = null, string message = "String cannot be empty or whitespace.")
    {
        if (string.IsNullOrWhiteSpace(argument))
        {
            ThrowArgumentException(paramName, message);
        }
    }

    public static void ThrowIfOutOfRange<T>(T argument, T minValue, T maxValue, string? paramName = null, string message = "Value must be within the specified range.") where T : IComparable<T>
    {
        if (argument.CompareTo(minValue) < 0 || argument.CompareTo(maxValue) > 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfLessThan<T>(T argument, T threshold, string? paramName = null, string message = "Value must be greater than or equal to the threshold.") where T : IComparable<T>
    {
        if (argument.CompareTo(threshold) < 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfLessThanOrEqualTo<T>(T argument, T threshold, string? paramName = null, string message = "Value must be greater than the threshold.") where T : IComparable<T>
    {
        if (argument.CompareTo(threshold) <= 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfGreaterThan<T>(T argument, T threshold, string? paramName = null, string message = "Value must be less than the threshold.") where T : IComparable<T>
    {
        if (argument.CompareTo(threshold) > 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfGreaterThanOrEqualTo<T>(T argument, T threshold, string? paramName = null, string message = "Value must be less than or equal to the threshold.") where T : IComparable<T>
    {
        if (argument.CompareTo(threshold) >= 0)
        {
            ThrowArgumentOutOfRangeException(paramName, message);
        }
    }

    public static void ThrowIfEqual<T>(T argument, T target, string? paramName = null, string message = "Parameter cannot be equal to its default value.")
    {
        if (EqualityComparer<T>.Default.Equals(argument, target))
        {
            ThrowArgumentException(paramName, message);
        }
    }

    [DoesNotReturn]
    private static void ThrowNullOrEmptyException([NotNull] string? argument, string? paramName, string message)
    {
        ThrowIfNull(argument, paramName, message);
        throw new ArgumentException(message, paramName);
    }

    [DoesNotReturn]
    private static void ThrowArgumentException(string? paramName, string message) => throw new ArgumentException(message, paramName);

    [DoesNotReturn]
    private static void ThrowArgumentOutOfRangeException(string? paramName, string message) => throw new ArgumentOutOfRangeException(paramName, message);

    [DoesNotReturn]
    private static void ThrowArgumentNullException(string? paramName, string message) => throw new ArgumentNullException(paramName, message);
}
\$\endgroup\$
5
  • 3
    \$\begingroup\$ Shouldn't ThrowIfNullOrEmpty throw ArgumentNullException if null and ArgumentException if empty? Yours seem to only throw ArgumentException in both cases. \$\endgroup\$
    – slepic
    Commented Apr 15, 2023 at 15:56
  • \$\begingroup\$ @slepic, you're right. I just inspected ArgumentException.ThrowIfNullOrEmpty and in fact they do ArgumentNullException.ThrowIfNull(argument, paramName); throw new ArgumentException(SR.Argument_EmptyString, paramName); \$\endgroup\$
    – nop
    Commented Apr 16, 2023 at 1:58
  • \$\begingroup\$ @slepic, pastebin.com/7iKrpfNB this is the edited version. However, I feel like ThrowIfNullOrWhiteSpace should be done in a similar fashion. Or why you don't just post it as an answer \$\endgroup\$
    – nop
    Commented Apr 16, 2023 at 2:07
  • \$\begingroup\$ @slepic, what about ThrowIfNullOrEmpty<T>([NotNull] ICollection<T>? argument and ThrowIfNullOrEmpty<T>([NotNull] T[]? argument? \$\endgroup\$
    – nop
    Commented Apr 16, 2023 at 2:09
  • 1
    \$\begingroup\$ Sorry I don't have any more insight and didn't feel like enough to post an answer... Anyway while nobody posted an answer yet, feel free to edit your question and include the modifications... \$\endgroup\$
    – slepic
    Commented Apr 16, 2023 at 4:19

1 Answer 1

1
\$\begingroup\$

Your code is like a good champagne, it is semi-DRY :) (Sorry for the bad joke)

The exception creations and throw command are extracted into their own methods but the guard expression is still repeated.

IMHO you should define a single extension method to the conditional throw and inline the exception creation.

static class BoolExtension
{
    public static void ThrowIf(this bool condition, Exception ex)
    {
        if (condition) throw ex;
    }
}

Or call it IfTrueThenThrow, FailWith or whatever if it makes more sense :)

public static class Assertions
{
    public static void ThrowIfNull<T>([NotNull] T? argument, string? paramName = null, string message = "Parameter cannot be null.") where T : class
        => (argument == null).ThrowIf(new ArgumentNullException(paramName, message));

    public static void ThrowIfNullOrEmpty(string argument, string? paramName = null, string message = "String cannot be empty.")
    {
        ThrowIfNull(argument, paramName, message);
        string.IsNullOrEmpty(argument).ThrowIf(new ArgumentException(message, paramName));
    }

    public static void ThrowIfNullOrEmpty<T>([NotNull] ICollection<T>? argument, string? paramName = null, string message = "Collection must have elements.")
        => (argument == null || !argument.Any()).ThrowIf(new ArgumentException(message, paramName));

    public static void ThrowIfNullOrEmpty<T>([NotNull] T[]? argument, string? paramName = null, string message = "Array must have elements.")
        => (argument == null || argument.Length == 0).ThrowIf(new ArgumentException(message, paramName));

    public static void ThrowIfNullOrWhiteSpace(string argument, string? paramName = null, string message = "String cannot be empty or whitespace.")
        => string.IsNullOrWhiteSpace(argument).ThrowIf(new ArgumentException(message, paramName));

    public static void ThrowIfOutOfRange<T>(T argument, T minValue, T maxValue, string? paramName = null, string message = "Value must be within the specified range.") where T : IComparable<T>
        => (argument.CompareTo(minValue) < 0 || argument.CompareTo(maxValue) > 0).ThrowIf(new ArgumentOutOfRangeException(paramName, message));

    public static void ThrowIfLessThan<T>(T argument, T threshold, string? paramName = null, string message = "Value must be greater than or equal to the threshold.") where T : IComparable<T>
        => (argument.CompareTo(threshold) < 0).ThrowIf(new ArgumentOutOfRangeException(paramName, message));

    public static void ThrowIfLessThanOrEqualTo<T>(T argument, T threshold, string? paramName = null, string message = "Value must be greater than the threshold.") where T : IComparable<T>
        => (argument.CompareTo(threshold) <= 0).ThrowIf(new ArgumentOutOfRangeException(paramName, message));

    public static void ThrowIfGreaterThan<T>(T argument, T threshold, string? paramName = null, string message = "Value must be less than the threshold.") where T : IComparable<T>
        => (argument.CompareTo(threshold) > 0).ThrowIf(new ArgumentOutOfRangeException(paramName, message));

    public static void ThrowIfGreaterThanOrEqualTo<T>(T argument, T threshold, string? paramName = null, string message = "Value must be less than or equal to the threshold.") where T : IComparable<T>
        => (argument.CompareTo(threshold) >= 0).ThrowIf(new ArgumentOutOfRangeException(paramName, message));

    public static void ThrowIfEqual<T>(T argument, T target, string? paramName = null, string message = "Parameter cannot be equal to its default value.")
        => EqualityComparer<T>.Default.Equals(argument, target).ThrowIf(new ArgumentException(message, paramName));
}

Also, I think these methods are good candidates to become extension methods. (Even though [NotNull] this T? argument feels weird)

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thank you very much! ^^ I feel like everything should be [NotNull] this T? argument, and also [DoesNotReturn]. I've been looking from the source code of ArgumentException.ThrowIfNullOrEmpty \$\endgroup\$
    – nop
    Commented Apr 18, 2023 at 11:28
  • 1
    \$\begingroup\$ @nop Yepp, if you want to use this as a polyfill then definitely try to follow the new version's interface as much as possible. \$\endgroup\$ Commented Apr 18, 2023 at 11:42
  • \$\begingroup\$ The error message don't include parameter name. it is nice if you can use CallerArgumentExpression attribute \$\endgroup\$
    – M.Hassan
    Commented Feb 23 at 13:50
  • \$\begingroup\$ @M.Hassan Yes, that's true. The parameter name is passed directly (alongside with the message) to the exceptions' constructor. Most of the logging framework don't just capture the Message but the entire Exception class as well. BUT of course there can be many ways to pass the parameter name. \$\endgroup\$ Commented Feb 23 at 14:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.