Skip to content

Commit 19864d3

Browse files
Merge pull request #11 from svetstoykov/b-fix-append-error-generic-type
Add `AppendErrors` method to `OperationResult<TResult>`
2 parents c43786b + d08d4d9 commit 19864d3

File tree

3 files changed

+326
-1
lines changed

3 files changed

+326
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<PackageRequireLicenseAcceptance>False</PackageRequireLicenseAcceptance>
3333
<PackageReadmeFile>README.md</PackageReadmeFile>
3434
<PackageTags>OneBitSoftware; OperationResult;</PackageTags>
35-
<Version>2.0.0</Version>
35+
<Version>2.1.0</Version>
3636
</PropertyGroup>
3737

3838
</Project>

src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,17 @@ public OperationResult(TResult resultObject) : base()
295295
/// </summary>
296296
public TResult? ResultObject { get; set; }
297297

298+
/// <summary>
299+
/// Appends error from <paramref name="otherOperationResult"/> to the current instance.
300+
/// </summary>
301+
/// <param name="otherOperationResult">The <see cref="OperationResult"/> to append from.</param>
302+
/// <returns>The original <see cref="OperationResult"/> with the appended messages from <paramref name="otherOperationResult"/>.</returns>
303+
public new OperationResult<TResult> AppendErrors(OperationResult? otherOperationResult)
304+
{
305+
base.AppendErrors(otherOperationResult);
306+
return this;
307+
}
308+
298309
/// <summary>
299310
/// This method will append an error with a specific `user-friendly` message to this operation result instance.
300311
/// </summary>

tests/OneBitSoftware.Utilities.OperationResultTests/OperationResultAppendErrorsTests.cs

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,318 @@ public void AppendErrors_ShouldLogWhenCreatedWithNoLogger()
177177
// Assert
178178
Assert.Equal(1, testLogger.LogMessages.Count);
179179
}
180+
181+
[Fact]
182+
public void AppendErrors_NonGenericSource_To_GenericTarget_Retains_Generic_Type_And_MergesErrors()
183+
{
184+
// Arrange
185+
var source = new OperationResult();
186+
source.AppendError("E1", 101, LogLevel.Warning, "D1");
187+
188+
var target = new OperationResult<string>();
189+
target.AppendError("E0", 100, LogLevel.Information, "D0");
190+
191+
// Act
192+
var returned = target.AppendErrors(source);
193+
194+
// Assert
195+
Assert.Same(target, returned);
196+
Assert.True(target.Fail);
197+
Assert.Equal(2, target.Errors.Count);
198+
Assert.NotNull(target.Errors.Single(e => e is { Code: 100, Message: "E0" }));
199+
Assert.NotNull(target.Errors.Single(e => e is { Code: 101, Message: "E1" }));
200+
}
201+
202+
[Fact]
203+
public void AppendErrors_GenericSource_To_GenericTarget_Retains_Generic_Type_And_MergesErrors()
204+
{
205+
// Arrange
206+
var source = new OperationResult<double>();
207+
source.AppendError("E1", 101, LogLevel.Warning, "D1");
208+
209+
var target = new OperationResult<string>();
210+
target.AppendError("E0", 100, LogLevel.Information, "D0");
211+
212+
// Act
213+
var returned = target.AppendErrors(source);
214+
215+
// Assert
216+
Assert.Same(target, returned);
217+
Assert.True(target.Fail);
218+
Assert.Equal(2, target.Errors.Count);
219+
Assert.NotNull(target.Errors.Single(e => e is { Code: 100, Message: "E0" }));
220+
Assert.NotNull(target.Errors.Single(e => e is { Code: 101, Message: "E1" }));
221+
}
222+
223+
[Fact]
224+
public void AppendErrors_WithNullSource_On_NonGenericTarget_Returns_Same_Instance_And_NoChange()
225+
{
226+
// Arrange
227+
var target = new OperationResult();
228+
target.AppendError("E0", 100, LogLevel.Information, "D0");
229+
var beforeCount = target.Errors.Count;
230+
231+
// Act
232+
var returned = target.AppendErrors(null);
233+
234+
// Assert
235+
Assert.Same(target, returned);
236+
Assert.Equal(beforeCount, target.Errors.Count);
237+
}
238+
239+
[Fact]
240+
public void AppendErrors_WithNullSource_On_GenericTarget_Returns_Same_Instance_And_NoChange()
241+
{
242+
// Arrange
243+
var target = new OperationResult<object>();
244+
target.AppendError("E0", 100, LogLevel.Information, "D0");
245+
var beforeCount = target.Errors.Count;
246+
247+
// Act
248+
var returned = target.AppendErrors((OperationResult)null);
249+
250+
// Assert
251+
Assert.Same(target, returned);
252+
Assert.Equal(beforeCount, target.Errors.Count);
253+
}
254+
255+
[Fact]
256+
public void AppendErrors_PreservesResultObject_WhenMergingErrors()
257+
{
258+
// Arrange
259+
var originalResult = new { Id = 42, Name = "Test" };
260+
var target = new OperationResult<object>(originalResult);
261+
262+
var source = new OperationResult<int>();
263+
source.AppendError("Source error", 500);
264+
265+
// Act
266+
var returned = target.AppendErrors(source);
267+
268+
// Assert
269+
Assert.Same(target, returned);
270+
Assert.Same(originalResult, target.ResultObject);
271+
Assert.Single(target.Errors);
272+
Assert.True(target.Fail);
273+
}
274+
275+
[Fact]
276+
public void AppendErrors_MaintainsFluentChaining_WithGenericType()
277+
{
278+
// Arrange
279+
var source1 = new OperationResult<double>();
280+
source1.AppendError("E1", 1);
281+
282+
var source2 = new OperationResult<int>();
283+
source2.AppendError("E2", 2);
284+
285+
var target = new OperationResult<string>();
286+
287+
// Act - Chain multiple AppendErrors calls
288+
var returned = target
289+
.AppendErrors(source1)
290+
.AppendErrors(source2)
291+
.AppendError("E3", 3);
292+
293+
// Assert
294+
Assert.Same(target, returned);
295+
Assert.IsType<OperationResult<string>>(returned);
296+
Assert.Equal(3, target.Errors.Count);
297+
}
298+
299+
[Fact]
300+
public void AppendErrors_PreservesInitialException_FromSource()
301+
{
302+
// Arrange
303+
var sourceException = new InvalidOperationException("Source exception");
304+
var source = new OperationResult<int>();
305+
source.AppendException(sourceException, 999);
306+
307+
var target = new OperationResult<double>();
308+
309+
// Act
310+
target.AppendErrors(source);
311+
312+
// Assert
313+
Assert.Null(target.InitialException); // Target doesn't inherit source's InitialException
314+
Assert.Single(target.Errors);
315+
Assert.Contains(sourceException.ToString(), target.Errors[0].Message);
316+
}
317+
318+
[Fact]
319+
public void AppendErrors_HandlesSourceWithMultipleErrorTypes()
320+
{
321+
// Arrange
322+
var source = new OperationResult<List<string>>();
323+
source.AppendError("Standard error", 100);
324+
source.AppendException(new ArgumentNullException("param"), 200);
325+
source.AppendError("Another error", 300, LogLevel.Critical, "Critical details");
326+
327+
var target = new OperationResult<int>();
328+
329+
// Act
330+
target.AppendErrors(source);
331+
332+
// Assert
333+
Assert.Equal(3, target.Errors.Count);
334+
Assert.NotNull(target.Errors.Single(e => e.Code == 100));
335+
Assert.NotNull(target.Errors.Single(e => e.Code == 200));
336+
Assert.NotNull(target.Errors.Single(e => e is { Code: 300, Details: "Critical details" }));
337+
}
338+
339+
[Fact]
340+
public void AppendErrors_MergesSuccessMessages_AreNotTransferred()
341+
{
342+
// Arrange
343+
var source = new OperationResult<double>();
344+
source.AddSuccessMessage("Success from source");
345+
source.AppendError("But has error", 1);
346+
347+
var target = new OperationResult<string>();
348+
target.AddSuccessMessage("Success from target");
349+
350+
// Act
351+
target.AppendErrors(source);
352+
353+
// Assert - SuccessMessages are not merged in AppendErrors
354+
Assert.Single(target.SuccessMessages);
355+
Assert.Contains("Success from target", target.SuccessMessages);
356+
}
357+
358+
[Fact]
359+
public void AppendErrors_EmptySource_DoesNotAffectTarget()
360+
{
361+
// Arrange
362+
var source = new OperationResult<double>();
363+
var target = new OperationResult<string>();
364+
target.AppendError("Target error", 1);
365+
366+
// Act
367+
var returned = target.AppendErrors(source);
368+
369+
// Assert
370+
Assert.Same(target, returned);
371+
Assert.Single(target.Errors);
372+
Assert.True(target.Fail);
373+
}
374+
375+
[Fact]
376+
public void AppendErrors_EmptyTarget_AcceptsSourceErrors()
377+
{
378+
// Arrange
379+
var source = new OperationResult<object>();
380+
source.AppendError("Source error", 100);
381+
382+
var target = new OperationResult<string>();
383+
384+
// Act
385+
target.AppendErrors(source);
386+
387+
// Assert
388+
Assert.True(target.Fail);
389+
Assert.Single(target.Errors);
390+
Assert.Equal("Source error", target.Errors[0].Message);
391+
}
392+
393+
[Fact]
394+
public void AppendErrors_WithDifferentGenericTypes_MaintainsTargetType()
395+
{
396+
// Arrange
397+
var sourceInt = new OperationResult<int> { ResultObject = 42 };
398+
sourceInt.AppendError("Int error", 1);
399+
400+
var targetString = new OperationResult<string> { ResultObject = "test" };
401+
402+
// Act
403+
var returned = targetString.AppendErrors(sourceInt);
404+
405+
// Assert
406+
Assert.IsType<OperationResult<string>>(returned);
407+
Assert.Equal("test", targetString.ResultObject);
408+
Assert.Single(targetString.Errors);
409+
}
410+
411+
[Fact]
412+
public void AppendErrors_ChainedCalls_AccumulatesAllErrors()
413+
{
414+
// Arrange
415+
var s1 = new OperationResult();
416+
s1.AppendError("E1", 1);
417+
418+
var s2 = new OperationResult<int>();
419+
s2.AppendError("E2", 2);
420+
421+
var s3 = new OperationResult();
422+
s3.AppendError("E3", 3);
423+
424+
var target = new OperationResult<string>();
425+
426+
// Act
427+
target.AppendErrors(s1).AppendErrors(s2).AppendErrors(s3);
428+
429+
// Assert
430+
Assert.Equal(3, target.Errors.Count);
431+
Assert.Contains(target.Errors, e => e.Code == 1);
432+
Assert.Contains(target.Errors, e => e.Code == 2);
433+
Assert.Contains(target.Errors, e => e.Code == 3);
434+
}
435+
436+
[Fact]
437+
public void AppendErrors_WithComplexGenericType_PreservesTypeIntegrity()
438+
{
439+
// Arrange
440+
var complexObject = new Dictionary<string, List<int>>
441+
{
442+
["key1"] = [1, 2, 3]
443+
};
444+
445+
var target = new OperationResult<Dictionary<string, List<int>>>(complexObject);
446+
var source = new OperationResult<int>();
447+
source.AppendError("Complex type error", 999);
448+
449+
// Act
450+
var returned = target.AppendErrors(source);
451+
452+
// Assert
453+
Assert.Same(complexObject, target.ResultObject);
454+
Assert.IsType<OperationResult<Dictionary<string, List<int>>>>(returned);
455+
Assert.Single(target.Errors);
456+
}
457+
458+
[Fact]
459+
public void AppendErrors_ReturnsCorrectType_AfterMultipleOperations()
460+
{
461+
// Arrange
462+
var target = new OperationResult<int> { ResultObject = 100 };
463+
var source1 = new OperationResult<double>();
464+
source1.AppendError("E1", 1);
465+
466+
// Act
467+
var step1 = target.AppendErrors(source1);
468+
var step2 = step1.AppendError("E2", 2);
469+
var step3 = step2.AppendException(new Exception("E3"), 3);
470+
471+
// Assert
472+
Assert.IsType<OperationResult<int>>(step1);
473+
Assert.IsType<OperationResult<int>>(step2);
474+
Assert.IsType<OperationResult<int>>(step3);
475+
Assert.Same(target, step3);
476+
Assert.Equal(3, target.Errors.Count);
477+
}
478+
479+
[Fact]
480+
public void AppendErrors_GenericTarget_ShouldLogWhenCreatedWithALogger()
481+
{
482+
// Arrange
483+
var testLogger = new TestLogger();
484+
var operationResultNoLogger = new OperationResult();
485+
var operationResultWithLogger = new OperationResult<string>(testLogger);
486+
487+
// Act
488+
operationResultNoLogger.AppendError("test");
489+
operationResultWithLogger.AppendErrors(operationResultNoLogger);
490+
491+
// Assert
492+
Assert.Single(testLogger.LogMessages);
493+
}
180494
}

0 commit comments

Comments
 (0)