SimpleResults

A lightweight implementation of the Result pattern for error handling without exceptions
git clone git://git.hanetzok.net/SimpleResults
Log | Files | Refs

commit a5eca42d2dea30ab5265953f10765e5f87e4be3d
parent 3743d650067816b5cd38967be75a4ddb7cd3b035
Author: Markus Hanetzok <markus@hanetzok.net>
Date:   Fri, 24 Oct 2025 21:31:58 +0200

Update project references and improve result pattern documentation

- Enhanced XML documentation for `Result` and `TypedResult` classes.
- Added usage examples and detailed remarks for better developer guidance.
- Introduced improved factory method descriptions and safe access functions (`TryGetValue`, `TryGetError`).

Diffstat:
MSimpleResults/Results/Result.cs | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
MSimpleResults/Results/TypedResult.cs | 231++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
2 files changed, 338 insertions(+), 59 deletions(-)

diff --git a/SimpleResults/Results/Result.cs b/SimpleResults/Results/Result.cs @@ -1,18 +1,18 @@ /* - This file is part of Foobar. + This file is part of SimpleResults. - Foobar is free software: you can redistribute it and/or modify + SimpleResults is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - Foobar is distributed in the hope that it will be useful, + SimpleResults is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with Foobar. If not, see <http://www.gnu.org/licenses/>. + along with SimpleResults. If not, see <http://www.gnu.org/licenses/>. */ using SimpleResults.Errors; @@ -25,19 +25,52 @@ namespace SimpleResults.Results; /// Represents the result of an operation that does not return a value, indicating either success or failure. /// </summary> /// <remarks> +/// <para> /// This class implements the Result pattern, providing a type-safe way to handle operation outcomes /// without throwing exceptions. It encapsulates success/failure state and error details. +/// </para> +/// <para> /// Use <see cref="TypedResult{T}"/> when the operation needs to return a value on success. +/// </para> /// </remarks> +/// <example> +/// <code> +/// public Result ValidateAndSaveData(Data data) +/// { +/// if (data == null) +/// { +/// return Result.Failure(400, "Data cannot be null", null); +/// } +/// +/// try +/// { +/// _repository.Save(data); +/// return Result.Success(); +/// } +/// catch (Exception ex) +/// { +/// return Result.Failure(500, "Failed to save data", ex); +/// } +/// } +/// +/// // Usage +/// var result = ValidateAndSaveData(myData); +/// if (result.IsFailure) +/// { +/// var error = result.TryGetError(); +/// _logger.LogError(error.Message); +/// } +/// </code> +/// </example> public class Result { /// <summary> - /// Gets or sets a value indicating whether the operation completed successfully. + /// Gets a value indicating whether the operation completed successfully. /// </summary> /// <value> /// <c>true</c> if the operation succeeded; otherwise, <c>false</c>. /// </value> - private bool IsSuccess { get; } + public bool IsSuccess { get; } /// <summary> /// Gets a value indicating whether the operation failed. @@ -49,20 +82,24 @@ public class Result public bool IsFailure => !IsSuccess; /// <summary> - /// Gets or sets the error details when the operation fails. + /// Contains the error details when the operation fails. /// </summary> /// <value> /// An <see cref="ErrorDetail"/> object containing information about the failure, /// or <c>null</c> if the operation succeeded. /// </value> - private ErrorDetail? Error { get; } + /// <remarks> + /// This property should not be accessed directly. Use <see cref="TryGetError"/> instead + /// to safely retrieve error details with proper validation. + /// </remarks> + private readonly ErrorDetail? _error; /// <summary> /// Initializes a new instance of the <see cref="Result"/> class representing a successful operation. /// </summary> /// <remarks> /// This constructor is protected and should not be called directly. - /// Use the <see cref="Success"/> factory method instead. + /// Use the <see cref="Success"/> factory method instead to create successful results. /// </remarks> protected Result() { @@ -75,23 +112,32 @@ public class Result /// <param name="errorDetail">The details of the error that caused the operation to fail.</param> /// <remarks> /// This constructor is protected and should not be called directly. - /// Use the Failure factory methods instead. + /// Use the <see cref="Failure(ErrorDetail)"/> or <see cref="Failure(int, string, Exception?)"/> + /// factory methods instead to create failure results. /// </remarks> protected Result(ErrorDetail errorDetail) { IsSuccess = false; - Error = errorDetail; + _error = errorDetail; } /// <summary> /// Creates a new <see cref="Result"/> representing a successful operation. /// </summary> - /// <returns>A <see cref="Result"/> instance with <see cref="IsSuccess"/> set to <c>true</c>.</returns> + /// <returns>A <see cref="Result"/> instance with <see cref="IsSuccess"/> set to <c>true</c> + /// and <see cref="IsFailure"/> set to <c>false</c>.</returns> + /// <remarks> + /// Use this factory method to indicate that an operation completed successfully without errors. + /// Each call creates a new instance of <see cref="Result"/>. + /// </remarks> /// <example> /// <code> - /// public Result ProcessData() + /// public Result ProcessData(Data data) /// { - /// // ... perform operation ... + /// // Perform operation + /// _repository.Save(data); + /// + /// // Return success /// return Result.Success(); /// } /// </code> @@ -102,18 +148,26 @@ public class Result /// Creates a new <see cref="Result"/> representing a failed operation with the specified error details. /// </summary> /// <param name="error">The error details describing why the operation failed.</param> - /// <returns>A <see cref="Result"/> instance with <see cref="IsSuccess"/> set to <c>false</c> - /// and <see cref="ErrorDetail"/> populated with the provided error information.</returns> + /// <returns>A <see cref="Result"/> instance with <see cref="IsSuccess"/> set to <c>false</c>, + /// <see cref="IsFailure"/> set to <c>true</c>, and <see cref="_error"/> populated with the provided + /// error information.</returns> + /// <remarks> + /// Use this factory method when you already have an <see cref="ErrorDetail"/> object. + /// If you need to create the error detail inline, consider using + /// <see cref="Failure(int, string, Exception?)"/> instead. + /// </remarks> /// <example> /// <code> - /// public Result ProcessData() + /// public Result ProcessData(Data data) /// { /// if (data == null) /// { - /// var error = new ErrorDetail(404, "Data not found"); + /// var error = new ErrorDetail(400, "Data cannot be null"); /// return Result.Failure(error); /// } - /// // ... continue processing ... + /// + /// _repository.Save(data); + /// return Result.Success(); /// } /// </code> /// </example> @@ -124,30 +178,86 @@ public class Result /// </summary> /// <param name="code">A numeric error code identifying the type of failure.</param> /// <param name="msg">A human-readable message describing the failure.</param> - /// <param name="e">An optional exception that caused the failure. Can be <c>null</c>.</param> - /// <returns>A <see cref="Result"/> instance with <see cref="IsSuccess"/> set to <c>false</c> - /// and <see cref="ErrorDetail"/> populated with the provided error information.</returns> + /// <param name="e">An optional exception that caused the failure. Pass <c>null</c> if the failure + /// was not caused by an exception (e.g. validation failures).</param> + /// <returns>A <see cref="Result"/> instance with <see cref="IsSuccess"/> set to <c>false</c>, + /// <see cref="IsFailure"/> set to <c>true</c>, and <see cref="_error"/> populated with an + /// <see cref="ErrorDetail"/> created from the provided parameters.</returns> + /// <remarks> + /// This is a convenience factory method that creates an <see cref="ErrorDetail"/> internally. + /// Use this method when you want to create a failure result without explicitly creating + /// an <see cref="ErrorDetail"/> object first. + /// </remarks> /// <example> /// <code> - /// public Result ProcessData() + /// public Result ProcessData(Data data) /// { + /// if (data == null) + /// { + /// return Result.Failure(400, "Data cannot be null", null); + /// } + /// /// try /// { - /// // ... perform operation ... + /// _repository.Save(data); + /// return Result.Success(); /// } - /// catch (Exception ex) + /// catch (DbException ex) /// { - /// return Result.Failure(500, "Processing failed", ex); + /// return Result.Failure(500, "Failed to save data to database", ex); /// } - /// return Result.Success(); /// } /// </code> /// </example> public static Result Failure(int code, string msg, Exception? e) => new(new ErrorDetail(code, msg, e)); + /// <summary> + /// Attempts to retrieve the error details from a failed operation. + /// </summary> + /// <returns>An <see cref="ErrorDetail"/> object containing information about the failure.</returns> + /// <exception cref="InvalidOperationException"> + /// Thrown when attempting to get the error from a successful result. + /// Always check <see cref="IsFailure"/> before calling this method. + /// </exception> + /// <exception cref="NullReferenceException"> + /// Thrown if the internal error is null, which should not occur in normal usage + /// but indicates a programming error if it does. + /// </exception> + /// <remarks> + /// <para> + /// This method provides safe access to error details with validation. It ensures that: + /// <list type="number"> + /// <item><description>The result is in a failure state before returning error details</description></item> + /// <item><description>The error details are not null</description></item> + /// </list> + /// </para> + /// <para> + /// Best practice: Always check <see cref="IsFailure"/> before calling this method to avoid exceptions. + /// </para> + /// </remarks> + /// <example> + /// <code> + /// var result = ProcessData(myData); + /// + /// if (result.IsFailure) + /// { + /// var error = result.TryGetError(); + /// _logger.LogError($"Operation failed with code {error.ErrorCode}: {error.Message}"); + /// + /// if (error.Exception != null) + /// { + /// _logger.LogError(error.Exception, "Exception details"); + /// } + /// } + /// else + /// { + /// _logger.LogInformation("Operation completed successfully"); + /// } + /// </code> + /// </example> public ErrorDetail TryGetError() { if (IsSuccess) throw new InvalidOperationException("Can't get error: Result state is success!"); - return Error ?? throw new NullReferenceException("TypedResult.Error is null"); + return _error ?? throw new NullReferenceException("TypedResult.Error is null"); } } \ No newline at end of file diff --git a/SimpleResults/Results/TypedResult.cs b/SimpleResults/Results/TypedResult.cs @@ -1,18 +1,18 @@ /* - This file is part of Foobar. + This file is part of SimpleResults. - Foobar is free software: you can redistribute it and/or modify + SimpleResults is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - Foobar is distributed in the hope that it will be useful, + SimpleResults is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with Foobar. If not, see <http://www.gnu.org/licenses/>. + along with SimpleResults. If not, see <http://www.gnu.org/licenses/>. */ using SimpleResults.Errors; @@ -27,23 +27,65 @@ namespace SimpleResults.Results; /// Represents the result of an operation that returns a value of type <typeparamref name="T"/>, /// indicating either success with a value or failure with error details. /// </summary> -/// <typeparam name="T">The type of the value returned on successful operation. -/// Must have a parameterless constructor.</typeparam> +/// <typeparam name="T">The type of the value returned on successful operation.</typeparam> /// <remarks> +/// <para> /// This class implements the Result pattern with a typed return value, providing a type-safe way /// to handle operation outcomes without throwing exceptions. It encapsulates a success/failure state, /// a return value (on success), and error details (on failure). +/// </para> +/// <para> /// Use <see cref="Result"/> when the operation does not need to return a value. +/// </para> /// </remarks> +/// <example> +/// <code> +/// public TypedResult&lt;User&gt; GetUserById(int id) +/// { +/// if (id &lt;= 0) +/// { +/// return TypedResult&lt;User&gt;.Failure(400, "Invalid user ID", null); +/// } +/// +/// try +/// { +/// var user = _repository.FindById(id); +/// if (user == null) +/// { +/// return TypedResult&lt;User&gt;.Failure(404, "User not found", null); +/// } +/// +/// return TypedResult&lt;User&gt;.Success(user); +/// } +/// catch (Exception ex) +/// { +/// return TypedResult&lt;User&gt;.Failure(500, "Failed to retrieve user", ex); +/// } +/// } +/// +/// // Usage +/// var result = GetUserById(123); +/// if (result.IsFailure) +/// { +/// var error = result.TryGetError(); +/// _logger.LogError($"Error {error.ErrorCode}: {error.Message}"); +/// } +/// else +/// { +/// var user = result.TryGetValue(); +/// Console.WriteLine($"Found user: {user.Name}"); +/// } +/// </code> +/// </example> public class TypedResult<T> { /// <summary> - /// Gets or sets a value indicating whether the operation completed successfully. + /// Gets a value indicating whether the operation completed successfully. /// </summary> /// <value> /// <c>true</c> if the operation succeeded; otherwise, <c>false</c>. /// </value> - private bool IsSuccess { get; } + public bool IsSuccess { get; } /// <summary> /// Gets a value indicating whether the operation failed. @@ -55,24 +97,29 @@ public class TypedResult<T> public bool IsFailure => !IsSuccess; /// <summary> - /// Gets or sets the value returned by the successful operation. + /// Gets the value returned by the successful operation. /// </summary> /// <value> - /// The value of type <typeparamref name="T"/> produced by the operation if it succeeded. - /// If the operation failed, this will be a default instance created using the parameterless constructor. + /// The value of type <typeparamref name="T"/> produced by the operation if it succeeded, + /// or <c>null</c> if the operation failed. /// </value> /// <remarks> - /// Always check <see cref="IsSuccess"/> before accessing this property to ensure the operation succeeded. + /// This field should not be accessed directly. Use <see cref="TryGetValue"/> instead + /// to safely retrieve the value with proper validation. /// </remarks> private readonly T? _value; /// <summary> - /// Gets or sets the error details when the operation fails. + /// Gets the error details when the operation fails. /// </summary> /// <value> - /// An <see cref="_error"/> object containing information about the failure, + /// An <see cref="ErrorDetail"/> object containing information about the failure, /// or <c>null</c> if the operation succeeded. /// </value> + /// <remarks> + /// This field should not be accessed directly. Use <see cref="TryGetError"/> instead + /// to safely retrieve error details with proper validation. + /// </remarks> private readonly ErrorDetail? _error; /// <summary> @@ -82,7 +129,7 @@ public class TypedResult<T> /// <param name="value">The value produced by the successful operation.</param> /// <remarks> /// This constructor is protected and should not be called directly. - /// Use the <see cref="Success"/> factory method instead. + /// Use the <see cref="Success"/> factory method instead to create successful results. /// </remarks> protected TypedResult(T value) { @@ -96,7 +143,8 @@ public class TypedResult<T> /// <param name="error">The details of the error that caused the operation to fail.</param> /// <remarks> /// This constructor is protected and should not be called directly. - /// Use the Failure factory methods instead. + /// Use the <see cref="Failure(ErrorDetail)"/> or <see cref="Failure(int, string, Exception?)"/> + /// factory methods instead to create failure results. /// </remarks> protected TypedResult(ErrorDetail error) { @@ -108,14 +156,24 @@ public class TypedResult<T> /// Creates a new <see cref="TypedResult{T}"/> representing a successful operation with the specified value. /// </summary> /// <param name="value">The value produced by the successful operation.</param> - /// <returns>A <see cref="TypedResult{T}"/> instance with <see cref="IsSuccess"/> set to <c>true</c> - /// and <see cref="_value"/> set to the provided value.</returns> + /// <returns>A <see cref="TypedResult{T}"/> instance with <see cref="IsSuccess"/> set to <c>true</c>, + /// <see cref="IsFailure"/> set to <c>false</c>, and the internal value set to the provided parameter.</returns> + /// <remarks> + /// Use this factory method to indicate that an operation completed successfully and produced a value. + /// Each call creates a new instance of <see cref="TypedResult{T}"/>. + /// The value can be retrieved later using <see cref="TryGetValue"/>. + /// </remarks> /// <example> /// <code> /// public TypedResult&lt;User&gt; GetUser(int id) /// { - /// var user = database.FindUser(id); - /// return TypedResult&lt;User&gt;.Success(user); + /// var user = _repository.FindById(id); + /// if (user != null) + /// { + /// return TypedResult&lt;User&gt;.Success(user); + /// } + /// + /// return TypedResult&lt;User&gt;.Failure(404, "User not found", null); /// } /// </code> /// </example> @@ -125,18 +183,25 @@ public class TypedResult<T> /// Creates a new <see cref="TypedResult{T}"/> representing a failed operation with the specified error details. /// </summary> /// <param name="error">The error details describing why the operation failed.</param> - /// <returns>A <see cref="TypedResult{T}"/> instance with <see cref="IsSuccess"/> set to <c>false</c> - /// and <see cref="_error"/> populated with the provided error information.</returns> + /// <returns>A <see cref="TypedResult{T}"/> instance with <see cref="IsSuccess"/> set to <c>false</c>, + /// <see cref="IsFailure"/> set to <c>true</c>, and the internal error populated with the provided + /// error information.</returns> + /// <remarks> + /// Use this factory method when you already have an <see cref="ErrorDetail"/> object. + /// If you need to create the error detail inline, consider using + /// <see cref="Failure(int, string, Exception?)"/> instead. + /// </remarks> /// <example> /// <code> /// public TypedResult&lt;User&gt; GetUser(int id) /// { - /// var user = database.FindUser(id); + /// var user = _repository.FindById(id); /// if (user == null) /// { /// var error = new ErrorDetail(404, "User not found"); /// return TypedResult&lt;User&gt;.Failure(error); /// } + /// /// return TypedResult&lt;User&gt;.Success(user); /// } /// </code> @@ -147,35 +212,139 @@ public class TypedResult<T> /// Creates a new <see cref="TypedResult{T}"/> representing a failed operation /// with the specified error information. /// </summary> - /// <param name="code">A numeric error code identifying the type of failure.</param> - /// <param name="msg">A human-readable message describing the failure.</param> - /// <param name="e">An optional exception that caused the failure. Can be <c>null</c>.</param> - /// <returns>A <see cref="TypedResult{T}"/> instance with <see cref="IsSuccess"/> set to <c>false</c> - /// and <see cref="_error"/> populated with the provided error information.</returns> + /// <param name="code">A numeric error code identifying the type of failure. + /// Common conventions include HTTP status codes (e.g., 400 for bad request, 404 for not found, 500 for server error).</param> + /// <param name="msg">A human-readable message describing the failure. This message should be + /// clear and informative for logging and debugging purposes.</param> + /// <param name="e">An optional exception that caused the failure. Pass <c>null</c> if the failure + /// was not caused by an exception (e.g., validation failures, not found scenarios).</param> + /// <returns>A <see cref="TypedResult{T}"/> instance with <see cref="IsSuccess"/> set to <c>false</c>, + /// <see cref="IsFailure"/> set to <c>true</c>, and the internal error populated with an + /// <see cref="ErrorDetail"/> created from the provided parameters.</returns> + /// <remarks> + /// This is a convenience factory method that creates an <see cref="ErrorDetail"/> internally. + /// Use this method when you want to create a failure result without explicitly creating + /// an <see cref="ErrorDetail"/> object first. + /// </remarks> /// <example> /// <code> /// public TypedResult&lt;User&gt; GetUser(int id) /// { + /// if (id &lt;= 0) + /// { + /// return TypedResult&lt;User&gt;.Failure(400, "Invalid user ID", null); + /// } + /// /// try /// { - /// var user = database.FindUser(id); + /// var user = _repository.FindById(id); + /// if (user == null) + /// { + /// return TypedResult&lt;User&gt;.Failure(404, "User not found", null); + /// } + /// /// return TypedResult&lt;User&gt;.Success(user); /// } - /// catch (Exception ex) + /// catch (DbException ex) /// { - /// return TypedResult&lt;User&gt;.Failure(500, "Database error", ex); + /// return TypedResult&lt;User&gt;.Failure(500, "Failed to retrieve user from database", ex); /// } /// } /// </code> /// </example> public static TypedResult<T> Failure(int code, string msg, Exception? e) => new(new ErrorDetail(code, msg, e)); + /// <summary> + /// Attempts to retrieve the value from a successful operation. + /// </summary> + /// <returns>The value of type <typeparamref name="T"/> produced by the successful operation.</returns> + /// <exception cref="InvalidOperationException"> + /// Thrown when attempting to get the value from a failed result. + /// Always check <see cref="IsFailure"/> before calling this method. + /// </exception> + /// <exception cref="NullReferenceException"> + /// Thrown if the internal value is null, which should not occur in normal usage + /// but indicates a programming error if it does. + /// </exception> + /// <remarks> + /// <para> + /// This method provides safe access to the result value with validation. It ensures that: + /// <list type="number"> + /// <item><description>The result is in a success state before returning the value</description></item> + /// <item><description>The value is not null</description></item> + /// </list> + /// </para> + /// <para> + /// Best practice: Always check <see cref="IsSuccess"/> (or its inverse) before calling this method to avoid exceptions. + /// </para> + /// </remarks> + /// <example> + /// <code> + /// var result = GetUserById(123); + /// + /// if (!result.IsFailure) + /// { + /// var user = result.TryGetValue(); + /// Console.WriteLine($"User found: {user.Name}"); + /// } + /// else + /// { + /// var error = result.TryGetError(); + /// _logger.LogWarning($"Failed to get user: {error.Message}"); + /// } + /// </code> + /// </example> public T TryGetValue() { if (IsFailure) throw new InvalidOperationException("Can't get value: Result state is failure!"); return _value ?? throw new NullReferenceException("TypedResult.Value is null"); } + /// <summary> + /// Attempts to retrieve the error details from a failed operation. + /// </summary> + /// <returns>An <see cref="ErrorDetail"/> object containing information about the failure.</returns> + /// <exception cref="InvalidOperationException"> + /// Thrown when attempting to get the error from a successful result. + /// Always check <see cref="IsFailure"/> before calling this method. + /// </exception> + /// <exception cref="NullReferenceException"> + /// Thrown if the internal error is null, which should not occur in normal usage + /// but indicates a programming error if it does. + /// </exception> + /// <remarks> + /// <para> + /// This method provides safe access to error details with validation. It ensures that: + /// <list type="number"> + /// <item><description>The result is in a failure state before returning error details</description></item> + /// <item><description>The error details are not null</description></item> + /// </list> + /// </para> + /// <para> + /// Best practice: Always check <see cref="IsFailure"/> before calling this method to avoid exceptions. + /// </para> + /// </remarks> + /// <example> + /// <code> + /// var result = GetUserById(123); + /// + /// if (result.IsFailure) + /// { + /// var error = result.TryGetError(); + /// _logger.LogError($"Operation failed with code {error.ErrorCode}: {error.Message}"); + /// + /// if (error.Exception != null) + /// { + /// _logger.LogError(error.Exception, "Exception details"); + /// } + /// } + /// else + /// { + /// var user = result.TryGetValue(); + /// ProcessUser(user); + /// } + /// </code> + /// </example> public ErrorDetail TryGetError() { if (IsSuccess) throw new InvalidOperationException("Can't get error: Result state is success!");