unosquare / embedio

A tiny, cross-platform, module based web server for .NET
http://unosquare.github.io/embedio
Other
1.45k stars 175 forks source link

New utility classes for argument validation #551

Open rdeago opened 2 years ago

rdeago commented 2 years ago

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.

michael-hawker commented 2 years ago

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.