diff --git a/Orleans-Results-Example.png b/Orleans-Results-Example.png
index eb9d9d4..328b977 100644
Binary files a/Orleans-Results-Example.png and b/Orleans-Results-Example.png differ
diff --git a/README.md b/README.md
index 3feb7e9..8a5e099 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
# Orleans.Results
Concise, version-tolerant result pattern implementation for [Microsoft Orleans 4](https://github.com/dotnet/orleans/releases/tag/v4.0.0-preview1).
-The result pattern solves a common problem: it returns an object indicating success or failure of an operation instead of throwing/using exceptions (see [why](#why) below).
+The result pattern solves a common problem: it returns an object indicating success or failure of an operation instead of throwing exceptions (see [why](#why) below).
This implementation leverages [immutability in Orleans](https://github.com/dotnet/orleans/blob/b7bb116ba4f98b64428d449d26f20ea37d3501b6/src/Orleans.Serialization.Abstractions/Annotations.cs#L430) to optimize performance.
-## Usage
+## Basic usage
Define error codes:
```csharp
@@ -14,7 +14,7 @@ public enum ErrorCode
UserNotFound = 1
}
```
-> Note that this enum is used to define convenience classes:
`Result : ResultBase` and `Result : ResultBase`
These classes save you from having to specify `` in every grain method signature
+> Note that this enum is used to define convenience classes:
`Result : ResultBase` and `Result : ResultBase`
These classes save you from having to specify `` as type parameter in every grain method signature
Grain contract:
```csharp
@@ -62,27 +62,90 @@ static class Errors
}
```
-The `Result` convenience classes have implicit convertors to allow concise assignment of errors and values, e.g.
+## Convenience features
+The `Result` class is intended for methods that return either a value or error(s), while the `Result` class is intended for methods that return either success (`Result.Ok`) or error(s).
+
+The `Result` and `Result` convenience classes have implicit convertors to allow concise returning of errors and values:
```csharp
- Result r1 = ErrorCode.UserNotFound;
- Result r2 = "Hi";
- Result r3 = (ErrorCode.UserNotFound, $"User {id} not found");
+async Task> GetString(int i) => i switch {
+ 0 => "Success!",
+ 1 => ErrorCode.NotFound,
+ 2 => (ErrorCode.NotFound, "Not found"),
+ 3 => new Error(ErrorCode.NotFound, "Not found"),
+ 4 => new List(/*...*/)
+};
```
-The `With` methods allow you to specify multiple errors in a result:
+The implicit convertor only supports multiple errors with `List`; you can use the public constructor to specify multiple errors with any `IEnumerable`:
```csharp
- Result r = ErrorCode.AnError;
- var r2 = r
- .With(ErrorCode.AnotherError)
- .With(ErrorCode.YetAnotherError, "This is the 3rd error");
+async Task> GetString()
+{
+ IEnumerable errors = new HashSet();
+ // ... check for errors
+ if (errors.Any()) return new(errors);
+ return "Success!";
+}
```
-The `ValidationErrors` property is convenient to for use with [ValidationProblemDetails](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails?view=aspnetcore-6.0) (in MVC):
+## Validation errors
+The `TryAsValidationErrors` method is covenient for returning [RFC7807](https://tools.ietf.org/html/rfc7807) based problem detail responses. This method is designed to be used with [ValidationProblemDetails](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails?view=aspnetcore-6.0) (in MVC):
```csharp
-r => ValidationProblem(new ValidationProblemDetails(r.ValidationErrors))
+return result.TryAsValidationErrors(ErrorCode.ValidationError, out var validationErrors)
+ ? ValidationProblem(new ValidationProblemDetails(validationErrors))
+
+ : result switch
+ {
+ { IsSuccess: true } r => Ok(r.Value),
+ { ErrorCode: ErrorCode.NoUsersAtAddress } r => NotFound(r.ErrorsText),
+ { } r => throw r.UnhandledErrorException()
+ };
```
-and for [Results.ValidationProblem](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.results.validationproblem?view=aspnetcore-6.0) (in minimal API's):
+and with [Results.ValidationProblem](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.results.validationproblem?view=aspnetcore-6.0) (in minimal API's):
```csharp
-r => Results.ValidationProblem(r.ValidationErrors)
+return result.TryAsValidationErrors(ErrorCode.ValidationError, out var validationErrors)
+ ? Results.ValidationProblem(validationErrors)
+
+ : result switch
+ {
+ { IsSuccess: true } r => Results.Ok(r.Value),
+ { ErrorCode: ErrorCode.NoUsersAtAddress } r => Results.NotFound(r.ErrorsText),
+ { } r => throw r.UnhandledErrorException()
+ };
```
+
+To use `TryAsValidationErrors`, your `ErrorCode` must be a `[Flags] enum` with a flag that identifies which error codes are validation errors:
+```csharp
+[Flags]
+public enum ErrorCode
+{
+ NoUsersAtAddress = 1,
+
+ ValidationError = 1024,
+ InvalidZipCode = 1 | ValidationError,
+ InvalidHouseNr = 2 | ValidationError,
+}
+```
+`TryAsValidationErrors` is designed to support this implementation pattern:
+```csharp
+public async Task> GetUsersAtAddress(string zip, string nr)
+{
+ List errors = new();
+
+ // First check for validation errors - don't perform the operation if there are any.
+ if (!Regex.IsMatch(zip, @"^\d\d\d\d[A-Z]{2}$")) errors.Add(Errors.InvalidZipCode(zip));
+ if (!Regex.IsMatch(nr, @"^\d+[a-z]?$")) errors.Add(Errors.InvalidHouseNr(nr));
+ if (errors.Any()) return errors;
+
+ // If there are no validation errors, perform the operation - this may return non-validation errors
+ // ... do the operation
+ if (...) errors.Add(Errors.NoUsersAtAddress($"{zip} {nr}"));
+ return errors.Any() ? errors : "Success!";
+}
+```
+
+## Immutability and performance
+To optimize performance, `Result` and `Error` are implemented as immutable types and are marked with the Orleans [[Immutable] attribute](https://github.com/dotnet/orleans/blob/b7bb116ba4f98b64428d449d26f20ea37d3501b6/src/Orleans.Serialization.Abstractions/Annotations.cs#L430). This means that Orleans will not create a deep copy of these types for grain calls within the same silo, passing instance references instead.
+
+The performance of `Result` can be optimized similarly by judiciously marking specific `T` types as `[Immutable]` - exactly the same way as when you would directly pass `T` around, instead of `Result`. The fact that `Result` itself is not marked immutable does not significantly reduce the performance benefits gained; in cases where immutability makes a difference `T` typically has a much higher serialization cost than the wrapping result (which is very lightweight).
+## Full example
The [example in the repo](https://github.com/Applicita/Orleans.Results/tree/main/src/Example) demonstrates using Orleans.Results with both ASP.NET Core minimal API's and MVC:
![Orleans Results Example](Orleans-Results-Example.png)
## How do I get it?
@@ -111,6 +174,8 @@ The result pattern solves a common problem: it returns an object indicating succ
Using return values also allows you to use [code analysis rule CA1806](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1806) to alert you where you forgot to check the return value (you can use a *discard* `_ =` to express intent to ignore a return value)
### Orleans 4 introduces version-tolerant, high-performance serialization
-However this is not compatible with existing Result pattern implementations like [FluentResults](https://github.com/altmann/FluentResults). The result object must be annotated with the Orleans `[GenerateSerializer]` and `[Id]` attributes, and cannot contain arbitrary objects.
+However existing Result pattern implementations like [FluentResults](https://github.com/altmann/FluentResults) are not designed for serialization, let alone Orleans serialization. Orleans requires that you annotate your result types - including all types contained within - with the Orleans `[GenerateSerializer]` and `[Id]` attributes, or alternatively that you write additional code to serialize external types.
+
+This means that result objects that can contain contain arbitrary objects as part of the errors (like exceptions) require an open-ended amount of work. Orleans.Results avoids this work by defining an error to be an `enum` code plus a `string` message.
-Orleans.Results adheres to these guidelines, which enables compatibility with future changes in the result object serialization.
\ No newline at end of file
+Orleans.Results adheres to the Orleans 4 serialization guidelines, which enables compatibility with future changes in the result object serialization.
\ No newline at end of file