diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index befaeda..19e34ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,12 +15,12 @@ jobs: NUGET_SOURCE: https://api.nuget.org/v3/index.json steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 9.x - name: Restore dependencies run: dotnet restore ./src/OneBitSoftware.Utilities.OperationResult.sln diff --git a/.github/workflows/pull-request-validation.yml b/.github/workflows/pull-request-validation.yml index 8fc26d2..8e8339f 100644 --- a/.github/workflows/pull-request-validation.yml +++ b/.github/workflows/pull-request-validation.yml @@ -15,12 +15,12 @@ jobs: NUGET_SOURCE: https://api.nuget.org/v3/index.json steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 9.x - name: Restore dependencies run: dotnet restore ./src/OneBitSoftware.Utilities.OperationResult.sln diff --git a/src/OneBitSoftware.Utilities.OperationResult.sln b/src/OneBitSoftware.Utilities.OperationResult.sln index 692c185..69984dd 100644 --- a/src/OneBitSoftware.Utilities.OperationResult.sln +++ b/src/OneBitSoftware.Utilities.OperationResult.sln @@ -7,6 +7,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneBitSoftware.Utilities.Op EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneBitSoftware.Utilities.OperationResultTests", "..\tests\OneBitSoftware.Utilities.OperationResultTests\OneBitSoftware.Utilities.OperationResultTests.csproj", "{142313C6-5DC0-4428-AE63-487B8D41552E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{6F29D051-AD77-482A-99A7-4E5ED288AB22}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\main.yml = ..\.github\workflows\main.yml + ..\.github\workflows\pull-request-validation.yml = ..\.github\workflows\pull-request-validation.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +33,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6F29D051-AD77-482A-99A7-4E5ED288AB22} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3A384A11-1CD9-4A02-A6BB-3EC782DBF254} EndGlobalSection diff --git a/src/OneBitSoftware.Utilities.OperationResult/Errors/IOperationError.cs b/src/OneBitSoftware.Utilities.OperationResult/Errors/IOperationError.cs index bec1ac6..d9abc5e 100644 --- a/src/OneBitSoftware.Utilities.OperationResult/Errors/IOperationError.cs +++ b/src/OneBitSoftware.Utilities.OperationResult/Errors/IOperationError.cs @@ -1,5 +1,7 @@ namespace OneBitSoftware.Utilities.Errors { + using Microsoft.Extensions.Logging; + public interface IOperationError { int? Code { get; set; } diff --git a/src/OneBitSoftware.Utilities.OperationResult/Errors/OperationError.cs b/src/OneBitSoftware.Utilities.OperationResult/Errors/OperationError.cs index 9a50063..0b4d0d5 100644 --- a/src/OneBitSoftware.Utilities.OperationResult/Errors/OperationError.cs +++ b/src/OneBitSoftware.Utilities.OperationResult/Errors/OperationError.cs @@ -1,6 +1,7 @@ namespace OneBitSoftware.Utilities.Errors { using System.Text; + using Microsoft.Extensions.Logging; public class OperationError : IOperationError { diff --git a/src/OneBitSoftware.Utilities.OperationResult/OneBitSoftware.Utilities.OperationResult.csproj b/src/OneBitSoftware.Utilities.OperationResult/OneBitSoftware.Utilities.OperationResult.csproj index 96edbb8..6881c37 100644 --- a/src/OneBitSoftware.Utilities.OperationResult/OneBitSoftware.Utilities.OperationResult.csproj +++ b/src/OneBitSoftware.Utilities.OperationResult/OneBitSoftware.Utilities.OperationResult.csproj @@ -1,7 +1,7 @@  - net6.0;net5.0 + net9.0 disable enable @@ -14,7 +14,7 @@ - + @@ -32,7 +32,7 @@ False README.md OneBitSoftware; OperationResult; - 1.4.6 + 2.0.0 diff --git a/src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs b/src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs index 10de94e..bc060b8 100644 --- a/src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs +++ b/src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs @@ -13,9 +13,14 @@ /// public class OperationResult { - private readonly List _successMessages = new List(); + /// + /// Contains instances that have not been logged. + /// + private readonly List<(IOperationError Error, LogLevel? LogLevel)> _errorsNotLogged = new(); - protected readonly ILogger? _logger; + private readonly List _successMessages = new(); + + private readonly ILogger? _logger; /// /// Gets or sets a value indicating whether the operation is successful or not. @@ -44,7 +49,7 @@ public IEnumerable? SuccessMessages /// Gets an containing the error codes and messages of the . /// public List Errors { get; internal set; } = new List(); - + /// /// Gets or sets the first exception that resulted from the operation. /// @@ -93,6 +98,19 @@ public OperationResult AppendErrors(OperationResult otherOperationResult) { if (otherOperationResult is null) return this; + // store any messages for logging at a later stage, when merged to an OperationResult with a logger. + if (this._logger is null) + { + this._errorsNotLogged.AddRange(otherOperationResult._errorsNotLogged); + } + else + { + foreach (var (error, logLevel) in otherOperationResult._errorsNotLogged) + this.LogInternal(error, logLevel); + + otherOperationResult._errorsNotLogged.Clear(); + } + // Append the error message without logging (presuming that there is already a log message). foreach (var error in otherOperationResult.Errors) this.AppendErrorInternal(error); @@ -146,25 +164,19 @@ public OperationResult AppendError(string message, int? code = null, LogLevel public OperationResult AppendError(IOperationError error, LogLevel? logLevel = LogLevel.Error) { this.AppendErrorInternal(error); - - if (this._logger != null) - { -#pragma warning disable CA2254 // Template should be a static expression - this._logger.Log(GetLogLevel(logLevel), error.Message); -#pragma warning restore CA2254 // Template should be a static expression - } + this.LogInternal(error, logLevel); return this; } /// - /// Appends an exception to the error message collection and logs the full exception as an Error level. A call to this method will set the Success property to false. + /// Appends an exception to the error message collection and logs the full exception as an Error level. A call to this method will set the Success property to false. /// /// The exception to log. /// The error code. - /// The logging severity. + /// The logging severity. /// The current instance of the . - public OperationResult AppendException(Exception exception, int? errorCode = null, LogLevel? logLevel = null) + public OperationResult AppendException(Exception exception, int errorCode = 0, LogLevel? logLevel = null) { if (exception is null) throw new ArgumentNullException(nameof(exception)); @@ -210,14 +222,28 @@ public static OperationResult FromError(string message, int? code = null, LogLev return result.AppendError(message, code, logLevel, details); } - // TODO: this method needs completing. - protected static LogLevel GetLogLevel(LogLevel? optionalLevel) => optionalLevel ?? LogLevel.Error; - /// /// Appends an to the internal errors collection. /// /// An instance of to add to the internal errors collection. protected void AppendErrorInternal(IOperationError error) => this.Errors.Add(error); + + /// + /// Logs to the internal logger if it is set, otherwise it will add the error to the internal errors collection. + /// + /// The to log. + /// The log level. + private void LogInternal(IOperationError error, LogLevel? logLevel) + { + if (this._logger is null) + { + this._errorsNotLogged.Add((Error: error, LogLevel: logLevel)); + } + else + { + this._logger.Log(logLevel ?? LogLevel.Error, error.Message); + } + } } /// @@ -284,53 +310,6 @@ public OperationResult(TResult resultObject) : base() return this; } - /// - /// Appends an to the internal errors collection. - /// - /// An instance of to add to the internal errors collection. - /// The logging level. - /// The current instance of the . - public new OperationResult AppendError(IOperationError error, LogLevel? logLevel = LogLevel.Error) - { - base.AppendErrorInternal(error); - - if (this._logger != null) - { -#pragma warning disable CA2254 // Template should be a static expression - this._logger.Log(GetLogLevel(logLevel), error.Message); -#pragma warning restore CA2254 // Template should be a static expression - } - - return this; - } - - /// - /// Appends error messages from to the current instance. - /// - /// The to append from. - /// A type that inherits from . - /// The original with the appended messages from . - [Obsolete("Please use AppendErrors instead. This method will be removed to avoid confusion.")] - public OperationResult AppendErrorMessages(TOther otherOperationResult) - where TOther : OperationResult - { - base.AppendErrors(otherOperationResult); - - return this; - } - - /// - /// Appends error from to the current instance. - /// - /// The to append from. - /// The original with the appended messages from . - public new OperationResult AppendErrors(OperationResult otherOperationResult) - { - base.AppendErrors(otherOperationResult); - - return this; - } - /// /// Appends an exception to the error message collection and logs the full exception as an Error level. A call to this method will set the Success property to false. /// @@ -338,7 +317,7 @@ public OperationResult AppendErrorMessages(TOther otherOperatio /// The error code. /// The logging severity. /// The current instance of the . - public new OperationResult AppendException(Exception exception, int? errorCode = null, LogLevel? logLevel = null) + public new OperationResult AppendException(Exception exception, int errorCode = 0, LogLevel? logLevel = null) { base.AppendException(exception, errorCode, logLevel); diff --git a/src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs b/src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs index 9045bdb..65a8cfa 100644 --- a/src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs +++ b/src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs @@ -54,7 +54,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions if (this._typeMappings.TryGetValue(value.GetType(), out var typeValue) == false) throw new InvalidOperationException($"Model of type {value.GetType()} cannot be successfully serialized."); var tempBufferWriter = new ArrayBufferWriter(); - var tempWriter = new Utf8JsonWriter(tempBufferWriter); + var tempWriter = new Utf8JsonWriter(tempBufferWriter); // TODO: dispose with using var var fallbackDeserializationOptions = this.ConstructSafeFallbackOptions(options); JsonSerializer.Serialize(tempWriter, value, value.GetType(), fallbackDeserializationOptions); diff --git a/tests/OneBitSoftware.Utilities.OperationResultTests/OneBitSoftware.Utilities.OperationResultTests.csproj b/tests/OneBitSoftware.Utilities.OperationResultTests/OneBitSoftware.Utilities.OperationResultTests.csproj index e593810..8826bd1 100644 --- a/tests/OneBitSoftware.Utilities.OperationResultTests/OneBitSoftware.Utilities.OperationResultTests.csproj +++ b/tests/OneBitSoftware.Utilities.OperationResultTests/OneBitSoftware.Utilities.OperationResultTests.csproj @@ -1,17 +1,17 @@ - net6.0 + net9.0 enable enable - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/OneBitSoftware.Utilities.OperationResultTests/OperationResultAppendErrorsTests.cs b/tests/OneBitSoftware.Utilities.OperationResultTests/OperationResultAppendErrorsTests.cs index cd8ef05..b18b318 100644 --- a/tests/OneBitSoftware.Utilities.OperationResultTests/OperationResultAppendErrorsTests.cs +++ b/tests/OneBitSoftware.Utilities.OperationResultTests/OperationResultAppendErrorsTests.cs @@ -62,7 +62,7 @@ public void AppendErrorsT_ShouldListAllErrors() operationResultBase.AppendError(message2, errorCode2, LogLevel.Debug, detail2); // Act - AppendErrorMessages is to be removed - operationResultBase.AppendErrorMessages(operationResultTarget); + operationResultBase.AppendErrors(operationResultTarget); // Assert Assert.False(operationResultBase.Success); @@ -111,4 +111,70 @@ public void AppendErrorsStringInt_ShouldListAllErrors() Assert.NotNull(operationResultBase.Errors.Single(r => r.Message.Equals(message2))); Assert.NotNull(operationResultBase.Errors.Single(r => r.Details is not null && r.Details.Equals(detail2))); } + + [Fact] + public void AppendErrors_ShouldLogWhenCreatedWithALogger() + { + // Arrange + var testLogger = new TestLogger(); + var operationResultNoLogger = new OperationResult(); + var operationResultWithLogger = new OperationResult(testLogger); + + // Act + operationResultNoLogger.AppendError("test"); + operationResultWithLogger.AppendErrors(operationResultNoLogger); + + // Assert + Assert.Equal(1, testLogger.LogMessages.Count); + } + + [Fact] + public void AppendErrors_ShouldLogOnceWhenCreatedWithALogger() + { + // Arrange + var testLogger = new TestLogger(); + var operationResultWithLogger = new OperationResult(testLogger); + var operationResultWithLogger2 = new OperationResult(testLogger); + + // Act + operationResultWithLogger2.AppendError("test"); + operationResultWithLogger.AppendErrors(operationResultWithLogger2); + + // Assert + Assert.Equal(1, testLogger.LogMessages.Count); + } + + [Fact] + public void AppendErrors_ShouldLogOnceWhenNestingWithALogger() + { + // Arrange + var testLogger = new TestLogger(); + var operationResultWithLogger = new OperationResult(testLogger); + var operationResultWithLogger2 = new OperationResult(testLogger); + var operationResultWithLogger3 = new OperationResult(testLogger); + + // Act + operationResultWithLogger3.AppendError("test1"); + operationResultWithLogger2.AppendError("test2"); + operationResultWithLogger.AppendErrors(operationResultWithLogger2); + + // Assert + Assert.Equal(2, testLogger.LogMessages.Count); + } + + [Fact] + public void AppendErrors_ShouldLogWhenCreatedWithNoLogger() + { + // Arrange + var testLogger = new TestLogger(); + var operationResultNoLogger = new OperationResult(); + var operationResultWithLogger = new OperationResult(testLogger); + + // Act + operationResultWithLogger.AppendError("test"); + operationResultNoLogger.AppendErrors(operationResultNoLogger); + + // Assert + Assert.Equal(1, testLogger.LogMessages.Count); + } } diff --git a/tests/OneBitSoftware.Utilities.OperationResultTests/TestLogger.cs b/tests/OneBitSoftware.Utilities.OperationResultTests/TestLogger.cs new file mode 100644 index 0000000..1a744e3 --- /dev/null +++ b/tests/OneBitSoftware.Utilities.OperationResultTests/TestLogger.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace OneBitSoftware.Utilities.OperationResultTests +{ + public class TestLogger : ILogger + { + private readonly List _logMessages = new List(); + + public IReadOnlyList LogMessages => _logMessages.AsReadOnly(); + + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (formatter != null) + { + _logMessages.Add(formatter(state, exception)); + } + } + } +}