From 7a0670a0d97e446d2375ff94f4ea23f34f6a7d66 Mon Sep 17 00:00:00 2001 From: Jason Barden Date: Sat, 11 Jul 2026 21:02:53 +0100 Subject: [PATCH 1/2] test(functional): add failing tests for Result OrElseAsync fallback operator Co-Authored-By: Claude Fable 5 --- .../GivenOrElseAsync.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/core/functional/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenOrElseAsync.cs diff --git a/packages/core/functional/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenOrElseAsync.cs b/packages/core/functional/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenOrElseAsync.cs new file mode 100644 index 00000000..cbe9f250 --- /dev/null +++ b/packages/core/functional/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenOrElseAsync.cs @@ -0,0 +1,60 @@ +using Shouldly; +using Xunit; + +namespace AStar.Dev.FunctionalParadigm.Tests.Unit; + +public class GivenOrElseAsync +{ + [Fact] + public async Task when_result_is_success_then_returns_original_without_invoking_fallback() + { + var resultTask = Task.FromResult(Result.Success(42)); + bool invoked = false; + + var actual = await resultTask.OrElseAsync(_ => + { + invoked = true; + + return Task.FromResult(Result.Success(0)); + }); + + actual.ShouldBe(new Ok(42)); + invoked.ShouldBeFalse(); + } + + [Fact] + public async Task when_result_is_failure_then_returns_fallback_result() + { + var resultTask = Task.FromResult(Result.Failure("first failed")); + + var actual = await resultTask.OrElseAsync(_ => Task.FromResult(Result.Success(7))); + + actual.ShouldBe(new Ok(7)); + } + + [Fact] + public async Task when_result_is_failure_then_fallback_receives_original_error() + { + var resultTask = Task.FromResult(Result.Failure("original error")); + string? receivedError = null; + + _ = await resultTask.OrElseAsync(error => + { + receivedError = error; + + return Task.FromResult(Result.Success(1)); + }); + + receivedError.ShouldBe("original error"); + } + + [Fact] + public async Task when_result_and_fallback_both_fail_then_returns_fallback_failure() + { + var resultTask = Task.FromResult(Result.Failure("first failed")); + + var actual = await resultTask.OrElseAsync(_ => Task.FromResult(Result.Failure("fallback failed"))); + + actual.ShouldBe(new Fail("fallback failed")); + } +} From 7bd91dd826fe0b830e934f233eb2f7772f84b04d Mon Sep 17 00:00:00 2001 From: Jason Barden Date: Sat, 11 Jul 2026 21:12:11 +0100 Subject: [PATCH 2/2] refactor(scraper): replace await-then-Match with OrElseAsync/BindAsync operators Adds OrElseAsync fallback operator to AStar.Dev.FunctionalParadigm and uses it to remove the last await-then-Match patterns from TopWallpapersWorkflow, SubscriptionsWorkflow and SearchWorkflow. Closes #725. Relates to #729. Co-Authored-By: Claude Fable 5 --- .../Workflows/SearchWorkflow.cs | 9 ++++----- .../Workflows/SubscriptionsWorkflow.cs | 9 +++------ .../Workflows/TopWallpapersWorkflow.cs | 9 +++------ .../AStar.Dev.FunctionalParadigm/ResultExtensions.cs | 8 ++++++++ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SearchWorkflow.cs b/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SearchWorkflow.cs index 0e989385..c816c6d9 100644 --- a/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SearchWorkflow.cs +++ b/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SearchWorkflow.cs @@ -22,17 +22,16 @@ public Task> RunAsync(CancellationToken cancellationTo private async Task> ProcessSearchCategoriesAsync(IReadOnlyList searchCategories, CancellationToken cancellationToken) { + Result outcome = Unit.Value; + foreach (var searchCategory in searchCategories) { cancellationToken.ThrowIfCancellationRequested(); - var categoryResult = await ProcessSearchCategoryAsync(searchCategory, cancellationToken).ConfigureAwait(false); - bool categoryFailed = categoryResult.Match(_ => false, _ => true); - - if (categoryFailed) return categoryResult; + outcome = await outcome.BindAsync(_ => ProcessSearchCategoryAsync(searchCategory, cancellationToken)).ConfigureAwait(false); } - return Unit.Value; + return outcome; } private Task> ProcessSearchCategoryAsync(Category searchCategory, CancellationToken cancellationToken) diff --git a/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SubscriptionsWorkflow.cs b/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SubscriptionsWorkflow.cs index 219555cc..e84556ac 100644 --- a/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SubscriptionsWorkflow.cs +++ b/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/SubscriptionsWorkflow.cs @@ -29,12 +29,9 @@ private async Task> RunSubscriptionsAsync(Cancellation } private async Task LoadStartingPageAsync() - { - var loadResult = await subscriptionsImagesListPage.LoadSubscriptionResultsPageAsync(FirstPageNumber).ConfigureAwait(false); - bool loadedSuccessfully = loadResult.Match(_ => true, _ => false); - - if (!loadedSuccessfully) _ = await subscriptionsImagesListPage.LoadSubscriptionResultsPageAsync(FirstPageNumber).ConfigureAwait(false); - } + => _ = await subscriptionsImagesListPage.LoadSubscriptionResultsPageAsync(FirstPageNumber) + .OrElseAsync(_ => subscriptionsImagesListPage.LoadSubscriptionResultsPageAsync(FirstPageNumber)) + .ConfigureAwait(false); private async Task> ProcessSubscriptionsAsync(PageInfo pageInfo, CancellationToken cancellationToken) { diff --git a/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/TopWallpapersWorkflow.cs b/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/TopWallpapersWorkflow.cs index 3359e1e4..307d8d51 100644 --- a/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/TopWallpapersWorkflow.cs +++ b/apps/desktop/Scraper/AStar.Dev.Wallpaper.Scraper/Workflows/TopWallpapersWorkflow.cs @@ -33,12 +33,9 @@ private async Task> RunTopWallpapersAsync(Cancellation } private async Task LoadStartingPageAsync(int startingPageNumber) - { - var loadResult = await topWallpapersPage.LoadTopWallpapersPageAsync(startingPageNumber).ConfigureAwait(false); - bool loadedSuccessfully = loadResult.Match(_ => true, _ => false); - - if (!loadedSuccessfully) _ = await topWallpapersPage.LoadTopWallpapersPageAsync(FirstPageNumber).ConfigureAwait(false); - } + => _ = await topWallpapersPage.LoadTopWallpapersPageAsync(startingPageNumber) + .OrElseAsync(_ => topWallpapersPage.LoadTopWallpapersPageAsync(FirstPageNumber)) + .ConfigureAwait(false); private async Task> ProcessTopWallpapersAsync(SearchConfiguration searchConfiguration, CancellationToken cancellationToken) { diff --git a/packages/core/functional/AStar.Dev.FunctionalParadigm/ResultExtensions.cs b/packages/core/functional/AStar.Dev.FunctionalParadigm/ResultExtensions.cs index 78f6c0c1..24252cec 100644 --- a/packages/core/functional/AStar.Dev.FunctionalParadigm/ResultExtensions.cs +++ b/packages/core/functional/AStar.Dev.FunctionalParadigm/ResultExtensions.cs @@ -107,6 +107,14 @@ public static Result Bind(this Result _ => throw new InvalidOperationException("Unexpected result type.") }; + public static async Task> OrElseAsync(this Task> resultTask, Func>> fallback) + => await resultTask.ConfigureAwait(false) switch + { + Ok ok => ok, + Fail fail => await fallback(fail.Error).ConfigureAwait(false), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + public static Result Ensure(this Result result, Action finallyAction) { finallyAction();