Skip to content

Commit 92e8e13

Browse files
removed netstandard2.1 and added net5.0. Added serialization support
1 parent 7d6f64d commit 92e8e13

8 files changed

Lines changed: 255 additions & 7 deletions

src/OneBitSoftware.Utilities.OperationResult/OneBitSoftware.Utilities.OperationResult.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
4+
<TargetFrameworks>net6.0;net5.0</TargetFrameworks>
55
<ImplicitUsings>disable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
</PropertyGroup>

src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
/// </summary>
1313
public class OperationResult
1414
{
15-
private readonly List<IOperationError> _errors = new List<IOperationError>();
1615
private readonly List<string> _successMessages = new List<string>();
1716

1817
protected readonly ILogger? _logger;
@@ -41,7 +40,7 @@ public IEnumerable<string> SuccessMessages
4140
/// <summary>
4241
/// Gets an <see cref="List{T}"/> containing the error codes and messages of the <see cref="OperationResult{T}" />.
4342
/// </summary>
44-
public IReadOnlyCollection<IOperationError> Errors => this._errors.AsReadOnly();
43+
public List<IOperationError> Errors { get; internal set; } = new List<IOperationError>();
4544

4645
/// <summary>
4746
/// Gets or sets the first exception that resulted from the operation.
@@ -214,7 +213,7 @@ public static OperationResult FromError(string message, int? code = null, LogLev
214213
/// Appends an <see cref="IOperationError"/> to the internal errors collection.
215214
/// </summary>
216215
/// <param name="error">An instance of <see cref="IOperationError"/> to add to the internal errors collection.</param>
217-
protected void AppendErrorInternal(IOperationError error) => this._errors.Add(error);
216+
protected void AppendErrorInternal(IOperationError error) => this.Errors.Add(error);
218217
}
219218

220219
/// <summary>
@@ -236,7 +235,7 @@ public OperationResult()
236235
/// </summary>
237236
/// <param name="logger">An instance of <see cref="ILoggerService"/>.</param>
238237
/// <remarks>If the operation is a get operation, an empty result must return a truthy Success value.</remarks>
239-
public OperationResult(ILogger logger) : base(logger)
238+
public OperationResult(ILogger? logger) : base(logger)
240239
{
241240
}
242241

@@ -246,7 +245,7 @@ public OperationResult(ILogger logger) : base(logger)
246245
/// <param name="resultObject">An initial failure message for the operation result. This will fail the success status.</param>
247246
/// <param name="logger">An instance of <see cref="ILogger"/>.</param>
248247
/// <remarks>If the operation is a get operation, an empty result must return a truthy Success value.</remarks>
249-
public OperationResult(TResult resultObject, ILogger logger) : base(logger)
248+
public OperationResult(TResult resultObject, ILogger? logger) : base(logger)
250249
{
251250
this.ResultObject = resultObject;
252251
}
@@ -264,7 +263,7 @@ public OperationResult(TResult resultObject) : base()
264263
/// <summary>
265264
/// Gets or sets the related result object of the operation.
266265
/// </summary>
267-
public TResult? ResultObject { get; set; }
266+
public TResult ResultObject { get; set; }
268267

269268
/// <summary>
270269
/// This method will append an error with a specific `user-friendly` message to this operation result instance.

src/OneBitSoftware.Utilities.OperationResult/OperationResultValidationExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,5 +96,25 @@ public static void ValidateNull<T>(this OperationResult<T> operationResult, obje
9696
var errorMessage = $"{className}, {methodName} - The {propertyName} is null.";
9797
operationResult.AppendError(errorMessage, logLevel: level);
9898
}
99+
100+
/// <summary>
101+
/// Use this method to check if a value is not null.
102+
/// If you want to validate that an entity exists, use the "ValidateExist" extension method.
103+
/// If you want to validate that the currently authenticated user is not null, use the "ValidateUser" extension method.
104+
/// If <paramref name="value"/> is null, an error message should be appended and a log of the passed <paramref name="level"/> severity would be created.
105+
/// </summary>
106+
/// <param name="value">The value that should be validated.</param>
107+
/// <param name="className">The name of the class where the <paramref name="methodName"/> is defined.</param>
108+
/// <param name="methodName">The name of he method where <paramref name="value"/> is used.</param>
109+
/// <param name="propertyName">The name of the property.</param>
110+
/// <param name="level">The logging severity.</param>
111+
public static void ValidateNull(this OperationResult operationResult, object value, string className, string methodName, string propertyName, LogLevel level = LogLevel.Error)
112+
{
113+
// If the passed value is null, log and append an error message.
114+
if (value != null) return;
115+
116+
var errorMessage = $"{className}, {methodName} - The {propertyName} is null.";
117+
operationResult.AppendError(errorMessage, logLevel: level);
118+
}
99119
}
100120
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Collections.Generic;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using OneBitSoftware.Utilities.Errors;
7+
8+
namespace OneBitSoftware.Utilities
9+
{
10+
public class PolymorphicOperationErrorConverter<T> : JsonConverter<T>
11+
where T : IOperationError
12+
{
13+
private readonly Dictionary<string, Type> _valueMappings = new Dictionary<string, Type>();
14+
private readonly Dictionary<Type, string> _typeMappings = new Dictionary<Type, string>();
15+
protected virtual string TypePropertyName => "type";
16+
17+
public override bool CanConvert(Type typeToConvert)
18+
{
19+
if (typeToConvert is null) return false;
20+
return typeof(OperationError).IsAssignableFrom(typeToConvert);
21+
}
22+
23+
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
24+
{
25+
try
26+
{
27+
// Deserialize the JSON to the specified type.
28+
var serializationOptions = this.ConstructSafeFallbackOptions(options);
29+
serializationOptions.Converters.Add(new ReadOnlyPartialConverter(this));
30+
return (T)JsonSerializer.Deserialize(ref reader, typeToConvert, serializationOptions);
31+
}
32+
catch (Exception ex)
33+
{
34+
throw new InvalidOperationException("Invalid JSON in request.", ex);
35+
}
36+
}
37+
38+
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
39+
{
40+
if (value is null)
41+
{
42+
writer.WriteNullValue();
43+
return;
44+
}
45+
46+
if (this._typeMappings.TryGetValue(value.GetType(), out var typeValue) == false) throw new InvalidOperationException($"Model of type {value.GetType()} cannot be successfully serialized.");
47+
48+
var tempBufferWriter = new ArrayBufferWriter<byte>();
49+
var tempWriter = new Utf8JsonWriter(tempBufferWriter);
50+
51+
var fallbackDeserializationOptions = this.ConstructSafeFallbackOptions(options);
52+
JsonSerializer.Serialize(tempWriter, value, value.GetType(), fallbackDeserializationOptions);
53+
54+
tempWriter.Flush();
55+
var jsonDocument = JsonDocument.Parse(tempBufferWriter.WrittenMemory);
56+
57+
writer.WriteStartObject();
58+
writer.WriteString(this.TypePropertyName, typeValue);
59+
60+
foreach (var property in jsonDocument.RootElement.EnumerateObject()) property.WriteTo(writer);
61+
writer.WriteEndObject();
62+
}
63+
64+
65+
protected bool AddMapping(string typeValue, Type type)
66+
{
67+
if (string.IsNullOrWhiteSpace(typeValue) || type is null) return false;
68+
if (this._valueMappings.ContainsKey(typeValue) || this._typeMappings.ContainsKey(type)) return false;
69+
70+
this._valueMappings[typeValue] = type;
71+
this._typeMappings[type] = typeValue;
72+
return true;
73+
}
74+
75+
private Type GetType(JsonElement typeElement)
76+
{
77+
if (typeElement.ValueKind != JsonValueKind.String) return null;
78+
79+
var stringValue = typeElement.GetString();
80+
if (string.IsNullOrWhiteSpace(stringValue)) return null;
81+
82+
this._valueMappings.TryGetValue(stringValue, out var type);
83+
return type;
84+
}
85+
86+
private JsonSerializerOptions ConstructSafeFallbackOptions(JsonSerializerOptions options)
87+
{
88+
var fallbackSerializationOptions = new JsonSerializerOptions(options);
89+
fallbackSerializationOptions.Converters.Remove(this);
90+
return fallbackSerializationOptions;
91+
}
92+
93+
private class ReadOnlyPartialConverter : JsonConverter<T>
94+
{
95+
private readonly PolymorphicOperationErrorConverter<T> _polymorphicConverter;
96+
97+
internal ReadOnlyPartialConverter(PolymorphicOperationErrorConverter<T> polymorphicConverter)
98+
{
99+
this._polymorphicConverter = polymorphicConverter ?? throw new ArgumentNullException(nameof(polymorphicConverter));
100+
}
101+
102+
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T);
103+
104+
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
105+
{
106+
// We copy the value here so we can easily reuse the reader for the subsequent deserialization.
107+
var readerRestore = reader;
108+
109+
// Get the `type` value by parsing the JSON string into a JsonDocument.
110+
var jsonDocument = JsonDocument.ParseValue(ref reader);
111+
jsonDocument.RootElement.TryGetProperty(this._polymorphicConverter.TypePropertyName, out var typeElement);
112+
113+
var returnType = this._polymorphicConverter.GetType(typeElement);
114+
if (returnType is null) throw new InvalidOperationException("The received JSON cannot be deserialized to any known type.");
115+
116+
try
117+
{
118+
// Deserialize the JSON to the specified type.
119+
return (T)JsonSerializer.Deserialize(ref readerRestore, returnType, options);
120+
}
121+
catch (Exception ex)
122+
{
123+
throw new InvalidOperationException("Invalid JSON in request.", ex);
124+
}
125+
}
126+
127+
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => throw new InvalidOperationException("The `Read only partial converter` cannot be used for serialization.");
128+
}
129+
}
130+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace OneBitSoftware.Utilities.OperationResultTests
2+
{
3+
internal class CustomError
4+
{
5+
public string CustomProperty { get; set; } = null!;
6+
}
7+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace OneBitSoftware.Utilities.OperationResultTests
2+
{
3+
using OneBitSoftware.Utilities.Errors;
4+
5+
/// <summary>
6+
/// A <see cref="System.Text.Json.Serialization.JsonConverter"/> inheriting class for polymorphic serialization.deserialization of custom <see cref="IOperationError"/> implementations.
7+
/// </summary>
8+
internal class CustomErrorPolymorphicConverter : PolymorphicOperationErrorConverter<IOperationError>
9+
{
10+
public CustomErrorPolymorphicConverter()
11+
{
12+
// Define your discriminator and custom type mapping
13+
this.AddMapping("custom_error", typeof(CustomError));
14+
}
15+
}
16+
}

tests/OneBitSoftware.Utilities.OperationResultTests/OneBitSoftware.Utilities.OperationResultTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
11+
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
1112
<PackageReference Include="xunit" Version="2.4.1" />
1213
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
1314
<PrivateAssets>all</PrivateAssets>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Text.Json;
6+
using System.Threading.Tasks;
7+
using OneBitSoftware.Utilities.Errors;
8+
using Xunit;
9+
10+
namespace OneBitSoftware.Utilities.OperationResultTests
11+
{
12+
public class OperationResultSerializationTests
13+
{
14+
private JsonSerializerOptions GetSerializationOptions()
15+
{
16+
var serializeOptions = new JsonSerializerOptions();
17+
serializeOptions.Converters.Add(new CustomErrorPolymorphicConverter());
18+
return serializeOptions;
19+
}
20+
21+
[Fact]
22+
public async Task CanSerializeWithSystemTextJson()
23+
{
24+
// Arrange
25+
var testText = "Test details";
26+
var outputStream = new MemoryStream();
27+
var operationResult = new OperationResult();
28+
operationResult.AppendError(new OperationError(message: "Test") { Code = 123, Details = testText });
29+
30+
// Act
31+
await JsonSerializer.SerializeAsync<OperationResult>(outputStream, operationResult, GetSerializationOptions());
32+
outputStream.Position = 0;
33+
string text = new StreamReader(outputStream).ReadToEnd();
34+
35+
// Assert
36+
Assert.Contains(testText, text);
37+
}
38+
39+
[Fact]
40+
public async Task CanSerializeAndDeserializeWithSystemTextJson()
41+
{
42+
// Arrange
43+
var testText = "Test details";
44+
var serializeStream = new MemoryStream();
45+
var deserializeStream = new MemoryStream();
46+
var operationResult = new OperationResult();
47+
operationResult.AppendError(new OperationError(message: "Test") { Code = 123, Details = testText });
48+
49+
// Act
50+
var serializeString = JsonSerializer.Serialize<OperationResult>(operationResult, GetSerializationOptions());
51+
52+
var resultObject = JsonSerializer.Deserialize<OperationResult>(serializeString, GetSerializationOptions());
53+
54+
// Assert
55+
Assert.NotNull(resultObject);
56+
Assert.Equal(operationResult.Errors.Count(), resultObject?.Errors.Count());
57+
}
58+
59+
[Fact]
60+
public async Task CanSerializeWithNewtonSoftJson()
61+
{
62+
// Arrange
63+
var testText = "Test details";
64+
var outputStream = new MemoryStream();
65+
var operationResult = new OperationResult();
66+
operationResult.AppendError(new OperationError(message: "Test") { Code = 123, Details = testText });
67+
68+
// Act
69+
var resultString = Newtonsoft.Json.JsonConvert.SerializeObject(operationResult);
70+
71+
// Assert
72+
Assert.Contains(testText, resultString);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)