A few small types that make C# safer.
Kalicz.StrongTypes is a C# NuGet package that adds no-code validations enforced by the compiler. Invalid data never reaches your application code.
Why bother with strong types?
Stop validating input everywhere. Validate once, at the boundary, and let the compiler enforce the rest.
Invariants in the type
If a method takes NonEmptyString, it cannot receive an empty one. The compiler — not a runtime guard clause — does the work.
Validation at the edge
Built-in System.Text.Json converters reject invalid payloads during deserialization, before your endpoint method is even called.
Expressive intent
A signature like Result<Order, OrderError> Place(Positive<int> qty) tells the reader exactly what's allowed and what can fail.
Small and focused
No heavy framework, no runtime reflection magic. Drop it in alongside your existing code — types compose with what you already have.
What's in the box
Validated wrappers
-
NonEmptyStringGuaranteed non-null, non-empty, non-whitespace string.
-
Positive<T>, NonNegative<T>, Negative<T>, NonPositive<T>Sign-enforcing wrappers over any INumber<T>.
-
NonEmptyEnumerable<T>Read-only sequence with at least one element — Head and Tail are always safe to access.
Algebraic types
-
Maybe<T>Optional values with Map and FlatMap. Useful for HTTP PATCH (don't touch / clear / set).
-
Result<T, TError>Explicit failure handling without exceptions. Includes async support.
Built-in integrations
-
System.Text.JsonAutomatic converters — no registration required.
-
EF Core value convertersVia the Kalicz.StrongTypes.EfCore companion package.
-
FsCheck generatorsVia the Kalicz.StrongTypes.FsCheck companion package for property-based testing.
How it looks
A few snippets to give you the feel of it. Full docs and more examples are on GitHub.
Validate at the API boundary
The JSON converter rejects invalid input during deserialization. Your endpoint never sees a bad value.
public record CreateUserRequest(
NonEmptyString Name,
Positive<int> Age);
[HttpPost]
public async Task<IResult> Create(CreateUserRequest req)
{
// No extra validation needed here:
// ASP pipeline rejects invalid payloads with 400.
_users.Add(req.Name, req.Age);
await _users.SaveChangesAsync();
return Results.NoContent();
} Construct values explicitly
Each type has Create (throws on invalid) and TryCreate (returns null). String types also have a fluent extension form.
// Strings — three call styles // throws if invalid NonEmptyString name = NonEmptyString.Create(input); // null if invalid NonEmptyString? safe = NonEmptyString.TryCreate(input); // fluent extension NonEmptyString? fluent = input.AsNonEmpty(); // Numbers — sign-enforced over INumber<T> Positive<int> qty = Positive<int>.Create(5); NonNegative<decimal>? price = amount.AsNonNegative(); // Sequences — at least one element, Head safe NonEmptyEnumerable<string> tags = ["red", "blue"]; NonEmptyEnumerable<string>? maybe = list.AsNonEmpty(); string first = tags.Head;
Explicit failures with Result<T, TError>
Pattern-match on Success or Error — no exceptions, no out parameters.
Result<int, string> Parse(string s) =>
int.TryParse(s, out var n) ? n : "not a number";
var result = Parse(input);
if (result.Success is { } value)
Console.WriteLine($"got {value}");
if (result.Error is { } msg)
Console.WriteLine($"failed: {msg}"); Three-state PATCH with Maybe<T>
Distinguish "don't touch", "clear to null", and "set to value" — a long-standing pain point in REST APIs.
public record PatchUser(Maybe<string>? Nickname);
// null → don't touch the field
// Some(null) → clear it
// Some("abc") → set it
if (request.Nickname is { } change)
user.Nickname = change.Value; Compose with Maybe<T>
Chain optional operations without nested null checks. Implicit operators handle the wrapping — you return a T or Maybe.None and it just works.
// Producer: implicit conversion from T or Maybe.None
Maybe<int> Parse(string s) =>
int.TryParse(s, out var n) ? n : Maybe.None;
// Map: T -> U, auto-wraps in Maybe
Maybe<int> doubled = Parse("21").Map(x => x * 2);
// Some(42)
// FlatMap: T -> Maybe<U>, no double-wrap
Maybe<int> sum = Parse("1").FlatMap(a =>
Parse("2").Map(b => a + b));
// Some(3) Get started
Source, packages, and docs.
GitHub repository
Source, README, releases, and issue tracker.
KaliCZ/StrongTypesNuGet — Core
The main package. Contains all validated and algebraic types.
Kalicz.StrongTypesNuGet — EF Core
Value converters for persisting StrongTypes with Entity Framework Core.
Kalicz.StrongTypes.EfCoreNuGet — FsCheck
Generators for property-based testing of StrongTypes values.
Kalicz.StrongTypes.FsCheck