FluentResults
FluentResults 是一个轻量级的 .NET 库,旨在解决一个常见问题。它返回一个对象来指示操作的成功或失败,而不是抛出/使用异常。
你可以通过 NuGet 安装 FluentResults:
Install-Package FluentResults
:heart: 最需要的社区功能已推送到 nuget: FluentResults.Extensions.AspNetCore 阅读文档。尝试一下,测试一下,给出反馈。
主要特性
- 通用容器,适用于所有上下文(ASP.NET MVC/WebApi、WPF、DDD 领域模型等)
- 在一个 Result 对象中存储多个错误
- 存储强大且详细的 Error 和 Success 对象,而不是仅存储字符串格式的错误消息
- 以面向对象的方式设计 Errors/Success
- 以层次化方式存储带有错误链的根本原因
- 提供
- 新功能 增强的 FluentAssertions 扩展,以优雅的方式断言 FluentResult 对象
- 预览中 从 ASP.NET 控制器返回 Result 对象
为什么使用 Results 而不是异常
老实说,返回一个表示成功或失败的 Result 对象的模式并不是一个全新的想法。这种模式源自函数式编程语言。通过 FluentResults,这种模式也可以在 .NET/C# 中应用。
文章 Exceptions for Flow Control by Vladimir Khorikov 很好地描述了在哪些场景下 Result 模式有意义,在哪些场景下没有意义。查看最佳实践列表和资源列表以了解更多关于 Result 模式的信息。
创建 Result
一个 Result 可以存储多个 Error 和 Success 消息。
// 创建一个表示成功的 result
Result successResult1 = Result.Ok();
// 创建一个表示失败的 result
Result errorResult1 = Result.Fail("My error message");
Result errorResult2 = Result.Fail(new Error("My error message"));
Result errorResult3 = Result.Fail(new StartDateIsAfterEndDateError(startDate, endDate));
Result errorResult4 = Result.Fail(new List<string> { "Error 1", "Error 2" });
Result errorResult5 = Result.Fail(new List<IError> { new Error("Error 1"), new Error("Error 2") });
Result
类通常用于没有返回值的 void 方法。
public Result DoTask()
{
if (this.State == TaskState.Done)
return Result.Fail("Task is in the wrong state.");
// 其余逻辑
return Result.Ok();
}
如果需要,还可以存储特定类型的值。
// 创建一个表示成功的 result
Result<int> successResult1 = Result.Ok(42);
Result<MyCustomObject> successResult2 = Result.Ok(new MyCustomObject());
// 创建一个表示失败的 result
Result<int> errorResult = Result.Fail<int>("My error message");
Result<T>
类通常用于有返回类型的方法。
public Result<Task> GetTask()
{
if (this.State == TaskState.Deleted)
return Result.Fail<Task>("Deleted Tasks can not be displayed.");
// 其余逻辑
return Result.Ok(task);
}
处理 Result
从方法获得 Result 对象后,你需要处理它。这意味着,你必须检查操作是否成功完成。Result 对象中的 IsSuccess
和 IsFailed
属性表示成功或失败。Result<T>
的值可以通过 Value
和 ValueOrDefault
属性访问。
Result<int> result = DoSomething();
// 获取 result 对象表示成功或失败的所有原因。
// 包含 Error 和 Success 消息
IEnumerable<IReason> reasons = result.Reasons;
// 获取所有 Error 消息
IEnumerable<IError> errors = result.Errors;
// 获取所有 Success 消息
IEnumerable<ISuccess> successes = result.Successes;
if (result.IsFailed)
{
// 处理错误情况
var value1 = result.Value; // 抛出异常,因为 result 处于失败状态
var value2 = result.ValueOrDefault; // 返回默认值(=0),因为 result 处于失败状态
return;
}
// 处理成功情况
var value3 = result.Value; // 返回值,不抛出异常,因为 result 处于成功状态
var value4 = result.ValueOrDefault; // 返回值,因为 result 处于成功状态
设计错误和成功消息
许多 Result 库只存储简单的字符串消息。相比之下,FluentResults 存储强大的面向对象的 Error 和 Success 对象。优点是错误或成功的所有相关信息都封装在一个类中。
该库的整个公共 API 使用 IReason
、IError
和 ISuccess
接口来表示原因、错误或成功。IError
和 ISuccess
继承自 IReason
。如果 Reasons
属性中存在至少一个 IError
对象,则结果表示失败,IsSuccess
属性为 false。
你可以通过继承 ISuccess
或 IError
,或继承 Success
或 Error
来创建自己的 Success
或 Error
类。
public class StartDateIsAfterEndDateError : Error
{
public StartDateIsAfterEndDateError(DateTime startDate, DateTime endDate)
: base($"The start date {startDate} is after the end date {endDate}")
{
Metadata.Add("ErrorCode", "12");
}
}
通过这种机制,你还可以创建一个 Warning
类。你可以选择在你的系统中 Warning 是表示成功还是失败,通过继承 Success
或 Error
类来实现。
更多特性
链接错误和成功消息
在某些情况下,需要在一个 result 对象中链接多个错误和成功消息。
var result = Result.Fail("error message 1")
.WithError("error message 2")
.WithError("error message 3")
.WithSuccess("success message 1");
根据成功/失败条件创建 result
通常,你需要根据条件创建失败或成功的 result。通常可以这样写:
var result = string.IsNullOrEmpty(firstName) ? Result.Fail("First Name is empty") : Result.Ok();
使用 FailIf()
和 OkIf()
方法,你可以以更易读的方式编写:
var result = Result.FailIf(string.IsNullOrEmpty(firstName), "First Name is empty");
如果需要延迟初始化错误实例,可以使用接受 Func<string>
或 Func<IError>
的重载:
var list = Enumerable.Range(1, 9).ToList();
var result = Result.FailIf(
list.Any(IsDivisibleByTen),
() => new Error($"Item {list.First(IsDivisibleByTen)} should not be on the list"));
bool IsDivisibleByTen(int i) => i % 10 == 0;
// 其余代码
Try
在某些场景中,你想执行一个操作。如果这个操作抛出异常,则应捕获异常并将其转换为 result 对象。
var result = Result.Try(() => DoSomethingCritical());
你也可以返回自己的 Result
对象
var result = Result.Try(() => {
if(IsInvalid())
{
return Result.Fail("Some error");
}
int id = DoSomethingCritical();
return Result.Ok(id);
});
在上面的例子中,使用了默认的 catchHandler。可以通过全局 Result 设置覆盖默认 catchHandler 的行为(见下一个例子)。你可以控制 Error 对象的外观。
Result.Setup(cfg =>
{
cfg.DefaultTryCatchHandler = exception =>
{
if (exception is SqlTypeException sqlException)
return new ExceptionalError("Sql 错误", sqlException);
if (exception is DomainException domainException)
return new Error("Domain 错误")
.CausedBy(new ExceptionError(domainException.Message, domainException));
return new Error(exception.Message);
};
});
var result = Result.Try(() => DoSomethingCritical());
也可以通过 Try(..)
方法传递自定义 catchHandler。
var result = Result.Try(() => DoSomethingCritical(), ex => new MyCustomExceptionError(ex));
错误的根本原因
你还可以在错误对象中存储错误的根本原因。使用 CausedBy(...)
方法,可以将根本原因作为 Error、Error 列表、字符串、字符串列表或异常传递。根本原因存储在错误对象的 Reasons
属性中。
示例 1 - 根本原因是一个异常
try
{
//导出 csv 文件
}
catch(CsvExportException ex)
{
return Result.Fail(new Error("CSV 导出未成功执行").CausedBy(ex));
}
示例 2 - 根本原因是一个错误
Error rootCauseError = new Error("这是错误的根本原因");
Result result = Result.Fail(new Error("执行某事失败", rootCauseError));
示例 3 - 从错误中读取根本原因
Result result = ....;
if (result.IsSuccess)
return;
foreach(IError error in result.Errors)
{
foreach(ExceptionalError causedByExceptionalError in error.Reasons.OfType<ExceptionalError>())
{
Console.WriteLine(causedByExceptionalError.Exception);
}
}
元数据
可以为 Error 或 Success 对象添加元数据。
一种方法是在创建 result 对象时直接调用 WithMetadata(...)
方法。
var result1 = Result.Fail(new Error("Error 1").WithMetadata("metadata name", "metadata value"));
var result2 = Result.Ok()
.WithSuccess(new Success("Success 1")
.WithMetadata("metadata name", "metadata value"));
另一种方法是在 Error
或 Success
类的构造函数中调用 WithMetadata(...)
。
public class DomainError : Error
{
public DomainError(string message)
: base(message)
{
WithMetadata("ErrorCode", "12");
}
}
合并
可以使用静态方法 Merge()
合并多个结果。
var result1 = Result.Ok();
var result2 = Result.Fail("first error");
var result3 = Result.Ok<int>();
var mergedResult = Result.Merge(result1, result2, result3);
可以使用扩展方法 Merge()
将结果列表合并为一个结果。
var result1 = Result.Ok();
var result2 = Result.Fail("first error");
var result3 = Result.Ok<int>();
var results = new List<Result> { result1, result2, result3 };
var mergedResult = results.Merge();
转换和变换
可以使用 ToResult()
和 ToResult<TValue>()
方法将一个 result 对象转换为
// 检查Result对象是否包含特定类型的异常以及特定条件
result.HasException
所有`HasException()`方法都有一个可选的out参数result来访问找到的错误。
### 模式匹配
```csharp
var result = Result.Fail<int>("错误 1");
var outcome = result switch
{
{ IsFailed: true } => $"出错原因 {result.Errors}",
{ IsSuccess: true } => $"值为 {result.Value}",
_ => null
};
解构运算符
var (isSuccess, isFailed, value, errors) = Result.Fail<bool>("失败 1");
var (isSuccess, isFailed, errors) = Result.Fail("失败 1");
日志记录
有时需要记录结果日志。首先创建一个日志记录器:
public class MyConsoleLogger : IResultLogger
{
public void Log(string context, string content, ResultBase result, LogLevel logLevel)
{
Console.WriteLine("结果: {0} {1} <{2}>", result.Reasons.Select(reason => reason.Message), content, context);
}
public void Log<TContext>(string content, ResultBase result, LogLevel logLevel)
{
Console.WriteLine("结果: {0} {1} <{2}>", result.Reasons.Select(reason => reason.Message), content, typeof(TContext).FullName);
}
}
然后你必须在Result设置中注册你的日志记录器:
var myLogger = new MyConsoleLogger();
Result.Setup(cfg => {
cfg.Logger = myLogger;
});
最后可以在任何结果上使用日志记录器:
var result = Result.Fail("操作失败")
.Log();
此外,还可以以字符串形式或泛型类型参数的形式传递上下文。还可以传递提供更多信息的自定义消息作为内容。
var result = Result.Fail("操作失败")
.Log("日志记录器上下文", "关于结果的更多信息");
var result2 = Result.Fail("操作失败")
.Log<MyLoggerContext>("关于结果的更多信息");
也可以指定所需的日志级别:
var result = Result.Ok().Log(LogLevel.Debug);
var result = Result.Fail().Log<MyContext>("额外上下文", LogLevel.Error);
你还可以只在成功或失败时记录结果:
Result<int> result = DoSomething();
// 使用默认日志级别"Information"记录
result.LogIfSuccess();
// 使用默认日志级别"Error"记录
result.LogIfFailed();
断言FluentResult对象
尝试使用FluentAssertions和FluentResults.Extensions.FluentAssertions的强大功能。从v2.0开始,断言包已经脱离了实验阶段,它真的是以流畅的方式断言结果对象的一个很好的增强。
.NET 目标
FluentResults 3.x及更高版本支持.NET Standard 2.0和.NET Standard 2.1。 如果你需要支持.NET Standard 1.1、.NET 4.6.1或.NET 4.5,请使用FluentResults 2.x。
示例/最佳实践
以下是在使用FluentResult或一般的Result模式时应遵循的一些示例和最佳实践,涉及一些著名或常用的框架和库。
受领域驱动设计启发的强大领域模型
- 带有命令处理程序的领域模型
- 通过使用返回Result对象的工厂方法等来保护领域不变量
- 通过创建继承自IError接口或Error类的自定义Error类使每个错误唯一
- 如果方法没有失败场景,则不要使用Result类作为返回类型
- 请注意,你可以合并多个失败的结果或尽快返回第一个失败的结果
序列化Result对象(ASP.NET WebApi, Hangfire)
- Asp.net WebController
- Hangfire Job
- 不要序列化FluentResult结果对象。
- 为系统边界中的公共API创建自己的自定义ResultDto类
- 这样你可以控制提交哪些数据以及序列化哪些数据
- 你的公共API独立于像FluentResults这样的第三方库
- 你可以保持公共API的稳定性
返回Result对象的MediatR请求处理程序
- 带有命令/查询和ValidationPipelineBehavior的完整功能.NET Core示例代码
- 通过Result对象从MediatR请求处理程序返回业务验证错误给消费者
- 不要根据业务验证错误抛出异常
- 通过MediatR PipelineBehavior注入命令和查询验证,并返回Result对象而不是抛出异常
关于Result模式的有趣资源
- 错误处理 — 返回结果 作者Michael Altmann
- 操作结果模式 作者Carl-Hugo Marcotte
- C#中的流程控制异常 作者Vladimir Khorikov
- 错误处理:异常还是结果? 作者Vladimir Khorikov
- 代码中的异常情况是什么? 作者Vladimir Khorikov
- 高级错误处理技术 作者Vladimir Khorikov
- 简单指南 作者Isaac Cummings
- 使用Result类进行灵活的错误处理 作者Khalil Stemmler
- 将ASP.NET Core验证属性与值对象结合使用 作者Vladimir Khorikov
捐赠
我热爱这个项目,但实现功能、回答问题或维护CI/发布管道需要时间 - 这是我的空闲时间。如果你喜欢FluentResult并且觉得它有用,请考虑捐赠。点击右上角的赞助按钮。
贡献者
感谢所有贡献者和提供反馈的人!
版权
版权所有 (c) Michael Altmann。有关详细信息,请参阅LICENSE。