diff --git a/WheelWizard.Test/GlobalUsings.cs b/WheelWizard.Test/GlobalUsings.cs index 6ac12af2..c728b1f1 100644 --- a/WheelWizard.Test/GlobalUsings.cs +++ b/WheelWizard.Test/GlobalUsings.cs @@ -1 +1,2 @@ global using NSubstitute; +global using static WheelWizard.Shared.OperationResult; diff --git a/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs b/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs index 03117e95..cf1035cb 100644 --- a/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs +++ b/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs @@ -1,6 +1,8 @@ using WheelWizard.Shared; -namespace WheelWizard.Test; +namespace WheelWizard.Test.Shared.OperationResultTests; + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public class OperationResultTests { @@ -8,7 +10,7 @@ public class OperationResultTests public void CreateSuccessResult_ShouldHaveCorrectState() { // Act - var operationResult = OperationResult.Ok(); + var operationResult = Ok(); // Assert Assert.Null(operationResult.Error); @@ -50,7 +52,7 @@ public void CreateFailureResult_ShouldHaveCorrectState() var error = new OperationError { Message = "Error message" }; // Act - var operationResult = OperationResult.Fail(error); + var operationResult = Fail(error); // Assert Assert.Equal(error, operationResult.Error); @@ -88,7 +90,7 @@ public void NewSuccessGenericResult_ShouldHaveCorrectState() Assert.True(operationResult.IsSuccess); Assert.Equal(value, operationResult.Value); } - + [Fact(DisplayName = "Create success generic result, should have correct state")] public void CreateSuccessGenericResult_ShouldHaveCorrectState() { @@ -96,7 +98,7 @@ public void CreateSuccessGenericResult_ShouldHaveCorrectState() var value = new object(); // Act - var operationResult = OperationResult.Ok(value); + var operationResult = Ok(value); // Assert Assert.Null(operationResult.Error); @@ -104,7 +106,7 @@ public void CreateSuccessGenericResult_ShouldHaveCorrectState() Assert.True(operationResult.IsSuccess); Assert.Equal(value, operationResult.Value); } - + [Fact(DisplayName = "New failure generic result, should have correct state")] public void NewFailureGenericResult_ShouldHaveCorrectState() { @@ -119,7 +121,7 @@ public void NewFailureGenericResult_ShouldHaveCorrectState() Assert.True(operationResult.IsFailure); Assert.False(operationResult.IsSuccess); } - + [Fact(DisplayName = "Create failure generic result, should have correct state")] public void CreateFailureGenericResult_ShouldHaveCorrectState() { @@ -127,14 +129,14 @@ public void CreateFailureGenericResult_ShouldHaveCorrectState() var error = new OperationError { Message = "Error message" }; // Act - var operationResult = OperationResult.Fail(error); + var operationResult = Fail(error); // Assert Assert.Equal(error, operationResult.Error); Assert.True(operationResult.IsFailure); Assert.False(operationResult.IsSuccess); } - + [Fact(DisplayName = "Implicit generic result from error, should have correct state")] public void ImplicitGenericResultFromError_ShouldHaveCorrectState() { @@ -149,7 +151,7 @@ public void ImplicitGenericResultFromError_ShouldHaveCorrectState() Assert.True(operationResult.IsFailure); Assert.False(operationResult.IsSuccess); } - + [Fact(DisplayName = "Implicit generic result from value, should have correct state")] public void ImplicitGenericResultFromValue_ShouldHaveCorrectState() { @@ -165,4 +167,267 @@ public void ImplicitGenericResultFromValue_ShouldHaveCorrectState() Assert.True(operationResult.IsSuccess); Assert.Equal(value, operationResult.Value); } + + [Fact(DisplayName = "Implicit result from string, should have failed state")] + public void ImplicitResultFromString_ShouldHaveFailedState() + { + // Arrange + const string errorMessage = "Error message"; + + // Act + OperationResult operationResult = errorMessage; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(errorMessage, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Implicit result from exception, should have failed state")] + public void ImplicitResultFromException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + // Act + OperationResult operationResult = exception; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(exception.Message, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Implicit generic result from string, should have correct failed state")] + public void ImplicitGenericResultFromString_ShouldHaveCorrectFailedState() + { + // Arrange + const string errorMessage = "Error message"; + + // Act + OperationResult operationResult = errorMessage; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(errorMessage, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Implicit generic result from exception, should have correct failed state")] + public void ImplicitGenericResultFromException_ShouldHaveCorrectFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + // Act + OperationResult operationResult = exception; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(exception.Message, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Try catch without exception, should have correct success state")] + public void TryCatchWithoutException_ShouldHaveCorrectSuccessState() + { + // Arrange + void Action() + { + } + + // Act + var result = TryCatch(Action); + + // Assert + Assert.True(result.IsSuccess); + } + + [Fact(DisplayName = "Try catch with exception, should have failed state")] + public void TryCatchWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + void Action() => throw exception; + + // Act + var result = TryCatch(Action); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Try catch with exception with override, should have failed state with message")] + public void TryCatchWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + + // Act + void Action() => throw exception; + + var result = TryCatch(Action, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Generic try catch without exception, should have correct success state")] + public void GenericTryCatchWithoutException_ShouldHaveCorrectSuccessState() + { + // Arrange + int Func() => 42; + + // Act + var result = TryCatch(Func); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact(DisplayName = "Generic try catch with exception, should have failed state")] + public void GenericTryCatchWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + int Func() => throw exception; + + // Act + var result = TryCatch(Func); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Generic try catch with exception with override, should have failed state with message")] + public void GenericTryCatchWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + + int Func() => throw exception; + + // Act + var result = TryCatch(Func, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Generic safe execute async without exception, should have success state")] + public async Task GenericTryCatchAsyncWithoutException_ShouldHaveSuccessState() + { + // Arrange + const int expectedValue = 42; + + async Task Func() => await Task.FromResult(expectedValue); + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(expectedValue, result.Value); + } + + + [Fact(DisplayName = "Generic safe execute async with exception, should have failed state")] + public async Task GenericTryCatchAsyncWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + async Task Func() => throw exception; + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Generic safe execute async with exception with override, should have failed state with message")] + public async Task GenericTryCatchAsyncWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + async Task Func() => throw exception; + + // Act + var result = await TryCatch(Func, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Try catch async without exception, should have success state")] + public async Task TryCatchAsyncWithoutException_ShouldHaveSuccessState() + { + // Arrange + async Task Func() => await Task.CompletedTask; + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsSuccess); + } + + [Fact(DisplayName = "Try catch async with exception, should have failed state")] + public async Task TryCatchAsyncWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + async Task Func() => throw exception; + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Try catch async with exception with override, should have failed state with message")] + public async Task TryCatchAsyncWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + + async Task Func() => throw exception; + // Act + var result = await TryCatch(Func, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } } + +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously diff --git a/WheelWizard.Test/Shared/Services/AvaloniaLoggerAdapterTests.cs b/WheelWizard.Test/Shared/Services/AvaloniaLoggerAdapterTests.cs new file mode 100644 index 00000000..00bb199b --- /dev/null +++ b/WheelWizard.Test/Shared/Services/AvaloniaLoggerAdapterTests.cs @@ -0,0 +1,94 @@ +using Avalonia; +using Avalonia.Logging; +using Microsoft.Extensions.Logging; +using WheelWizard.Shared.Services; + +namespace WheelWizard.Test.Shared.Services; + +public class AvaloniaLoggerAdapterTests +{ + public static TheoryData LogLevels = + new() + { + { LogEventLevel.Verbose, LogLevel.Trace }, + { LogEventLevel.Debug, LogLevel.Debug }, + { LogEventLevel.Information, LogLevel.Information }, + { LogEventLevel.Warning, LogLevel.Warning }, + { LogEventLevel.Error, LogLevel.Error }, + { LogEventLevel.Fatal, LogLevel.Critical } + }; + + [Theory(DisplayName = "Log adapter message, should convert to logger message")] + [MemberData(nameof(LogLevels))] + public void LogAdapterMessage_ShouldConvertToLoggerMessage(LogEventLevel level, LogLevel expectedLevel) + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + var args = new object[] { "testValue" }; + + // Act + logAdapter.Log(level, "TestArea", null, "Test message {Arg}", args); + + // Assert + logger.Received(1).Log(expectedLevel, "Test message {Arg}", args); + } + + [Fact(DisplayName = "Log adapter without args, should log message without args")] + public void LogAdapterWithoutArgs_ShouldLogMessageWithoutArgs() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act + logAdapter.Log(LogEventLevel.Information, "TestArea", null, "Test message"); + + // Assert + logger.Received(1).Log(LogLevel.Information, "Test message"); + } + + [Fact(DisplayName = "Log adapter with layout area, should not log")] + public void LogAdapterWithLayoutArea_ShouldNotLog() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act + logAdapter.Log(LogEventLevel.Information, "Layout", null, "Test message"); + + // Assert +#pragma warning disable CA2254 + // ReSharper disable once TemplateIsNotCompileTimeConstantProblem + logger.DidNotReceive().Log(LogLevel.Information, "Test message"); +#pragma warning restore CA2254 + } + + [Fact(DisplayName = "Log adapter is enabled, should be true")] + public void LogAdapterIsEnabled_ShouldBeTrue() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act + var result = logAdapter.IsEnabled(LogEventLevel.Information, "TestArea"); + + // Assert + Assert.True(result); + } + + [Fact(DisplayName = "Invalid log event level log, should throw exception")] + public void InvalidLogEventLevelLog_ShouldThrowException() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act & Assert + Assert.Throws(() => + logAdapter.Log((LogEventLevel)999, "TestArea", null, "Test message")); + } +} diff --git a/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs b/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs index 19a519ac..ce21c945 100644 --- a/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs +++ b/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs @@ -30,13 +30,13 @@ public async Task ExecuteUpdateAsync(string downloadUrl) { var currentExecutablePath = Environment.ProcessPath; if (currentExecutablePath is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; var currentExecutableName = fileSystem.Path.GetFileName(currentExecutablePath); var currentFolder = fileSystem.Path.GetDirectoryName(currentExecutablePath); if (currentFolder is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; // Download the new executable to a temporary file. var newFilePath = fileSystem.Path.Combine(currentFolder, currentExecutableName + "_new"); @@ -67,7 +67,7 @@ private OperationResult CreateAndRunShellScript(string currentFilePath, string n { var currentFolder = fileSystem.Path.GetDirectoryName(currentFilePath); if (currentFolder is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; var scriptFilePath = fileSystem.Path.Combine(currentFolder, "update.sh"); var originalFileName = fileSystem.Path.GetFileName(currentFilePath); @@ -98,30 +98,24 @@ sleep 1 fileSystem.File.WriteAllText(scriptFilePath, scriptContent); // Ensure the script is executable. - try - { - Process.Start(new ProcessStartInfo - { - FileName = "/usr/bin/env", - ArgumentList = + var chmodResult = TryCatch(() => + Process.Start(new ProcessStartInfo { - "chmod", - "+x", - "--", - scriptFilePath, - }, - CreateNoWindow = true, - UseShellExecute = false - })?.WaitForExit(); - } - catch (Exception ex) - { - return Fail(new() - { - Message = "Failed to set execute permission for the update script.", - Exception = ex - }); - } + FileName = "/usr/bin/env", + ArgumentList = + { + "chmod", + "+x", + "--", + scriptFilePath, + }, + CreateNoWindow = true, + UseShellExecute = false + })?.WaitForExit(), errorMessage: "Failed to set execute permission for the update script." + ); + + if (chmodResult.IsFailure) + return chmodResult; var processStartInfo = new ProcessStartInfo { @@ -137,19 +131,6 @@ sleep 1 WorkingDirectory = currentFolder }; - try - { - Process.Start(processStartInfo); - } - catch (Exception ex) - { - return Fail(new() - { - Message = "Failed to execute the update script.", - Exception = ex - }); - } - - return Ok(); + return TryCatch(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); } } diff --git a/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs b/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs index 6bfbea12..1538e356 100644 --- a/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs +++ b/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs @@ -44,16 +44,11 @@ private static OperationResult RestartAsAdmin() Verb = "runas" // This verb asks for elevation. }; - try + return TryCatch(() => { Process.Start(startInfo); Environment.Exit(0); - return Ok(); - } - catch (Exception) - { - return Phrases.PopupText_RestartAdminFail; - } + }, errorMessage: Phrases.PopupText_RestartAdminFail); } private static bool IsAdministrator() @@ -108,7 +103,7 @@ private OperationResult CreateAndRunPowerShellScript(string currentFilePath, str { var currentFolder = fileSystem.Path.GetDirectoryName(currentFilePath); if (currentFolder is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; var scriptFilePath = fileSystem.Path.Combine(currentFolder, "update.ps1"); var originalFileName = fileSystem.Path.GetFileName(currentFilePath); @@ -179,18 +174,6 @@ exit 1 WorkingDirectory = currentFolder }; - try - { - Process.Start(processStartInfo); - return Ok(); - } - catch (Exception ex) - { - return Fail(new() - { - Message = "Failed to execute the update script.", - Exception = ex - }); - } + return TryCatch(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); } } diff --git a/WheelWizard/Shared/OperationResult/OperationError.cs b/WheelWizard/Shared/OperationResult/OperationError.cs index 3c8647d1..c05a1c25 100644 --- a/WheelWizard/Shared/OperationResult/OperationError.cs +++ b/WheelWizard/Shared/OperationResult/OperationError.cs @@ -15,5 +15,15 @@ public class OperationError /// public Exception? Exception { get; init; } - public static implicit operator OperationError(string message) => new() { Message = message }; + #region Implicit Operators + + public static implicit operator OperationError(string errorMessage) => new() { Message = errorMessage }; + + public static implicit operator OperationError(Exception exception) => new() + { + Message = exception.Message, + Exception = exception + }; + + #endregion } diff --git a/WheelWizard/Shared/OperationResult/OperationResult.cs b/WheelWizard/Shared/OperationResult/OperationResult.cs index c8944b50..09900474 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult.cs @@ -43,6 +43,8 @@ public OperationResult(OperationError error) Error = error; } + #region Creation Methods + /// /// Creates a new instance of the class with the specified error. /// @@ -72,6 +74,113 @@ public OperationResult(OperationError error) /// A new instance of the class. public static OperationResult Ok(T value) => new(value); - public static implicit operator OperationResult(OperationError error) => new(error); - public static implicit operator OperationResult(string errorMessage) => new(errorMessage); + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The function to execute. + /// The error message to return if the function fails. + /// The type of the value. + /// An that indicates the result of the operation. + public static OperationResult TryCatch(Func func, string? errorMessage = null) + { + try + { + var value = func(); + return Ok(value); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The function to execute. + /// The error message to return if the function fails. + /// The type of the value. + /// An that indicates the result of the operation. + public static async Task> TryCatch(Func> func, string? errorMessage = null) + { + try + { + var value = await func(); + return Ok(value); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The action to execute. + /// The error message to return if the function fails. + /// An that indicates the result of the operation. + public static OperationResult TryCatch(Action action, string? errorMessage = null) + { + try + { + action(); + return Ok(); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The action to execute. + /// The error message to return if the function fails. + /// An that indicates the result of the operation. + public static async Task TryCatch(Func action, string? errorMessage = null) + { + try + { + await action(); + return Ok(); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + + #endregion + + #region Implicit Operators + + public static implicit operator OperationResult(OperationError error) => Fail(error); + + public static implicit operator OperationResult(string errorMessage) => Fail(errorMessage); + + public static implicit operator OperationResult(Exception exception) => Fail(exception); + + #endregion } diff --git a/WheelWizard/Shared/OperationResult/OperationResult`1.cs b/WheelWizard/Shared/OperationResult/OperationResult`1.cs index 2ea73757..335e3845 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult`1.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult`1.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace WheelWizard.Shared; +namespace WheelWizard.Shared; /// /// Represents the result of an operation. @@ -8,16 +6,13 @@ namespace WheelWizard.Shared; /// The type of the value. public class OperationResult : OperationResult { - [MemberNotNullWhen(true, nameof(Value))] - public override bool IsSuccess => base.IsSuccess; - - [MemberNotNullWhen(false, nameof(Value))] - public override bool IsFailure => base.IsFailure; + private readonly T _value; /// /// The value of the operation result. /// - public T? Value { get; } + /// Thrown if the operation was not successful. + public T Value => IsSuccess ? _value : throw new InvalidOperationException("The operation was not successful."); /// /// Initializes a new instance of the class. @@ -25,7 +20,7 @@ public class OperationResult : OperationResult /// The value of the operation result. public OperationResult(T value) { - Value = value; + _value = value; } /// @@ -34,10 +29,18 @@ public OperationResult(T value) /// The error that occurred during the operation. public OperationResult(OperationError error) : base(error) { + _value = default!; } - public static implicit operator OperationResult(OperationError error) => new(error); - public static implicit operator OperationResult(string errorMessage) => new(errorMessage); + #region Implicit Operators + + public static implicit operator OperationResult(T value) => Ok(value); + + public static implicit operator OperationResult(OperationError error) => Fail(error); + + public static implicit operator OperationResult(string errorMessage) => Fail(errorMessage); + + public static implicit operator OperationResult(Exception exception) => Fail(exception); - public static implicit operator OperationResult(T value) => new(value); + #endregion } diff --git a/WheelWizard/Shared/Services/ApiCaller.cs b/WheelWizard/Shared/Services/ApiCaller.cs index feed780f..2a6890ee 100644 --- a/WheelWizard/Shared/Services/ApiCaller.cs +++ b/WheelWizard/Shared/Services/ApiCaller.cs @@ -12,9 +12,12 @@ public interface IApiCaller where TApi : class /// /// Calls the specified API method asynchronously. /// - /// The API method to call. + /// The API method to call. Make sure you name the variable clearly as it is used in the logs. /// The type of the result. /// An representing the result of the API call. + /// + /// Uses the expression and the name of for logging purposes. + /// Task> CallApiAsync(Expression>> apiCall); } @@ -23,22 +26,16 @@ public class ApiCaller(IServiceScopeFactory scopeFactory, ILogger> CallApiAsync(Expression>> apiCall) { var apiCallString = apiCall.Body.ToString(); + var apiCallFunction = apiCall.Compile(); + var apiName = typeof(T).Name[1..]; - try - { - using var scope = scopeFactory.CreateScope(); - var api = scope.ServiceProvider.GetRequiredService(); + using var scope = scopeFactory.CreateScope(); + var api = scope.ServiceProvider.GetRequiredService(); - return await apiCall.Compile().Invoke(api); - } - catch (Exception exception) - { - logger.LogError(exception, "API call '{ApiCall}' failed: {Message}", apiCallString, exception.Message); - return new OperationError - { - Message = $"API call '{apiCallString}' failed: {exception.Message}", - Exception = exception - }; - } + var result = await TryCatch(async () => await apiCallFunction.Invoke(api), errorMessage: $"{apiName} call failed"); + if (!result.IsSuccess) + logger.LogError(result.Error.Exception, "API method '{ApiCall}' failed: {Message}", apiCallString, result.Error.Message); + + return result; } }