Error

Model Class

An immutable model that carries information about an error that occurred. Supports a human-readable description, a technical detail string for developers, and the originating Exception — all optional.

Constructor

public Error(
    string? description = null,
    string? technicalDetail = null,
    Exception? exception = null
)

Parameters

NameTypeDescription
descriptionstring?A non-technical, human-readable description of the error. Suitable to display to end users.
technicalDetailstring?A technical explanation for developers — for example, which service or operation failed.
exceptionException?The exception that caused or is related to this error, if any.

Properties

PropertyTypeDescription
Descriptionstring?Non-technical error description for end users.
TechnicalDetailstring?Developer-facing technical reason for the error.
ExceptionException?The associated exception, if one was provided.

Methods

ToString()

Returns a formatted string containing all non-null details. Returns "Error — no details provided" when all fields are null.

Examples

// Simple user-facing error
Error userError = new Error("The email address is already in use.");

// Error with technical detail
Error dbError = new Error(
    description: "We couldn't save your changes. Please try again.",
    technicalDetail: "INSERT into Users failed — UNIQUE constraint on Email."
);

// Error wrapping a caught exception
try
{
    externalService.Send(payload);
}
catch (HttpRequestException ex)
{
    Error networkError = new Error(
        description: "Failed to reach the payment gateway.",
        technicalDetail: "POST /api/charge returned 503.",
        exception: ex
    );
    return new Result(networkError);
}

// Using ToString()
Console.WriteLine(dbError.ToString());
// Output:
// Description: We couldn't save your changes. Please try again.
// Technical Detail: INSERT into Users failed — UNIQUE constraint on Email.

IValidatable

Interface

Marks a class as self-validating. Implement this interface on domain models that need to validate their own state. Works with EnsureIsValid<T> to throw on invalid models automatically.

Gubbins.Models.Interfaces

Members

MemberTypeDescription
IsValid()boolReturns true if the model is in a valid state; false otherwise.
ErrorsIList<Error>A list of validation errors. Should be empty when IsValid() returns true.

Example — Implementing IValidatable

using Gubbins.Models;
using Gubbins.Models.Interfaces;

public class CreateUserRequest : IValidatable
{
    public string? Name { get; set; }
    public string? Email { get; set; }
    public int Age { get; set; }

    public IList<Error> Errors { get; } = new List<Error>();

    public bool IsValid()
    {
        Errors.Clear();

        if (string.IsNullOrWhiteSpace(Name))
        {
            Errors.Add(new Error("Name is required."));
        }

        if (string.IsNullOrWhiteSpace(Email))
        {
            Errors.Add(new Error("Email is required."));
        }

        if (Age < 18)
        {
            Errors.Add(new Error("Must be 18 or older."));
        }

        return Errors.Count == 0;
    }
}

// Usage with EnsureIsValid
using Gubbins.Validation;

CreateUserRequest request = GetRequestFromInput();
request.EnsureIsValid(); // throws ArgumentException if invalid

Result

Model Class

A non-generic result model for communicating whether an operation succeeded or failed, along with structured error details. Use this when there is no value to return on success.

Constructors

// Success (no arguments needed)
Result()

// Explicit success/failure flag
Result(bool succeeded)

// Failure — one or more Error objects
Result(params Error[]? error)
Result(List<Error>? errors)

// Failure — inline error details
Result(
    string errorDescription,
    string? errorTechnicalDetails = null,
    Exception? errorException = null
)

Properties

PropertyTypeDescription
SucceededboolTrue when the operation completed successfully.
FailedboolTrue when the operation did not succeed (inverse of Succeeded).
ErrorsList<Error>Collection of errors. Empty on success.

Methods

AddError(Error error)

Appends an additional error to the Errors list. The error must not be null.

ToString()

Returns "Operation succeeded." on success, or a string listing all errors on failure.

Examples

// --- Success ---
Result success = new Result();
// or explicitly:
Result success = new Result(true);

// --- Failure with inline message ---
Result failed = new Result("Could not delete the record.");

// --- Failure with technical detail ---
Result failed = new Result(
    errorDescription: "Failed to send the confirmation email.",
    errorTechnicalDetails: "SMTP server rejected the connection on port 587."
);

// --- Failure with a pre-built Error object ---
Error error = new Error("Not found.", "No row with ID 42 in Users table.");
Result notFound = new Result(error);

// --- Handling the result ---
Result result = DeleteUser(userId);

if (result.Failed)
{
    foreach (Error error in result.Errors)
    {
        logger.LogError(error.TechnicalDetail ?? error.Description);
    }
    return;
}

// --- Adding errors after construction ---
Result result = new Result(false);
result.AddError(new Error("Validation failed for field Name."));
result.AddError(new Error("Email must be a valid address."));

Result<T>

Model Class

A generic result model that extends Result with a typed Output property. Use this when an operation should return a value on success. Inherits all constructors, properties, and methods from Result.

Note:

Result<bool> is not permitted and will throw an ArgumentException at construction time. Use the non-generic Result instead for boolean outcomes.

Additional Constructors

// Success with output value
Result<T>(T? output)

// Output with explicit success flag
Result<T>(T? output, bool succeeded)

All failure constructors from Result are also available.

Additional Properties

PropertyTypeDescription
OutputT?The result value. Populated on success; null on failure.

Examples

// --- Success with output ---
User user = userRepository.GetById(id)!;
Result<User> result = new Result<User>(user);

// --- Failure ---
Result<User> notFound = new Result<User>("No user found with that ID.");

// --- Service method pattern ---
public Result<Order> PlaceOrder(int userId, List<CartItem> items)
{
    if (items.Count == 0)
    {
        return new Result<Order>("Your cart is empty.");
    }

    Order order = orderRepository.Create(userId, items);
    return new Result<Order>(order);
}

// --- Consuming the result ---
Result<Order> result = PlaceOrder(currentUserId, cart);

if (result.Failed)
{
    return BadRequest(result.Errors.First().Description);
}

Order order = result.Output!; // safe: Succeeded guarantees Output is set
return Ok(order);

// --- Result<bool> is not valid, use Result instead ---
// Result<bool> invalid = new Result<bool>(true); // throws ArgumentException
Result boolResult = new Result(true); // correct

MultiDisposable

Class IDisposable

Manages a collection of IDisposable resources and disposes them together in reverse order (last-in, first-out) — matching the behaviour of nested using statements. Not thread-safe.

Disposal order:

Resources are disposed in LIFO order. If an exception occurs during disposal, all remaining resources are still disposed before the last exception is rethrown.

Constructor

MultiDisposable(params IDisposable[] disposables)

Creates a new MultiDisposable and optionally seeds it with existing disposable instances.

Methods

Add<T>(T disposable)T

Registers a disposable resource for tracking and returns it, allowing inline use in variable assignments.

ParameterTypeDescription
disposableT : IDisposableThe resource to track.
Dispose()

Disposes all tracked resources in reverse registration order. On exception, continues disposing remaining resources then rethrows the last exception.

Examples

// --- Basic usage with constructor seeding ---
using (MultiDisposable disposer = new MultiDisposable(connection, transaction))
{
    // Both disposed in reverse: transaction first, then connection
}

// --- Dynamic registration with Add() ---
using (MultiDisposable disposer = new MultiDisposable())
{
    SqlConnection connection = disposer.Add(new SqlConnection(connString));
    SqlCommand command = disposer.Add(connection.CreateCommand());
    SqlDataReader reader = disposer.Add(command.ExecuteReader());

    while (reader.Read())
    {
        // process rows
    }
    // reader, command, connection disposed in that order
}

// --- Mixing constructor and Add() ---
using (MultiDisposable disposer = new MultiDisposable(fileStream))
{
    StreamReader reader = disposer.Add(new StreamReader(fileStream));
    // reader disposed first, then fileStream
}