Predicates, casing, reshaping, parsing, humanizing, encoding and fuzzy matching.
A latency report, straight to the console
One outlier turns a 114 ms p95 into 1,104 ms. WithoutOutliers, Percentile, ToSparkline, ToBarChart and ToTable show it in the console, with no charting or table package.
double[] latencies = [118, 92, 104, 97, 2310, 101, 95, 99, 110, 93, 106, 98];
double[] typical = latencies.WithoutOutliers().ToArray();
Console.WriteLine($"raw {latencies.ToSparkline(),-12} p95 {latencies.Percentile(95):0} ms");
Console.WriteLine($"clean {typical.ToSparkline(),-12} p95 {typical.Percentile(95):0} ms");
Endpoint[] endpoints =
[
new("GET /orders", 1240, 104),
new("GET /users", 655, 97),
new("POST /orders", 410, 231),
];
Console.WriteLine(endpoints.ToBarChart(e => e.Route, e => e.Calls, 20));
Console.WriteLine(endpoints.ToTable());
record Endpoint(string Route, int Calls, int P95);
dotnet runraw p95 1104 msclean p95 114 msGET /orders 1,240GET /users 655POST /orders 410┌──────────────┬───────┬─────┐│ Route │ Calls │ P95 │├──────────────┼───────┼─────┤│ GET /orders │ 1240 │ 104 ││ GET /users │ 655 │ 97 ││ POST /orders │ 410 │ 231 │└──────────────┴───────┴─────┘
This is the real output of the program, run on .NET 10. Console output guide
- 0third-party dependencies. Only System.Text.Json, and only on netstandard2.1.
- netstandard2.1 + net10.0Any .NET Core 3.0+ or .NET 5+ app, with a tuned .NET 10 build.
- Trim + AOTAnnotated: warnings appear only where reflection is used.
- 558public extension methods and members, overloads counted.
C# that reads like the framework
C# binds await, foreach, deconstruction and query syntax to methods by name, so an extension method can switch them on. The package does that for the types you already use.
- Await anything
- foreach and deconstruction
- C# 14 extension members
- Strings for people
- Pipelines and resilience
C# awaits any value that has a GetAwaiter method, and an extension method counts. The package adds one to task tuples, task lists, TimeSpan, SemaphoreSlim, Process and CancellationToken. Learn more
(User user, Order[] orders) = await (GetUserAsync(id), GetOrdersAsync(id)); // both at once
string[] pages = await urls.Select(FetchAsync); // a whole list of tasks
await 2.Seconds(); // a delay
using (await _lock) { /* one caller at a time */ } // a SemaphoreSlim
int exitCode = await Process.Start("git", "pull"); // a process, to its exit code
await stoppingToken; // until shutdown is requested
foreach needs only a GetEnumerator method, and deconstruction only a Deconstruct method. Extensions supply both for ranges, lists, time spans, dates and readers. Learn more
foreach (int i in 0..5) { } // 0, 1, 2, 3, 4
foreach (int i in (0..100).Step(25)) { } // 0, 25, 50, 75
IEnumerable<int> squares = from i in 0..10 where i % 2 == 0 select i * i; // 0, 4, 16, 36, 64
(string name, string domain) = "ahmad@example.com".Split('@'); // "ahmad", "example.com"
(int hours, int minutes, int seconds) = TimeSpan.FromMinutes(150); // 2, 30, 0
foreach (DateTime day in start.To(end)) { } // every day, both ends included
foreach ((string item, int index) in items.WithIndex()) { } // each item with its position
foreach (string line in reader) { } // a TextReader, line by line
Extension properties, static members and operators, where they read best. The syntax needs the .NET 10 SDK; older compilers still get every classic method. Learn more
if (name.IsBlank) { } // a property on string
string rule = "-" * 40; // an operator on string
Dictionary<string, string> settings = defaults + overrides; // merge, the right side wins
string id = Guid.NewShortId(); // a static member on Guid
TimeSpan fortnight = TimeSpan.FromWeeks(2); // 14.00:00:00
DateTime moment = DateTime.FromUnixTimeSeconds(1715774400); // 2024-05-15 12:00:00 UTC
Recase, slug, mask, pluralize, spell out and suggest. Formatting is invariant and comparisons are ordinal, so results do not change with the server culture. Learn more
"getHTTPResponse".ToKebabCase(); // "get-http-response"
"Hello, World! 2024".ToSlug(); // "hello-world-2024"
"4111111111111111".Mask(); // "************1111"
"box".ToQuantity(2); // "2 boxes"
123.ToWords(); // "one hundred twenty-three"
"1d 2h 30m".ToTimeSpan().ToHumanString(); // "1 day, 2 hours, 30 minutes"
commands.ClosestMatch("statsu"); // "status"
now.AddMinutes(-5).ToRelativeTime(now); // "5 minutes ago"
Bounded concurrency, retries with backoff, timing, fallbacks, single-flight loads and promise-style chaining, as extensions on sequences, Task and Func, plus the static Retry class. Learn more
string[] bodies = await urls.SelectAsync(FetchAsync, maxConcurrency: 4); // bounded, in order
Order order = await Retry.ExecuteAsync(() => api.GetOrderAsync(id), 3, 200.Milliseconds());
(Report report, TimeSpan elapsed) = await BuildReportAsync().Timed(); // result + duration
byte[] file = await mirrors.Select(DownloadAsync).FirstSuccessful(); // fastest healthy mirror
decimal price = await GetPriceAsync().WithFallback(0m, 2.Seconds()); // never hang the page
Func<Task<Config>> sharedLoad = load.SingleFlight(); // many callers, one load
Config config = await LoadAsync().Then(Parse).Catch<ParseException, Config>(ex => Config.Default);
The helpers you keep rewriting, written once
Every method validates its arguments, carries nullable annotations and is covered by tests, so you can delete the copy in your utilities folder.
Text for people
Plurals, numbers in words, durations and relative times, all in invariant English, so a label reads the same on every server.
Read the guide"box".ToQuantity(2) - 2 boxes
123.ToWords() - one hundred twenty-three
21.ToOrdinal() - 21st
90.Minutes() .ToHumanString() - 1 hour, 30 minutes
now.AddHours(-3) .ToRelativeTime(now) - 3 hours ago
Read what people type
"10 MB", "1d 2h 30m" and "/users/{id}" templates become numbers, durations and values. TryParse<int>() returns null instead of throwing.
Statistics in one call
Percentile, Median and StandardDeviation for int, long, double and decimal, plus WithoutOutliers before you report.
Diffs and build order
DiffWith splits two lists into added, removed, changed and unchanged by key. TopologicalSort orders dependencies and names the cycle.
Paging and safe sorting
ToPage returns a Page<T> with totals. SortBy reads a ?sort= string and, given an allow-list, sorts only by the properties you name. Both run in SQL on IQueryable.
Did you mean?
ClosestMatch suggests "status" for "statsu", ToClosestEnum forgives typos, and OrderByNatural puts file2 before file10.
Logs that keep secrets
ToSafeJson replaces passwords, tokens, API keys and card numbers with "***" at any depth, so a request or an entity can go straight into a log.
account.ToSafeJson(){"Name": "Ahmad", "Email": "ahmad@example.com", "Password": "***", "ApiToken": "***"}Compare, clone and map
CompareProperties for audit trails, DeepClone, MapTo for DTOs and UpdateFrom for PATCH, without a mapping library.
Functions with manners
Memoize, Debounce, Throttle and SingleFlight wrap a delegate and return one you call as before.
Trim and AOT aware
The .NET 10 build is marked trim- and AOT-compatible. Reflection-based members say so with a warning at the call site.
Read the guideSix areas, one package
Start from the area you need. Each method name opens the guide that explains it.
Query, transform, measure, diff, page and sort sequences, dictionaries and lists.
Convert, compare, clone and map objects, and turn them into JSON that is safe to log.
Run tasks with limits, retries and timeouts, and wrap functions to memoize or debounce them.
Dates and durations, number formatting and conversion, and repeatable random values.
Types, enums, exceptions, disposables, streams, processes, culture and URLs.
Write less glue code today.
The Quick Start takes one console app through strings, parsing, collections, tasks, objects, time and console output in seven short steps.