Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pull-request-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/OneBitSoftware.Utilities.OperationResult.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace OneBitSoftware.Utilities.Errors
{
using Microsoft.Extensions.Logging;

public interface IOperationError
{
int? Code { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace OneBitSoftware.Utilities.Errors
{
using System.Text;
using Microsoft.Extensions.Logging;

public class OperationError : IOperationError
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net5.0</TargetFrameworks>
<TargetFrameworks>net9.0</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand All @@ -14,7 +14,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
</ItemGroup>

<PropertyGroup>
Expand All @@ -32,7 +32,7 @@
<PackageRequireLicenseAcceptance>False</PackageRequireLicenseAcceptance>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>OneBitSoftware; OperationResult;</PackageTags>
<Version>1.4.6</Version>
<Version>2.0.0</Version>
</PropertyGroup>

</Project>
107 changes: 43 additions & 64 deletions src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
/// </summary>
public class OperationResult
{
private readonly List<string> _successMessages = new List<string>();
/// <summary>
/// Contains <see cref="IOperationError"/> instances that have not been logged.
/// </summary>
private readonly List<(IOperationError Error, LogLevel? LogLevel)> _errorsNotLogged = new();

protected readonly ILogger? _logger;
private readonly List<string> _successMessages = new();

private readonly ILogger? _logger;

/// <summary>
/// Gets or sets a value indicating whether the operation is successful or not.
Expand Down Expand Up @@ -44,7 +49,7 @@ public IEnumerable<string>? SuccessMessages
/// Gets an <see cref="List{T}"/> containing the error codes and messages of the <see cref="OperationResult{T}" />.
/// </summary>
public List<IOperationError> Errors { get; internal set; } = new List<IOperationError>();

/// <summary>
/// Gets or sets the first exception that resulted from the operation.
/// </summary>
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -146,25 +164,19 @@ public OperationResult AppendError<T>(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;
}

/// <summary>
/// Appends an exception to the error message collection and logs the full exception as an Error <see cref="LogEventLevel"/> 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 <see cref="LogLevel"/> level. A call to this method will set the Success property to false.
/// </summary>
/// <param name="exception">The exception to log.</param>
/// <param name="errorCode">The error code.</param>
/// <param name="logLevel">The <see cref="LogEventLevel"/> logging severity.</param>
/// <param name="logLevel">The <see cref="LogLevel"/> logging severity.</param>
/// <returns>The current instance of the <see cref="OperationResult"/>.</returns>
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));

Expand Down Expand Up @@ -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;

/// <summary>
/// Appends an <see cref="IOperationError"/> to the internal errors collection.
/// </summary>
/// <param name="error">An instance of <see cref="IOperationError"/> to add to the internal errors collection.</param>
protected void AppendErrorInternal(IOperationError error) => this.Errors.Add(error);

/// <summary>
/// Logs to the internal logger if it is set, otherwise it will add the error to the internal errors collection.
/// </summary>
/// <param name="error">The <see cref="IOperationError"/> to log.</param>
/// <param name="logLevel">The log level.</param>
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);
}
}
}

/// <summary>
Expand Down Expand Up @@ -284,61 +310,14 @@ public OperationResult(TResult resultObject) : base()
return this;
}

/// <summary>
/// Appends an <see cref="IOperationError"/> to the internal errors collection.
/// </summary>
/// <param name="error">An instance of <see cref="IOperationError"/> to add to the internal errors collection.</param>
/// <param name="logLevel">The logging level.</param>
/// <returns>The current instance of the <see cref="OperationResult"/>.</returns>
public new OperationResult<TResult> 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;
}

/// <summary>
/// Appends error messages from <paramref name="otherOperationResult"/> to the current instance.
/// </summary>
/// <param name="otherOperationResult">The <see cref="OperationResult"/> to append from.</param>
/// <typeparam name="TOther">A type that inherits from <see cref="OperationResult"/>.</typeparam>
/// <returns>The original <see cref="OperationResult"/> with the appended messages from <paramref name="otherOperationResult"/>.</returns>
[Obsolete("Please use AppendErrors instead. This method will be removed to avoid confusion.")]
public OperationResult<TResult> AppendErrorMessages<TOther>(TOther otherOperationResult)
where TOther : OperationResult
{
base.AppendErrors(otherOperationResult);

return this;
}

/// <summary>
/// Appends error from <paramref name="otherOperationResult"/> to the current instance.
/// </summary>
/// <param name="otherOperationResult">The <see cref="OperationResult"/> to append from.</param>
/// <returns>The original <see cref="OperationResult"/> with the appended messages from <paramref name="otherOperationResult"/>.</returns>
public new OperationResult<TResult> AppendErrors(OperationResult otherOperationResult)
{
base.AppendErrors(otherOperationResult);

return this;
}

/// <summary>
/// Appends an exception to the error message collection and logs the full exception as an Error <see cref="LogEventLevel"/> level. A call to this method will set the Success property to false.
/// </summary>
/// <param name="exception">The exception to log.</param>
/// <param name="errorCode">The error code.</param>
/// <param name="logLevel">The <see cref="LogEventLevel"/> logging severity.</param>
/// <returns>The current instance of the <see cref="OperationResult{TResult}"/>.</returns>
public new OperationResult<TResult> AppendException(Exception exception, int? errorCode = null, LogLevel? logLevel = null)
public new OperationResult<TResult> AppendException(Exception exception, int errorCode = 0, LogLevel? logLevel = null)
{
base.AppendException(exception, errorCode, logLevel);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
// Deserialize the JSON to the specified type.
var serializationOptions = this.ConstructSafeFallbackOptions(options);
serializationOptions.Converters.Add(new ReadOnlyPartialConverter(this));
return (T)JsonSerializer.Deserialize(ref reader, typeToConvert, serializationOptions);

Check warning on line 38 in src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs

View workflow job for this annotation

GitHub Actions / Build and test

Converting null literal or possible null value to non-nullable type.

Check warning on line 38 in src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs

View workflow job for this annotation

GitHub Actions / Build and test

Possible null reference return.
}
catch (Exception ex)
{
Expand All @@ -54,7 +54,7 @@
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<byte>();
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);
Expand Down Expand Up @@ -82,10 +82,10 @@

private Type GetType(JsonElement typeElement)
{
if (typeElement.ValueKind != JsonValueKind.String) return null;

Check warning on line 85 in src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs

View workflow job for this annotation

GitHub Actions / Build and test

Possible null reference return.

var stringValue = typeElement.GetString();
if (string.IsNullOrWhiteSpace(stringValue)) return null;

Check warning on line 88 in src/OneBitSoftware.Utilities.OperationResult/PolymorphicOperationErrorSerializer.cs

View workflow job for this annotation

GitHub Actions / Build and test

Possible null reference return.

this._valueMappings.TryGetValue(stringValue, out var type);
return type;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
Loading
Loading