Skip to content

Commit

Permalink
feat: Add type-safe generics and improve error handling in ResultObject
Browse files Browse the repository at this point in the history
- Introduced `IResult<TValue>` interface with `IsSuccess`, `IsFailure`, `Value`, and `Error` properties.
- Removed `ValueOrDefault` in favor of nullable `Value` property.
- Implemented `Cast<T>()` 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.
  • Loading branch information
ahmedkamalio committed Oct 14, 2024
1 parent 539811c commit 5c0558d
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 191 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<T>()` method for safe type casting with error preservation.
- Deprecated `ToFailureResult<T>()` in favor of the new `Cast<T>()` 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<T>()` behavior.
113 changes: 42 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>()` 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<TValue>` 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<int>(new ResultError("404", "Not Found", "The requested resource was not found."));
// or
var failureResult = Result.Failure<int>("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)
{
Expand All @@ -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<T>()` 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<object>("Test Value");
var castResult = result.Cast<string>();

### 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<int>(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<T>()`.
A result with a `null` value is treated as a failure.

```csharp
var failureResult = Result.Failure<int>("500", "Server Error", "An unexpected error occurred.");
var convertedFailure = failureResult.ToFailureResult<string>(); // Failure with string type.
var result = Result.Success<string?>(null);

if (result.IsFailure)
{
Console.WriteLine("Result is not successful.");
}
```

## API Reference

### `Result<TValue>`

Represents the result of an operation with the following properties:
### `IResult<TValue>`

- `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<TValue>`

- `ToFailureResult<T>()`: Converts a failure result to a result with a different value type while preserving error
details.
- **Methods**:
- `Cast<T>()`: Safely cast the result's value to a different type, or propagate the error if the cast fails.

### `Result`

Helper class to create `Result<TValue>` instances:

- `Success<TValue>(TValue value)`: Creates a success result with a value.
- `Failure<TValue>(ResultError error)`: Creates a failure result with error details.
- `Failure<TValue>(string code, string reason, string message)`: Shorthand to create a failure result with error
details.
- **Static Methods**:
- `Success<TValue>(TValue value)`: Creates a success result.
- `Failure<TValue>(ResultError error)`: Creates a failure result with error details.
- `Failure<TValue>(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

Expand Down
1 change: 1 addition & 0 deletions ResultObject.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions src/ResultObject/IResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Diagnostics.CodeAnalysis;

namespace ResultObject;

/// <summary>
/// Represents a result of an operation, providing information about success or failure.
/// </summary>
public interface IResult<out TValue>
{
/// <summary>
/// Gets a value indicating whether the operation was successful.
/// </summary>
[MemberNotNullWhen(true, nameof(Value))]
[MemberNotNullWhen(false, nameof(Error))]
bool IsSuccess { get; }

/// <summary>
/// Gets a value indicating whether the operation failed.
/// </summary>
[MemberNotNullWhen(true, nameof(Error))]
[MemberNotNullWhen(false, nameof(Value))]
bool IsFailure { get; }

/// <summary>
/// Gets the value returned by the operation if it was successful.
/// </summary>
TValue? Value { get; }

/// <summary>
/// Gets the error information associated with the failure, if any.
/// </summary>
ResultError? Error { get; }

/// <summary>
/// Casts the value of this result to a new type if the result is successful.
/// </summary>
/// <typeparam name="T">The new type to cast the value to.</typeparam>
/// <returns>
/// A new <see cref="Result{T}"/> with the value cast to the specified type, or the same error if the result was a failure.
/// </returns>
/// <exception cref="InvalidCastException">Thrown if the value cannot be cast to the specified type.</exception>
IResult<T> Cast<T>();
}
99 changes: 42 additions & 57 deletions src/ResultObject/Result.cs
Original file line number Diff line number Diff line change
@@ -1,82 +1,67 @@
using System.Diagnostics.CodeAnalysis;

namespace ResultObject;

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TValue">The type of the value associated with a successful result.</typeparam>
public class Result<TValue>(TValue? value, ResultError? error)
/// <typeparam name="TValue">The type of the value contained in the result when it is successful.</typeparam>
public class Result<TValue>(TValue? value, ResultError? error) : IResult<TValue>
{
/// <summary>
/// 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.
/// </summary>
public bool IsSuccess { get; } = error == null;
/// <remarks>
/// This property is <c>true</c> if the <see cref="Value"/> is not <c>null</c> and there is no error.
/// </remarks>
[MemberNotNullWhen(true, nameof(Value))]
[MemberNotNullWhen(false, nameof(Error))]
public bool IsSuccess { get; } = error == null && value != null;

/// <summary>
/// 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.
/// </summary>
public bool IsFailure { get; } = error != null;
/// <remarks>
/// This property is <c>true</c> if the <see cref="Error"/> is not <c>null</c>.
/// </remarks>
[MemberNotNullWhen(true, nameof(Error))]
[MemberNotNullWhen(false, nameof(Value))]
public bool IsFailure { get; } = error != null || value == null;

/// <summary>
/// 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, <c>null</c>.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when trying to access the value of a failed result, or when the result is successful but the value is null.
/// </exception>
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;

/// <summary>
/// Gets the value if the result is successful, or the default value for the type <typeparamref name="TValue"/> if the result is a failure.
/// Gets the error associated with the result if it is a failure; otherwise, <c>null</c>.
/// </summary>
public TValue? ValueOrDefault => value;
public ResultError? Error => error;

/// <summary>
/// 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.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when trying to access the error of a successful result.
/// </exception>
public ResultError Error
/// <typeparam name="T">The new type to cast the value to.</typeparam>
/// <returns>
/// A new <see cref="Result{T}"/> with the value cast to the specified type, or the same error if the result was a failure.
/// </returns>
/// <exception cref="InvalidCastException">Thrown if the value cannot be cast to the specified type.</exception>
public IResult<T> Cast<T>()
{
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<T>(default, Error);
}
}

/// <summary>
/// Gets the error if the result is a failure, or null if the result is successful.
/// </summary>
public ResultError? ErrorOrDefault => error;
if (value is T castValue)
{
// Successfully cast the value to the target type T.
return new Result<T>(castValue, null);
}

/// <summary>
/// Creates a new <c>Result</c> 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.
/// </summary>
/// <typeparam name="T">The new type for the value in the returned <c>Result</c>.</typeparam>
/// <returns>A new <c>Result</c> instance representing the failure, with the value type changed to <typeparamref name="T"/>.</returns>
public Result<T> ToFailureResult<T>() => 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}.");
}
}
2 changes: 1 addition & 1 deletion src/ResultObject/ResultObject.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<!-- NuGet Package Metadata -->
<PackageId>ResultObject</PackageId>
<Version>1.0.2</Version>
<Version>1.0.3</Version>
<Authors>Ahmed Kamal</Authors>
<Description>A simple result object for DotNet.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Loading

0 comments on commit 5c0558d

Please sign in to comment.