Skip to content

Commit

Permalink
Initial iteration of v9.0. (#39)
Browse files Browse the repository at this point in the history
* Changed Result into struct.

* Added implicit operator and ToResult method.

* Reworked the IsSuccess property into a method.

* Cleaned up warnings.

* Optimized IL for properties.

* Added additional methods and adjusted interfaces.

* Adjusted default struct behavior.

* Added Bind and Match support.

* Removed properties.

* Updated version.

* Added support for BindAsync.

* Cleanup and build for .NET 9.0.

* Added .NET 9.0 SDK.

* More performance optimizations.

* Split into multiple files.

* Removed bind and match.

* Updated version.

* Cleanup.

* Removed dead code, improved tests, and changed ToResult to AsFailed.

* Slight adjustment to interfaces.

* Add `HasError<T>(out T error)` method (#31)

* Updated dependencies and version.

* New overloads and changes to underlying collections. (#33)

* Expose internals visible to. (#34)

* Expose internal properties. (#35)

* Reverted internals. (#36)

* Reworked API surface.

* Updated version.

* More test coverage.

* Merged source files.

* Improved API surface.

* Fixed tests and updated documentation.

* Updated documentation.

* Updated benchmarks.

* Initial final version.

---------

Co-authored-by: Chris Kelly <[email protected]>
  • Loading branch information
jscarle and PressXtoChris authored Dec 28, 2024
1 parent 904339b commit 0e28ae9
Show file tree
Hide file tree
Showing 43 changed files with 5,394 additions and 1,479 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
on:
push:
branches: ["main"]

workflow_dispatch:

permissions:
Expand All @@ -28,10 +27,10 @@ jobs:
- name: Setup Pages
uses: actions/configure-pages@v4

- name: Setup .NET 8.0
- name: Setup .NET 9.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x
dotnet-version: 9.x

- name: Setup docfx
run: dotnet tool update -g docfx
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x

- name: Setup .NET 9.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x

- name: Restore dependencies
run: dotnet restore
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,19 @@ jobs:
with:
dotnet-version: 8.x

- name: Setup .NET 9.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --configuration Release --no-restore
run: dotnet build ./src/LightResults/LightResults.csproj --configuration Release --no-restore

- name: Pack
run: dotnet pack --configuration Release --no-build --output .
run: dotnet pack ./src/LightResults/LightResults.csproj --configuration Release --no-build --output .

- name: Push to NuGet
run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
7 changes: 7 additions & 0 deletions LightResults.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_TYPE_CONSTRAINTS_ON_SAME_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_MULTIPLE_TYPE_PARAMEER_CONSTRAINTS_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
182 changes: 123 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ excellent work with [FluentResults](https://github.com/altmann/FluentResults).

## References

This library targets .NET Standard 2.0, .NET 6.0, .NET 7.0, and .NET 8.0.
This library targets .NET Standard 2.0, .NET 6.0, .NET 7.0, .NET 8.0, and .NET 9.0.

## Dependencies

Expand All @@ -28,7 +28,8 @@ This library has no dependencies.
- 🧵 Thread-safe — The Error list and Metadata dictionary use Immutable classes for thread-safety.
- ✨ Modern — Built against the latest version of .NET using the most recent best practices.
- 🧪 Native — Written, compiled, and tested against the latest versions of .NET.
- ❤️ Compatible — Available for dozens of versions of .NET as a [.NET Standard 2.0](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) library.
- ❤️ Compatible — Available for dozens of versions of .NET as a
[.NET Standard 2.0](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) library.
- 🪚 Trimmable — Compatible with [ahead-of-time compilation](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) (AOT) as of .NET 7.0.
- 🚀 Performant — Heavily optimized and [benchmarked](https://jscarle.github.io/LightResults/docs/performance.html) to aim for the highest possible performance.

Expand All @@ -45,59 +46,62 @@ Make sure to [read the docs](https://jscarle.github.io/LightResults/) for the fu
LightResults consists of only three classes `Result`, `Result<TValue>`, and `Error`.

- The `Result` class represents a generic result indicating success or failure.
- The `Result<TValue>` class represents a result with a value.
- The `Error` class represents an error with a message and associated metadata.
- The `Result<TValue>` class represents a success or failure result with a value.
- The `Error` class represents an error with a message and optional associated metadata.

### Creating a successful result

Successful results can be created using the `Ok` method.
Successful results can be created using the `Success` method.

```csharp
var successResult = Result.Ok();
var successResult = Result.Success();

var successResultWithValue = Result.Ok(349.4);
var successResultWithValue = Result.Success(349.4);
```

### Creating a failed result
### Creating a failure result

Failed results can be created using the `Fail` method.
Failed results can be created using the `Failure` method.

```csharp
var failedResult = Result.Fail();
var failureResult = Result.Failure();

var failedResultWithMessage = Result.Fail("Operation failed!");
var failureResultWithMessage = Result.Failure("Operation failure!");

var failedResultWithMessageAndMetadata = Result.Fail("Operation failed!", ("Exception", ex));
var failureResultWithMessageAndMetadata = Result.Failure("Operation failure!", ("UserId", userId));

var failureResultWithMessageAndException = Result.Failure("Operation failure!", ex);
```

### Checking the state of a result

There are two properties for results, `IsSuccess` and `IsFailed`. Both are mutually exclusive.
There are two methods used to check a result, `IsSuccess()` and `IsFailed()`. Both of which have several overloads to obtain the
value and error.

```csharp
if (result.IsSuccess)
if (result.IsSuccess())
{
// The result is successful therefore IsFailed will be false.
// The result is successful.
}

if (result.IsFailed)
if (result.IsFailure(out var error))
{
// The result is failed therefore IsSuccess will be false.
if (result.Error.Message.Length > 0)
Console.WriteLine(result.Error.Message);
// The result is failure.
if (error.Message.Length > 0)
Console.WriteLine(error.Message);
else
Console.WriteLine("An unknown error occured!");
}
```

### Getting the value

The value from a successful result can be retrieved through the `Value` property.
The value from a successful result can be retrieved through the `out` parameter of the `Success()` method.

```csharp
if (result.IsSuccess)
if (result.IsSuccess(out var value))
{
var value = result.Value;
Console.WriteLine($"Value is {value}");
}
```

Expand All @@ -111,15 +115,6 @@ var errorWithoutMessage = new Error();
var errorWithMessage = new Error("Something went wrong!");
```

With metadata.

```csharp
var errorWithMetadataTuple = new Error(("Key", "Value"));

var metadata = new Dictionary<string, object> { { "Key", "Value" } };
var errorWithMetadataDictionary = new Error(metadata);
```

Or with a message and metadata.

```csharp
Expand All @@ -144,46 +139,36 @@ public sealed class NotFoundError : Error
}

var notFoundError = new NotFoundError();
var notFoundResult = Result.Fail(notFoundError);
var notFoundResult = Result.Failure(notFoundError);
```

Then the result can be checked against that error type.

```csharp
if (result.IsFailed && result.HasError<NotFound>())
if (result.IsFailure(out var error) && error is NotFoundError)
{
// Looks like the resource was not found, we better do something about that!
}
```

This can be especially useful when combined with metadata to handle exceptions.
Or checked to see if there are any errors of that type.

```csharp
public sealed class UnhandledExceptionError : Error
if (result.IsFailure() && result.HasError<NotFoundError>())
{
public UnhandledExceptionError(Exception ex)
: base("An unhandled exception occured.", ("Exception", ex))
{
}
// At least one of the errors was a NotFoundError.
}
```

Which allows us to continue using try-catch blocks in our code but return from them with a result instead of throwing an exception.
This can be especially useful when combined with metadata that is related to a specific type of error.

```csharp
public Result DoSomeWork()
public sealed class HttpError : Error
{
try
{
// We must not throw an exception in this method!
}
catch(Exception ex)
public HttpError(HttpStatusCode statusCode)
: base("An HTTP error occured.", ("StatusCode", statusCode))
{
var unhandledExceptionError = new UnhandledExceptionError(ex);
return Result.Fail(unhandledExceptionError);
}

return Result.Ok();
}
```

Expand All @@ -195,13 +180,13 @@ public static AppError
public Result NotFound()
{
var notFoundError = new NotFoundError();
return Result.Fail(notFoundError);
return Result.Failure(notFoundError);
}

public Result UnhandledException(Exception ex)
public Result HttpError(HttpStatusCode statusCode)
{
var unhandledExceptionError = new UnhandledExceptionError(ex)
return Result.Fail(unhandledExceptionError);
var httpError = new HttpError(statusCode)
return Result.Failure(httpError);
}
}
```
Expand All @@ -216,15 +201,37 @@ public Result GetPerson(int id)
if (person is null)
return AppError.NotFound();

return Result.Ok();
return Result.Success();
}
```

## Static abstract members in interfaces
### Handling Exceptions

Specific overloads have been added to `Failure()` and `Failure<TValue>()` to simplify using try-catch blocks and return from them with a result instead of
throwing.

```csharp
public Result DoSomeWork()
{
try
{
// We must not throw an exception in this method!
}
catch(Exception ex)
{
return Result.Failure(ex);
}

return Result.Success();
}
```

### Static abstract members in interfaces

_Note: Applies to .NET 7.0 (C# 11.0) or higher only!_

Thanks to the [static abstract members in interfaces](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/static-abstract-interface-methods)
Thanks to the
[static abstract members in interfaces](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/static-abstract-interface-methods)
introduced in .NET 7.0 (C# 11.0), it is possible to use generics to obtain access to the methods
of the generic variant of the result. As such the error factory can be enhanced to take advantage of that.

Expand All @@ -234,13 +241,70 @@ public static AppError
public Result NotFound()
{
var notFoundError = new NotFoundError();
return Result.Failt(notFoundError);
return Result.Failure(notFoundError);
}

public TResult NotFound<TResult>()
{
var notFoundError = new NotFoundError();
return TResult.Fail(notFoundError);
return TResult.Failure(notFoundError);
}
}
```

## What's new in v9.0

The API for LightResults was completely redesigned for v9.0 to improve performance and remove any potential pits of failure caused by the prior version's use
of properties that could result in exceptions being thrown when values were accessed without checking the state of the result. Thus, there are several breaking
changes, detailed below, that developers must be aware of when upgrading from v8.0 to v9.0.

### Breaking changes between v8.0 and v9.0

- Factory methods for creating generic results have changed names.
- `Result.Ok()` has been renamed to `Result.Success()`.
- `Result.Fail()` has been renamed to `Result.Failure()`.
- Factory methods for creating results with values have changed names and type to allow omitting the type when it is known.
- `Result<TValue>.Ok()` has been renamed and moved to `Result.Success<TValue>()`.
- `Result<TValue>.Fail()` has been renamed to `Result.Failure<TValue>()`.
- The `Value` and `Error` properties have been removed.
- `result.Value` has been replaced by `result.IsSuccess(out var value)`.
- `result.Error` has been replaced by `result.IsError(out var error)`.
- Several constructors of the `Error` type have been removed or have changed.
- `Error((string Key, object Value) metadata)` has been removed.
- `Error(IDictionary<string, object> metadata)` has been removed.
- `Error(string message, IDictionary<string, object> metadata)` has been changed to
`Error(string message, IReadOnlyDictionary<string, object> metadata)`.

### Migrating

The following steps in the following order will reduce the amount of manual work required to migrate and refactor code to use the new API.

1. Find and replace all instances of `Result.Ok(` with `Result.Success(`.
2. Find and replace all instances of `Result.Fail(` with `Result.Failure(`.
3. Regex find and replace all instances of `Result(<[^>]+>)\.Ok\(` with `Result.Success$1(`.
4. Regex find and replace all instances of `Result(<[^>]+>)\.Fail\(` with `Result.Failure$1(`.
5. Find and replace all instances of `.IsSuccess` with `IsSuccess(out var value)`.
6. Find and replace all instances of `.IsFailed` with `IsFailure(out var error)`.
7. Find instances of `result.Value` and refactor them to the use the `value` exposed by the `IsSuccess()` method.
8. Find instances of `result.Error` and refactor them to the use the `error` exposed by the `IsFailure()` method.

### New method overloads and property initializers

- New overloads have been added for `KeyValuePair<string, object>` metadata.
- `Result.Failure(string errorMessage, KeyValuePair<string, object> metadata)` has been added.
- `Result.Failure<TValue>(string errorMessage, KeyValuePair<string, object> metadata)` has been added.
- New overloads have been added to simplify handling exceptions.
- `Result.Failure(Exception ex)` has been added.
- `Result.Failure(string errorMessage, Exception ex)` has been added.
- `Result.Failure<TValue>(Exception ex)` has been added.
- `Result.Failure<TValue>(string errorMessage, Exception ex)` has been added.
- New overloads where added to access the value.
- `result.IsSuccess(out TValue value)` has been added.
- `result.IsFailure(out IError error, out TValue value)` has been added.
- New overloads where added to access the first error.
- `result.IsFailure(out IError error)` has been added.
- `result.IsSuccess(out TValue value, out IError error)` has been added.
- `result.HasError<TError>(out IError error)` has been added.
- New property initializers where added to `Error`.
- `Message { get; }` has changed to `Message { get; init; }`.
- `Metadata { get; }` has changed to `Metadata { get; init; }`.
15 changes: 15 additions & 0 deletions docfx/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@
"properties": {
"TargetFramework": "net8.0"
}
},
{
"src": [
{
"src": "../src",
"files": [
"**/*.csproj"
]
}
],
"outputFormat": "apiPage",
"dest": "net9.0",
"properties": {
"TargetFramework": "net9.0"
}
}
],
"build": {
Expand Down
Loading

0 comments on commit 0e28ae9

Please sign in to comment.