首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >告别异常处理:用Result模式和Discriminated Union打造优雅的C#错误处理机制

告别异常处理:用Result模式和Discriminated Union打造优雅的C#错误处理机制

作者头像
郑子铭
发布2025-08-06 17:42:11
发布2025-08-06 17:42:11
6500
代码可运行
举报
运行总次数:0
代码可运行

假设你有一段根据输入参数返回不同结果的代码——这很常见。

有几种方法可以实现这个需求。为了说明我的意思,假设你有以下模型:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class Note
{
    public Guid Id { get; set; }

    public string Title { get; set; } = null!;

    public Guid UserId { get; set; }

    public DateTimeOffset CreatedAt { get; set; }

    public DateTimeOffset? UpdatedAt { get; set; }
};

这是一个尝试更新该模型的代码示例:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class UpdateNoteHandler(INoteRepository noteRepository)
{
    public async Task InvokeAsync(
        Guid noteId,
        string? title,
        Guid userId)
    {
        var note = await noteRepository.GetNoteOrNullAsync(noteId);

        if (note == null)
        {
            thrownew InvalidOperationException("Note was not found.");
        }

        if (note.UserId != userId)
        {
            thrownew InvalidOperationException("Forbidden.");
        }

        if (string.IsNullOrWhiteSpace(title))
        {
            thrownew InvalidOperationException("Invalid input.");
        }

        note.Title = title;
        note.UpdatedAt = DateTimeOffset.UtcNow;

        await noteRepository.UpdateNoteAsync(note);
    }
}

这个处理程序会为类中定义的每个错误抛出异常。

你还有一个中间件可以捕获所有异常并向用户返回适当的响应。

但过了一段时间后,你决定通过引入Result类型来重构代码,以避免使用异常——因为异常本应用于特殊情况。

假设你创建了这样的东西:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class Result
{
    public bool IsSuccess => string.IsNullOrWhiteSpace(Error);
    
    public string? Error { get; set; }
}

之后,你将方法更新为这样:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class UpdateNoteHandler(INoteRepository noteRepository)
{
    public async Task<Result> InvokeAsync(
        Guid noteId,
        string? title,
        Guid userId)
    {
        var note = await noteRepository.GetNoteOrNullAsync(noteId);

        if (note == null)
        {
            returnnew Result
            {
                Error = "Note was not found."
            };
        }

        if (note.UserId != userId)
        {
            returnnew Result
            {
                Error = "Forbidden."
            };
        }

        if (string.IsNullOrWhiteSpace(title))
        {
            returnnew Result
            {
                Error = "Invalid input."
            };
        }

        note.Title = title;
        note.UpdatedAt = DateTimeOffset.UtcNow;

        await noteRepository.UpdateNoteAsync(note);

        returnnew Result();
    }
}

但目前很难理解每个错误代表什么。因此,你希望根据错误类型对Result进行分类。

为此,你可能会引入一个ErrorType并像这样更新Result:

代码语言:javascript
代码运行次数:0
运行
复制
public enum ErrorType
{
    NotFound,
    Forbidden,
    InvalidInput
}

publicsealedclassResult
{
    publicbool IsSuccess => string.IsNullOrWhiteSpace(Error);

    publicstring? Error { get; set; }

    public ErrorType? ErrorType { get; set; }
}

然后,你将方法更新为这样:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class UpdateNoteHandler(INoteRepository noteRepository)
{
    public async Task<Result> InvokeAsync(
    Guid noteId,
    string? title,
    Guid userId)
    {
        var note = await noteRepository.GetNoteOrNullAsync(noteId);

        if (note == null)
        {
            returnnew Result
            {
                Error = "Note was not found.",
                ErrorType = ErrorType.NotFound
            };
        }

        if (note.UserId != userId)
        {
            returnnew Result
            {
                Error = "Forbidden.",
                ErrorType = ErrorType.Forbidden
            };
        }

        if (string.IsNullOrWhiteSpace(title))
        {
            returnnew Result
            {
                Error = "Invalid input.",
                ErrorType = ErrorType.InvalidInput
            };
        }

        note.Title = title;
        note.UpdatedAt = DateTimeOffset.UtcNow;

        await noteRepository.UpdateNoteAsync(note);

        returnnew Result();
    }
}

这个方法有效——但只到一定程度。

假设你想为笔记未找到的情况添加一个额外属性。

但该怎么做?你应该在通用的Result中引入一个新属性吗?

像这样:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class Result
{
    public bool IsSuccess => string.IsNullOrWhiteSpace(Error);

    public string? Error { get; set; }

    public ErrorType? ErrorType { get; set; }

    public Guid? NoteId { get; set; } // <-- 用于附加数据的新属性
}

嗯...如果需要添加更多额外数据呢?

这时Result会变得混乱且难以使用——你现在还必须检查额外数据。

可能会变成这样:

代码语言:javascript
代码运行次数:0
运行
复制
var result = await hanlder.InvokeAsync();

if(!result.IsSuccess &&
   result.ErrorType == ErrorType.NotFound &&
   result.NoteId.HasValue)
{
    return Results.NoteFound($"There's not such note with id `{result.NoteId.Value}`");
}

这相当烦人——但我很高兴告诉你有一个更好的方法来改进这段代码。

首先,让我们创建一个Reply类——它将作为我们处理程序结果的基类。

代码语言:javascript
代码运行次数:0
运行
复制
public class Reply;

现在,让我们为处理程序中的每种情况引入特定的Reply类型:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class NotFoundReply(Guid noteId) : Reply
{
    public Guid NoteId { get; } = noteId;
}

publicsealedclassForbiddenReply : Reply;

publicsealedclassEmptyTitleReply : Reply;

publicsealedclassSuccessReply : Reply;

如你所见,你的类中不再需要ErrorType枚举或Error属性——类型本身已经告诉你服务返回了哪种回复,这非常酷。

更棒的是,你可以只为特定情况扩展回复类所需的数据——就像NotFoundReply所做的那样。

很酷,不是吗?

但让我们回到我们离开的地方。

现在我要更新我们的处理程序——它将看起来像这样:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class UpdateNoteHandler(INoteRepository noteRepository)
{
    public async Task<Reply> InvokeAsync(
        Guid noteId,
        string? title,
        Guid userId)
    {
        var note = await noteRepository.GetNoteOrNullAsync(noteId);

        if (note == null)
        {
            returnnew NotFoundReply(noteId);
        }

        if (note.UserId != userId)
        {
            returnnew ForbiddenReply();
        }

        if (string.IsNullOrWhiteSpace(title))
        {
            returnnew EmptyTitleReply();
        }

        note.Title = title;
        note.UpdatedAt = DateTimeOffset.UtcNow;

        await noteRepository.UpdateNoteAsync(note);

        returnnew SuccessReply();
    }
}

这已经比之前使用Result的方法好多了——但我们还能做得更好。

更好?有什么能比多态方法更好?它有什么问题?

问题在于使用多态方法时,我们无法真正控制具体的类型。任何人都可以轻松添加Reply的新子类,而忘记在代码中的某处正确处理它。

为了避免这种情况,让我们引入一个Discriminated Union(可区分联合),并将我们的Reply重构为DU。

代码语言:javascript
代码运行次数:0
运行
复制
public abstractclassUpdateNoteReply
{
    private UpdateNoeReply()
    {
    }  

    public sealed class NotFoundReply(Guid noteId) : UpdateNoeReply
    {
        public Guid NoteId { get; } = noteId;
    }

    publicsealedclassForbidden : UpdateNoteReply;

    publicsealedclassEmptyTitle : UpdateNoteReply;

    publicsealedclassSuccess : UpdateNoteReply;
}

现在我们获得了使用Discriminated Union的一些优势:

  • • 更容易推理所有可能的状态
  • • 设计上就是不可变的

现在你的处理程序将如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
public sealed class UpdateNoteHandler4(INoteRepository noteRepository)
{
    public async Task<UpdateNoteReply> InvokeAsync(
        Guid noteId,
        string? title,
        Guid userId)
    {
        var note = await noteRepository.GetNoteOrNullAsync(noteId);

        if (note == null)
        {
            returnnew UpdateNoteReply.NotFound(noteId);
        }

        if (note.UserId != userId)
        {
            returnnew  UpdateNoteReply.Forbidden();
        }

        if (string.IsNullOrWhiteSpace(title))
        {
            returnnew UpdateNoteReply.EmptyTitle();
        }

        note.Title = title;
        note.UpdatedAt = DateTimeOffset.UtcNow;

        await noteRepository.UpdateNoteAsync(note);

        returnnew UpdateNoteReply.Success();
    }
}

之后,你也可以轻松更新你的端点:

代码语言:javascript
代码运行次数:0
运行
复制
app.MapPost("notes/{id:guid}", async (
    [FromServices] UpdateNoteHandler handler,
    [FromQuery] Guid id) =>
{
    var reply = await handler.InvokeAsync(id, title: "", userId: Guid.Empty);

    return reply switch
    {
        UpdateNoteReply.NotFound notFound => Results.NotFound(notFound.NoteId),
        UpdateNoteReply.EmptyTitle => Results.BadRequest(),
        UpdateNoteReply.Forbidden => Results.Forbid(),
        _ => Results.Ok()
    };
});

通过使用Discriminated Union,我们获得了:

  • • 干净可读的回复结构
  • • 能够使用switch语句
  • • 仅在真正需要的地方添加额外数据

你怎么看?你在服务中如何返回不同的回复?

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档