[.NET] 控制流程,不使用例外中斷流程控制的寫法

Exception 是微軟預設中斷流程的手段,要控制好流程就要好好的處理例外,也就是寫 try catch,這會讓流程控制的程式碼看起來很凌亂。於是我需要幾個原則

  1. 被流程所呼叫的方法都要處理好 Exception/錯誤(根據需求場景,例如:找不到某個資料) 並回傳 Failure 物件
  2. 若有攔截到 Exception,Failure 物件要裝載 Exception 訊息,並且給予該例外正確的錯誤訊息。
  3. 盡量在最外層紀錄 Failure 日誌,例如,Controller、Application Layer
  4. Web API 回傳值不可以包含 Exception

以上是我的原則,在你的團隊裡面應該也有自己的錯誤處理機制;接下來,我用 ASP.NET Core Web API 實際的演練一遍

Copy from BingChat
程式設計中要考慮可能發生的錯誤,並採取適當的措施來避免或處理錯誤。流程控制不要用 Exception 的原因可能有以下幾點:

  • 例外狀況是一種非正常的流程控制,它會打斷程式的正常執行,並可能導致資源洩漏、效能下降或不可預期的結果。
  • 例外狀況是一種昂貴的操作,它會產生額外的開銷,例如堆疊追蹤、記憶體分配和垃圾收集。
  • 例外狀況是一種不易測試和偵錯的機制,它會使程式碼難以閱讀和維護,並可能隱藏真正的問題或邏輯錯誤。
  • 例外狀況是一種不符合商業邏輯的表達方式,它會使程式碼與商業需求脫節,並可能造成使用者體驗不佳或客戶損失。

因此,在程式設計中,流程控制不要用 Exception ,而是要使用其他更合適的錯誤處理方式,例如:

  • 回傳值或物件,表示操作的結果或狀態,例如成功、失敗、警告或訊息。
  • 使用布林值或列舉型別,表示操作是否成功或失敗,並提供相關的資訊或建議。
  • 使用委派或回呼函數,將錯誤處理的邏輯交由呼叫者決定,並提供足夠的彈性和可擴充性。
  • 使用事件或觀察者模式,將錯誤通知給感興趣的物件或元件,並讓它們自行決定如何回應

擲回例外狀況 - Framework Design Guidelines | Microsoft Learn

❌ 請盡量不要對一般控制流程使用例外狀況。

除了具有潛在競爭條件的系統失敗和作業之外,架構設計人員應該設計 API,讓使用者可以撰寫不會擲回例外狀況的程式碼。 例如,您可以提供一個在呼叫成員之前檢查前置條件的方法,協助使用者撰寫不會擲回例外狀況的程式碼。

開發環境

  • Windows 11
  • ASP.NET Core 7
  • Rider 2023.2

實作

在流程控制的方法,以前我可能會這樣寫

public async Task<GetMemberResult> GetMemberAsync(int memberId,
    CancellationToken cancel = default)
{
    try
    {
        //模擬發生例外
        throw new Exception("Member not found.");
    }
    catch (Exception e)
    {
        throw;
    }
}

 

現在我改成這樣 Tuple(Failure,T),例外既然都處理了,就不再 throw,明確的回傳失敗原因

public async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync(int memberId,
    CancellationToken cancel = default)
{
    try
    {
        if (memberId == 1)
        {
            //模擬找不到資料所回傳的失敗
            return (new Failure
            {
                Code = FailureCode.MemberNotFound,
                Message = "member not found.",
            }, null);
        }
        //模擬發生例外
        throw new Exception($"can not connect db.");
    }
    catch (Exception e)
    {
        return (new Failure
        {
            Code = FailureCode.DbError,
            Message = e.Message,
            Data = memberId,
            Exception = e,
        }, null);
    }
}

 

不喜歡 Tuple,也可以用一個類別裝載起來

public class GenericResult<T>
{
    public Failure Failure { get; set; }

    public T Data { get; set; }
}

 

Failure 定義如下:

  • Exception:不序列化(回傳不顯示)
  • TraceId:追蹤,用來串接整個服務的 Request 識別,最好是各個服務使用的 Id 都是相同的
  • Data:存放 Model Validation 錯誤或是 Request Body
public class Failure
{
    public Failure()
    {
    }

    public Failure(FailureCode code, string message)
    {
        this.Code = code;
        this.Message = message;
    }

    /// <summary>
    /// 錯誤碼
    /// </summary>
    public FailureCode Code { get; init; }

    /// <summary>
    /// 錯誤訊息
    /// </summary>
    public string Message { get; init; }

    /// <summary>
    /// 錯誤發生時的資料
    /// </summary>
    public object Data { get; init; }

    /// <summary>
    /// 追蹤 Id
    /// </summary>
    public string TraceId { get; set; }

    /// <summary>
    /// 例外,不回傳給 Web API 
    /// </summary>
    [JsonIgnore]
    public Exception Exception { get; set; }

    public List<Failure> Details { get; init; } = new();

    //用了 [JsonIgnore] 似乎就不需要它了 QQ,寫完了才想到可以 Ignore,不過這仍然可以適用在其他場景,例如 CLI、Console App
    public Failure WithoutException()
    {
        List<Failure> details = new();
        foreach (var detail in this.Details)
        {
            details.Add(this.WithoutException(detail));
        }

        return new Failure(this.Code, this.Message)
        {
            Data = this.Data,
            Details = details,
            TraceId = this.TraceId,
        };
    }

    public Failure WithoutException(Failure error)
    {
        var result = new Failure(error.Code, error.Message)
        {
            TraceId = error.TraceId
        };

        foreach (var detailError in this.Details)
        {
            // 遞迴處理 Details 屬性
            var detailResult = this.WithoutException(detailError);
            result.Details.Add(detailResult);
        }

        return result;
    }
}

 

  • 把 Failure 的處理集中在 FailureContent 方法
  • 用關鍵字來代表  FailureCode,對應到 HttpStatusCode,主要的目的是簡化配置。
public class GenericController : ControllerBase
{
    public Dictionary<FailureCode, int> FailureCodeLookup => s_failureCodeLookupLazy.Value;

    private static readonly Lazy<Dictionary<FailureCode, int>> s_failureCodeLookupLazy = new(CreateFailureCodeLookup);

    private static Dictionary<string, int> CreateFailureCodeMappings()
    {
        //用關鍵字定義錯誤代碼
        return new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase)
        {
            { "error", StatusCodes.Status500InternalServerError },
            { "invalid", StatusCodes.Status400BadRequest },
            { "notfound", StatusCodes.Status404NotFound },
            { "concurrency", StatusCodes.Status429TooManyRequests },
            { "conflict", StatusCodes.Status404NotFound },
        };
    }

    [NonAction]
    public FailureObjectResult FailureContent(Failure failure)
    {
        if (string.IsNullOrWhiteSpace(failure.TraceId))
        {
            failure.TraceId = Activity.Current?.Id ?? this.HttpContext.TraceIdentifier;
        }

        if (FailureCodeLookup.TryGetValue(failure.Code, out int statusCode))
        {
            return new FailureObjectResult(failure, statusCode);
        }

        return new FailureObjectResult(failure);
    }

    private static Dictionary<FailureCode, int> CreateFailureCodeLookup()
    {
        var result = new Dictionary<FailureCode, int>();
        var type = typeof(FailureCode);
        var names = Enum.GetNames(type);
        var failureMappings = CreateFailureCodeMappings();
        foreach (var name in names)
        {
            var failureCode = FailureCode.Parse<FailureCode>(name);
            var isDefined = false;
            foreach (var mapping in failureMappings)
            {
                var key = mapping.Key;
                var statusCode = mapping.Value;
                if (name.Contains(key, StringComparison.OrdinalIgnoreCase))
                {
                    isDefined = true;
                    result.Add(failureCode, statusCode);
                    break;
                }
            }

            if (isDefined == false)
            {
                result.Add(failureCode, StatusCodes.Status500InternalServerError);
            }
        }

        return result;
    }
}

 

FailureObjectResult 處理 Web API 回傳結果,預設 Failure 不會有 Exception資訊

public class FailureObjectResult : ObjectResult
{
    public FailureObjectResult(Failure failure, int statusCode = StatusCodes.Status400BadRequest)
        : base(failure)
    {
        this.StatusCode = statusCode;
        // Failure.Exception 已經使用 [JsonIgnore],不會再回傳給調用端
        // this.Value = failure.WithoutException();
        this.Value = failure.WithoutException();
    }
}

 

Controller 實作 GenericController 即可

public class MembersController : GenericController
{
   …
}

 

  • Log Failure:當 MemberService 有 Failure 時,紀錄完整錯誤,並回傳不包含 Exception 的結果
  • Log EventId:由開發者決定,這對分析 Log 會很有用
[Produces("application/json")]
[HttpPost("{memberId}/bind-cellphone", Name = "BindCellphone")]
[ProducesResponseType(typeof(Failure), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> Post(int memberId,
	BindCellphoneRequest request,
	CancellationToken cancel = default)
{
	request.MemberId = memberId;
	var bindCellphoneResult =
		await this._memberService.BindCellphoneAsync(request, cancel);
	if (bindCellphoneResult .Failure != null)
	{
		this._logger.LogInformation(500, "Bind cellphone failure:{@Failure}", bindCellphoneResult .Failure);
		return this.FailureContent(createMemberResult.Failure);
	}

	return this.NoContent();
}

 

當一個聚合方法依賴了其他的方法,可能會有各種不同狀況的錯誤,只要有一個點發生了錯誤,就立即返回,例如下列 BindCellphoneAsync 方法,只要有一個錯誤就立即中止

public class MemberService1
{
    private readonly IValidator<BindCellphoneRequest> _validator;

    public MemberService1(IValidator<BindCellphoneRequest> validator)
    {
        this._validator = validator;
    }

    //一個方法有多種可能的 Failure
    public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        var validationResult = await this._validator.ValidateAsync(request, cancel);
        if (validationResult.IsValid == false)
        {
            return (validationResult.ToFailure(), false);
        }

        //找不到會員
        var getMemberResult = await this.GetMemberAsync(request.MemberId, cancel);
        if (getMemberResult.Failure != null)
        {
            return (getMemberResult.Failure, false);
        }

        //手機格式無效
        var validateCellphoneResult = await this.ValidateCellphoneAsync(getMemberResult.Data.Cellphone, cancel);
        if (validateCellphoneResult.Failure != null)
        {
            return validateCellphoneResult;
        }

        //資料衝突,手機已經被綁定
        var saveChangeResult = await this.SaveChangeAsync(request, cancel);

        return saveChangeResult;
    }

    public async Task<(Failure Failure, bool Data)> SaveChangeAsync(BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        try
        {
            //模擬發生例外
            throw new DBConcurrencyException("insert data row concurrency error.");
        }
        catch (Exception e)
        {
            return (new Failure
            {
                Code = FailureCode.DataConcurrency,
                Message = e.Message,
                Exception = e,
                Data = request
            }, false);
        }
    }

    public async Task<(Failure Failure, bool Data)> ValidateCellphoneAsync(string cellphone,
        CancellationToken cancel = default)
    {
        return (new Failure
        {
            Code = FailureCode.CellphoneFormatInvalid,
            Message = "Cellphone format invalid.",
            Data = cellphone
        }, false);
    }

    public async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync(int memberId,
        CancellationToken cancel = default)
    {
        try
        {
            if (memberId == 1)
            {
                //模擬找不到資料所回傳的失敗
                return (new Failure
                {
                    Code = FailureCode.MemberNotFound,
                    Message = "member not found.",
                }, null);
            }
            
            //模擬發生例外
            throw new Exception($"can not connect db.");
        }
        catch (Exception e)
        {
            return (new Failure
            {
                Code = FailureCode.DbError,
                Message = e.Message,
                Data = memberId,
                Exception = e,
            }, null);
        }
    }

    //具有多個 Detail 的 Failure
    public async Task<(Failure Failure, bool Data)> CreateMemberAsync(CreateMemberRequest request,
        CancellationToken cancel = default)
    {
        var failure = new Failure()
        {
            Code = FailureCode.InputInvalid,
            Message = "view detail errors",
            Details = new List<Failure>()
            {
                new(code: FailureCode.InputInvalid, message: "Input invalid."),
                new(code: FailureCode.CellphoneFormatInvalid, message: "Cellphone format invalid."),
                new(code: FailureCode.DataConflict, message: "Member already exist."),
            }
        };
        return (failure, false);
    }
}

 

或者是,有錯誤不返回,繼續往下執行,常見的場景,Model Validation、Batch Process

public async Task<(Failure Failure, bool Data)> CreateMemberAsync(CreateMemberRequest request,
    CancellationToken cancel = default)
{
    var failure = new Failure()
    {
        Code = FailureCode.InputInvalid,
        Message = "view detail errors",
        Details = new List<Failure>()
        {
            new(code: FailureCode.InputInvalid, message: "Input invalid."),
            new(code: FailureCode.CellphoneFormatInvalid, message: "Cellphone format invalid."),
            new(code: FailureCode.DataConflict, message: "Member already exist."),
        }
    };
    return (failure, false);
}

 

用 Fluent Pattern 優化一下,把相關的動作收攏在 MemberWorkflow:

  1. 讓每一個方法都回傳 MemberWorkflow
  2. 進入方法之前會先檢查全域 Failure
  3. 但由於 Fluent Pattern 處理非同步會麻煩些,會有一堆 await,像是這樣
        var result =
            await (await (await (await this._workflow.ValidateModelAsync(request, cancel))
                    .GetMemberAsync(request.MemberId, cancel))
                .ValidateCellphone(request.Cellphone, cancel)).SaveChangeAsync(request, cancel);

 

所以,我改用 Then 把它們連接起來

public class MemberService2
{
    private MemberWorkflow _workflow;

    public MemberService2(MemberWorkflow workflow)
    {
        this._workflow = workflow;
    }

    //一個方法有多種可能的 Failure
    public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        var result = await _workflow.ValidateModelAsync(request, cancel)
                .Then(p => _workflow.GetMemberAsync(request.MemberId, cancel))
                .Then(p => _workflow.ValidateCellphone(request.Cellphone, cancel))
                .Then(p => _workflow.SaveChangeAsync(request, cancel))
            ;
        if (result.Failure != null)
        {
            return (result.Failure, false);
        }

        return (null, true);
    }

    public class MemberWorkflow
    {
        private readonly IValidator<BindCellphoneRequest> _validator;

        public MemberWorkflow(IValidator<BindCellphoneRequest> validator)
        {
            this._validator = validator;
        }

        public Failure Failure { get; private set; }

        public async Task<MemberWorkflow> ValidateModelAsync(BindCellphoneRequest request,
            CancellationToken cancel = default)
        {
            var validationResult = await this._validator.ValidateAsync(request, cancel);
            if (validationResult.IsValid == false)
            {
                this.Failure = validationResult.ToFailure();
            }

            return this;
        }

        public async Task<MemberWorkflow> SaveChangeAsync(BindCellphoneRequest request,
            CancellationToken cancel = default)
        {
            if (this.Failure != null)
            {
                return this;
            }

            try
            {
                //模擬發生例外
                throw new DBConcurrencyException("insert data row concurrency error.");
            }
            catch (Exception e)
            {
                this.Failure = new Failure
                {
                    Code = FailureCode.DataConcurrency,
                    Message = e.Message,
                    Exception = e,
                    Data = request
                };
            }

            return this;
        }

        public async Task<MemberWorkflow> ValidateCellphone(string cellphone,
            CancellationToken cancel = default)
        {
            if (this.Failure != null)
            {
                return this;
            }

            this.Failure = new Failure
            {
                Code = FailureCode.CellphoneFormatInvalid,
                Message = "Cellphone format invalid.",
                Data = cellphone
            };
            return this;
        }

        public async Task<MemberWorkflow> GetMemberAsync(int memberId,
            CancellationToken cancel = default)
        {
            if (this.Failure != null)
            {
                return this;
            }

            try
            {
                if (memberId == 1)
                {
                    this.Failure = new Failure
                    {
                        Code = FailureCode.MemberNotFound,
                        Message = "member not found.",
                    };
                    return this;
                }
                
                //模擬發生例外
                throw new Exception($"can not connect db.");
            }
            catch (Exception e)
            {
                this.Failure = new Failure
                {
                    Code = FailureCode.DbError,
                    Message = e.Message,
                    Data = memberId,
                    Exception = e,
                };
            }

            return this;
        }
    }
}

 

再次優化,Workflow 管理每一個步驟之間的狀態,感覺是多餘的,可以拔掉,然後用 WhenSuccess 連接上下。

public class MemberService3
{
    private readonly IValidator<BindCellphoneRequest> _validator;

    public MemberService3(IValidator<BindCellphoneRequest> validator)
    {
        this._validator = validator;
    }

    public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request,
        CancellationToken cancel = default) =>
        await this.ValidateModelAsync(request, cancel)
            .WhenSuccess(p => this.GetMemberAsync(request.MemberId, cancel))
            .WhenSuccess(p => this.ValidateCellphoneAsync(p.Cellphone, cancel))
            .WhenSuccess(p => this.SaveChangeAsync(request, cancel));

    public async Task<(Failure Failure, bool Data)> ValidateModelAsync(BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        var validationResult = await this._validator.ValidateAsync(request, cancel);
        if (validationResult.IsValid == false)
        {
            return (validationResult.ToFailure(), false);
        }

        return (null, true);
    }

    public async Task<(Failure Failure, bool Data)> SaveChangeAsync(BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        try
        {
            //模擬發生例外
            throw new DBConcurrencyException("insert data row concurrency error.");
        }
        catch (Exception e)
        {
            return (new Failure
            {
                Code = FailureCode.DataConcurrency,
                Message = e.Message,
                Exception = e,
                Data = request
            }, false);
        }
    }

    public async Task<(Failure Failure, bool Data)> ValidateCellphoneAsync(string cellphone,
        CancellationToken cancel = default)
    {
        return (new Failure
        {
            Code = FailureCode.CellphoneFormatInvalid,
            Message = "Cellphone format invalid.",
            Data = cellphone
        }, false);
    }

    public async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync(int memberId,
        CancellationToken cancel = default)
    {
        try
        {
            if (memberId == 1)
            {
                return (new Failure
                {
                    Code = FailureCode.MemberNotFound,
                    Message = "member not found.",
                }, null);
            }
            
            //模擬發生例外
            throw new Exception($"can not connect db.");
        }
        catch (Exception e)
        {
            return (new Failure
            {
                Code = FailureCode.DbError,
                Message = e.Message,
                Data = memberId,
                Exception = e,
            }, null);
        }
    }
}

 

連接 Task 的擴充方法

public static class SecondStepExtensions
{
    /// <summary>
    /// 接續執行第二個方法
    /// </summary>
    /// <param name="first"></param>
    /// <param name="second"></param>
    /// <typeparam name="TSource"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <returns></returns>
    public static async Task<TResult> Then<TSource, TResult>(this Task<TSource> first,
        Func<TSource, Task<TResult>> second)
    {
        return await second(await first.ConfigureAwait(false)).ConfigureAwait(false);
    }

    /// <summary>
    /// 接續第二個方法,第一個方法有錯誤時,不執行第二個方法
    /// </summary>
    /// <param name="first"></param>
    /// <param name="second"></param>
    /// <typeparam name="TSource"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <returns></returns>
    public static async Task<(Failure Failure, TResult Data)> WhenSuccess<TSource, TResult>(
        this Task<(Failure Failure, TSource Data)> first,
        Func<TSource, Task<(Failure, TResult)>> second)
    {
        var result = await first.ConfigureAwait(false);
        if (result.Failure != null)
        {
            return (result.Failure, default(TResult));
        }

        return await second(result.Data).ConfigureAwait(false);
    }
}

 

既然每一個方法的合約都一樣了,Tuple(Failure,T),也已經知道要做甚麼事了,那麼就直接給他具名的方法名稱,捨棄 WhenSuccess+委派

static class MemberWorkflowExtensions
{
    public static async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync<TSource>(
        this Task<(Failure Failure, TSource Data)> previousStep,
        int memberId,
        CancellationToken cancel = default)
    {
        try
        {
            var previousStepResult = await previousStep;
            if (previousStepResult.Failure != null)
            {
                return (previousStepResult.Failure, null);
            }
            
            //模擬發生例外
            throw new Exception($"can not connect db.");
        }
        catch (Exception e)
        {
            return (new Failure
            {
                Code = FailureCode.DbError,
                Message = e.Message,
                Data = memberId,
                Exception = e,
            }, null);
        }
    }

    public static async Task<(Failure Failure, bool Data)> SaveChangeAsync<TSource>(
        this Task<(Failure Failure, TSource Data)> previousStep,
        BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        try
        {
            var previousStepResult = await previousStep;
            if (previousStepResult.Failure != null)
            {
                return (previousStepResult.Failure, false);
            }
            
            //模擬發生例外
            throw new DBConcurrencyException("insert data row concurrency error.");
        }
        catch (Exception e)
        {
            return (new Failure
            {
                Code = FailureCode.DataConcurrency,
                Message = e.Message,
                Exception = e,
                Data = request
            }, false);
        }
    }

    public static async Task<(Failure Failure, bool Data)> ValidateCellphoneAsync<TSource>(
        this Task<(Failure Failure, TSource Data)> previousStep,
        string cellphone,
        CancellationToken cancel = default)
    {
        var previousStepResult = await previousStep;
        if (previousStepResult.Failure != null)
        {
            return (previousStepResult.Failure, false);
        }

        return (new Failure
        {
            Code = FailureCode.CellphoneFormatInvalid,
            Message = "Cellphone format invalid.",
            Data = cellphone
        }, false);
    }
}

 

最後,調用端的寫法長的就像這樣

很明顯的,這方法沒有辦法直接在 Method 傳遞上下文,必須要把狀態記錄在全域變數,這不見得是最好的方法,提供另一種思路讓大家選擇

 

完整代碼如下

public class MemberService4
{
    private readonly IValidator<BindCellphoneRequest> _validator;

    public MemberService4(IValidator<BindCellphoneRequest> validator)
    {
        this._validator = validator;
    }

    //一個方法有多種可能的 Failure
    public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        var executeResult = await this.ValidateModelAsync(request, cancel)
            .GetMemberAsync(request.MemberId, cancel)
            .ValidateCellphoneAsync(request.Cellphone, cancel)
            .SaveChangeAsync(request, cancel);
        if (executeResult.Failure != null)
        {
            return (executeResult.Failure, false);
        }

        return (null, true);
    }

    public async Task<(Failure Failure, bool Data)> ValidateModelAsync(BindCellphoneRequest request,
        CancellationToken cancel = default)
    {
        var validationResult = await this._validator.ValidateAsync(request, cancel);
        if (validationResult.IsValid == false)
        {
            return (validationResult.ToFailure(), false);
        }

        return (null, true);
    }
}

我個人比較喜歡上一個方法,MemberService3 的寫法

 

實際運行一下,這幾個寫法的運行結果都一樣,隨便挑一個來跑,模擬找不到會員流程,如下圖:

 

模擬例外發生,確定 Exception 欄位沒有出現,[JsonIgnore] 真的有正常的工作,如下圖:

 

Log 則是完整記錄 Exception,若擔心 Log 記錄了機敏性資料,一樣可以加工處理。

 

心得

雖然說 throw exception,可以快速地替我們中斷工作流程,但是一旦需要複雜控制時,它用起來又非常的彆扭,或許 catch 例外後返回一個 Failure 是一個不錯的選擇,這也是我目前正在做的事,或許你也可以試試看。上面幾種方法裡, 個人認為 WhenSuccess + 委派是最有彈性的,缺點就是需要多一個連接詞,讀起來沒有那麼直觀。

參考其他語言的經驗,下圖是 Rust 的返回結果值,左邊是 Err,右邊是結果。

流程的控制,換成 WhenSuccess + 委派 讀起來的確是舒服了許多;實務上,你要視狀況來決定要拋 exception,或是 catch,不要一股腦都用相同的做法,目前我的作法是底層維持 throw,流程控制再依狀況處理例外

範例位置

sample.dotblog/Error Handler/Without Exception/Lab.ErrorHandler.API at e9f4ad712811e81f35e6bb489b333f06b867a138 · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo