Table of Contents

Utilities

Everything that doesn't fit under Claims or Hashing — the day-to-day helpers most consumers reach for.

Compare

Deep object comparison that returns a list of differences, with optional sort-order tolerance for collections.

var diffs = obj1.Compare(obj2);
foreach (var diff in diffs) Console.WriteLine(diff.Message);

var ignoreOrder = list1.Compare(list2, CompareExtensions.CompareMode.IgnoreSortOrder);

ManagedTimer

Async timer with interval correction (so drift doesn't accumulate), skip detection (knows when an iteration ran late), and rich events for state changes.

var timer = new ManagedTimer(
    TimeSpan.FromSeconds(5),
    async iteration => { /* your work */ },
    autoStart: true);

timer.BeforeExecuteEvent += (s, e) => { if (ShouldSkip()) e.Cancel = true; };
timer.AfterExecuteEvent  += (s, e) => { if (e.Exception != null) Log(e.Exception); };
timer.StateChangedEvent  += (s, e) => Console.WriteLine(e.State);

timer.Stop();

States: Stopped, Started, Waiting, Executing.

DateTime and duration

DateTime.UtcNow.ToLocalDateString();    // "2026-06-08"
DateTime.UtcNow.ToLocalTimeString();    // "14:30:00"

someDate.ToDurationString();            // "3 hours ago"
someDate.ToDurationString(new DurationOptions
{
    StringOptions = DurationStringOptionsExtensions.Get(Language.Sv)
});                                      // "3 timmar sedan"

TimeSpan.FromMinutes(45).ToTimeSpanString();   // "45 minutes"

Enumerable

items.TakeRandom();              // one random element
items.RandomOrder();             // shuffled enumeration
items.TakeAllButFirst();         // tail
items.TakeAllButLast();          // init
items.TakeChunks(10);            // groups of 10

EnumerableExtensions.IsNullOrEmpty(items);   // null-safe
EnumerableExtensions.EmptyIfNull(items);     // never null

Async equivalents in Tharga.Toolkit:

var picked = await asyncEnumerable.TakeRandomAsync();
await foreach (var x in asyncEnumerable.RandomOrderAsync()) { /* ... */ }

Luhn

Compute and validate Luhn check digits — works on string, int, long, and IList<int>:

"7992739871".CheckDigit();           // "3"
"7992739871".AppendCheckDigit();     // "79927398713"
"79927398713".HasValidCheckDigit();  // true

36155.CheckDigit();                  // 0

OrgNo (Swedish organization number)

if ("556123-4567".TryParseOrgNo(out var orgNo))
    Console.WriteLine(orgNo);   // "556123-4567" (normalized)

Byte size

((long)1536).ToReadableByteSize();                 // "1.5 KB"
((long)1536).ToReadableByteSize(decimalPlaces: 1); // "1.5 KB"

Collections

// Two-level concurrent dictionary
var dict = new ConcurrentTwoLevelDictionary<string, string, int>();
var (before, after) = dict.AddOrUpdate("users", "alice", 42);

// Observable concurrent dictionary (raises INotifyCollectionChanged)
var observable = new ObservableConcurrentDictionary<string, int>();
observable.CollectionChanged += (s, e) => { /* react */ };
observable.Add("key", 1);

SemaphoreExecutor — key-based concurrency

Same key → sequential. Different keys → concurrent. Useful when you need per-user or per-tenant serialization without a global lock.

var executor = new SemaphoreExecutor<string>();
var result = await executor.ExecuteAsync("user-123", async () =>
{
    // only one operation per "user-123" runs at a time
    return await ProcessAsync();
});

Smart Enum

public class Color : Enumeration
{
    public static readonly Color Red  = new(1, "Red");
    public static readonly Color Blue = new(2, "Blue");
    private Color(int id, string name) : base(id, name) { }
}

var all = Enumeration.GetAll<Color>();

Exception data

Fluent helpers for attaching context to thrown exceptions:

throw new InvalidOperationException("save failed")
    .AddData("userId", 123)
    .AddData("action", "save");

// Later, when handling:
var data = ex.ToDictionary();

URI (Tharga.Toolkit only)

var uri = new Uri("https://example.com/path?page=1&sort=name");
var clean  = uri.RemoveQuery();           // https://example.com/path
var values = uri.GetQueryValue("sort");   // ["name"]