2 Commits

Author SHA1 Message Date
b8ea74ec5b added missing Append extensions
All checks were successful
.NET Test / test (push) Successful in 3m18s
.NET Publish / publish (push) Successful in 3m28s
2023-12-12 19:02:11 +04:00
9ae185342b made Error fully immutable
All checks were successful
.NET Test / test (push) Successful in 4m16s
2023-12-12 18:55:53 +04:00
8 changed files with 332 additions and 125 deletions

106
README.md
View File

@@ -4,6 +4,108 @@ This library uses features of C# to achieve railway-oriented programming.
The desire is to make somewhat user-friendly experience while using result-object pattern. The desire is to make somewhat user-friendly experience while using result-object pattern.
## Contents ## Features
_Coming soon..._ - Immutable ```Error``` class
- ```Result``` object
- A bunch of extensions to use result-object pattern with
- ```Try``` extensions to wrap function calls with result-object
- ```Ensure``` extensions to utilize result-object in validation scenarios
## Getting Started
### Install from local Gitea package repository
```sh
# Setup NuGet registry from the command line
dotnet nuget add source --name gitea_jstdev https://gitea.jstdev.ru/api/packages/just/nuget/index.json
# then install the package using NuGet
dotnet add package --source gitea_jstdev Just.Railway
```
## Examples
### Error
```csharp
using Just.Railway;
Error expectedError = Error.New(type: "Some Error", message: "Some error detail");
Error exceptionalError = Error.New(new Exception("Some Exception"));
Error manyErrors = Error.Many(expectedError, exceptionalError);
// the same result while using .Append(..) or +
manyErrors = expectedError.Append(exceptionalError);
manyErrors = expectedError + exceptionalError;
```
> **Note**
> You can easily serialize/deserialize Error to and from JSON
### Result
#### As return value:
```csharp
Result Foo()
{
// ...
if (SomeCondition())
return Result.Failure(Error.New("Some Error"));
// or just: return Error.New("Some Error");
// ...
return Result.Success();
}
Result<T> Bar()
{
T value;
// ...
if (SomeCondition())
return Error.New("Some Error");
// ...
return value;
}
```
#### Consume Result object
```csharp
Result<int> result = GetResult();
var value = result
.Append("new")
.Map((i, s) => $"{s} result {i}")
.Match(
onSuccess: x => x,
onFailure: err => err.ToString()
);
// value: "new result 1"
Result<int> GetResult() => Result.Success(1);
```
### Try
```csharp
Result result = Try.Run(SomeAction);
// you can pass up to 5 arguments like this
result = Try.Run(SomeActionWithArguments, 1, 2.0, "3");
// you also can call functions
Result<int> resultWithValue = Try.Run(SomeFunction);
void SomeAction() {}
void SomeActionWithArguments(int a1, double a2, string? a3) {}
int SomeFunction() => 1;
```
### Ensure
```csharp
var value = GetValue();
Result<int> result = Ensure.That(value)
.NotNull()
.Satisfies(i => i < 100)
.Result();
int? GetValue() => 1;
```

View File

@@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen;
public sealed class EnsureExtensionExecutor : IGeneratorExecutor public sealed class EnsureExtensionsExecutor : IGeneratorExecutor
{ {
public void Execute(SourceProductionContext context, Compilation source) public void Execute(SourceProductionContext context, Compilation source)
{ {
@@ -80,7 +80,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor
typeOverloads.ForEach(def => sb.AppendLine($$""" typeOverloads.ForEach(def => sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<{{def.CollectionType}}> NotEmpty{{def.TemplateDef}}(this in Ensure<{{def.CollectionType}}> ensure, {{errorParameterDecl}}) public static Ensure<{{def.CollectionType}}> NotEmpty{{def.TemplateDef}}(this in Ensure<{{def.CollectionType}}> ensure, {{errorParameterDecl}})
{ {
return ensure.State switch return ensure.State switch
@@ -101,7 +101,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, {{errorParameterDecl}}) public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, {{errorParameterDecl}})
where T : struct where T : struct
{ {
@@ -117,7 +117,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor
"""); """);
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, {{errorParameterDecl}}) public static Ensure<T> NotNull<T>(this in Ensure<T?> ensure, {{errorParameterDecl}})
where T : notnull where T : notnull
{ {
@@ -138,7 +138,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} does not satisfy the requirement.\")"; string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} does not satisfy the requirement.\")";
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> Satisfies<T>(this in Ensure<T> ensure, Func<T, bool> requirement, {{errorParameterDecl}}) public static Ensure<T> Satisfies<T>(this in Ensure<T> ensure, Func<T, bool> requirement, {{errorParameterDecl}})
{ {
return ensure.State switch return ensure.State switch
@@ -160,7 +160,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor
{ {
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<Ensure<T>> Satisfies<T>(this {{taskType}}<Ensure<T>> ensureTask, Func<T, bool> requirement, {{errorParameterDecl}}) public static async {{taskType}}<Ensure<T>> Satisfies<T>(this {{taskType}}<Ensure<T>> ensureTask, Func<T, bool> requirement, {{errorParameterDecl}})
{ {
var ensure = await ensureTask.ConfigureAwait(false); var ensure = await ensureTask.ConfigureAwait(false);
@@ -176,7 +176,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor
"""); """);
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<Ensure<T>> Satisfies<T>(this Ensure<T> ensure, Func<T, {{taskType}}<bool>> requirement, {{errorParameterDecl}}) public static async {{taskType}}<Ensure<T>> Satisfies<T>(this Ensure<T> ensure, Func<T, {{taskType}}<bool>> requirement, {{errorParameterDecl}})
{ {
return ensure.State switch return ensure.State switch
@@ -191,7 +191,7 @@ public sealed class EnsureExtensionExecutor : IGeneratorExecutor
"""); """);
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<Ensure<T>> Satisfies<T>(this {{taskType}}<Ensure<T>> ensureTask, Func<T, {{taskType}}<bool>> requirement, {{errorParameterDecl}}) public static async {{taskType}}<Ensure<T>> Satisfies<T>(this {{taskType}}<Ensure<T>> ensureTask, Func<T, {{taskType}}<bool>> requirement, {{errorParameterDecl}})
{ {
var ensure = await ensureTask.ConfigureAwait(false); var ensure = await ensureTask.ConfigureAwait(false);

View File

@@ -19,7 +19,7 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator
new ResultTapExecutor(), new ResultTapExecutor(),
new ResultAppendExecutor(), new ResultAppendExecutor(),
new TryExtensionsExecutor(), new TryExtensionsExecutor(),
new EnsureExtensionExecutor(), new EnsureExtensionsExecutor(),
}; };
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)

View File

@@ -200,6 +200,48 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
string methodExpandedTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames); string methodExpandedTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames);
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, TNext next)
{
var result = await resultTask.ConfigureAwait(false);
return result.State switch
{
ResultState.Success => Result.Success({{JoinArguments(resultValueExpansion, "next")}}),
ResultState.Error => result.Error!,
_ => throw new ResultNotInitializedException(nameof(result))
};
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Result<TNext> next)
{
var result = await resultTask.ConfigureAwait(false);
if ((result.State & next.State) == ResultState.Bottom)
{
throw new ResultNotInitializedException(string.Join(';', GetBottom(result.State, next.State)));
}
Error? error = null;
if (result.IsFailure)
{
error += result.Error;
}
if (next.IsFailure)
{
error += next.Error;
}
return error is null
? Result.Success({{JoinArguments(resultValueExpansion, "next.Value")}})
: error;
}
""");
sb.AppendLine($$""" sb.AppendLine($$"""
[PureAttribute] [PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")]

View File

@@ -1,4 +1,5 @@
using System.Collections; using System.Collections;
using System.Collections.Immutable;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text; using System.Text;
@@ -10,8 +11,6 @@ namespace Just.Railway;
[JsonDerivedType(typeof(ManyErrors))] [JsonDerivedType(typeof(ManyErrors))]
public abstract class Error : IEquatable<Error>, IComparable<Error> public abstract class Error : IEquatable<Error>, IComparable<Error>
{ {
private IDictionary<string, object>? _extensionData;
protected internal Error(){} protected internal Error(){}
/// <summary> /// <summary>
@@ -77,28 +76,8 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
[Pure] public abstract string Type { get; } [Pure] public abstract string Type { get; }
[Pure] public abstract string Message { get; } [Pure] public abstract string Message { get; }
[Pure, JsonExtensionData] public IDictionary<string, object> ExtensionData [Pure, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ImmutableDictionary<string, string>? ExtensionData { get; init; }
{ [Pure] public string? this[string key] => ExtensionData?.TryGetValue(key, out var value) == true ? value : null;
get => _extensionData ??= new Dictionary<string, object>();
init => _extensionData = value ?? new Dictionary<string, object>();
}
[Pure] public object? this[string name]
{
get => _extensionData?.TryGetValue(name, out var val) == true ? val : null;
set
{
if (value is null)
{
_extensionData?.Remove(name);
}
else
{
_extensionData ??= new Dictionary<string, object>();
_extensionData[name] = value;
}
}
}
[Pure, JsonIgnore] public abstract int Count { get; } [Pure, JsonIgnore] public abstract int Count { get; }
[Pure, JsonIgnore] public abstract bool IsEmpty { get; } [Pure, JsonIgnore] public abstract bool IsEmpty { get; }
@@ -199,13 +178,13 @@ public sealed class ExceptionalError : Error
: this(exception.GetType().Name, exception.Message) : this(exception.GetType().Name, exception.Message)
{ {
Exception = exception; Exception = exception;
FillExtensionData(exception); ExtensionData = ExtractExtensionData(exception);
} }
internal ExceptionalError(string message, Exception exception) internal ExceptionalError(string message, Exception exception)
: this(exception.GetType().Name, message) : this(exception.GetType().Name, message)
{ {
Exception = exception; Exception = exception;
FillExtensionData(exception); ExtensionData = ExtractExtensionData(exception);
} }
[JsonConstructor] [JsonConstructor]
@@ -231,15 +210,28 @@ public sealed class ExceptionalError : Error
yield return this; yield return this;
} }
private void FillExtensionData(Exception exception) private static ImmutableDictionary<string, string>? ExtractExtensionData(Exception exception)
{ {
if (!(exception.Data?.Count > 0))
return null;
List<KeyValuePair<string, string>>? values = null;
foreach (var key in exception.Data.Keys) foreach (var key in exception.Data.Keys)
{ {
if (key is null) continue;
var value = exception.Data[key]; var value = exception.Data[key];
if (key is null || value is null) if (value is null) continue;
continue;
this.ExtensionData[key.ToString() ?? string.Empty] = value; var keyString = key.ToString();
var valueString = value.ToString();
if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(valueString)) continue;
values ??= [];
values.Add(new(keyString, valueString));
} }
return values?.ToImmutableDictionary();
} }
} }

View File

@@ -1,3 +1,5 @@
using System.Collections.Immutable;
namespace Railway.Tests.Errors; namespace Railway.Tests.Errors;
public class Serialization public class Serialization
@@ -8,9 +10,8 @@ public class Serialization
// Given // Given
Error many_errors = new ManyErrors(new Error[]{ Error many_errors = new ManyErrors(new Error[]{
new ExpectedError("err1", "msg1"){ new ExpectedError("err1", "msg1"){
ExtensionData = { ExtensionData = ImmutableDictionary<string, string>.Empty
["ext"] = "ext_value" .Add("ext", "ext_value"),
}
}, },
new ExceptionalError(new Exception("msg2")), new ExceptionalError(new Exception("msg2")),
}); });
@@ -18,7 +19,7 @@ public class Serialization
var result = JsonSerializer.Serialize(many_errors); var result = JsonSerializer.Serialize(many_errors);
// Then // Then
Assert.Equal( Assert.Equal(
expected: "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ext\":\"ext_value\"},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]", expected: "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ExtensionData\":{\"ext\":\"ext_value\"}},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]",
result); result);
} }
@@ -26,7 +27,7 @@ public class Serialization
public void WhenDeserializingManyErrors() public void WhenDeserializingManyErrors()
{ {
// Given // Given
var json = "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ext\":\"ext_value\"},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]"; var json = "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ExtensionData\":{\"ext1\":\"ext_value1\",\"ext2\":\"ext_value2\"}},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]";
// When // When
var result = JsonSerializer.Deserialize<Error[]>(json); var result = JsonSerializer.Deserialize<Error[]>(json);
// Then // Then
@@ -39,7 +40,10 @@ public class Serialization
result result
); );
Assert.Equal( Assert.Equal(
expected: "ext_value", expected: "ext_value1",
result[0].ExtensionData["ext"].ToString()); result[0]["ext1"]);
Assert.Equal(
expected: "ext_value2",
result[0]["ext2"]);
} }
} }

View File

@@ -0,0 +1,78 @@
namespace Raliway.Tests.Results;
public class Combine
{
[Fact]
public void TwoResultCombination_WhenThereIsAnError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Failure(Error.New("some error"));
// When
var result = Result.Combine(result1, result2);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result2.Error, result.Error);
}
[Fact]
public void TwoResultCombination_WhenThereAreTwoErrors()
{
// Given
var result1 = Result.Failure<byte>(Error.New("1"));
var result2 = Result.Failure(Error.New("2"));
// When
var result = Result.Combine(result1, result2);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result1.Error + result2.Error, result.Error);
}
[Fact]
public void TwoResultCombination_WhenThereIsNoError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Success(3.14);
// When
var result = Result.Combine(result1, result2);
// Then
Assert.True(result.IsSuccess);
}
[Fact]
public void ThreeResultCombination_WhenThereIsAnError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Success(3.14);
var result3 = Result.Failure(Error.New("some error"));
// When
Result<(int, double)> result = Result.Combine(result1, result2, result3);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result3.Error, result.Error);
}
[Fact]
public void ThreeResultCombination_WhenThereAreTwoErrors()
{
// Given
var result1 = Result.Failure<int?>(Error.New("1"));
var result2 = Result.Success(3.14);
var result3 = Result.Failure(Error.New("3"));
// When
Result<(int?, double)> result = Result.Combine(result1, result2, result3);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result1.Error + result3.Error, result.Error);
}
[Fact]
public void ThreeResultCombination_WhenThereIsNoError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Success(3.14);
var result3 = Result.Success();
// When
var result = Result.Combine(result1, result2, result3);
// Then
Assert.True(result.IsSuccess);
}
}

View File

@@ -2,80 +2,6 @@ namespace Raliway.Tests.Results;
public class GeneralUsage public class GeneralUsage
{ {
[Fact]
public void TwoResultCombination_WhenThereIsAnError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Failure(Error.New("some error"));
// When
var result = Result.Combine(result1, result2);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result2.Error, result.Error);
}
[Fact]
public void TwoResultCombination_WhenThereAreTwoErrors()
{
// Given
var result1 = Result.Failure<byte>(Error.New("1"));
var result2 = Result.Failure(Error.New("2"));
// When
var result = Result.Combine(result1, result2);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result1.Error + result2.Error, result.Error);
}
[Fact]
public void TwoResultCombination_WhenThereIsNoError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Success(3.14);
// When
var result = Result.Combine(result1, result2);
// Then
Assert.True(result.IsSuccess);
}
[Fact]
public void ThreeResultCombination_WhenThereIsAnError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Success(3.14);
var result3 = Result.Failure(Error.New("some error"));
// When
Result<(int, double)> result = Result.Combine(result1, result2, result3);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result3.Error, result.Error);
}
[Fact]
public void ThreeResultCombination_WhenThereAreTwoErrors()
{
// Given
var result1 = Result.Failure<int?>(Error.New("1"));
var result2 = Result.Success(3.14);
var result3 = Result.Failure(Error.New("3"));
// When
Result<(int?, double)> result = Result.Combine(result1, result2, result3);
// Then
Assert.True(result.IsFailure);
Assert.Equal(result1.Error + result3.Error, result.Error);
}
[Fact]
public void ThreeResultCombination_WhenThereIsNoError()
{
// Given
var result1 = Result.Success(1);
var result2 = Result.Success(3.14);
var result3 = Result.Success();
// When
var result = Result.Combine(result1, result2, result3);
// Then
Assert.True(result.IsSuccess);
}
[Fact] [Fact]
public void ChainedResultExtensions_WhenThereIsNoError() public void ChainedResultExtensions_WhenThereIsNoError()
{ {
@@ -104,10 +30,9 @@ public class GeneralUsage
public void ChainedResultExtensions_WhenThereIsAnError() public void ChainedResultExtensions_WhenThereIsAnError()
{ {
// Given // Given
// When
var error = Error.New("test"); var error = Error.New("test");
// When
var result = Result.Success() var result = Result.Success()
.Append(() => Result.Failure<int>(error)) .Append(() => Result.Failure<int>(error))
@@ -115,7 +40,7 @@ public class GeneralUsage
.Map((i, s) => .Map((i, s) =>
{ {
Assert.Fail(); Assert.Fail();
return Result.Success(""); return "";
}) })
.Append("some") .Append("some")
.Bind((s1, s2) => .Bind((s1, s2) =>
@@ -139,4 +64,68 @@ public class GeneralUsage
// Then // Then
Assert.Equal("satisfied", result); Assert.Equal("satisfied", result);
} }
[Fact]
public async Task ChainedResultAsyncExtensions_WhenThereIsNoError()
{
// Given
// When
var result = await Result.Success()
.Append(() => ValueTask.FromResult(Result.Success(1)))
.Append("test")
.Map((i, s) => $"{s}_{i}")
.Append("some")
.Bind(async (s1, s2) => await ValueTask.FromResult(Result.Success(string.Join(';', s1, s2))))
.Match(
onSuccess: s => s.ToUpper(),
onFailure: _ =>
{
Assert.Fail();
return "";
}
);
Assert.Equal("TEST_1;SOME", result);
}
[Fact]
public async Task ChainedResultAsyncExtensions_WhenThereIsAnError()
{
// Given
var error = Error.New("test");
// When
var result = await Result.Success()
.Append(() => Task.FromResult(Result.Failure<int>(error)))
.Append("test")
.Map((i, s) =>
{
Assert.Fail();
return "";
})
.Append("some")
.Bind(async (s1, s2) =>
{
Assert.Fail();
await Task.CompletedTask;
return Result.Success("");
})
.Match(
onSuccess: _ =>
{
Assert.Fail();
return "";
},
onFailure: err =>
{
Assert.Equal(error, err);
return "satisfied";
}
);
// Then
Assert.Equal("satisfied", result);
}
} }