From 2b24d1ea608ebd847669e330c37469155317e0dc Mon Sep 17 00:00:00 2001 From: Andrej Dyck <43913051+andrej-dyck@users.noreply.github.com> Date: Tue, 9 Aug 2022 17:49:36 +0200 Subject: [PATCH 1/3] Use argument expression as require's expectation message --- .../PreconditionsTests.cs | 16 ++++++++++++- DotNet.Extensions.Require/Preconditions.cs | 24 +++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/DotNet.Extensions.Require.Test/PreconditionsTests.cs b/DotNet.Extensions.Require.Test/PreconditionsTests.cs index 353f51e..967ba28 100644 --- a/DotNet.Extensions.Require.Test/PreconditionsTests.cs +++ b/DotNet.Extensions.Require.Test/PreconditionsTests.cs @@ -32,6 +32,12 @@ public sealed class BasicRequire : PreconditionsTests { protected override T Require(T subject, bool condition, string expectation = "") => subject.Require(condition, expectation); + + [Test] + public void Require_UsesArgumentExpressionAsExpectation_WhenNoMessageIsGiven() => + Assert.Throws( + () => 1.Require(condition: 1 < 0) + )?.WithMessage("expected: 1 < 0"); } public sealed class RequireWithLazyMessage : PreconditionsTests @@ -83,6 +89,12 @@ public sealed class RequireWithPredicateAndConstantMessage : RequireWithPredicat { protected override T Require(T subject, Predicate requirement, string expectation = "") => subject.Require(requirement, expectation); + + [Test] + public void Require_UsesArgumentExpression_WhenNoExpectationMessageIsGiven() => + Assert.Throws( + () => 1.Require(requirement: n => n < 0) + )?.WithMessage("expected: n => n < 0"); } public sealed class RequireWithPredicateAndLazyMessage : RequireWithPredicate @@ -111,6 +123,8 @@ public void Require_CanUseTheValue_ToConstructTheExpectation() => [Test] public void Require_DoesNotBuildExpectationMessage_WhenRequirementIsSatisfied() => Assert.DoesNotThrow( - () => 1.Require(requirement: _ => true, value => throw new NUnitException($"must not be called for {value}")) + () => 1.Require( + requirement: _ => true, + value => throw new NUnitException($"must not be called for {value}")) ); } diff --git a/DotNet.Extensions.Require/Preconditions.cs b/DotNet.Extensions.Require/Preconditions.cs index 71cddf3..89de2d7 100644 --- a/DotNet.Extensions.Require/Preconditions.cs +++ b/DotNet.Extensions.Require/Preconditions.cs @@ -1,9 +1,17 @@ -namespace DotNet.Extensions.Require; +using System.Runtime.CompilerServices; + +namespace DotNet.Extensions.Require; public static class Preconditions { - public static T Require(this T @this, bool condition, string expectation) => - condition ? @this : throw new ArgumentException(expectation); + public static T Require( + this T @this, + bool condition, + string? expectation = null, + [CallerArgumentExpression("condition")] + string argExpr = "?" + ) => + condition ? @this : throw new ArgumentException(expectation ?? $"expected: {argExpr}"); public static T Require(this T @this, bool condition, Func expectation) => condition ? @this : throw new ArgumentException(expectation()); @@ -11,8 +19,14 @@ public static T Require(this T @this, bool condition, Func expectatio public static T Require(this T @this, bool condition, Func expectation) => condition ? @this : throw new ArgumentException(expectation(@this)); - public static T Require(this T @this, Predicate requirement, string expectation) => - requirement(@this) ? @this : throw new ArgumentException(expectation); + public static T Require( + this T @this, + Predicate requirement, + string? expectation = null, + [CallerArgumentExpression("requirement")] + string argExpr = "?" + ) => + requirement(@this) ? @this : throw new ArgumentException(expectation ?? $"expected: {argExpr}"); public static T Require(this T @this, Predicate requirement, Func expectation) => requirement(@this) ? @this : throw new ArgumentException(expectation()); From 290db94a23fff615d46f3a979fc4fa12b13a5b9f Mon Sep 17 00:00:00 2001 From: Andrej Dyck <43913051+andrej-dyck@users.noreply.github.com> Date: Tue, 9 Aug 2022 17:55:01 +0200 Subject: [PATCH 2/3] Use argument expression as checks's expectation message --- DotNet.Extensions.Require.Test/ChecksTests.cs | 6 ++++++ DotNet.Extensions.Require/Checks.cs | 14 +++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/DotNet.Extensions.Require.Test/ChecksTests.cs b/DotNet.Extensions.Require.Test/ChecksTests.cs index 134aaec..bf2cc2f 100644 --- a/DotNet.Extensions.Require.Test/ChecksTests.cs +++ b/DotNet.Extensions.Require.Test/ChecksTests.cs @@ -11,6 +11,12 @@ public void Check_ThrowsCheckFailed_WhenRequirementIsNotSatisfied() => () => 1.Check(v => v != 1, expectation: "message") )?.WithMessage("message"); + [Test] + public void Check_UsesArgumentExpressionAsExpectation_WhenNoMessageIsGiven() => + Assert.Throws( + () => 1.Check(v => v != 1) + )?.WithMessage("expected: v => v != 1"); + [Test] public void Check_DoesNotThrow_WhenRequirementIsSatisfied() => Assert.DoesNotThrow( diff --git a/DotNet.Extensions.Require/Checks.cs b/DotNet.Extensions.Require/Checks.cs index 6fc75ce..bc61a07 100644 --- a/DotNet.Extensions.Require/Checks.cs +++ b/DotNet.Extensions.Require/Checks.cs @@ -1,9 +1,17 @@ -namespace DotNet.Extensions.Require; +using System.Runtime.CompilerServices; + +namespace DotNet.Extensions.Require; public static class Checks { - public static T Check(this T @this, Predicate requirement, string expectation) => - requirement(@this) ? @this : throw new CheckFailed(expectation); + public static T Check( + this T @this, + Predicate requirement, + string? expectation = null, + [CallerArgumentExpression("requirement")] + string argExpr = "?" + ) => + requirement(@this) ? @this : throw new CheckFailed(expectation ?? $"expected: {argExpr}"); public static T Check(this T @this, Predicate requirement, Func expectation) => requirement(@this) ? @this : throw new CheckFailed(expectation()); From 391551c1e86d5aba5fd5154856ec372c4b4e5633 Mon Sep 17 00:00:00 2001 From: Andrej Dyck <43913051+andrej-dyck@users.noreply.github.com> Date: Tue, 9 Aug 2022 18:01:38 +0200 Subject: [PATCH 3/3] Update readme --- README.md | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c0fe28f..4400568 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![codecov](https://codecov.io/gh/andrej-dyck/dotnet-extensions-require/branch/main/graph/badge.svg?token=9IL6K5CX37)](https://codecov.io/gh/andrej-dyck/dotnet-extensions-require) [![NuGet](https://badgen.net/nuget/v/Require-Expressions)](https://www.nuget.org/packages/Require-Expressions/) -A small library that provides _pre-condition_ checks as an extension method on types. +A small library that provides _pre-condition_ checks as an extension method on types. This allows the client code to check a condition on arguments and use the value directly if no exception is thrown. ```csharp @@ -24,18 +24,23 @@ dotnet add package Require-Expressions ## Examples for `Require` Basic `Require` function + ```csharp +var requestedSeats = seats.Require(condition: seats > 0); +// throws ArgumentException with "expected: seats > 0" as message when condition is not met + var requestedSeats = seats.Require( condition: seats > 0, - expectation: "expected: seats > 0" -); // throws ArgumentException with expectation as message when condition is not met + expectation: "Expected positive number of seats" // or with custom expectation message +); ``` `Require` functions with lazily constructed expectation messages + ```csharp var requestedSeats = seats.Require( condition: seats > 0, - expectation: () => "expected: seats > 0" + expectation: () => "Expected positive number of seats" ); var requestedDate = reservationDate.Require( @@ -45,10 +50,14 @@ var requestedDate = reservationDate.Require( ``` `Require` functions with predicate for convenience + ```csharp +var requestedSeats = seats.Require(condition: s => s > 0); +// throws ArgumentException with "expected: s => s > 0" as message when condition is not met + var requestedSeats = seats.Require( condition: s => s > 0, - expectation: "expected: seats > 0" + expectation: "Expected positive number of seats" // or with custom expectation message ); var requestedDate = reservationDate.Require( @@ -60,14 +69,16 @@ var requestedDate = reservationDate.Require( ## Examples for `Check` For checks other than on arguments, the `Check` function can be used + ```csharp var reservationRequest = JsonSerializer.Deserialize(request) - .Check(requirement: r => r.Seats > 0, expectation: "expected: seats > 0") + .Check(requirement: r => r.Seats > 0) // expectation message will be "expected r => r.Seats > 0" .Check(r => r.Date > now, r => $"Reservation date {r.Date} must be in the future") // throws CheckFailed with expectation as message when requirement is not satisfied ``` With `Check`, custom exceptions can be constructed + ```csharp var reservationRequest = JsonSerializer.Deserialize(request) .Check(r => r.Seats > 0, exception: () => new SeatsMustBePositive()) @@ -78,11 +89,15 @@ var reservationRequest = JsonSerializer.Deserialize(request) **Why yet another preconditions library?** -Most libraries I found use precondition _statements_, while I found preconditions as _expressions_ more useful. For example, the latter allows for [_expression-bodied_ functions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members). +Most libraries I found use precondition _statements_, while I found preconditions as _expressions_ more useful. +For example, the latter allows for [_expression-bodied_ functions]( +https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members +). **I don't like extension methods** -No problem, there are many other great precondition libraries. Have a look at [.Net's contracts](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.contracts?view=net-6.0). +No problem, there are many other great precondition libraries. Have a look +at [.Net's contracts](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.contracts?view=net-6.0). **I don't want yet another dependency** @@ -90,16 +105,18 @@ No problem, just copy&paste the code you need into your project. Or use the buil **Can you add this function too?** -Probably not as I want to keep this library small and focused. But you are very welcome to post a PR. +Probably not as I want to keep this library small and focused. But you are very welcome to post a PR. ## Build & Test **Build project** + ```shell dotnet build ``` **Run unit tests** + ```shell dotnet test ```