ASP.NET Core集成Result Pattern

在ASP.NET Core中集成Result Pattern
csharp
design-pattern
openapi
Author

XU HUI

Published

August 9, 2025

去年读过 blog 这篇博客就实现了一个简单的Result Pattern,但怎么说呢,在AspNetCore中用起来并不流畅,于是考虑给做一个集成。

去年版本

这个版本单纯在railway oriented programming角度来看是够用了的,但假如要在AspNetCore中使用,既不能直接返回Result类型,也无法自动处理错误,就颇为无力。

using System.Diagnostics.CodeAnalysis;

namespace OALY2000.Results;

public class Result<T>
{
    public T? Value { get; private init; }
    public Exception? Error { get; private init; }

    private Result(T value)
    {
        IsSuccess = true;
        Value = value;
        Error = null;
    }

    private Result(Exception error)
    {
        IsSuccess = false;
        Value = default;
        Error = error;
    }

    [MemberNotNullWhen(true, nameof(Value))]
    [MemberNotNullWhen(false, nameof(Error))]
    public bool IsSuccess { get; private init; }

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Fail(Exception error) => new(error);

    public static implicit operator Result<T>(T value) => Success(value);
    public static implicit operator Result<T>(Exception error) => Fail(error);
}


public static class ResultExtensions
{
    public static Result<TReturn> Switch<T, TReturn>(this Result<T> source, Func<T, Result<TReturn>> onSuccess, Func<Exception, Result<TReturn>> onFailure)
    {
        if (source.IsSuccess) return onSuccess(source.Value);
        else return onFailure(source.Error);
    }

    public static Result<TResult> Select<TFrom, TResult>(this Result<TFrom> source, Func<TFrom, TResult> selector)
    {
        return source.Switch(r => selector(r), Result<TResult>.Fail);
    }

    public static Result<TResult> SelectMany<TSource, TMiddle, TResult>(
        this Result<TSource> source,
        Func<TSource, Result<TMiddle>> collectionSelector,
        Func<TSource, TMiddle, TResult> resultSelector)
    {
        if (source.IsSuccess) return collectionSelector(source.Value).Select(v => resultSelector(source.Value!, v));
        else return source.Error;
    }

    public static async Task<Result<TResult>> Select<TFrom, TResult>(this Task<Result<TFrom>> source, Func<TFrom, TResult> selector)
        => (await source).Select(selector);

    public static async Task<Result<TResult>> SelectMany<TSource, TMiddle, TResult>(
        this Task<Result<TSource>> source,
        Func<TSource, Task<Result<TMiddle>>> collectionSelector,
        Func<TSource, TMiddle, TResult> resultSelector)
    {
        var result = await source;

        if (result.IsSuccess) return (await collectionSelector(result.Value)).Select(v => resultSelector(result.Value!, v));
        else return result.Error;
    }
}

集成ASP.NET Core

一开始的时候我希望是通过Filter来实现,但始终不理想,而后我想到了Minimal Api中有这样的用法:

app.MapGet("/", () => 
{
    Result<string> result = GetSomething();

    if (result.IsSuccess)
    {
        return result.Value;
    }
    else 
    {
        throw result.Error;
    }
});

app.MapGet("/alternative", IResult () => 
{
    Result<string> result = GetSomething();

    if (result.IsSuccess)
    {
        return TypedResults.Ok(result.Value)
    }
    else 
    {
        return TypedResults.Problem(result.Error.Message)
    }
});

显然TypedResultsIResult对AspNetCore来说颇为特殊,于是我翻阅了其源码,IResult.ExecuteAsync 方法定义了如何响应的问题,如此一来便可以在端点直接返回Result对象并集中做错误处理了。

IEndpointMetadataProvider接口则定义了openapi/swagger所需的信息,居然顺道把swagger集成也做了。

public class Result<T> : IResult, IEndpointMetadataProvider
{
    // ...

    public Task ExecuteAsync(HttpContext httpContext)
    {
        if (IsSuccess)
        {
            return TypedResults.Ok(Value).ExecuteAsync(httpContext);
        }

        if (httpContext.RequestServices.GetService<IFailedResultHandler>() is { } handler)
        {
            return handler.ExecuteAsync(httpContext, Error);
        }

        return TypedResults.Problem(title: Error.Message).ExecuteAsync(httpContext);
    }

    static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        ArgumentNullException.ThrowIfNull(method);
        ArgumentNullException.ThrowIfNull(builder);

        builder.Metadata.Add(new ProducesResponseTypeMetadata(200, typeof(T), ["application/json"]));
    }
}

public interface IFailedResultHandler
{
    Task ExecuteAsync(HttpContext context, Exception error);
}

现在,我们可以在接口直接返回Result对象了:

Result<string> GetSomething()
{
    var ran = new Random();
    
    if (ran.Next(0, 10) > 5) return "success";

    return new Exception("failed");
}

app.MapGet("/", () => GetSomething());