Skip to content

Instantly share code, notes, and snippets.

@amantinband
Created March 24, 2022 10:48
Show Gist options
  • Save amantinband/f86084c72a414dbafa784a2b66033e6e to your computer and use it in GitHub Desktop.
Save amantinband/f86084c72a414dbafa784a2b66033e6e to your computer and use it in GitHub Desktop.
Struct vs class validatable
using OneOf;
namespace Throw;
/// <summary>
/// The exception customizations.
/// Contains a discriminated union of all possible exception customization options.
/// </summary>
public struct ExceptionCustomizations
{
/// <summary>
/// A discriminated union of all possible exception customization options.
/// </summary>
public OneOf<string, Type, Func<Exception>, Func<string, Exception>> Customization { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionCustomizations"/> class.
/// </summary>
public ExceptionCustomizations(OneOf<string, Type, Func<Exception>, Func<string, Exception>> customization)
{
this.Customization = customization;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionCustomizations"/> class.
/// The customization will be the given <paramref name="message"/>.
/// </summary>
public static implicit operator ExceptionCustomizations(string message) => new(message);
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionCustomizations"/> class.
/// The customization will be an exception of the given <paramref name="type"/>.
/// </summary>
public static implicit operator ExceptionCustomizations(Type type) => new(type);
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionCustomizations"/> class.
/// The customization will be the given exception returning <paramref name="func"/>.
/// </summary>
public static implicit operator ExceptionCustomizations(Func<Exception> func) => new(func);
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionCustomizations"/> class.
/// The customization will be the given exception returning <paramref name="func"/>.
/// </summary>
public static implicit operator ExceptionCustomizations(Func<string, Exception> func) => new(func);
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionCustomizations"/> class.
/// The customization will match the given <paramref name="customizations"/>.
/// </summary>
public static implicit operator ExceptionCustomizations(OneOf<string, Type, Func<Exception>, Func<string, Exception>> customizations) => new(customizations);
}
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Throw;
/// <summary>
/// Exception throwing extensions.
/// </summary>
public static class ExceptionThrower
{
/// <summary>
/// Throws an <see cref="ArgumentNullException"/>, unless the <paramref name="exceptionCustomizations"/> defines a custom exception.
/// </summary>
[DoesNotReturn, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowNull(
string paramName,
ExceptionCustomizations? exceptionCustomizations = null,
string? generalMessage = "Value cannot be null.")
{
if (exceptionCustomizations is null)
{
throw new ArgumentNullException(paramName: paramName, message: generalMessage);
}
throw exceptionCustomizations.Value.Customization.Match(
message => new ArgumentNullException(paramName: paramName, message: message ?? generalMessage),
type => (Exception)Activator.CreateInstance(type)!,
func => func(),
func => func(paramName));
}
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/>, unless the <paramref name="exceptionCustomizations"/> defines a custom exception.
/// </summary>
[DoesNotReturn, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowOutOfRange<TValue>(
string paramName,
TValue actualValue,
ExceptionCustomizations? exceptionCustomizations = null,
string? generalMessage = "Specified argument was out of the range of valid values.")
{
if (exceptionCustomizations is null)
{
throw new ArgumentOutOfRangeException(paramName: paramName, actualValue, message: generalMessage);
}
throw exceptionCustomizations.Value.Customization.Match(
message => new ArgumentOutOfRangeException(paramName: paramName, actualValue, message: message ?? generalMessage),
type => (Exception)Activator.CreateInstance(type)!,
func => func(),
func => func(paramName));
}
/// <summary>
/// Throws an <see cref="ArgumentException"/>, unless the <paramref name="exceptionCustomizations"/> defines a custom exception.
/// </summary>
[DoesNotReturn, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Throw(
string paramName,
ExceptionCustomizations? exceptionCustomizations = null,
string? generalMessage = null)
{
if (exceptionCustomizations is null)
{
throw new ArgumentException(message: generalMessage, paramName: paramName);
}
throw exceptionCustomizations.Value.Customization.Match(
message => new ArgumentException(message: message ?? generalMessage, paramName: paramName),
type => (Exception)Activator.CreateInstance(type)!,
func => func(),
func => func(paramName));
}
}
// dotnet add package BenchmarkDotNet
// dotnet add package OneOf
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Throw;
BenchmarkRunner.Run<Benchy>();
public readonly record struct ValidatableStruct<TValue>(TValue Value, string ParamName, ExceptionCustomizations? ExceptionCustomizations = null);
public record ValidatableRecord<TValue>(TValue Value, string ParamName, ExceptionCustomizations? ExceptionCustomizations = null) : IValidatable<TValue>;
public interface IValidatable<out TValue>
{
TValue Value { get; }
string ParamName { get; }
ExceptionCustomizations? ExceptionCustomizations { get; }
}
[MemoryDiagnoser]
public class Benchy
{
[Benchmark(Baseline = true)]
public void ThrowStruct()
{
"Hello".ThrowStruct().IfLongerThan(10).IfShorterThan(2).IfLengthEquals(15).IfLengthNotEquals(5);
}
[Benchmark]
public void ThrowRecord()
{
"Hello".ThrowRecord().IfLongerThan(10).IfShorterThan(2).IfLengthEquals(15).IfLengthNotEquals(5);
}
}
public static class Extensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ValidatableStruct<TValue> ThrowStruct<TValue>(
[DisallowNull, NotNull] this TValue value,
ExceptionCustomizations? exceptionCustomizations = null,
[CallerArgumentExpression("value")] string? paramName = null)
where TValue : notnull
{
return new ValidatableStruct<TValue>(value, paramName!, exceptionCustomizations);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IValidatable<TValue> ThrowRecord<TValue>(
[DisallowNull, NotNull] this TValue value,
ExceptionCustomizations? exceptionCustomizations = null,
[CallerArgumentExpression("value")] string? paramName = null)
where TValue : notnull
{
return new ValidatableRecord<TValue>(value, paramName!, exceptionCustomizations);
}
}
public static partial class ValidatableExtensions
{
/// <summary>
/// Throws an exception if the string is longer than <paramref name="length"/> characters.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref readonly ValidatableStruct<string> IfLongerThan(this in ValidatableStruct<string> validatable, int length)
{
Validator.ThrowIfLongerThan(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return ref validatable;
}
/// <summary>
/// Throws an exception if the string is shortter than <paramref name="length"/> characters.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref readonly ValidatableStruct<string> IfShorterThan(this in ValidatableStruct<string> validatable, int length)
{
Validator.ThrowIfShorterThan(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return ref validatable;
}
/// <summary>
/// Throws an exception if the string length is equal to <paramref name="length"/>.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref readonly ValidatableStruct<string> IfLengthEquals(this in ValidatableStruct<string> validatable, int length)
{
Validator.ThrowIfLengthEquals(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return ref validatable;
}
/// <summary>
/// Throws an exception if the string length is not equal to <paramref name="length"/>.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref readonly ValidatableStruct<string> IfLengthNotEquals(
this in ValidatableStruct<string> validatable,
int length)
{
Validator.ThrowIfLengthNotEquals(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return ref validatable;
}
}
public static partial class ValidatableExtensions
{
/// <summary>
/// Throws an exception if the string is longer than <paramref name="length"/> characters.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IValidatable<string> IfLongerThan(this IValidatable<string> validatable, int length)
{
Validator.ThrowIfLongerThan(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return validatable;
}
/// <summary>
/// Throws an exception if the string is shortter than <paramref name="length"/> characters.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IValidatable<string> IfShorterThan(this IValidatable<string> validatable, int length)
{
Validator.ThrowIfShorterThan(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return validatable;
}
/// <summary>
/// Throws an exception if the string length is equal to <paramref name="length"/>.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IValidatable<string> IfLengthEquals(this IValidatable<string> validatable, int length)
{
Validator.ThrowIfLengthEquals(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return validatable;
}
/// <summary>
/// Throws an exception if the string length is not equal to <paramref name="length"/>.
/// </summary>
/// <remarks>
/// The default exception thrown is an <see cref="ArgumentException"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IValidatable<string> IfLengthNotEquals(
this IValidatable<string> validatable,
int length)
{
Validator.ThrowIfLengthNotEquals(
validatable.Value,
validatable.ParamName,
validatable.ExceptionCustomizations,
length);
return validatable;
}
}
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
namespace Throw;
internal static partial class Validator
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfLongerThan(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
int length)
{
if (value.Length > length)
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not be longer than {length} characters.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfShorterThan(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
int length)
{
if (value.Length < length)
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not be shorter than {length} characters.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfEmpty(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations)
{
if (value.Length == 0)
{
ExceptionThrower.Throw(paramName, exceptionCustomizations, "String should not be empty.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNullOrEmpty(
string? value,
string paramName,
ExceptionCustomizations? exceptionCustomizations)
{
if (string.IsNullOrEmpty(value))
{
ExceptionThrower.Throw(paramName, exceptionCustomizations, "String should not be null or empty.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNullOrWhiteSpace(
string? value,
string paramName,
ExceptionCustomizations? exceptionCustomizations)
{
if (string.IsNullOrWhiteSpace(value))
{
ExceptionThrower.Throw(paramName, exceptionCustomizations, "String should not be null or whitespace.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfWhiteSpace(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations)
{
if (value.All(char.IsWhiteSpace))
{
ExceptionThrower.Throw(paramName, exceptionCustomizations, "String should not be white space only.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfEquals(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string otherString,
StringComparison comparisonType)
{
if (string.Equals(value, otherString, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not be equal to '{otherString}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNotEquals(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string otherString,
StringComparison comparisonType)
{
if (!string.Equals(value, otherString, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should be equal to '{otherString}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfLengthEquals(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
int length)
{
if (value.Length == length)
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String length should not be equal to {length}.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfLengthNotEquals(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
int length)
{
if (value.Length != length)
{
ExceptionThrower.Throw(paramName, exceptionCustomizations, $"String length should be equal to {length}.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfEndsWith(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string str,
StringComparison comparisonType)
{
if (value.EndsWith(str, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not end with '{str}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNotEndsWith(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string str,
StringComparison comparisonType)
{
if (!value.EndsWith(str, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should end with '{str}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfStartsWith(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string str,
StringComparison comparisonType)
{
if (value.StartsWith(str, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not start with '{str}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNotStartsWith(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string str,
StringComparison comparisonType)
{
if (!value.StartsWith(str, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should start with '{str}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfContains(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string otherString,
StringComparison comparisonType)
{
if (value.Contains(otherString, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not contain '{otherString}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNotContains(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string otherString,
StringComparison comparisonType)
{
if (!value.Contains(otherString, comparisonType))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should contain '{otherString}' (comparison type: '{comparisonType}').");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfMatches(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string regexPattern,
RegexOptions regexOptions)
{
var regex = new Regex(regexPattern, regexOptions);
if (regex.IsMatch(value))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not match RegEx pattern '{regexPattern}'");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfMatches(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
Regex regex)
{
if (regex.IsMatch(value))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should not match RegEx pattern '{regex}'");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNotMatches(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
string regexPattern,
RegexOptions regexOptions)
{
var regex = new Regex(regexPattern, regexOptions);
if (!regex.IsMatch(value))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should match RegEx pattern '{regexPattern}'");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowIfNotMatches(
string value,
string paramName,
ExceptionCustomizations? exceptionCustomizations,
Regex regex)
{
if (!regex.IsMatch(value))
{
ExceptionThrower.Throw(
paramName,
exceptionCustomizations,
$"String should match RegEx pattern '{regex}'");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment