Skip to content

Releases: KaliCZ/StrongTypes

OpenAPI + WPF + Binding from query, headers, form etc.

05 May 08:42
aabda14

Choose a tag to compare

Kalicz.StrongTypes v1.1.0

Big release: OpenAPI support, new Email strong type and overall 4 new packages added.

Package layout

Kalicz.StrongTypes

  • Email strong typereadonly struct to support parsing emails from requests in the API. Value property returns a standard C# MailAddress to be used in business logic. Ships with EmailJsonConverter and an EmailValueConverter for EF Core.
  • NonEmptyString.Count + char indexer — string is a collection of chars. NonEmptyString isn't, because it would mess up the OpenAPI docs, but at least we can make the usages feel like it is. You can always resolve the .Value and get the string inside.
  • IParsable<TSelf> on every wrapper — strong types now bind from [FromQuery], [FromRoute], [FromHeader], [FromForm], and minimal-API implicit query/route binding without any custom binder. NonEmptyEnumerable<T> is the only exception that needs the AspNetCore package (see more below).

Kalicz.StrongTypes.EfCore

  • New MailAddressValueConverter — allows storing System.Net.Mail.MailAddress on entities, stored into a plain string column.
  • Includes .Unwrap() method for MailAddress, so you can invoke string operations e.g. the LIKE function. db.Users.Where(u => EF.Functions.Like(u.Email.Unwrap(), "%@example.com"))

Kalicz.StrongTypes.FsCheck

  • New Email arbitrary, plus its NullableEmail and MaybeEmail siblings.

Kalicz.StrongTypes.OpenApi.Swashbuckle (new)

Wires StrongTypes into the swagger docs (AddSwaggerGen()). Register with options.AddStrongTypes(). Supports both OpenAPI 3.0 and 3.1. Recommended over the Microsoft adapter if you have a free choice.

Examples (3.1 encoding shown; 3.0 encodes exclusive bounds as {minimum:0, exclusiveMinimum:true} instead):

  • NonEmptyString{type:string, minLength:1}
  • Positive<decimal>{type:number, format:double, exclusiveMinimum:0}
  • NonEmptyEnumerable<T>{type:array, minItems:1, items:<T schema>}

Two analyzers (ST0002 / ST0003) prompt a project to install the matching adapter when it sees a strong-type property exposed through OpenAPI.

Kalicz.StrongTypes.OpenApi.Microsoft (new)

Wires StrongTypes into the Microsoft.AspNetCore.OpenApi pipeline (AddOpenApi(), the .NET 9+ default). Register with options.AddStrongTypes(). Supports both OpenAPI 3.0 and 3.1.

Not recommended unless you're already committed to AddOpenApi(). The Microsoft pipeline has a few rough edges the framework gives no public hook to fix — [EmailAddress] never surfaces as format: email (on string or on a wrapper); a Dictionary<string, int> emits { "format": "int32", "pattern": "^-?(?:0|[1-9]\\d*)$" } for the int value instead of { "type": "integer", "format": "int32" }; and a handful of caller annotations Swashbuckle handles natively get silently dropped. Swashbuckle exposes richer extension points and produces a faithful document in those cases.

Kalicz.StrongTypes.AspNetCore (new)

A niche companion package containing the one model binder the framework can't synthesise from IParsable<T> alone:

  • NonEmptyEnumerableModelBinder<T> — reads multiple raw strings from the binding source, parses each via IParsable<T>, wraps via NonEmptyEnumerable.TryCreateRange. Empty/missing source surfaces as 400 + ValidationProblemDetails (or null when the action parameter is NonEmptyEnumerable<T>?).
  • One-line registration: services.AddStrongTypes()

Not needed for JSON APIs[FromBody] already round-trips NonEmptyEnumerable<T> via the core package's converters. Only install when you bind it from [FromForm] (the primary use), [FromQuery], [FromHeader], or [FromRoute]. Every other wrapper binds from non-body sources via the framework's built-in IParsable<T> binder without this package.

Kalicz.StrongTypes.Wpf (new)

Kalicz.StrongTypes.Wpf allows binding StrongTypes from UI to ViewModel via a generic ParsableTypeConverter<T> registered into TypeDescriptor. One call in App.OnStartup covers every strong type, including every closed instantiation of the generic numeric wrappers (Positive<int>, Negative<decimal>, …):

protected override void OnStartup(StartupEventArgs e)
{
    this.UseStrongTypes();
    base.OnStartup(e);
}

Targets net10.0-windows with UseWPF=true. Other UI frameworks aren't covered yet — see issue #94.

Tooling and docs

  • Claude / Codex skillSkill/SKILL.md + per-package references ship as a strongtypes-skill.tar.gz release asset on every tagged release. Drop it under .claude/skills/strongtypes/ (or .codex/skills/strongtypes/) and the agent picks it up.
  • Diagrams in the readme adapt to dark mode via <picture> + prefers-color-scheme.

Breaking changes

None.

Install

dotnet add package Kalicz.StrongTypes
dotnet add package Kalicz.StrongTypes.EfCore
dotnet add package Kalicz.StrongTypes.FsCheck
dotnet add package Kalicz.StrongTypes.OpenApi.Swashbuckle     # or .Microsoft
dotnet add package Kalicz.StrongTypes.AspNetCore              # only for non-body NonEmptyEnumerable<T>
dotnet add package Kalicz.StrongTypes.Wpf

Small readme improvements

23 Apr 16:47
d9e647d

Choose a tag to compare

Just very small improvements to the readme.md - releasing new version as it's the simplest way to upload to nuget.org

Initial release

23 Apr 15:32
953fd34

Choose a tag to compare

StrongTypes v1.0.0 🎉

First stable release. StrongTypes ships small, focused types that make everyday C# safer and more expressive. Composed in 3 packages:

Contents


Kalicz.StrongTypes

Validated types

  • NonEmptyString — a string guaranteed non-null, non-empty, and not just whitespace. Exposes the common string surface (Length, Contains, StartsWith, Substring, …) and implicitly converts to string.
  • Numeric wrappers over any INumber<T> (int, long, decimal, double, …):
    • Positive<T> — strictly greater than zero
    • NonNegative<T>>= 0
    • Negative<T> — strictly less than zero
    • NonPositive<T><= 0
  • Digit — a single decimal digit 0–9.

Every type implements IEquatable<T>, IComparable<T>, all six comparison operators, GetHashCode / Equals, and a sensible ToString() — drop them straight into dictionaries, sorted collections, and OrderBy.

NonEmptyEnumerable<T>

  • Read-only sequence guaranteed to contain at least one element.
  • Invariant-preserving LINQ (Select, SelectMany, Distinct, Concat, Reverse, Prepend, Append) returns NonEmptyEnumerable<TResult> so the guarantee doesn't get lost.
  • Total aggregate overloads (Max, Min, Last, Average, Aggregate) — never throw InvalidOperationException, and return T instead of T? for value types.
  • Collection-expression support: NonEmptyEnumerable<int> xs = [1, 2, 3];. (may throw exception if you provide empty)

Helpers

  • Enums — cached extension members on every enum type: MyEnum.Parse, MyEnum.TryParse, MyEnum.AllValues, plus [Flags] helpers AllFlagValues, AllFlagsCombined, and value.GetFlags() decomposition. [Flags]-only helpers throw loudly if the attribute is missing.
  • Stringsstring? extensions AsInt / AsDecimal / AsDateTime / AsEnum<T> / AsNonEmpty returning nullables, plus matching To… variants that throw.
  • Map on T? lets you pass a nullable into a function that expects the non-null form (e.g. a constructor) without a ternary at every call site. The mapper only runs when the input is present; the null short-circuit is implicit.
  • MapTrue / MapFalse on bool do the same for boolean-guarded expressions.

Maybe<T>

  • Value-type option with Some / None, constrained to where T : notnull so None and Some(null) can't collapse.
  • Implicit conversions from T and from the untyped Maybe.NoneMaybe<int> x = 42; and Maybe<int> n = Maybe.None; both work, including inside collection expressions.
  • Idiomatic unwrap via the maybe.Value is { } v pattern.
  • Composition: Map, FlatMap, Where, Match, LINQ query syntax via Select / SelectMany.
  • Solves the HTTP PATCH three-state problem (skip / set to null / set to value) via Maybe<T>? in request DTOs.
  • System.Text.Json converter built in: { "Value": x } for Some, { "Value": null } for None, {} accepted as None on deserialize.

Result<T> and Result<T, TError>

  • Either a success T or an error TError — makes the failure path explicit in the signature instead of hiding it behind exceptions.
  • Implicit operators on both sides, so return someT; and return someTError; just work.
  • Pattern-match via r.Success is { } v / r.Error is { } e, or branch-agnostic helpers IsSuccess / IsError.
  • Transformation: Map, MapError, FlatMap, Match, plus ThrowIfError(...) for bailing out when the error isn't worth propagating. Every sync method has an …Async counterpart.
  • Result.Catch / Result.CatchAsync wrap throwing calls without a try/catch, with a propagateCancellation flag so OperationCanceledException still unwinds the way it should by default.
  • Result.Aggregate combines up to eight results — or an IEnumerable<> of them — collecting every error, which is what you want when validating a user input.

JSON serialization

  • Every type (except Result) ships a System.Text.Json converter attached via [JsonConverter] — no registration and no custom JsonSerializerOptions required.
  • Maybe<T> serializes as { "Value": x } / { "Value": null }, with {} also accepted as None.
  • Other types serialize in the same format as their underlying type - just throwing a JsonException if the value is invalid.

Kalicz.StrongTypes.EfCore

Lets you use strong types as regular entity properties — they round-trip through scalar columns, and LINQ predicates over them translate to server-side SQL.

  • One-line registration: options.UseSqlServer(...).UseStrongTypes().
  • Column shape matches the underlying type: nvarchar for NonEmptyString, int for Positive<int>, decimal(18,2) for NonNegative<decimal>, etc. Nullable forms (NonEmptyString?, Positive<int>?) map to nullable columns.
  • Equality, null-checks, ordering, and grouping works directly on the property inside LINQ queries.
  • .Unwrap() is a marker call the translator rewrites to a plain column reference — reach for it when you need string operators (StartsWith, Contains, EF.Functions.Like, EF.Functions.Collate) or arithmetic against the underlying primitive. The in-memory implementation just returns .Value, so client-side LINQ works too.
  • Provider-agnostic — verified against SQL Server and PostgreSQL (via Testcontainers); works against every relational provider.
  • The core package ships an analyzer that nudges you to install this package whenever it sees a strong-type property on an EF-mapped entity.

Kalicz.StrongTypes.FsCheck

Write property tests against code that takes or returns strong types without hand-rolling generators that re-derive each type's invariants.

  • One-attribute registration: [Properties(Arbitrary = new[] { typeof(Generators) })].
  • Scalar strong types ship in three shapes:
    • The type itself (e.g. NonEmptyString, PositiveInt)
    • Its nullable form (e.g. NullableNonEmptyString) with ~5% null
    • Its Maybe<T> form (e.g. MaybeNonEmptyString) with ~5% None
  • Coverage includes NonEmptyString, Digit, and all four numeric wrappers over int.
  • Collection generator: NonEmptyEnumerableIntNonEmptyEnumerable<int>.
  • Maybe<T> generators for common primitives: MaybeBool, MaybeInt, MaybeLong, MaybeDouble, MaybeChar, MaybeString, MaybeGuid.

Install

dotnet add package Kalicz.StrongTypes
dotnet add package Kalicz.StrongTypes.EfCore
dotnet add package Kalicz.StrongTypes.FsCheck

Improved readme

23 Apr 14:14
4c64260

Choose a tag to compare

Improved readme Pre-release
Pre-release

Packages v0.6.1

  • Improved readme for nuget.org by rendering the svg images using markdown instead of html
  • A weird build script that replaces the link to a tagged link to github content to conserve the content during release.

Added Results and finalized documentation

23 Apr 13:55
570c892

Choose a tag to compare

Packages v0.6.0

StrongTypes

  • New Result<T> type for explicit success/failure flows.
  • Documentation overhaul: added SVG diagrams to the readme, expanded the readme itself,
  • Audited XML doc comments across the public API for consistency and caller-focused content.

StrongTypes.EfCore

  • No change.

StrongTypes.FsCheck

  • Consolidated generators shipped into a cohesive list.

Object.Map and removed Coproduct

21 Apr 22:56
01aaa30

Choose a tag to compare

Pre-release

Packages v0.5.0

StrongTypes

  • No longer contains Coproduct (OneOf)
  • Map functions on nullable object and boolean.

StrongTypes.EfCore

  • No change

StrongTypes.FsCheck

  • No change

3 Separate packages

21 Apr 16:31
Immutable release. Only release title and notes can be modified.
8f80c21

Choose a tag to compare

3 Separate packages Pre-release
Pre-release

What's new

  • Rolled out 2 extra packages for support of EF core and generative testing.
  • Bunch of performance optimalizations
  • Removed a lot of dead/pointless code
  • Moving closer to a full release of v1.0

Packages v0.4.0

StrongTypes

  • Removed a bunch of bloat
  • Introduced Maybe<T>
  • Modernized NonEmptyEnumerable<T>

StrongTypes.EfCore

  • Initial release

StrongTypes.FsCheck

  • Initial release