SaddamHossain.Toolkit.Extensions 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package SaddamHossain.Toolkit.Extensions --version 1.0.0
                    
NuGet\Install-Package SaddamHossain.Toolkit.Extensions -Version 1.0.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="SaddamHossain.Toolkit.Extensions" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SaddamHossain.Toolkit.Extensions" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="SaddamHossain.Toolkit.Extensions" />
                    
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 SaddamHossain.Toolkit.Extensions --version 1.0.0
                    
#r "nuget: SaddamHossain.Toolkit.Extensions, 1.0.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 SaddamHossain.Toolkit.Extensions@1.0.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=SaddamHossain.Toolkit.Extensions&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=SaddamHossain.Toolkit.Extensions&version=1.0.0
                    
Install as a Cake Tool

SaddamHossain.Toolkit.Extensions

NuGet Downloads Build License: MIT .NET

A lightweight, dependency-free collection of high-performance extension methods for modern .NET applications.

Status: in development. Version 1.0.0 has not been published to NuGet.org yet. The package infrastructure is complete and CI-verified; the extension methods listed under Roadmap are being implemented one feature at a time. This README documents the API as it lands — nothing below is illustrative or aspirational unless explicitly marked.


Introduction

Every .NET codebase accumulates the same small helper methods: slugify a string, truncate it safely, humanise a timestamp, format a byte count. They get copied between projects, drift apart, and are rarely tested at the edges.

SaddamHossain.Toolkit.Extensions is that layer, done once and done properly — argument validation on every public entry point, allocation-conscious implementations, culture correctness where it matters, and a test for every boundary case.

Features

  • Zero dependencies. The package references no other NuGet package. Nothing is pulled into your dependency graph but this assembly.
  • Multi-targeted for net8.0, net9.0 and net10.0, using the best API available on each runtime rather than the lowest common denominator.
  • Trim- and AOT-safe. Marked IsAotCompatible, so Native AOT and trimmed publishes stay warning-free.
  • Thread-safe. All extensions are pure functions over their inputs — no shared mutable state, no hidden side effects.
  • Allocation-conscious. Span<T> / ReadOnlySpan<T> where they genuinely help, ordinal string comparison by default, and no LINQ on hot paths.
  • Fully documented. Every public member ships XML documentation with parameters, return values, thrown exceptions and a worked example.
  • Source Link + symbols. Step straight into the source from your debugger.

Installation

dotnet add package SaddamHossain.Toolkit.Extensions

Or via the Package Manager Console:

Install-Package SaddamHossain.Toolkit.Extensions

Quick Start

Every public extension lives in a single namespace, so one using makes the whole library discoverable through IntelliSense:

using SaddamHossain.Toolkit.Extensions;

That is deliberate. The library is organised into folders internally, but splitting the namespace by feature would force you to import five namespaces to reach one package — the same reasoning behind System.Linq and Microsoft.Extensions.*.

Examples

Every example below is executed by the test suite and by the sample application, so none of them can drift from the implementation.

IsNullOrWhiteSpace()

Annotated with [NotNullWhen(false)], so the compiler narrows the type for you:

string? name = GetUserInput();

if (!name.IsNullOrWhiteSpace())
{
    Console.WriteLine(name.Length);   // No null warning, no `!` operator.
}

RemoveWhitespace()

"  hello   world  ".RemoveWhitespace();   // "helloworld"
"1 234 567".RemoveWhitespace();           // "1234567"
"a b".RemoveWhitespace();            // "ab"  — non-breaking space is whitespace too
"no-spaces".RemoveWhitespace();           // same instance returned; nothing allocated

Truncate()

The suffix is counted inside the budget, so the result never exceeds maxLength:

"Hello, World".Truncate(5);          // "Hello"
"Hello, World".Truncate(8, "…");     // "Hello, …"   — exactly 8 characters
"Hello, World".Truncate(9, "...");   // "Hello,..."  — exactly 9 characters
"Hello".Truncate(20, "…");           // "Hello"      — nothing removed, so no suffix
"ab\U0001F600".Truncate(3);          // "ab"         — never splits a surrogate pair

ToSlug()

"Hello, World!".ToSlug();          // "hello-world"
"  Crème Brûlée  ".ToSlug();       // "creme-brulee"   — Latin diacritics folded
"C# 13 -- What's New?".ToSlug();   // "c-13-what-s-new"
"São Paulo".ToSlug();              // "sao-paulo"
"***".ToSlug();                    // ""

ToSlug() uses the invariant culture, so "IDEA".ToSlug() is "idea" on every machine — including Turkish locales, where a culture-sensitive lowercase would produce "ıdea".

Two limitations are deliberate and documented rather than silently handled:

"Straße".ToSlug();   // "stra-e"  — ß has no Unicode decomposition to fold
"বাংলা".ToSlug();      // ""        — output is ASCII-only

Both are pinned by tests, so they cannot change without a visible, versioned decision.

GetAge()

Counts completed anniversaries, not elapsed days divided by 365.25 — a day-count drifts by a day every leap year and reports the wrong age on roughly one birthday in four.

var birthDate = new DateTime(1990, 6, 15);

birthDate.GetAge(new DateTime(2026, 6, 14));   // 35 — day before the birthday
birthDate.GetAge(new DateTime(2026, 6, 15));   // 36 — on the birthday
birthDate.GetAge();                            // measured against DateTime.Today

Leap-day birthdays are handled explicitly. The anniversary falls on 1 March in non-leap years:

var leapBirthDate = new DateTime(2000, 2, 29);

leapBirthDate.GetAge(new DateTime(2001, 2, 28));   // 0
leapBirthDate.GetAge(new DateTime(2001, 3, 1));    // 1
leapBirthDate.GetAge(new DateTime(2004, 2, 29));   // 4  — exact leap-day anniversary
leapBirthDate.GetAge(new DateTime(2100, 2, 28));   // 99 — 2100 is not a leap year

A birth date in the future throws ArgumentOutOfRangeException rather than returning a negative number, so a data-entry error cannot propagate silently.

ToHumanTime()

var now = new DateTime(2026, 8, 5, 12, 0, 0, DateTimeKind.Utc);

now.AddSeconds(-30).ToHumanTime(now);   // "just now"
now.AddMinutes(-1).ToHumanTime(now);    // "1 minute ago"
now.AddHours(-3).ToHumanTime(now);      // "3 hours ago"
now.AddDays(-10).ToHumanTime(now);      // "1 week ago"
now.AddDays(-90).ToHumanTime(now);      // "2 months ago"
now.AddMinutes(5).ToHumanTime(now);     // "in 5 minutes"

The single-argument overload picks its reference clock from DateTimeKindUtcNow for a UTC value, Now otherwise. This prevents the most common bug in relative-time formatting: comparing a UTC timestamp from a database against local wall-clock time and reporting something that happened seconds ago as "6 hours ago".

DateTime.UtcNow.AddHours(-3).ToHumanTime();   // "3 hours ago" — correct
DateTime.Now.AddHours(-3).ToHumanTime();      // "3 hours ago" — also correct

ToHumanTime() never throws, and its output is English-only with invariant digits.

ToReadableFileSize()

Binary steps (1024) with the customary unit names, matching what Windows Explorer shows and what users expect when they read "1 KB". Trailing zeros are dropped.

0.ToReadableFileSize();               // "0 B"     — allocates nothing
512.ToReadableFileSize();             // "512 B"
1024.ToReadableFileSize();            // "1 KB"
1536.ToReadableFileSize();            // "1.5 KB"
1048576.ToReadableFileSize();         // "1 MB"
1073741824.ToReadableFileSize();      // "1 GB"
1099511627776L.ToReadableFileSize();  // "1 TB"
long.MaxValue.ToReadableFileSize();   // "8 EB"

Negative values format rather than throw, since a size delta is a legitimate use:

(-1536L).ToReadableFileSize();        // "-1.5 KB"
long.MinValue.ToReadableFileSize();   // "-8 EB"  — no overflow

Custom precision, 0 to 15 places:

1590L.ToReadableFileSize(0);   // "2 KB"
1590L.ToReadableFileSize(3);   // "1.553 KB"

A value that would round up to a whole unit is promoted, so you never see the technically-true but jarring "1024 KB":

1048575L.ToReadableFileSize();    // "1 MB"          — 1023.999… KB promoted
1048575L.ToReadableFileSize(3);   // "1023.999 KB"   — precision keeps it below the boundary

ToOrdinal()

1.ToOrdinal();     // "1st"
2.ToOrdinal();     // "2nd"
3.ToOrdinal();     // "3rd"
4.ToOrdinal();     // "4th"
11.ToOrdinal();    // "11th"   — the teens exception
12.ToOrdinal();    // "12th"
13.ToOrdinal();    // "13th"
21.ToOrdinal();    // "21st"
22.ToOrdinal();    // "22nd"
23.ToOrdinal();    // "23rd"
111.ToOrdinal();   // "111th"  — decided by the last TWO digits, not the last one
0.ToOrdinal();     // "0th"

The suffix is chosen from the last two digits, which is exactly where hand-rolled implementations get 111 wrong. Never throws, including at int.MinValue.

IsNullOrEmpty()

One overload covers every sequence type. Anything exposing a Count is answered in O(1) with zero allocations; only a genuinely lazy sequence is enumerated, and then exactly one element is pulled:

List<int>? list = null;
list.IsNullOrEmpty();                              // true

new List<int>().IsNullOrEmpty();                   // true
new[] { 1, 2, 3 }.IsNullOrEmpty();                 // false
new Dictionary<string, int>().IsNullOrEmpty();     // true
new HashSet<int>().IsNullOrEmpty();                // true

IEnumerable<int> query = Enumerable.Range(1, 10).Where(n => n > 100);
query.IsNullOrEmpty();                             // true — one element pulled, no Count()

// Flow analysis: no null-forgiving operator needed.
if (!list.IsNullOrEmpty())
{
    Console.WriteLine(list.Count);
}

Prefer string.IsNullOrEmpty(s) for strings. string is an IEnumerable<char> but implements neither collection interface, so it would take the enumeration path here.

IsEmpty()

Guid.Empty.IsEmpty();       // true
default(Guid).IsEmpty();    // true
Guid.NewGuid().IsEmpty();   // false

A 16-byte struct comparison the JIT inlines away entirely. No allocation, nothing to throw.

Requirements

Runtime .NET 8.0, .NET 9.0 or .NET 10.0
SDK (to build) .NET 10.0 SDK or later
Language C# 12 or later
OS Any platform supported by .NET — no OS-specific code

Roadmap

1.0.0 — initial release

Status Method Target type Purpose
ToSlug() string URL-safe slug, invariant culture
RemoveWhitespace() string Strip all whitespace
Truncate() string Length-limit, suffix counted within the budget
IsNullOrWhiteSpace() string? Null/whitespace check with flow analysis
GetAge() DateTime Completed anniversaries, leap-day correct
ToHumanTime() DateTime Relative phrasing — "3 hours ago"
IsNullOrEmpty() IEnumerable<T>? Null/empty check, O(1) for counted sequences
ToReadableFileSize() long, int Byte count to "1.5 KB"
ToOrdinal() int "1st", "2nd", "3rd"
IsEmpty() Guid Compare against Guid.Empty

All ten methods for 1.0.0 are implemented, with 310 tests running against every target framework.

IsNullOrWhiteSpace() was originally planned as IsNullOrWhiteSpaceEx(). The Ex suffix is reserved by the Framework Design Guidelines (CA1711), and there is no ambiguity to disambiguate: string has no instance member of that name, only the static string.IsNullOrWhiteSpace(x).

Beyond 1.0.0

Additional feature areas will be introduced only where they earn their place. Public API changes follow Semantic Versioning — no breaking change without a major version bump.

Versioning

This package follows Semantic Versioning 2.0.0.

Change Version bump Example
Breaking change to the public API Major2.0.0 A method removed, renamed, or its signature or documented behaviour changed
New API, fully backward compatible Minor1.1.0 A new extension method or a new overload
Bug fix with no API change Patch1.0.1 A correctness fix, a performance improvement

Three guarantees come with that:

  • AssemblyVersion moves only on a major release. Code compiled against 1.0.0 keeps loading 1.4.2 with no binding redirect. A unit test enforces this, so it cannot drift by accident.
  • The public API surface is pinned by a test. Any addition, removal or signature change fails the build with a readable diff, which makes every contract change a deliberate, reviewed decision rather than something that slips through.
  • Every change is recorded in the CHANGELOG, with breaking changes listed under their own heading and a migration note.

Pre-releases use the standard suffix form — 1.1.0-preview.1 — and are never promoted to stable without a version bump.

🌐 Website saddamhossain.net
📦 NuGet nuget.org/packages/SaddamHossain.Toolkit.Extensions
💻 Source github.com/saddamhossain/SaddamHossain.Toolkit.Extensions
🐛 Issues Report a bug or request a feature
📋 Changelog CHANGELOG.md

Contributing

Contributions are welcome. Before opening a pull request:

  1. Open an issue describing the problem or the proposed API. Design discussion happens before implementation, not during review.
  2. Ensure dotnet build -c Release produces zero warnings — warnings are errors here.
  3. Ensure dotnet format --verify-no-changes passes.
  4. Add tests covering the happy path, edge cases, null inputs, boundary values and invalid arguments. Follow the MethodName_Should_ExpectedBehavior_When_State convention.
  5. Add XML documentation to every public member, including an <example>.

License

Licensed under the MIT License — free for commercial and personal use, with no attribution required beyond retaining the notice. A copy of the licence ships inside the NuGet package itself.

Copyright © 2026 Md. Saddam Hossain


Built by Md. Saddam Hossain. If this package saves you time, a ⭐ on GitHub is appreciated.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

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
1.1.0 105 8/6/2026
1.0.0 101 8/6/2026