Created
March 24, 2022 10:48
-
-
Save amantinband/f86084c72a414dbafa784a2b66033e6e to your computer and use it in GitHub Desktop.
Struct vs class validatable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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