diff --git a/src/LuYao.Common/Guard.cs b/src/LuYao.Common/Guard.cs new file mode 100644 index 0000000..9d6e208 --- /dev/null +++ b/src/LuYao.Common/Guard.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading.Tasks; + +namespace LuYao; + +/// +/// Provides guarded execution helpers to reduce try/catch boilerplate +/// while keeping behavior observable and configurable. +/// +public static class Guard +{ + /// + /// Executes an action with guarded exception handling. + /// + /// The action to execute. + /// An optional callback invoked when a handled exception is caught. + /// An optional predicate to determine whether an exception should be handled. + /// If , rethrows handled exceptions after invoking . + /// when execution succeeds; otherwise for handled exceptions. + public static bool TryRun(Action action, Action? onError = null, Func? when = null, bool rethrow = false) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + try + { + action(); + return true; + } + catch (Exception ex) when (CanHandle(ex, when)) + { + onError?.Invoke(ex); + if (rethrow) throw; + return false; + } + } + + /// + /// Executes an asynchronous action with guarded exception handling. + /// + /// The asynchronous action to execute. + /// An optional callback invoked when a handled exception is caught. + /// An optional predicate to determine whether an exception should be handled. + /// If , rethrows handled exceptions after invoking . + /// A task that returns when execution succeeds; otherwise for handled exceptions. + public static async Task TryRunAsync(Func action, Action? onError = null, Func? when = null, bool rethrow = false) + { + try + { + await action(); + return true; + } + catch (Exception ex) when (CanHandle(ex, when)) + { + onError?.Invoke(ex); + if (rethrow) throw; + return false; + } + } + + /// + /// Executes a function and returns its result, or a fallback value when a handled exception occurs. + /// + /// The result type. + /// The function to execute. + /// The fallback value returned when a handled exception occurs. + /// An optional callback invoked when a handled exception is caught. + /// An optional predicate to determine whether an exception should be handled. + /// If , rethrows handled exceptions after invoking . + /// The function result when successful; otherwise for handled exceptions. + public static T TryGet(Func func, T fallback = default!, Action? onError = null, Func? when = null, bool rethrow = false) + { + if (func == null) throw new ArgumentNullException(nameof(func)); + + try + { + return func(); + } + catch (Exception ex) when (CanHandle(ex, when)) + { + onError?.Invoke(ex); + if (rethrow) throw; + return fallback; + } + } + + /// + /// Executes an asynchronous function and returns its result, + /// or a fallback value when a handled exception occurs. + /// + /// The result type. + /// The asynchronous function to execute. + /// The fallback value returned when a handled exception occurs. + /// An optional callback invoked when a handled exception is caught. + /// An optional predicate to determine whether an exception should be handled. + /// If , rethrows handled exceptions after invoking . + /// A task that returns the function result when successful; otherwise for handled exceptions. + public static async Task TryGetAsync(Func> func, T fallback = default!, Action? onError = null, Func? when = null, bool rethrow = false) + { + if (func == null) throw new ArgumentNullException(nameof(func)); + + try + { + return await func(); + } + catch (Exception ex) when (CanHandle(ex, when)) + { + onError?.Invoke(ex); + if (rethrow) throw; + return fallback; + } + } + + private static bool CanHandle(Exception ex, Func? when) + { + if (IsCritical(ex)) return false; + if (when == null) return true; + return when(ex); + } + + private static bool IsCritical(Exception ex) + { + return ex is OperationCanceledException + || ex is OutOfMemoryException + || ex is StackOverflowException + || ex is AccessViolationException + || ex is AppDomainUnloadedException + || ex is BadImageFormatException + || ex is CannotUnloadAppDomainException + || ex is InvalidProgramException; + } +} \ No newline at end of file diff --git a/tests/LuYao.Common.UnitTests/GuardTests.cs b/tests/LuYao.Common.UnitTests/GuardTests.cs new file mode 100644 index 0000000..18cf131 --- /dev/null +++ b/tests/LuYao.Common.UnitTests/GuardTests.cs @@ -0,0 +1,95 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LuYao; + +[TestClass] +public class GuardTests +{ + [TestMethod] + public void TryRun_WhenActionSucceeds_ReturnsTrue() + { + bool result = Guard.TryRun(() => { }); + + Assert.IsTrue(result); + } + + [TestMethod] + public void TryRun_WhenActionThrows_ReturnsFalseAndInvokesOnError() + { + Exception? captured = null; + + bool result = Guard.TryRun(() => throw new InvalidOperationException("boom"), ex => captured = ex); + + Assert.IsFalse(result); + Assert.IsNotNull(captured); + Assert.IsInstanceOfType(captured); + } + + [TestMethod] + public void TryRun_WhenRethrowIsTrue_RethrowsHandledException() + { + Assert.Throws(() => + Guard.TryRun(() => throw new InvalidOperationException("boom"), rethrow: true)); + } + + [TestMethod] + public void TryRun_WhenFilterReturnsFalse_DoesNotHandleException() + { + Assert.Throws(() => + Guard.TryRun(() => throw new InvalidOperationException("boom"), when: _ => false)); + } + + [TestMethod] + public void TryRun_WhenOperationCanceledExceptionThrown_DoesNotHandleException() + { + Assert.Throws(() => + Guard.TryRun(() => throw new OperationCanceledException())); + } + + [TestMethod] + public void TryGet_WhenFuncThrows_ReturnsFallback() + { + int value = Guard.TryGet(() => throw new InvalidOperationException("boom"), fallback: 42); + + Assert.AreEqual(42, value); + } + + [TestMethod] + public async Task TryRunAsync_WhenActionThrows_ReturnsFalseAndInvokesOnError() + { + Exception? captured = null; + + bool result = await Guard.TryRunAsync( + async () => + { + await Task.Yield(); + throw new InvalidOperationException("boom"); + }, + ex => captured = ex); + + Assert.IsFalse(result); + Assert.IsNotNull(captured); + Assert.IsInstanceOfType(captured); + } + + [TestMethod] + public async Task TryRunAsync_WhenOperationCanceledExceptionThrown_DoesNotHandleException() + { + await Assert.ThrowsAsync(async () => + await Guard.TryRunAsync(async () => + { + await Task.Yield(); + throw new OperationCanceledException(); + })); + } + + [TestMethod] + public async Task TryGetAsync_WhenFuncThrows_ReturnsFallback() + { + int value = await Guard.TryGetAsync( + () => Task.FromException(new InvalidOperationException("boom")), + fallback: 64); + + Assert.AreEqual(64, value); + } +} \ No newline at end of file