Models
Core data structures used throughout the library. These models provide a consistent, descriptive way to represent operation outcomes and errors.
Error
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
| Name | Type | Description |
|---|---|---|
| description | string? | A non-technical, human-readable description of the error. Suitable to display to end users. |
| technicalDetail | string? | A technical explanation for developers — for example, which service or operation failed. |
| exception | Exception? | The exception that caused or is related to this error, if any. |
Properties
| Property | Type | Description |
|---|---|---|
| Description | string? | Non-technical error description for end users. |
| TechnicalDetail | string? | Developer-facing technical reason for the error. |
| Exception | Exception? | 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
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.
Members
| Member | Type | Description |
|---|---|---|
| IsValid() | bool | Returns true if the model is in a valid state; false otherwise. |
| Errors | IList<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
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
| Property | Type | Description |
|---|---|---|
| Succeeded | bool | True when the operation completed successfully. |
| Failed | bool | True when the operation did not succeed (inverse of Succeeded). |
| Errors | List<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>
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.
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
| Property | Type | Description |
|---|---|---|
| Output | T? | 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
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.
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) → TRegisters a disposable resource for tracking and returns it, allowing inline use in variable assignments.
| Parameter | Type | Description |
|---|---|---|
| disposable | T : IDisposable | The 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
}