Skip to content

Commit

Permalink
Added ResultErrorBase
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedkamalio committed Nov 15, 2024
1 parent 73ae2f3 commit 866abd9
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 58 deletions.
159 changes: 112 additions & 47 deletions src/ResultObject/ResultError.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
namespace ResultObject;

/// <summary>
/// Base record for error result implementations providing common functionality.
/// </summary>
public abstract record ResultErrorBase(
string Code,
string Reason,
string Message,
ResultError? InnerError,
string? StackTrace
)
{
/// <summary>
/// Defines the level of detail to include when sanitizing error information.
/// </summary>
public enum SanitizationLevel
{
/// <summary>
/// No information is removed or modified.
/// </summary>
None,

/// <summary>
/// Only sensitive message content is sanitized while preserving error structure.
/// </summary>
MessageOnly,

/// <summary>
/// All potentially sensitive information is removed, including stack traces and inner errors.
/// </summary>
Full
}

/// <summary>
/// Creates a new instance of the error with the current stack trace information.
/// </summary>
/// <returns>A new error instance with the current stack trace.</returns>
public abstract ResultErrorBase WithStackTrace();

/// <summary>
/// Creates a sanitized version of the error suitable for external presentation.
/// </summary>
protected ResultErrorBase Sanitize(SanitizationLevel level) =>
level switch
{
SanitizationLevel.None => this,
SanitizationLevel.MessageOnly => this with
{
Message = "An error occurred.",
StackTrace = null
},
SanitizationLevel.Full => this with
{
Message = "An error occurred.",
Reason = "Internal Error",
StackTrace = null,
InnerError = null
},
_ => this
};

/// <summary>
/// Returns a string representation of the error, including code, reason, and message.
/// </summary>
public override string ToString() =>
$"Code: {Code}, Reason: {Reason}, Message: {Message}" +
(StackTrace != null ? $"\nStack Trace: {StackTrace}" : string.Empty) +
(InnerError != null ? $"\nInner Error: {InnerError}" : string.Empty);
}

/// <summary>
/// Represents a strongly-typed error result with a customizable error category.
/// </summary>
Expand Down Expand Up @@ -36,8 +105,8 @@ public record ResultError<TErrorCategory>(
string Message,
TErrorCategory? Category = null,
ResultError? InnerError = null,
string? StackTrace = null
) where TErrorCategory : struct, Enum
string? StackTrace = null) : ResultErrorBase(Code, Reason, Message, InnerError, StackTrace)
where TErrorCategory : struct, Enum
{
/// <summary>
/// Creates a new instance of the error with the current stack trace information.
Expand All @@ -53,15 +122,13 @@ public record ResultError<TErrorCategory>(
/// .WithStackTrace();
/// </code>
/// </example>
public ResultError<TErrorCategory> WithStackTrace() =>
new(Code, Reason, Message, Category, InnerError, Environment.StackTrace);
public override ResultError<TErrorCategory> WithStackTrace() => this with { StackTrace = Environment.StackTrace };

/// <summary>
/// Creates a sanitized version of the error suitable for external presentation.
/// </summary>
/// <param name="level">The desired level of sanitization to apply.</param>
/// <returns>A new <see cref="ResultError{TErrorCategory}"/> instance with sanitized information.</returns>
/// <exception cref="ArgumentException">Thrown when an invalid sanitization level is provided.</exception>
/// <remarks>
/// Sanitization helps prevent sensitive information leakage when errors are exposed to external systems or end users.
/// Different levels of sanitization can be applied based on the security requirements:
Expand All @@ -79,27 +146,8 @@ public ResultError<TErrorCategory> WithStackTrace() =>
/// // Results in a sanitized error with generic message
/// </code>
/// </example>
public ResultError<TErrorCategory> Sanitize(
ResultError.SanitizationLevel level = ResultError.SanitizationLevel.MessageOnly)
{
return level switch
{
ResultError.SanitizationLevel.None => this,
ResultError.SanitizationLevel.MessageOnly => this with
{
Message = "An error occurred.",
StackTrace = null
},
ResultError.SanitizationLevel.Full => this with
{
Message = "An error occurred.",
Reason = "Internal Error",
StackTrace = null,
InnerError = null
},
_ => throw new ArgumentException($"Invalid sanitization level: {level}")
};
}
public new ResultError<TErrorCategory> Sanitize(SanitizationLevel level = SanitizationLevel.MessageOnly) =>
(ResultError<TErrorCategory>)base.Sanitize(level);

/// <summary>
/// Returns a string representation of the error, including category, code, reason, and message.
Expand All @@ -109,10 +157,7 @@ public ResultError<TErrorCategory> Sanitize(
/// The string representation includes all critical error information and any inner error details.
/// This is useful for logging and debugging purposes.
/// </remarks>
public override string ToString() =>
$"[{Category}] Code: {Code}, Reason: {Reason}, Message: {Message}" +
(StackTrace != null ? $"\nStack Trace: {StackTrace}" : string.Empty) +
(InnerError != null ? $"\nInner Error: {InnerError}" : string.Empty);
public override string ToString() => $"[{Category}] {base.ToString()}";
}

/// <summary>
Expand Down Expand Up @@ -148,25 +193,45 @@ public record ResultError(
: ResultError<ErrorCategory>(Code, Reason, Message, Category, InnerError, StackTrace)
{
/// <summary>
/// Defines the level of detail to include when sanitizing error information.
/// Creates a new instance of the error with the current stack trace information.
/// </summary>
public enum SanitizationLevel
{
/// <summary>
/// No information is removed or modified.
/// </summary>
None,

/// <summary>
/// Only sensitive message content is sanitized while preserving error structure.
/// </summary>
MessageOnly,
/// <returns>A new <see cref="ResultError"/> instance with the current stack trace.</returns>
/// <remarks>
/// This method is useful for capturing the call stack at specific points during error handling.
/// The stack trace can help with debugging and error tracking in development environments.
/// </remarks>
/// <example>
/// <code>
/// var error = new ResultError("CODE", "Reason", "Message")
/// .WithStackTrace();
/// </code>
/// </example>
public new ResultError WithStackTrace() => (ResultError)base.WithStackTrace();

/// <summary>
/// All potentially sensitive information is removed, including stack traces and inner errors.
/// </summary>
Full
}
/// <summary>
/// Creates a sanitized version of the error suitable for external presentation.
/// </summary>
/// <param name="level">The desired level of sanitization to apply.</param>
/// <returns>A new <see cref="ResultError"/> instance with sanitized information.</returns>
/// <remarks>
/// Sanitization helps prevent sensitive information leakage when errors are exposed to external systems or end users.
/// Different levels of sanitization can be applied based on the security requirements:
/// - None: No sanitization is applied
/// - MessageOnly: Only the message is sanitized
/// - Full: All potentially sensitive information is removed
/// </remarks>
/// <example>
/// <code>
/// var error = new ResultError(
/// "SENSITIVE_ERROR",
/// "Database Error",
/// "Failed to connect to db server: myserver:1433")
/// .Sanitize(SanitizationLevel.Full);
/// // Results in a sanitized error with generic message
/// </code>
/// </example>
public new ResultError Sanitize(SanitizationLevel level = SanitizationLevel.MessageOnly) =>
(ResultError)base.Sanitize(level);

/// <summary>
/// Returns a string representation of the error, including category, code, reason, and message.
Expand Down
5 changes: 3 additions & 2 deletions src/ResultObject/ResultObject.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
<Nullable>enable</Nullable>

<!-- NuGet Package Metadata -->
<Title>.Net ResultObject</Title>
<PackageId>ResultObject</PackageId>
<Version>1.1.0</Version>
<Version>1.1.1</Version>
<Authors>Ahmed Kamal</Authors>
<Description>A robust Result type for .NET with built-in error categorization, supporting type-safe success/failure scenarios without exceptions.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/ahmedkamalio/DotNet.ResultObject</RepositoryUrl>
<PackageProjectUrl>https://github.com/ahmedkamalio/DotNet.ResultObject</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>Utility, Result, ErrorHandling, DotNet</PackageTags>
<PackageTags>Utility, Result, ErrorHandling, DotNet, MonadPattern, RailwayOriented, TypeSafe, ExceptionFree, Validation, ErrorCategories, FunctionalProgramming, DomainDriven, CleanCode, CSharp, NetCore, Nullable</PackageTags>

<!-- Optional NuGet Metadata -->
<RepositoryType>git</RepositoryType>
Expand Down
17 changes: 8 additions & 9 deletions test/ResultObject.Tests/ResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,10 @@ public void WithStackTrace_ShouldCreateNewInstanceWithStackTrace()
}

[Theory]
[InlineData(ResultError.SanitizationLevel.None)]
[InlineData(ResultError.SanitizationLevel.MessageOnly)]
[InlineData(ResultError.SanitizationLevel.Full)]
public void Sanitize_WithDifferentLevels_ShouldSanitizeAppropriately(
ResultError.SanitizationLevel level)
[InlineData(ResultErrorBase.SanitizationLevel.None)]
[InlineData(ResultErrorBase.SanitizationLevel.MessageOnly)]
[InlineData(ResultErrorBase.SanitizationLevel.Full)]
public void Sanitize_WithDifferentLevels_ShouldSanitizeAppropriately(ResultErrorBase.SanitizationLevel level)
{
// Arrange
var error = new ResultError(
Expand All @@ -192,18 +191,18 @@ public void Sanitize_WithDifferentLevels_ShouldSanitizeAppropriately(
// Assert
switch (level)
{
case ResultError.SanitizationLevel.None:
case ResultErrorBase.SanitizationLevel.None:
sanitized.Should().BeEquivalentTo(error);
break;

case ResultError.SanitizationLevel.MessageOnly:
case ResultErrorBase.SanitizationLevel.MessageOnly:
sanitized.Code.Should().Be(error.Code);
sanitized.Reason.Should().Be(error.Reason);
sanitized.Message.Should().Be("An error occurred.");
sanitized.StackTrace.Should().BeNull();
break;

case ResultError.SanitizationLevel.Full:
case ResultErrorBase.SanitizationLevel.Full:
sanitized.Code.Should().Be(error.Code);
sanitized.Reason.Should().Be("Internal Error");
sanitized.Message.Should().Be("An error occurred.");
Expand Down Expand Up @@ -305,7 +304,7 @@ public void ComplexScenario_WithErrorHandling_ShouldPreserveErrorInformation()

// Act
var sanitizedResult = Result.Failure<string, ErrorCategory>(
result.Error!.Sanitize(ResultError.SanitizationLevel.Full)
result.Error!.Sanitize(ResultErrorBase.SanitizationLevel.Full)
);

// Assert
Expand Down

0 comments on commit 866abd9

Please sign in to comment.