From 5c0558dd940798b18fe6deafb54bf2fa2ece8294 Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Date: Mon, 14 Oct 2024 09:21:14 +0300 Subject: [PATCH] feat: Add type-safe generics and improve error handling in ResultObject - Introduced `IResult` interface with `IsSuccess`, `IsFailure`, `Value`, and `Error` properties. - Removed `ValueOrDefault` in favor of nullable `Value` property. - Implemented `Cast()` for type-safe value conversion with error propagation. - Removed exceptions for incorrect access of `Value` or `Error`. - Updated unit tests to align with new behavior and ensure correctness. - Bumped version from 1.0.2 to 1.0.3. - Refreshed README with new examples, API references, and behavior clarifications. --- CHANGELOG.md | 15 ++++ README.md | 113 +++++++++---------------- ResultObject.sln | 1 + src/ResultObject/IResult.cs | 43 ++++++++++ src/ResultObject/Result.cs | 99 +++++++++------------- src/ResultObject/ResultObject.csproj | 2 +- test/ResultObject.Tests/ResultTests.cs | 108 ++++++++++------------- 7 files changed, 190 insertions(+), 191 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/ResultObject/IResult.cs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7370d5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## v1.0.3 + +1. `IResult`: + - Introduced type-safe generics to `IResult`. +2. `Result` Class Changes: + - Removed the old `ValueOrDefault` feature in favor of nullable `Value` and `Error`. + - Removed the enforced exception for accessing values or errors incorrectly. + - Added a new `Cast()` method for safe type casting with error preservation. + - Deprecated `ToFailureResult()` in favor of the new `Cast()` method. +3. Tests Updated: + - Improved error handling validation in tests. + - Removed exceptions where not necessary (like accessing `Value` on failure). + - Updated tests to reflect the new `Cast()` behavior. diff --git a/README.md b/README.md index bdbf933..e2dec55 100644 --- a/README.md +++ b/README.md @@ -4,53 +4,39 @@ ## Overview -The **ResultObject** package provides a utility to return errors instead of throwing exceptions, making it a generic and -easy-to-use mechanism for handling the outcome of operations. It encapsulates either a success with a value or a failure -with error information, promoting cleaner code by eliminating the need for exception-based error handling and offering a -clear distinction between success and failure scenarios. +The **ResultObject** package provides a utility to handle the outcome of operations, either as a success or failure, +without relying on exceptions. This promotes cleaner code by clearly separating success from failure scenarios and +encapsulating error information. ## Features -- **Success/Failure Handling**: Easily handle success or failure outcomes of operations. -- **Generic Result Type**: Support for results containing any type of value. -- **Error Handling**: Encapsulate detailed error information on failure. -- **Type-Safe Failures**: Convert failure results into different value types while preserving error information. -- **Default Value Handling**: Use `ValueOrDefault` to safely retrieve the value or a default when the result is a - failure. -- **Strict Value Enforcement**: Use the `Value` property to enforce non-null results, throwing an exception on failure. +- **Success/Failure Handling**: Easily manage the outcome of operations with type-safe results. +- **Generic Result Type**: Results can encapsulate any type of value. +- **Error Handling**: Detailed error information available in case of failures. +- **Type-Safe Casting**: Use `Cast()` to transform the result's value while maintaining error information. +- **Nullable Value Access**: Retrieve values or errors directly without throwing exceptions. ## Installation -You can install the **ResultObject** package via NuGet. Use the following command in your project. - -DotNet CLI: +You can install the **ResultObject** package via NuGet: ```bash dotnet add package ResultObject ``` -Alternatively, you can add it to your project using the Visual Studio package manager UI by searching for " -ResultObject." +Alternatively, use the Visual Studio package manager UI to search for "ResultObject." ## Usage ### Basic Result Example -The `Result` class represents the outcome of an operation. It can either be a success (containing a value) or a -failure (containing error details). - ```csharp var successResult = Result.Success(42); - -var failureResult = Result.Failure(new ResultError("404", "Not Found", "The requested resource was not found.")); -// or var failureResult = Result.Failure("404", "Not Found", "The requested resource was not found."); ``` ### Checking Success or Failure -You can check whether an operation was successful or failed using the `IsSuccess` and `IsFailure` properties. - ```csharp if (successResult.IsSuccess) { @@ -63,86 +49,71 @@ if (failureResult.IsFailure) } ``` -### Retrieving Values Safely +### Type Casting Results -The `Value` property gives you the result value if the operation succeeded, but throws an exception if accessed on a -failure. If you want to avoid exceptions, you can use the `ValueOrDefault` property to get the value or `null`/default. +Use `Cast()` to cast the value to a different type while preserving error details on failure. ```csharp -var value = successResult.ValueOrDefault; // Retrieves value or default if failed. -``` +var result = Result.Success("Test Value"); +var castResult = result.Cast(); -### Enforcing Non-Null Results - -The `Value` property enforces that the result must be successful and non-null. Accessing it on failure or null value -throws an `InvalidOperationException`. - -```csharp -try -{ - var result = FunctionThatReturnsResult(); - int value = result.Value; // Throws if result is failure or value is null. - Console.WriteLine("Value: " + value); -} -catch (InvalidOperationException ex) +if (castResult.IsSuccess) { - Console.WriteLine("Error: " + ex.Message); + Console.WriteLine("Successfully cast value: " + castResult.Value); } ``` -### Handling Errors +### Error Handling -The `ResultError` class encapsulates details about the failure. This includes an error code, a reason, and a message. +Create and inspect detailed errors with `ResultError`. ```csharp var error = new ResultError("500", "Server Error", "An unexpected error occurred."); var failedResult = Result.Failure(error); + +Console.WriteLine($"Error Code: {failedResult.Error.Code}"); ``` -### Converting Failure to a Different Value Type +### Handling Nullable Values -You can convert a failure result to a result of a different value type while keeping the error information using -`ToFailureResult()`. +A result with a `null` value is treated as a failure. ```csharp -var failureResult = Result.Failure("500", "Server Error", "An unexpected error occurred."); -var convertedFailure = failureResult.ToFailureResult(); // Failure with string type. +var result = Result.Success(null); + +if (result.IsFailure) +{ + Console.WriteLine("Result is not successful."); +} ``` ## API Reference -### `Result` - -Represents the result of an operation with the following properties: +### `IResult` - `IsSuccess`: Indicates if the operation succeeded. - `IsFailure`: Indicates if the operation failed. -- `Value`: The result value if the operation succeeded (or throws an exception if it failed). -- `ValueOrDefault`: The result value if the operation succeeded, or `default(TValue)` if it failed. -- `Error`: Contains error details if the operation failed (or throws an exception if it succeeded). -- `ErrorOrDefault`: The error details if the operation failed, or `null` if it succeeded. +- `Value`: The value of the operation if successful, otherwise `null`. +- `Error`: The error information if the operation failed, otherwise `null`. -#### Methods +### `Result` -- `ToFailureResult()`: Converts a failure result to a result with a different value type while preserving error - details. +- **Methods**: + - `Cast()`: Safely cast the result's value to a different type, or propagate the error if the cast fails. ### `Result` -Helper class to create `Result` instances: - -- `Success(TValue value)`: Creates a success result with a value. -- `Failure(ResultError error)`: Creates a failure result with error details. -- `Failure(string code, string reason, string message)`: Shorthand to create a failure result with error - details. +- **Static Methods**: + - `Success(TValue value)`: Creates a success result. + - `Failure(ResultError error)`: Creates a failure result with error details. + - `Failure(string code, string reason, string message)`: Creates a failure result with a detailed error. ### `ResultError` -Represents an error with the following properties: - -- `Code`: A string representing the error code. -- `Reason`: A brief reason for the error. -- `Message`: A detailed message explaining the error. +- **Properties**: + - `Code`: A code representing the type of error. + - `Reason`: A brief reason for the error. + - `Message`: A detailed message describing the error. ## License diff --git a/ResultObject.sln b/ResultObject.sln index 3ca358b..f611db4 100644 --- a/ResultObject.sln +++ b/ResultObject.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt README.md = README.md LICENSE = LICENSE .github\workflows\publish.yml = .github\workflows\publish.yml + CHANGELOG.md = CHANGELOG.md EndProjectSection EndProject Global diff --git a/src/ResultObject/IResult.cs b/src/ResultObject/IResult.cs new file mode 100644 index 0000000..c0d479e --- /dev/null +++ b/src/ResultObject/IResult.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ResultObject; + +/// +/// Represents a result of an operation, providing information about success or failure. +/// +public interface IResult +{ + /// + /// Gets a value indicating whether the operation was successful. + /// + [MemberNotNullWhen(true, nameof(Value))] + [MemberNotNullWhen(false, nameof(Error))] + bool IsSuccess { get; } + + /// + /// Gets a value indicating whether the operation failed. + /// + [MemberNotNullWhen(true, nameof(Error))] + [MemberNotNullWhen(false, nameof(Value))] + bool IsFailure { get; } + + /// + /// Gets the value returned by the operation if it was successful. + /// + TValue? Value { get; } + + /// + /// Gets the error information associated with the failure, if any. + /// + ResultError? Error { get; } + + /// + /// Casts the value of this result to a new type if the result is successful. + /// + /// The new type to cast the value to. + /// + /// A new with the value cast to the specified type, or the same error if the result was a failure. + /// + /// Thrown if the value cannot be cast to the specified type. + IResult Cast(); +} \ No newline at end of file diff --git a/src/ResultObject/Result.cs b/src/ResultObject/Result.cs index 6991993..9c6d8de 100755 --- a/src/ResultObject/Result.cs +++ b/src/ResultObject/Result.cs @@ -1,82 +1,67 @@ +using System.Diagnostics.CodeAnalysis; + namespace ResultObject; /// -/// Represents the result of an operation, which can either be a success with a value or a failure with an error. +/// Represents the result of an operation, which can either be a success or a failure. /// -/// The type of the value associated with a successful result. -public class Result(TValue? value, ResultError? error) +/// The type of the value contained in the result when it is successful. +public class Result(TValue? value, ResultError? error) : IResult { /// - /// Gets a value indicating whether the result represents a successful outcome (i.e., no error occurred). + /// Gets a value indicating whether the result represents a success. /// - public bool IsSuccess { get; } = error == null; + /// + /// This property is true if the is not null and there is no error. + /// + [MemberNotNullWhen(true, nameof(Value))] + [MemberNotNullWhen(false, nameof(Error))] + public bool IsSuccess { get; } = error == null && value != null; /// - /// Gets a value indicating whether the result represents a failure (i.e., an error occurred). + /// Gets a value indicating whether the result represents a failure. /// - public bool IsFailure { get; } = error != null; + /// + /// This property is true if the is not null. + /// + [MemberNotNullWhen(true, nameof(Error))] + [MemberNotNullWhen(false, nameof(Value))] + public bool IsFailure { get; } = error != null || value == null; /// - /// Gets the value of a successful result. - /// Throws an exception if the result represents a failure or if the value is null even though the result is marked as successful. + /// Gets the value of the result if it is a success; otherwise, null. /// - /// - /// Thrown when trying to access the value of a failed result, or when the result is successful but the value is null. - /// - public TValue Value - { - get - { - if (IsFailure) - { - throw new InvalidOperationException("Cannot retrieve value from a failed result."); - } - - if (value == null) - { - throw new InvalidOperationException("Value is null despite the operation being successful."); - } - - return value; - } - } + public TValue? Value => value; /// - /// Gets the value if the result is successful, or the default value for the type if the result is a failure. + /// Gets the error associated with the result if it is a failure; otherwise, null. /// - public TValue? ValueOrDefault => value; + public ResultError? Error => error; /// - /// Gets the error associated with a failed result. - /// Throws an exception if accessed on a successful result. + /// Casts the value of this result to a new type if the result is successful. /// - /// - /// Thrown when trying to access the error of a successful result. - /// - public ResultError Error + /// The new type to cast the value to. + /// + /// A new with the value cast to the specified type, or the same error if the result was a failure. + /// + /// Thrown if the value cannot be cast to the specified type. + public IResult Cast() { - get + if (IsFailure) { - if (IsSuccess) - { - throw new InvalidOperationException("Cannot retrieve error from a successful result."); - } - - return error!; + // If it's a failure result, return a failure with the existing error. + return new Result(default, Error); } - } - /// - /// Gets the error if the result is a failure, or null if the result is successful. - /// - public ResultError? ErrorOrDefault => error; + if (value is T castValue) + { + // Successfully cast the value to the target type T. + return new Result(castValue, null); + } - /// - /// Creates a new Result instance representing the same failure as this result, - /// but with the value type cast to a different type. - /// The new result will contain the same error, while the value will be set to the default value of the new type. - /// - /// The new type for the value in the returned Result. - /// A new Result instance representing the failure, with the value type changed to . - public Result ToFailureResult() => new(default, Error); + // The value could not be cast to the target type. + throw new InvalidCastException( + $"Cannot cast value of type {typeof(TValue).FullName} to {typeof(T).FullName}."); + } } \ No newline at end of file diff --git a/src/ResultObject/ResultObject.csproj b/src/ResultObject/ResultObject.csproj index eac5324..ff41f3a 100755 --- a/src/ResultObject/ResultObject.csproj +++ b/src/ResultObject/ResultObject.csproj @@ -7,7 +7,7 @@ ResultObject - 1.0.2 + 1.0.3 Ahmed Kamal A simple result object for DotNet. MIT diff --git a/test/ResultObject.Tests/ResultTests.cs b/test/ResultObject.Tests/ResultTests.cs index de23a4e..18b2dfa 100755 --- a/test/ResultObject.Tests/ResultTests.cs +++ b/test/ResultObject.Tests/ResultTests.cs @@ -27,39 +27,16 @@ public void SuccessResult_Value_ShouldReturnCorrectValue() } [Fact] - public void SuccessResult_ValueOrDefault_ShouldReturnCorrectValue() + public void SuccessResult_Error_ShouldReturnNull() { // Arrange var result = Result.Success("Test Value"); // Act - var valueOrDefault = result.ValueOrDefault; - - // Assert - Assert.Equal("Test Value", valueOrDefault); - } - - [Fact] - public void SuccessResult_Error_ShouldThrowInvalidOperationException() - { - // Arrange - var result = Result.Success("Test Value"); - - // Act & Assert - Assert.Throws(() => result.Error); - } - - [Fact] - public void SuccessResult_ErrorOrDefault_ShouldReturnNull() - { - // Arrange - var result = Result.Success("Test Value"); - - // Act - var errorOrDefault = result.ErrorOrDefault; + var error = result.Error; // Assert - Assert.Null(errorOrDefault); + Assert.Null(error); } [Fact] @@ -75,89 +52,94 @@ public void FailureResult_IsFailure_ShouldBeTrue() } [Fact] - public void FailureResult_Value_ShouldThrowInvalidOperationException() + public void FailureResult_Value_ShouldReturnNull() { // Arrange var error = new ResultError("404", "NotFound", "The item was not found."); var result = Result.Failure(error); - // Act & Assert - Assert.Throws(() => result.Value); + // Act + var value = result.Value; + + // Assert + Assert.Null(value); } [Fact] - public void FailureResult_ValueOrDefault_ShouldReturnDefaultValue() + public void FailureResult_Error_ShouldReturnCorrectError() { // Arrange var error = new ResultError("404", "NotFound", "The item was not found."); var result = Result.Failure(error); // Act - var valueOrDefault = result.ValueOrDefault; + if (result.IsSuccess) + { + throw new InvalidOperationException("Result expected to be failure."); + } // Assert - Assert.Null(valueOrDefault); + Assert.Equal("404", result.Error.Code); + Assert.Equal("NotFound", result.Error.Reason); + Assert.Equal("The item was not found.", result.Error.Message); } [Fact] - public void FailureResult_Error_ShouldReturnCorrectError() + public void FailureResult_WithErrorCode_ShouldReturnCorrectError() { // Arrange - var error = new ResultError("404", "NotFound", "The item was not found."); - var result = Result.Failure(error); + var result = Result.Failure("500", "InternalError", "An unexpected error occurred."); // Act - var resultError = result.Error; + if (result.IsSuccess) + { + throw new InvalidOperationException("Result expected to be failure."); + } // Assert - Assert.Equal("404", resultError.Code); - Assert.Equal("NotFound", resultError.Reason); - Assert.Equal("The item was not found.", resultError.Message); + Assert.Equal("500", result.Error.Code); + Assert.Equal("InternalError", result.Error.Reason); + Assert.Equal("An unexpected error occurred.", result.Error.Message); } [Fact] - public void FailureResult_ErrorOrDefault_ShouldReturnCorrectError() + public void CastResult_SuccessfulCast_ShouldReturnNewResult() { // Arrange - var error = new ResultError("404", "NotFound", "The item was not found."); - var result = Result.Failure(error); + var result = Result.Success("Test Value"); // Act - var errorOrDefault = result.ErrorOrDefault; + var castResult = result.Cast(); // Assert - Assert.Equal(error, errorOrDefault); + Assert.True(castResult.IsSuccess); + Assert.Equal("Test Value", castResult.Value); } [Fact] - public void FailureResult_WithErrorCode_ShouldReturnCorrectError() + public void CastResult_Failure_ShouldPreserveError() { // Arrange - var result = Result.Failure("500", "InternalError", "An unexpected error occurred."); + var error = new ResultError("404", "NotFound", "The item was not found."); + var result = Result.Failure(error); // Act - var error = result.Error; + var castResult = result.Cast(); // Assert - Assert.Equal("500", error.Code); - Assert.Equal("InternalError", error.Reason); - Assert.Equal("An unexpected error occurred.", error.Message); + Assert.True(castResult.IsFailure); + Assert.Equal(error, castResult.Error); + Assert.Null(castResult.Value); } [Fact] - public void ToFailureResult_ShouldConvertToNewFailureResult() + public void CastResult_InvalidCast_ShouldThrowException() { // Arrange - var error = new ResultError("404", "NotFound", "The item was not found."); - var result = Result.Failure(error); + var result = Result.Success(42); - // Act - var newResult = result.ToFailureResult(); - - // Assert - Assert.True(newResult.IsFailure); - Assert.Equal(error, newResult.Error); - Assert.Equal(default, newResult.ValueOrDefault); + // Act & Assert + Assert.Throws(() => result.Cast()); } [Fact] @@ -174,12 +156,14 @@ public void ResultError_ToString_ShouldReturnFormattedString() } [Fact] - public void SuccessResult_WithNullValue_ShouldThrowInvalidOperationException() + public void SuccessResult_WithNullValue_ShouldHaveIsSuccessFalse() { // Arrange var result = Result.Success(null); // Act & Assert - Assert.Throws(() => result.Value); + Assert.False(result.IsSuccess); + Assert.True(result.IsFailure); + Assert.Null(result.Value); } } \ No newline at end of file