Releases: KaliCZ/StrongTypes
Release list
2.0.1: [Range] support
A patch release: DataAnnotations range validation now sees through the numeric wrappers, WinForms two-way binding is verified end-to-end, and the Roslyn components got a structural hardening.
[Range] works on numeric wrappers (#126)
The numeric types and Digit now implement IConvertible. Therefore:
[Range(1, 100)] public Positive<int>? Quantity { get; set; } // this now works
[Range(typeof(Positive<int>), "1", "100")] public Positive<int>? Quantity { get; set; } // this has always workedSmall improvements
- Full E2E suite for WinForms to validate the parsing and two-way binding. (#127)
- Bump to Roslyn 5.0.0, because we target dotnet 10 anyway. (#128)
- The code-fix providers moved into separate project so the analyzer DLL no longer references
Microsoft.CodeAnalysis.Workspaces. No change for users - still part of the core package (#128)
Skill
The Claude / Codex skill ships as strongtypes-skill.tar.gz below:
mkdir -p .claude/skills/strongtypes
curl -L https://github.com/KaliCZ/StrongTypes/releases/download/v2.0.1/strongtypes-skill.tar.gz | tar -xz -C .claude/skills/strongtypesFull changelog: v2.0.0...v2.0.1
Strong types 2.0.0: configuration works, no more WPF package
Strong types now carry their TypeConverter on the type itself, which makes two integration surfaces work with zero setup — and retires a package.
Breaking changes
Kalicz.StrongTypes.Wpfis retired. The types now carry[TypeConverter], so binding works out-of-the-box.PartitionMatchparameter names are nowsuccesses/errorsinstead ofsuccess/error. The callbacks receive whole partitions.
Configuration binding — the invariant is the validation rule
A wrapper sits directly on an options class; ConfigurationBinder converts through the type's own TypeConverter, so a bad value fails with the config path, the value, and the broken invariant — no [Range], no custom validator. Add ValidateOnStart() to fail at startup instead of on the first request.
New package: Kalicz.StrongTypes.Configuration
BindStrongTypes() closes the gap validation cannot see: the key that isn't there. It fails startup when any property declared non-nullable — wrapper or plain string — is still null after binding, at every depth (nested options, collection elements, dictionary values), reading the intent from your nullable annotations instead of per-property [Required]:
builder.Services.AddOptions<ClientOptions>()
.BindStrongTypes(builder.Configuration.GetSection("Client"))
.ValidateOnStart();New analyzer: ST0004
A plain Bind / Configure that would leave a non-nullable wrapper unchecked now gets a warning, with a code fix that rewrites Bind → BindStrongTypes (and names the package to add when it isn't referenced yet). Ships inside the core package.
Also
- Numeric wrappers implement
IFormattable,ISpanFormattable, andISpanParsable<T>. - The Claude / Codex skill was reworked for v2 (including a new
Emailreference) and ships asstrongtypes-skill.tar.gzbelow:
mkdir -p .claude/skills/strongtypes
curl -L https://github.com/KaliCZ/StrongTypes/releases/download/v2.0.0/strongtypes-skill.tar.gz | tar -xz -C .claude/skills/strongtypesFull changelog: v1.2.0...v2.0.0
Intervals
StrongTypes v1.2.0
Adds interval strong types — a full family of range types with JSON, EF Core, and OpenAPI support — plus EF Core support for Email and for non-public strong-type properties.
✨ Interval strong types (#77 + #119)
Four range types over any struct, IComparable<T> (int, DateOnly, DateTime, decimal, …):
FiniteInterval<T>— both ends requiredInterval<T>— both ends optional (one must be provided)IntervalFrom<T>/IntervalUntil<T>— one end bounded
Highlights:
- Always correct - start never after end.
- Supports any IComparable.
- Set operations:
Contains,Overlaps,GetOverlap; implicit widening and explicit narrowing between the four variants. - More helpers for DateTime and DateOnly intervals. Converting back and forth, count calendar days (
.Days()) and contains works across both types. - OpenAPI: interval schemas generated for both Microsoft.OpenApi and Swashbuckle.
- JSON: serializes as
{ "Start", "End" }; optionallyStartInclusiveandEndInclusiveproperties. - Inclusive by default, with per-bound inclusivity (half-open
[1, 5), etc.) and validated construction (Create/TryCreate).
🗄️ EF Core
- Intervals map two ways: two indexable endpoint columns (the default — inclusive-only) or one JSON column (keeps each value's inclusivity). A corrupt row (
Start > End) throws on read. Emailnow persists through EF Core (#115).- Non-public strong-type properties — an
internal/privateDDD backing field — get their value converter automatically once mapped (#113).
🔒 Security
- Bump
Microsoft.OpenApito clear GHSA-v5pm-xwqc-g5wc (#114).
Full changelog: v1.1.1...v1.2.0
Minor OpenAPI + validation improvements
What's changed
Fixed — nullable strong-type wrappers lose nullable in Swashbuckle (#108)
A nullable wrapper property (NonEmptyString?, Positive<int>?, Digit?, Email?, and the other numeric wrappers) was rendered by the Swashbuckle adapter as a bare $ref, which in OpenAPI 3.0 can't carry the member's nullability. The schema was repainted to the wrapper's wire shape but nullable: true was dropped — so generated clients (openapi-typescript, …) saw the field as non-null, making NonEmptyString? less precise than a plain string?.
Nullable wrappers now keep their nullability on both pipelines:
- OpenAPI 3.0:
{ "type": "string", "minLength": 1, "nullable": true } - OpenAPI 3.1:
{ "type": ["string", "null"], "minLength": 1 }
This makes a string? → NonEmptyString? migration safe — the contract tightens without regressing the client.
Notes
- The
Microsoft.AspNetCore.OpenApiadapter already emitted nullability correctly (3.0 and 3.1) and is unchanged. NonEmptyEnumerable<T>?was already correct; the fix covers the scalar/component-typed wrappers it previously missed.
Changed — normalized JSON request-body validation error keys (#106)
Kalicz.StrongTypes.AspNetCore now rewrites System.Text.Json error paths (e.g. $.value, $.items[0].name) into the same model-binding key form MVC uses for model-binding and data-annotation errors (Value, Items[0].Name). Body and model-binding validation errors now share one consistent key shape in the ValidationProblemDetails response.
Casing is configurable via JsonErrorKeyCasing (PascalCase, CamelCase, or StripOnly, which just removes the $. root prefix). Array indexers are preserved; a custom [JsonPropertyName] that isn't simply a re-cased property name can't be recovered at this layer.
Full changelog: v1.1.0...v1.1.1
OpenAPI + WPF + Binding from query, headers, form etc.
Kalicz.StrongTypes v1.1.0
Big release: OpenAPI support, new Email strong type and overall 4 new packages added.
Kalicz.StrongTypes.OpenApi.Microsoft— new — schema transformers forMicrosoft.AspNetCore.OpenApi(AddOpenApi())Kalicz.StrongTypes.OpenApi.Swashbuckle— new — schema filters forSwashbuckle.AspNetCore(AddSwaggerGen())Kalicz.StrongTypes.AspNetCore— new — MVC model binder forNonEmptyEnumerable<T>from non-body sourcesKalicz.StrongTypes.Wpf— new —TypeConverters for two-way MVVM binding
Kalicz.StrongTypes
Emailstrong type —readonly structto support parsing emails from requests in the API.Valueproperty returns a standard C#MailAddressto be used in business logic. Ships withEmailJsonConverterand anEmailValueConverterfor 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 storingSystem.Net.Mail.MailAddresson entities, stored into a plainstringcolumn. - 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
Emailarbitrary, plus itsNullableEmailandMaybeEmailsiblings.
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 viaIParsable<T>, wraps viaNonEmptyEnumerable.TryCreateRange. Empty/missing source surfaces as 400 +ValidationProblemDetails(ornullwhen the action parameter isNonEmptyEnumerable<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 skill —
Skill/SKILL.md+ per-package references ship as astrongtypes-skill.tar.gzrelease 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.WpfSmall readme improvements
Just very small improvements to the readme.md - releasing new version as it's the simplest way to upload to nuget.org
Initial release
StrongTypes v1.0.0 🎉
First stable release. StrongTypes ships small, focused types that make everyday C# safer and more expressive. Composed in 3 packages:
Kalicz.StrongTypes- Core types and JSON convertersKalicz.StrongTypes.EfCore- EF Core value converters + LINQ translatorKalicz.StrongTypes.FsCheck- Ready-made FsCheck arbitraries for property tests
Contents
Kalicz.StrongTypes
Validated types
NonEmptyString— a string guaranteed non-null, non-empty, and not just whitespace. Exposes the commonstringsurface (Length,Contains,StartsWith,Substring, …) and implicitly converts tostring.- Numeric wrappers over any
INumber<T>(int,long,decimal,double, …):Positive<T>— strictly greater than zeroNonNegative<T>—>= 0Negative<T>— strictly less than zeroNonPositive<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) returnsNonEmptyEnumerable<TResult>so the guarantee doesn't get lost. - Total aggregate overloads (
Max,Min,Last,Average,Aggregate) — never throwInvalidOperationException, and returnTinstead ofT?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]helpersAllFlagValues,AllFlagsCombined, andvalue.GetFlags()decomposition.[Flags]-only helpers throw loudly if the attribute is missing. - Strings —
string?extensionsAsInt/AsDecimal/AsDateTime/AsEnum<T>/AsNonEmptyreturning nullables, plus matchingTo…variants that throw. MaponT?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; thenullshort-circuit is implicit.MapTrue/MapFalseonbooldo the same for boolean-guarded expressions.
Maybe<T>
- Value-type option with
Some/None, constrained towhere T : notnullsoNoneandSome(null)can't collapse. - Implicit conversions from
Tand from the untypedMaybe.None—Maybe<int> x = 42;andMaybe<int> n = Maybe.None;both work, including inside collection expressions. - Idiomatic unwrap via the
maybe.Value is { } vpattern. - Composition:
Map,FlatMap,Where,Match, LINQ query syntax viaSelect/SelectMany. - Solves the HTTP PATCH three-state problem (skip / set to null / set to value) via
Maybe<T>?in request DTOs. System.Text.Jsonconverter built in:{ "Value": x }forSome,{ "Value": null }forNone,{}accepted asNoneon deserialize.
Result<T> and Result<T, TError>
- Either a success
Tor an errorTError— makes the failure path explicit in the signature instead of hiding it behind exceptions. - Implicit operators on both sides, so
return someT;andreturn someTError;just work. - Pattern-match via
r.Success is { } v/r.Error is { } e, or branch-agnostic helpersIsSuccess/IsError. - Transformation:
Map,MapError,FlatMap,Match, plusThrowIfError(...)for bailing out when the error isn't worth propagating. Every sync method has an…Asynccounterpart. Result.Catch/Result.CatchAsyncwrap throwing calls without atry/catch, with apropagateCancellationflag soOperationCanceledExceptionstill unwinds the way it should by default.Result.Aggregatecombines up to eight results — or anIEnumerable<>of them — collecting every error, which is what you want when validating a user input.
JSON serialization
- Every type (except
Result) ships aSystem.Text.Jsonconverter attached via[JsonConverter]— no registration and no customJsonSerializerOptionsrequired. Maybe<T>serializes as{ "Value": x }/{ "Value": null }, with{}also accepted asNone.- 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:
nvarcharforNonEmptyString,intforPositive<int>,decimal(18,2)forNonNegative<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
- The type itself (e.g.
- Coverage includes
NonEmptyString,Digit, and all four numeric wrappers overint. - Collection generator:
NonEmptyEnumerableInt—NonEmptyEnumerable<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.FsCheckImproved readme
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
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
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