API-Utilities 2.1.0

dotnet add package API-Utilities --version 2.1.0
                    
NuGet\Install-Package API-Utilities -Version 2.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="API-Utilities" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="API-Utilities" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="API-Utilities" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add API-Utilities --version 2.1.0
                    
#r "nuget: API-Utilities, 2.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package API-Utilities@2.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=API-Utilities&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=API-Utilities&version=2.1.0
                    
Install as a Cake Tool

API Utilities

A lightweight .NET library of reusable building blocks for API development: input guards, string helpers, uniform response envelopes, pagination, sorting, HTTP-aware exceptions, and a functional result type.

NuGet License: MIT


Installation

dotnet add package API-Utilities

Target framework: .NET 10.0


Namespaces

Namespace Contents
CommonUtils.Checks Check — static guard methods
CommonUtils.Extensions StringExtensions — string helpers
CommonUtils.Responses ApiResponse, ApiResponse<T> — response envelope
CommonUtils.Pagination PaginationParams, PagedResult<T>, SortParams, SortDirection
CommonUtils.Exceptions ApiException and HTTP-specific subclasses
CommonUtils.Results Result<T>, Result, Unit — functional result type

Check — Input Guards

Check is a static guard class. Every method validates a value and returns it unchanged if valid, so guards compose naturally inline or at the top of a method body. On failure it throws a standard .NET argument exception — your middleware decides how to map that to an HTTP response.

using CommonUtils.Checks;

public void CreateOrder(string customerId, int quantity, decimal price)
{
    customerId = Check.NotEmpty(customerId, nameof(customerId)); // trims and returns
    quantity   = Check.Positive(quantity, nameof(quantity));
    price      = Check.Positive(price, nameof(price));
}

String guards

Method Throws Notes
NotEmpty(string?, paramName) ArgumentException Null, empty, or whitespace. Returns trimmed value.
MaxLength(string, max, paramName) ArgumentException Trims before measuring. Returns original value.
MinLength(string, min, paramName) ArgumentException Trims before measuring. Returns original value.
Length(string, min, max, paramName) ArgumentException Combined min + max check.

Numeric guards — int, long, decimal, double

Method Throws Notes
Positive(value, paramName) ArgumentOutOfRangeException value > 0
NotNegative(value, paramName) ArgumentOutOfRangeException value ≥ 0
InRange(value, min, max, paramName) ArgumentOutOfRangeException int and decimal overloads

Collection guards

Method Throws Notes
NotNull<T>(value, paramName) ArgumentNullException Any reference type
NotEmpty<T>(IEnumerable<T>?, paramName) ArgumentException Null or empty
MaxCount<T>(ICollection<T>, max, paramName) ArgumentException
MinCount<T>(ICollection<T>, min, paramName) ArgumentException

Other guards

Method Throws Notes
NotEmpty(Guid, paramName) ArgumentException Guid.Empty check
Defined<T>(T, paramName) ArgumentException Enum value must be declared
NotDefault(DateTime, paramName) ArgumentException Rejects DateTime.MinValue
NotDefault(DateTimeOffset, paramName) ArgumentException
NotInPast(DateTime, paramName) ArgumentOutOfRangeException Calls NotDefault first
NotInFuture(DateTime, paramName) ArgumentOutOfRangeException Calls NotDefault first
NotInPast(DateTimeOffset, paramName) ArgumentOutOfRangeException
NotInFuture(DateTimeOffset, paramName) ArgumentOutOfRangeException
var id      = Check.NotEmpty(dto.Id, nameof(dto.Id));
var tags    = Check.NotEmpty(dto.Tags, nameof(dto.Tags));
var role    = Check.Defined(dto.Role, nameof(dto.Role));
var expires = Check.NotInPast(dto.ExpiresAt, nameof(dto.ExpiresAt));

Format guards

Method Throws Notes
Email(string?, paramName) ArgumentException RFC-style format check. Returns trimmed value.
Url(string?, paramName) ArgumentException Absolute HTTP/HTTPS URL only.
Matches(string?, pattern, paramName) ArgumentException Custom regex with 1-second timeout.
email    = Check.Email(dto.Email, nameof(dto.Email));
website  = Check.Url(dto.Website, nameof(dto.Website));
postCode = Check.Matches(dto.PostCode, @"^\d{5}$", nameof(dto.PostCode));

StringExtensions — String Helpers

using CommonUtils.Extensions;

Normalize

Trims leading/trailing whitespace and collapses internal runs of any whitespace (\t, \n, multiple spaces) to a single space. Useful for sanitising name and address fields from form input.

Note: Because string has a built-in Normalize() instance method (Unicode normalization), calling .Normalize() on a string will resolve to the BCL method, not this one. Call it explicitly as a static method:

string clean = StringExtensions.Normalize("  hello\t world  "); // "hello world"

Truncate

Returns the string as-is if it fits, or cuts it to exactly maxLength characters — never throws.

string preview = description.Truncate(160);

NullIfEmpty

Returns null for null, empty, or whitespace-only strings. Useful for optional fields stored as NULL in the database rather than empty strings.

string? nickname = dto.Nickname.NullIfEmpty(); // null when blank

ToSnakeCase

Converts PascalCase or camelCase to snake_case. Breaks on lowercase/digit → uppercase transitions.

"OrderId".ToSnakeCase()       // "order_id"
"createdAt".ToSnakeCase()     // "created_at"
"Version2Value".ToSnakeCase() // "version2_value"

Acronym behaviour: Only the last uppercase letter in a run is treated as a word boundary. "OrderID""order_id" (breaks before D). "HTTPServer""httpserver" (no internal break).

ToKebabCase

Converts PascalCase or camelCase to kebab-case. Useful for URL slugs and route segments.

"OrderLineItem".ToKebabCase() // "order-line-item"
"createdAt".ToKebabCase()     // "created-at"

ToPascalCase

Converts snake_case or kebab-case to PascalCase.

"order_line_item".ToPascalCase() // "OrderLineItem"
"order-line-item".ToPascalCase() // "OrderLineItem"
"hello".ToPascalCase()           // "Hello"

Mask

Masks the middle of a string, preserving a configurable number of characters at each end. Safe for logging emails, tokens, and phone numbers.

"user@example.com".Mask()              // "us**************" (2 visible start, default)
"user@example.com".Mask(2, 3)         // "us***********com"
"secret-token".Mask(0, 0, '#')        // "############"

Parameters: visibleStart (default 2), visibleEnd (default 0), maskChar (default *).
When visibleStart + visibleEnd ≥ length, the entire string is masked.


ApiResponse — Uniform Response Envelope

All endpoints return the same shape, making client-side parsing predictable.

using CommonUtils.Responses;

Endpoints that return data

// success
return Ok(ApiResponse.Ok(user));

// failure
return BadRequest(ApiResponse.Fail<User>("User not found."));

// multiple errors
return UnprocessableEntity(ApiResponse.Fail<User>(errors));

Shape:

{ "success": true,  "data": { ... }, "message": "optional", "errors": [] }
{ "success": false, "data": null,    "message": null,        "errors": ["..."] }

Endpoints with no payload (DELETE, commands, etc.)

return Ok(ApiResponse.Ok("Order cancelled."));

return BadRequest(ApiResponse.Fail("Insufficient stock."));

Shape:

{ "success": true,  "message": "Order cancelled.", "errors": [] }
{ "success": false, "message": null,               "errors": ["Insufficient stock."] }

Pagination

PaginationParams

Bind directly from the query string. Defaults to page 1, page size 20.

using CommonUtils.Pagination;

// GET /orders?page=2&pageSize=50
[HttpGet]
public IActionResult GetOrders([FromQuery] PaginationParams pagination)
{
    pagination.Validate(); // throws BadRequestException on bad input

    var items = _repo.GetOrders(skip: pagination.Skip, take: pagination.Take);
    // ...
}

Validate(maxPageSize) throws BadRequestException if Page < 1, PageSize < 1, or PageSize > maxPageSize (default 100).

PagedResult<T>

var items      = await _db.Orders.Skip(p.Skip).Take(p.Take).ToListAsync();
var totalCount = await _db.Orders.CountAsync();

PagedResult<OrderDto> result = PagedResult.Create(items, totalCount, pagination);

return Ok(ApiResponse.Ok(result));

Properties on PagedResult<T>:

Property Type Description
Items IReadOnlyList<T> Current page items
Page int Current page (1-based)
PageSize int Items per page
TotalCount int Total items across all pages
TotalPages int Computed: ⌈TotalCount / PageSize⌉
HasNextPage bool Page < TotalPages
HasPreviousPage bool Page > 1

Use PagedResult.Empty<T>(pagination) when the data source returns nothing.

SortParams

Companion to PaginationParams for list endpoints that support ordering. Bind directly from the query string.

// GET /orders?sortBy=createdAt&direction=Desc
[HttpGet]
public IActionResult GetOrders(
    [FromQuery] PaginationParams pagination,
    [FromQuery] SortParams sort)
{
    pagination.Validate();
    sort.Validate(["name", "createdAt", "price"]); // throws BadRequestException for unknown columns

    if (sort.IsActive)
    {
        query = sort.Direction == SortDirection.Desc
            ? query.OrderByDescending(sort.SortBy)
            : query.OrderBy(sort.SortBy);
    }
}

Validate(allowedColumns) is a no-op when SortBy is null or empty — safe to call unconditionally.


Exceptions

All exceptions extend ApiException, which carries an HTTP StatusCode and an optional machine-readable ErrorCode. Use a single exception-handling middleware to map them to responses — no per-endpoint try/catch needed.

using CommonUtils.Exceptions;

Available exceptions

Class Status When to use
BadRequestException 400 Malformed input, failed preconditions
UnauthorizedException 401 Missing or invalid credentials
ForbiddenException 403 Authenticated but not permitted
NotFoundException 404 Resource does not exist
ConflictException 409 Duplicate or state conflict
ValidationException 422 Semantic validation errors (field-level)
TooManyRequestsException 429 Rate limit exceeded
GoneException 410 Resource permanently removed
ServiceUnavailableException 503 Dependency down, maintenance, or transient outage

Usage

// Simple message
throw new NotFoundException("Order not found.", errorCode: "ORDER_NOT_FOUND");

// Convenience constructor (non-string key)
throw new NotFoundException("Order", orderId); // "Order with key '42' was not found."

// Field-level validation errors
throw new ValidationException("Email", "must be a valid email address");

// Multiple fields (e.g. from FluentValidation)
throw new ValidationException(new Dictionary<string, string[]>
{
    ["Email"]    = ["Required", "Must be a valid email"],
    ["Password"] = ["Must be at least 8 characters"],
});

New exceptions usage

// Rate limiting — include a retry hint for the client
throw new TooManyRequestsException(retryAfter: TimeSpan.FromSeconds(30));

// Map RetryAfter in middleware
if (ex is TooManyRequestsException tooMany && tooMany.RetryAfter.HasValue)
    context.Response.Headers["Retry-After"] = ((int)tooMany.RetryAfter.Value.TotalSeconds).ToString();

// Permanently removed resource
throw new GoneException("This API version has been retired.");

// Dependency or database unavailable
throw new ServiceUnavailableException("Payment provider is currently unavailable.", "PAYMENT_DOWN");

Middleware integration (ASP.NET Core)

app.UseExceptionHandler(builder => builder.Run(async context =>
{
    var ex = context.Features.Get<IExceptionHandlerFeature>()?.Error;

    if (ex is ApiException apiEx)
    {
        context.Response.StatusCode = apiEx.StatusCode;

        if (ex is TooManyRequestsException tooMany && tooMany.RetryAfter.HasValue)
            context.Response.Headers["Retry-After"] = ((int)tooMany.RetryAfter.Value.TotalSeconds).ToString();

        await context.Response.WriteAsJsonAsync(new
        {
            success = false,
            errorCode = apiEx.ErrorCode,
            errors = ex is ValidationException vex && vex.Errors.Count > 0
                ? vex.Errors
                : new[] { ex.Message }
        });
        return;
    }

    context.Response.StatusCode = 500;
    await context.Response.WriteAsJsonAsync(new { success = false, errors = new[] { "An unexpected error occurred." } });
}));

Result<T> — Functional Result Type

An alternative to throwing exceptions for expected failure paths. Operations return Result<T> — the caller decides what to do with a failure rather than catching an exception.

using CommonUtils.Results;

Returning results

// Success with a value
Result<Order> result = Result.Ok(order);

// Failure with a single error
Result<Order> result = Result.Fail<Order>("Order not found.");

// Failure with multiple errors
Result<Order> result = Result.Fail<Order>(["Stock too low", "Payment declined"]);

// No-value success (commands, void operations)
Result<Unit> result = Result.Ok();

// Implicit conversion from value
Result<int> result = 42; // equivalent to Result.Ok(42)

Consuming results

var result = _orderService.PlaceOrder(dto);

if (result.IsFailure)
    return BadRequest(ApiResponse.Fail<Order>(result.Errors));

return Ok(ApiResponse.Ok(result.Value!));

Properties on Result<T>

Property Type Description
IsSuccess bool true when the operation succeeded
IsFailure bool !IsSuccess
Value T? The result value; meaningful on success
Errors IReadOnlyList<string> Error messages; empty on success

Use Result<Unit> for operations with no return value. Unit.Value is the singleton instance.


License

MIT — see LICENSE.txt.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 88 5/16/2026
2.0.2 90 5/15/2026
2.0.1 77 5/15/2026
1.0.1 234 2/27/2024
1.0.0 179 2/27/2024

v2.1.0
     - Added TooManyRequestsException (429), GoneException (410), ServiceUnavailableException (503)
     - Added SortParams and SortDirection for standardised list endpoint ordering
     - Added Check.Email, Check.Url, Check.Matches format guards
     - Added ToKebabCase, ToPascalCase, Mask string extension methods
     - Added Result<T> functional result type with Unit for void operations