Releases: KaliCZ/StrongTypes
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
3 Separate packages
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