Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New utility classes for argument validation #551

Open
Tracked by #546
rdeago opened this issue Mar 9, 2022 · 1 comment
Open
Tracked by #546

New utility classes for argument validation #551

rdeago opened this issue Mar 9, 2022 · 1 comment
Assignees
Labels
enhancement pinned Pinned issues are not closed automatically when stale. v4.x

Comments

@rdeago
Copy link
Collaborator

rdeago commented Mar 9, 2022

Example code for argumkent validation in EmbedIO.Utilities v4.0.

#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;

using EmbedIO.Utilities.ArgumentValidation; // <-- Here's what you need

// Additional checks are in the same namespace as types they are related to
using EmbedIO.Utilities.IO; // For LocalPath
using EmbedIO.Utilities.Web; // For HttpToken, MimeType, UrlPath

namespace Example;

public static class Demonstrate
{
    // =======================================================================
    //             ARGUMENTS OF NON-NULLABLE REFERENCE TYPES
    // =======================================================================
    public static void NonNullableReferenceArguments(string str, IDisposable obj, EventHandler func)
    {
        // Ensure that an argument is not null.
        // You don't need nameof(str) - what you pass as parameter "becomes" its name in exceptions.
        _ = Arg.NotNull(str); // From here on, s1 is certainly not null (and Code Analysis knows it too)

        // The result is a "ref struct" that is implicitly convertible to the type of the argument,
        // so you can use it in an expression, assign it, etc.
        // Not useful in this case as it's the same as "str = str"...
        str = Arg.NotNull(str);

        // Also not useful here, because you are using the helper struct itself, not the value of the argument.
        var x = Arg.NotNull(str); // The type of x is NOT string!

        // ...but useful here.
        // This is not magic: an implicit conversion operator does the work.
        Console.WriteLine(Arg.NotNull(str));

        // Implicit conversion does not work in all contexts:
        // for instance, if the type of the argument is an interface or a delegate.
        // You can achieve the same result with the Value property.
        Arg.NotNull(obj).Dispose(); // Error
        Arg.NotNull(obj).Value.Dispose(); // OK
        someEvent += Arg.NotNull(func); // Error
        someEvent += Arg.NotNull(func).Value; // OK

        // Note that 
        // Shortcut methods for string arguments.
        // Obviously you normally don't use Arg twice on the same argument - this is just for demostration purposes.
        _ = Arg.NotNullOrEmpty(str);
        _ = Arg.NotNullOrWhiteSpace(str);

        // Further checks can be performed.
        // For example, let's ensure str is a valid local file system path.
        _ = Arg.NotNull(str).LocalPath();

        // You can optionally get the full path.
        // Note that, unlike the old Validate.LocalPath, we never modify the passed argument value;
        // "derived" values, such as fullPath here, are out parameters.
        _ = Arg.NotNull(str).LocalPath(out var fullPath);
        System.Console.WriteLine(fullPath);

        // Perform a custom check using a lambda.
        _ = Arg.NotNull(str).Check(s => s.Length > 4, "Argument must be longer than 4 characters.");

        // Custom check with custom message(s)
        // Your lambda takes a value and an out reference to the message; returns true for success, false for failure.
        _ = Arg.NotNull(str).Check((s, [MaybeNullWhen(true)] out m) =>
        {
            if (s.Length < 4)
            {
                m = "Argument must be at least 4 character long.");
                return false;
            }

            if (s.Length > 8)
            {
                m = "Argument must be at most 8 character long.");
                return false;
            }

            return true;
        });

        // Of course you can use a method or even a local function instead of a lambda.
        static bool HasCorrectLength(string str, [MaybeNullWhen(true)] out string message)
        {
            if (str.Length < 4)
            {
                message = "Argument must be at least 4 character long.");
                return false;
            }

            if (str.Length > 8)
            {
                message = "Argument must be at most 8 character long.");
                return false;
            }

            return true;
        }

        // Use the above local function
        _ = Arg.NotNull(str).Check(HasCorrectLength);

        // Of course you may chain as many checks as necessary.
        _ = Arg.NotNull(str).Check(HasCorrectLength).LocalPath();
    }

    // =======================================================================
    //               ARGUMENTS OF NULLABLE REFERENCE TYPES
    // =======================================================================
    public static void NullableReferenceArguments(string? str)
    {
        // Correct, although it does nothing useful by itself
        _ = Arg.Nullable(str);

        // You can obviously chain further checks after Nullable.
        _ = Arg.Nullable(str).NotEmpty(); // Null is valid; empty string causes ArgumentException

        // You can use the same checks you use on non-nullable types,
        // while always considering null a valid value.
        _ = Arg.Nullable(str).CheckUnlessNull(s => s.Length > 4, "Argument should be null or longer than 4 characters.");

        // Need to test for a condition that includes null as a valid value?
        // Just use a lambda (or method, or local function) that takes a bool and a nullable value
        // and returns true if the argument value is valid, false otherwise.
        // The first parameter passed to the lambda is true if the argument's value is not null.
        _ = Arg.Nullable(str).Check(
            (hasValue, s) => hasValue || DateTime.Now.DayOfWeek != DayOfWeek.Monday,
            "Argument should be non-null on a Monday"); // Don't you hate Mondays too?

        // Of course you also have the option of returning mthe exception message from your check method.
        _ = Arg.Nullable(str).Check((hasValue, s, [MaybeNullWhen(true)] out message) => 
        {
            message = hasValue || DateTime.Now.DayOfWeek != DayOfWeek.Monday
                ? "Argument should be non-null on a Monday"
                : null;

            return message is not null;
        });
    }

    // -----------------------------------------------------------------------
    //                        Custom check method
    // -----------------------------------------------------------------------
    private static bool IsAcceptableToday(bool hasValue, string str, [MaybeNullWhen(true)] out string message)
    {
        // We want null on weekends, no more than 20 characters on workdays.
        message = DateTime.Now.DayOfWeek switch
        {
            DayOfWeek.Saturday or DayOfWeek.Sunday => hasValue ? "Argument should be null on weekends." : null,
            _ => !hasValue ? "Argument should not be null on workdays."
                : str.Length > 20 ? "Argument should not be longer than 20 characters."
                : null,
        }

        return message is null;
    }

    // =======================================================================
    //               ARGUMENTS OF (NON-NULLABLE) VALUE TYPES
    // =======================================================================
    public static void NonNullableValueArguments(int num)
    {
        // Correct, although it does nothing useful by itself
        _ = Arg.Value(num);

        // You can obviously chain further checks after Value.
        // Standard comparisons work with any struct implementing IComparable<itself>;
        // the exception message will contain the string representation of the threshold
        // or range bounds, so if you plan to use these checks for your own types
        // you should override ToString() to provide a meaningful representation.
        // You don't want "Argument should be greater than {MyStruct}." as an exception message.
        _ = Arg.Value(num).GreaterThan(50);
        _ = Arg.Value(num).GreaterThanOrEqualTo(2);
        _ = Arg.Value(num).LessThan(1000);
        _ = Arg.Value(num).LessThanOrEqualTo(500);
        _ = Arg.Value(num).InRange(1, 5); // Open range (both 1 and 5 are valid)
        _ = Arg.Value(num).GreaterThanZero();

        // Custom checks.
        // Needless to say, you may use methods or local functions instead of lambdas
        // if you need reusable checks.
        _ = Arg.Value(num).Check(n => (n & 1) == 0, "Argument should be an even number.");
        _ = Arg.Value(num).Check((n, [MaybeNullWhen(true)] out message) =>
        {
            message = (n % 3) == 0 ? null
                : (n % 7) == 0 ? null
                : "Argument should be divisible by 3 and/or by 7.";

            return message is not null;
        });
    }

    // =======================================================================
    //                 ARGUMENTS OF NULLABLE VALUE TYPES
    // =======================================================================
    public static void NonNullableValueArguments(int? num)
    {
        // Correct, although it does nothing useful by itself
        _ = Arg.Nullable(num);

        // You can obviously chain further checks after Nullable.
        // Just like with nullable reference types, null is always considered valid.
        _ = Arg.Nullable(num).GreaterThan(50);
        _ = Arg.Nullable(num).GreaterThanOrEqualTo(2);
        _ = Arg.Nullable(num).LessThan(1000);
        _ = Arg.Nullable(num).LessThanOrEqualTo(500);
        _ = Arg.Nullable(num).InRange(1, 5); // Open range (both 1 and 5 are valid)
        _ = Arg.Nullable(num).GreaterThanZero();

        // Custom checks.
        // Needless to say, you may use methods or local functions instead of lambdas
        // if you need reusable checks.
        _ = Arg.Value(num).CheckUnlessNull(n => (n & 1) == 0, "Argument should be null or an even number.");

        // Need to test for a condition that includes null as a valid value?
        // Meet NullablePredicate, that takes a bool and a nullable value and returns true if successful.
        // The first parameter passed to the lambda is true if the argument's value is not null.
        _ = Arg.Nullable(str).Check(
            (hasValue, s) => hasValue || DateTime.Now.DayOfWeek != DayOfWeek.Monday,
            "Argument should be non-null on a Monday"); // Don't you hate Mondays too?

        // Custom check with nullability and custom messages.
        _ = Arg.Value(num).Check((hasValue, n, [MaybeNullWhen(true)] out message) =>
        {
            message = hasValue ? null
                DateTime.Now.DayOfWeek != DayOfWeek.Monday ? null
                : "Argument should be non-null on a Monday.";

            return message is not null;
        });
    }
}

Comments, criticisms, and questions are heartily welcome.

@rdeago rdeago added enhancement v4.x pinned Pinned issues are not closed automatically when stale. labels Mar 9, 2022
@rdeago rdeago self-assigned this Mar 9, 2022
@michael-hawker
Copy link

FYI, the .NET Community Toolkit has Guard and Throw Helper APIs for a similar purpose (though not setup to chain), but are optimized for the codegen. FYI @Sergio0694 if there's questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement pinned Pinned issues are not closed automatically when stale. v4.x
Projects
None yet
Development

No branches or pull requests

2 participants