Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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.0.x
Comment thread
TonyTroeff marked this conversation as resolved.
Outdated

- 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.0.x
Comment thread
TonyTroeff marked this conversation as resolved.
Outdated

- 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,11 +1,23 @@
namespace OneBitSoftware.Utilities.Errors
{
using Microsoft.Extensions.Logging;

public interface IOperationError
{
int? Code { get; set; }

string? Message { get; set; }

string? Details { get; set; }

/// <summary>
/// Defines if the error is logged or not. Used when merging <cref="OperationResult"/> instances and one of them does not have an ILogger.
/// </summary>
bool Logged { get; internal set; }

/// <summary>
/// Defines the log level for the error.
/// </summary>
LogLevel? LogLevel { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
namespace OneBitSoftware.Utilities.Errors
{
using System.Text;
using Microsoft.Extensions.Logging;

public class OperationError : IOperationError
{
public OperationError(string? message = null, int? code = null, string? details = null)
public OperationError(string? message = null, int? code = null, string? details = null, LogLevel? logLevel = null)
{
this.Message = message;
this.Code = code;
this.Details = details;
this.LogLevel = logLevel;
}

public int? Code { get; set; }
Expand All @@ -17,12 +19,20 @@ public OperationError(string? message = null, int? code = null, string? details

public string? Details { get; set; }

/// <inheritdoc />
public LogLevel? LogLevel { get; set; }

/// <inheritdoc />
bool IOperationError.Logged { get; set; }

public override string ToString()
{
var result = new StringBuilder();

if (this.Code != null) result.AppendLine($"Code: {this.Code}");

if (this.LogLevel is not null) result.AppendLine($"Severity: {this.LogLevel}"); // TODO: maybe convert to string?

if (!string.IsNullOrWhiteSpace(this.Message)) result.AppendLine($"Message: {this.Message}");

if (!string.IsNullOrWhiteSpace(this.Details)) result.AppendLine($"Trace: {this.Details}");
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>
43 changes: 15 additions & 28 deletions src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,17 @@ public OperationResult AppendErrors(OperationResult otherOperationResult)
{
if (otherOperationResult is null) return this;

// Append the error message without logging (presuming that there is already a log message).
foreach (var error in otherOperationResult.Errors) this.AppendErrorInternal(error);
foreach (var error in otherOperationResult.Errors)
{
this.AppendErrorInternal(error);

// Logs messages if the other operation result does not have a logger
if (this._logger is not null && otherOperationResult._logger is null && !error.Logged)
Comment thread
TonyTroeff marked this conversation as resolved.
Outdated
{
this._logger.Log(GetLogLevel(error.LogLevel), error.Message);
error.Logged = true;
}
}

return this;
}
Expand All @@ -111,7 +120,7 @@ public OperationResult AppendError(string message, int? code = null, LogLevel? l
{
if (string.IsNullOrWhiteSpace(message)) throw new ArgumentNullException(nameof(message));

var error = new OperationError(message, code) { Details = details };
var error = new OperationError(message, code) { Details = details, LogLevel = logLevel };
this.AppendError(error, logLevel);

return this;
Expand All @@ -131,7 +140,7 @@ public OperationResult AppendError<T>(string message, int? code = null, LogLevel
{
if (string.IsNullOrWhiteSpace(message)) throw new ArgumentNullException(nameof(message));

var error = new T() { Message = message, Code = code, Details = details };
var error = new T() { Message = message, Code = code, Details = details, LogLevel = logLevel };
this.AppendError(error, logLevel);

return this;
Expand All @@ -149,9 +158,8 @@ public OperationResult AppendError(IOperationError error, LogLevel? logLevel = L

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
error.Logged = true;
}

return this;
Expand All @@ -171,7 +179,7 @@ public OperationResult AppendException(Exception exception, int? errorCode = nul
// Append the exception as a first if it is not yet set.
this.InitialException ??= exception;

var error = new OperationError(exception.ToString(), errorCode);
var error = new OperationError(exception.ToString(), errorCode, null, LogLevel.Error);
this.AppendError(error, logLevel);

return this;
Expand Down Expand Up @@ -210,7 +218,6 @@ 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>
Expand Down Expand Up @@ -284,26 +291,6 @@ 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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
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 @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public async Task SerializeWithSystemTextJsonIncludesTypeDiscriminator()
// Assert
Assert.Contains(testText, text);
Assert.Contains(operationErrorText, text);
var expectText = @"{""Success"":false,""Errors"":[{""type"":""operation_error"",""Code"":123,""Message"":""Test"",""Details"":""\u0022type\u0022""}]}";
var expectText = @"{""Success"":false,""Errors"":[{""type"":""operation_error"",""Code"":123,""Message"":""Test"",""Details"":""\u0022type\u0022"",""LogLevel"":null}]}";
Assert.Equal(expectText, text);
}

Expand All @@ -67,15 +67,15 @@ public async Task SerializeWithSystemTextJsonSetsExpectedJson()
var testText = "\"type\"";
var outputStream = new MemoryStream();
var operationResult = new OperationResult();
operationResult.AppendError(new OperationError(message: "Test") { Code = 123, Details = testText });
operationResult.AppendError(new OperationError(message: "Test") { Code = 123, Details = testText, LogLevel = Microsoft.Extensions.Logging.LogLevel.Warning });

// Act
await JsonSerializer.SerializeAsync(outputStream, operationResult, GetSerializationOptions());
outputStream.Position = 0;
string text = new StreamReader(outputStream).ReadToEnd();

// Assert
var expectText = @"{""Success"":false,""Errors"":[{""type"":""operation_error"",""Code"":123,""Message"":""Test"",""Details"":""\u0022type\u0022""}]}";
var expectText = @"{""Success"":false,""Errors"":[{""type"":""operation_error"",""Code"":123,""Message"":""Test"",""Details"":""\u0022type\u0022"",""LogLevel"":3}]}";
Comment thread
TonyTroeff marked this conversation as resolved.
Outdated
Assert.Equal(expectText, text);
}

Expand Down
28 changes: 28 additions & 0 deletions tests/OneBitSoftware.Utilities.OperationResultTests/TestLogger.cs
Original file line number Diff line number Diff line change
@@ -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<string> _logMessages = new List<string>();

public IReadOnlyList<string> LogMessages => _logMessages.AsReadOnly();

public IDisposable BeginScope<TState>(TState state) => null;

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (formatter != null)
{
_logMessages.Add(formatter(state, exception));
}
}
}
}