Project Icon

error-or

优化C#错误处理和结果管理

ErrorOr是一个C#库,采用discriminated union模式简化错误处理。它支持多错误返回,提供丰富的功能方法,可替代异常抛出。通过链式调用处理结果,ErrorOr简化了错误处理逻辑,提高了代码可读性和可维护性。这个库适用于多种开发场景,为C#开发者提供了更优雅的错误管理方式。

drawing

NuGet

Build publish ErrorOr to nuget

GitHub contributors GitHub Stars GitHub license codecov

一个简单、流畅的错误或结果的区分联合。

dotnet add package ErrorOr

给它一个星星 ⭐!

喜欢它吗?通过给这个项目一个星星来表示你的支持!

入门 🏃

ErrorOr<T>替换抛出异常

这个 👇

public float Divide(int a, int b)
{
    if (b == 0)
    {
        throw new Exception("不能除以零");
    }

    return a / b;
}

try
{
    var result = Divide(4, 2);
    Console.WriteLine(result * 2); // 4
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
    return;
}

变成这个 👇

public ErrorOr<float> Divide(int a, int b)
{
    if (b == 0)
    {
        return Error.Unexpected(description: "不能除以零");
    }

    return a / b;
}

var result = Divide(4, 2);

if (result.IsError)
{
    Console.WriteLine(result.FirstError.Description);
    return;
}

Console.WriteLine(result.Value * 2); // 4

或者,使用Then/ElseSwitch/Match,你可以这样做 👇


Divide(4, 2)
    .Then(val => val * 2)
    .SwitchFirst(
        onValue: Console.WriteLine, // 4
        onFirstError: error => Console.WriteLine(error.Description));

支持多个错误

内部,ErrorOr对象有一个Error列表,所以如果你有多个错误,你不需要妥协只保留第一个。

public class User(string _name)
{
    public static ErrorOr<User> Create(string name)
    {
        List<Error> errors = [];

        if (name.Length < 2)
        {
            errors.Add(Error.Validation(description: "名字太短"));
        }

        if (name.Length > 100)
        {
            errors.Add(Error.Validation(description: "名字太长"));
        }

        if (string.IsNullOrWhiteSpace(name))
        {
            errors.Add(Error.Validation(description: "名字不能为空或只包含空白字符"));
        }

        if (errors.Count > 0)
        {
            return errors;
        }

        return new User(name);
    }
}

各种功能性方法和扩展方法

ErrorOr对象有各种方法,允许你以功能性的方式使用它。

这允许你将方法链接在一起,并以清晰简洁的方式处理结果。

现实世界的例子

return await _userRepository.GetByIdAsync(id)
    .Then(user => user.IncrementAge()
        .Then(success => user)
        .Else(errors => Error.Unexpected("不期望失败")))
    .FailIf(user => !user.IsOverAge(18), UserErrors.UnderAge)
    .ThenDo(user => _logger.LogInformation($"用户 {user.Id} 年龄增加到 {user.Age}"))
    .ThenAsync(user => _userRepository.UpdateAsync(user))
    .Match(
        _ => NoContent(),
        errors => errors.ToActionResult());

带中间步骤的简单示例

无失败

ErrorOr<string> foo = await "2".ToErrorOr()
    .Then(int.Parse) // 2
    .FailIf(val => val > 2, Error.Validation(description: $"{val} 太大了") // 2
    .ThenDoAsync(Task.Delay) // 睡眠2毫秒
    .ThenDo(val => Console.WriteLine($"等待 {val} 毫秒完成。")) // 等待2毫秒完成。
    .ThenAsync(val => Task.FromResult(val * 2)) // 4
    .Then(val => $"结果是 {val}") // "结果是4"
    .Else(errors => Error.Unexpected(description: "哎呀")) // "结果是4"
    .MatchFirst(
        value => value, // "结果是4"
        firstError => $"发生错误:{firstError.Description}");

失败

ErrorOr<string> foo = await "5".ToErrorOr()
    .Then(int.Parse) // 5
    .FailIf(val => val > 2, Error.Validation(description: $"{val} 太大了") // Error.Validation()
    .ThenDoAsync(Task.Delay) // Error.Validation()
    .ThenDo(val => Console.WriteLine($"等待 {val} 毫秒完成。")) // Error.Validation()
    .ThenAsync(val => Task.FromResult(val * 2)) // Error.Validation()
    .Then(val => $"结果是 {val}") // Error.Validation()
    .Else(errors => Error.Unexpected(description: "哎呀")) // Error.Unexpected()
    .MatchFirst(
        value => value,
        firstError => $"发生错误:{firstError.Description}"); // 发生错误:哎呀

创建ErrorOr实例

使用隐式转换

有从TResultErrorList<Error>ErrorOr<TResult>的隐式转换器

ErrorOr<int> result = 5;
ErrorOr<int> result = Error.Unexpected();
ErrorOr<int> result = [Error.Validation(), Error.Validation()];
public ErrorOr<int> IntToErrorOr()
{
    return 5;
}
public ErrorOr<int> SingleErrorToErrorOr()
{
    return Error.Unexpected();
}
public ErrorOr<int> MultipleErrorsToErrorOr()
{
    return [
        Error.Validation(description: "无效的名字"),
        Error.Validation(description: "无效的姓氏")
    ];
}

使用ErrorOrFactory

ErrorOr<int> result = ErrorOrFactory.From(5);
ErrorOr<int> result = ErrorOrFactory.From<int>(Error.Unexpected());
ErrorOr<int> result = ErrorOrFactory.From<int>([Error.Validation(), Error.Validation()]);
public ErrorOr<int> GetValue()
{
    return ErrorOrFactory.From(5);
}
public ErrorOr<int> SingleErrorToErrorOr()
{
    return ErrorOrFactory.From<int>(Error.Unexpected());
}
public ErrorOr<int> MultipleErrorsToErrorOr()
{
    return ErrorOrFactory.From([
        Error.Validation(description: "无效的名字"),
        Error.Validation(description: "无效的姓氏")
    ]);
}

使用ToErrorOr扩展方法

ErrorOr<int> result = 5.ToErrorOr();
ErrorOr<int> result = Error.Unexpected().ToErrorOr<int>();
ErrorOr<int> result = new[] { Error.Validation(), Error.Validation() }.ToErrorOr<int>();

属性

IsError

ErrorOr<int> result = User.Create();

if (result.IsError)
{
    // 结果包含一个或多个错误
}

Value

ErrorOr<int> result = User.Create();

if (!result.IsError) // 结果包含一个值
{
    Console.WriteLine(result.Value);
}

Errors

ErrorOr<int> result = User.Create();

if (result.IsError)
{
    result.Errors // 包含发生的错误列表
        .ForEach(error => Console.WriteLine(error.Description));
}

FirstError

ErrorOr<int> result = User.Create();

if (result.IsError)
{
    var firstError = result.FirstError; // 只有第一个发生的错误
    Console.WriteLine(firstError == result.Errors[0]); // true
}

ErrorsOrEmptyList

ErrorOr<int> result = User.Create();

if (result.IsError)
{
    result.ErrorsOrEmptyList // List<Error> { /* 一个或多个错误 */  }
    return;
}

result.ErrorsOrEmptyList // List<Error> { }

方法

Match

Match方法接收两个函数,onValueonError,如果结果成功则调用onValue,如果结果是错误则调用onError

Match

string foo = result.Match(
    value => value,
    errors => $"发生了 {errors.Count} 个错误。");

MatchAsync

string foo = await result.MatchAsync(
    value => Task.FromResult(value),
    errors => Task.FromResult($"发生了 {errors.Count} 个错误。"));

MatchFirst

MatchFirst方法接收两个函数,onValueonError,如果结果成功则调用onValue,如果结果是错误则调用onError

Match不同,如果状态是错误,MatchFirstonError函数只接收发生的第一个错误,而不是整个错误列表。

string foo = result.
```cs
错误 => Console.WriteLine($"发生了 {errors.Count} 个错误。"));

SwitchAsync

await result.SwitchAsync(
    value => { Console.WriteLine(value); return Task.CompletedTask; },
    errors => { Console.WriteLine($"发生了 {errors.Count} 个错误。"); return Task.CompletedTask; });

SwitchFirst

SwitchFirst 方法接收两个动作,onValueonError,如果结果成功则调用 onValue,如果结果是错误则调用 onError

Switch 不同,如果状态是错误,SwitchFirstonError 函数只接收发生的第一个错误,而不是整个错误列表。

result.SwitchFirst(
    value => Console.WriteLine(value),
    firstError => Console.WriteLine(firstError.Description));

SwitchFirstAsync

await result.SwitchFirstAsync(
    value => { Console.WriteLine(value); return Task.CompletedTask; },
    firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; });

Then

Then

Then 接收一个函数,并且仅在结果不是错误时调用它。

ErrorOr<int> foo = result
    .Then(val => val * 2);

可以链式调用多个 Then 方法。

ErrorOr<string> foo = result
    .Then(val => val * 2)
    .Then(val => $"结果是 {val}");

如果任何方法返回错误,链式调用将中断并返回错误。

ErrorOr<int> Foo() => Error.Unexpected();

ErrorOr<string> foo = result
    .Then(val => val * 2)
    .Then(_ => GetAnError())
    .Then(val => $"结果是 {val}") // 这个函数不会被调用
    .Then(val => $"结果是 {val}"); // 这个函数不会被调用

ThenAsync

ThenAsync 接收一个异步函数,并且仅在结果不是错误时调用它。

ErrorOr<string> foo = await result
    .ThenAsync(val => DoSomethingAsync(val))
    .ThenAsync(val => DoSomethingElseAsync($"结果是 {val}"));

ThenDoThenDoAsync

ThenDoThenDoAsync 类似于 ThenThenAsync,但它们调用的是动作而不是返回值的函数。

ErrorOr<string> foo = result
    .ThenDo(val => Console.WriteLine(val))
    .ThenDo(val => Console.WriteLine($"结果是 {val}"));
ErrorOr<string> foo = await result
    .ThenDoAsync(val => Task.Delay(val))
    .ThenDo(val => Console.WriteLine($"等待 {val} 秒完成。"))
    .ThenDoAsync(val => Task.FromResult(val * 2))
    .ThenDo(val => $"结果是 {val}");

混合使用 Then, ThenDo, ThenAsync, ThenDoAsync

你可以混合搭配使用 Then, ThenDo, ThenAsync, ThenDoAsync 方法。

ErrorOr<string> foo = await result
    .ThenDoAsync(val => Task.Delay(val))
    .Then(val => val * 2)
    .ThenAsync(val => DoSomethingAsync(val))
    .ThenDo(val => Console.WriteLine($"等待 {val} 秒完成。"))
    .ThenAsync(val => Task.FromResult(val * 2))
    .Then(val => $"结果是 {val}");

FailIf

FailIf 接收一个谓词和一个错误。如果谓词为真,FailIf 将返回错误。否则,它将返回结果的值。

ErrorOr<int> foo = result
    .FailIf(val => val > 2, Error.Validation(description: $"{val} 太大了"));

一旦返回错误,链式调用将中断并返回错误。

var result = "2".ToErrorOr()
    .Then(int.Parse) // 2
    .FailIf(val => val > 1, Error.Validation(description: $"{val} 太大了") // 验证错误
    .Then(num => num * 2) // 这个函数不会被调用
    .Then(num => num * 2) // 这个函数不会被调用

Else

Else 接收一个值或一个函数。如果结果是错误,Else 将返回该值或调用该函数。否则,它将返回结果的值。

Else

ErrorOr<string> foo = result
    .Else("后备值");
ErrorOr<string> foo = result
    .Else(errors => $"发生了 {errors.Count} 个错误。");

ElseAsync

ErrorOr<string> foo = await result
    .ElseAsync(Task.FromResult("后备值"));
ErrorOr<string> foo = await result
    .ElseAsync(errors => Task.FromResult($"发生了 {errors.Count} 个错误。"));

混合使用功能 (Then, FailIf, Else, Switch, Match)

你可以混合使用 Then, FailIf, Else, SwitchMatch 方法。

ErrorOr<string> foo = await result
    .ThenDoAsync(val => Task.Delay(val))
    .FailIf(val => val > 2, Error.Validation(description: $"{val} 太大了"))
    .ThenDo(val => Console.WriteLine($"等待 {val} 秒完成。"))
    .ThenAsync(val => Task.FromResult(val * 2))
    .Then(val => $"结果是 {val}")
    .Else(errors => Error.Unexpected())
    .MatchFirst(
        value => value,
        firstError => $"发生错误: {firstError.Description}");

错误类型

每个 Error 实例都有一个 Type 属性,它是一个枚举值,表示错误的类型。

内置错误类型

以下是内置的错误类型:

public enum ErrorType
{
    Failure,
    Unexpected,
    Validation,
    Conflict,
    NotFound,
    Unauthorized,
    Forbidden,
}

每种错误类型都有一个静态方法来创建该类型的错误。例如:

var error = Error.NotFound();

可选地,你可以为错误传递代码、描述和元数据:

var error = Error.Unexpected(
    code: "User.ShouldNeverHappen",
    description: "永远不应该发生的用户错误",
    metadata: new Dictionary<string, object>
    {
        { "user", user },
    });

ErrorType 枚举是对错误进行分类的好方法。

自定义错误类型

如果你想以不同方式对错误进行分类,可以创建自己的错误类型。

可以使用 Custom 静态方法创建自定义错误类型

public static class MyErrorTypes
{
    const int ShouldNeverHappen = 12;
}

var error = Error.Custom(
    type: MyErrorTypes.ShouldNeverHappen,
    code: "User.ShouldNeverHappen",
    description: "永远不应该发生的用户错误");

你可以使用 Error.NumericType 方法来获取错误的数字类型。

var errorMessage = Error.NumericType switch
{
    MyErrorType.ShouldNeverHappen => "考虑更换开发团队",
    _ => "发生未知错误。",
};

内置结果类型 (Result.Success, ..)

有几种内置的结果类型:

ErrorOr<Success> result = Result.Success;
ErrorOr<Created> result = Result.Created;
ErrorOr<Updated> result = Result.Updated;
ErrorOr<Deleted> result = Result.Deleted;

可以如下使用

ErrorOr<Deleted> DeleteUser(Guid id)
{
    var user = await _userRepository.GetByIdAsync(id);
    if (user is null)
    {
        return Error.NotFound(description: "未找到用户。");
    }

    await _userRepository.DeleteAsync(user);
    return Result.Deleted;
}

组织错误

一个不错的方法是创建一个包含预期错误的静态类。例如:

public static partial class DivisionErrors
{
    public static Error CannotDivideByZero = Error.Unexpected(
        code: "Division.CannotDivideByZero",
        description: "不能除以零。");
}

之后可以这样使用 👇

public ErrorOr<float> Divide(int a, int b)
{
    if (b == 0)
    {
        return DivisionErrors.CannotDivideByZero;
    }

    return a / b;
}

Mediator + FluentValidation + ErrorOr 🤝

使用 MediatR 时的一种常见方法是使用 FluentValidation 在请求到达处理程序之前对其进行验证。

通常,验证是通过一个在请求无效时抛出异常的 Behavior 来完成的。

使用 ErrorOr,我们可以创建一个返回错误而不是抛出异常的 Behavior

当项目使用 ErrorOr 时,这种方式很好,因为调用 Mediator 的层,与项目中的其他组件类似,只需接收一个 ErrorOr 并相应地处理它。

这里是一个验证请求并在请求无效时返回错误的 Behavior 示例 👇

public class ValidationBehavior<TRequest, TResponse>(IValidator<TRequest>? validator = null)
    : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IRequest<TResponse>
        where TResponse : IErrorOr
{
    private readonly IValidator<TRequest>? _validator = validator;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (_validator is null)
        {
            return await next();
        }

        var validationResult = await _validator.ValidateAsync(request, cancellationToken);

        if (validationResult.IsValid)
        {
            return await next();
        }

        var errors = validationResult.Errors
            .ConvertAll(error => Error.Validation(
                code: error.PropertyName,
                description: error.ErrorMessage));

        return (dynamic)errors;
    }
}

贡献 🤲

如果你有任何问题、评论或建议,请开启一个 issue 或创建一个 pull request 🙂

致谢 🙏

  • OneOf - 一个很棒的库,为 C# 提供了 F# 风格的区分联合行为

许可证 🪪

本项目基于 MIT 许可证授权。

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号