Skip to content

v0.3.0: EF Core package, Unwrap filtering, ST0001 analyzer, numeric As* helpers#27

Merged
KaliCZ merged 18 commits intomainfrom
claude/kind-blackburn-50efaf
Apr 19, 2026
Merged

v0.3.0: EF Core package, Unwrap filtering, ST0001 analyzer, numeric As* helpers#27
KaliCZ merged 18 commits intomainfrom
claude/kind-blackburn-50efaf

Conversation

@KaliCZ
Copy link
Copy Markdown
Owner

@KaliCZ KaliCZ commented Apr 17, 2026

Summary

This branch accumulated several related pieces that ship together as v0.3.0:

  • Server-side filtering on NonEmptyString via Unwrap() (closes Integration tests: SQL converter filtering and Unwrap for LINQ translations #21) — adds NonEmptyString.Unwrap() plus an EF Core IMethodCallTranslator plugin that rewrites it as a pass-through to the underlying string column, re-typed with a plain string mapping so downstream operators (Contains, StartsWith, EndsWith, EF.Functions.Like) translate to server-side SQL on both providers. Extended to every strong type, not just NonEmptyString.
  • StrongTypes.EfCore package extraction (closes Extract EF Core converters into StrongTypes.EFCore package #16) — EF-Core integration now ships as a separate Kalicz.StrongTypes.EfCore NuGet package with its own README, collapsed to a single UseStrongTypes() touchpoint.
  • ST0001 analyzer + code fix (Ship a Roslyn analyzer that nudges users toward StrongTypes.EFCore #17) — nudges users who reference strong types in an EF-Core project without the StrongTypes.EfCore package installed, with a one-click "Add package" code fix. Comes with a full analyzer test suite.
  • Numeric As* helpersAsPositive(), AsNonNegative(), AsNegative(), AsNonPositive() extensions on any INumber<T>, mirroring AsNonEmpty() on string?. Surfaced in the root README.

Why the Unwrap machinery

A naive DbFunction pass-through (HasDbFunction(...).HasTranslation(args => args[0])) fails for string operators because args[0] is the column expression with its NonEmptyStringValueConverter mapping still attached. Downstream translators pipe their string literals through that converter at SQL-parameter bind time and throw InvalidCastException. The plugin strips the mapping by converting to string with a fresh RelationalTypeMapping.

Arithmetic predicates that fold into SQL get an (long) cast on Unwrap() to avoid server-side overflow on int columns.

Test plan

  • dotnet test src/StrongTypes.Tests — all tests pass, including the new NumberExtensionsTests property coverage for the numeric As* helpers and the NonEmptyStringUnwrapTests.
  • dotnet test src/StrongTypes.Api.IntegrationTests — filter endpoints exercised against live SQL Server and PostgreSQL testcontainers.
  • dotnet test src/StrongTypes.Analyzers.Tests — ST0001 analyzer + code-fix suite.

🤖 Generated with Claude Code

…erver-side

Closes #21. Adds a NonEmptyString.Unwrap() extension plus an EF Core method-
call translator that rewrites it as a pass-through to the underlying string
column (re-typed with a plain string mapping so downstream LIKE/Contains
parameter binding doesn't hit the NonEmptyString value converter). New
filter endpoints on the NonEmptyString controller and integration tests on
both providers cover equality, null-checks, ordering, Contains/StartsWith/
EndsWith, and a dedicated EF.Functions.Like case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ self-assigned this Apr 17, 2026
KaliCZ and others added 15 commits April 17, 2026 12:34
Unwrap is a marker for EF translation, not an in-memory API. The numeric
generator now emits Unwrap on every wrapper's extensions class, and the
translator matches all five variants (the hand-written string one plus
the four generated numeric ones) by method definition.

Filter tests now query the DbContext directly — HTTP wasn't exercising
anything the translator cares about, and the scaffolding (controller filter
endpoints, protected DbSet, route helpers in the base class) is gone.
Numeric coverage added: one arithmetic predicate per wrapper shape through
Unwrap(), on both providers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EF Core value converters and the Unwrap LINQ translator now live in a
separate Kalicz.StrongTypes.EfCore NuGet package, shipped at 0.3.0. Core
library stays EF-free.

Consumer API splits in two clear touchpoints (same verb):

  protected override void ConfigureConventions(ModelConfigurationBuilder b)
      => b.UseStrongTypes();    // value converters, pre-convention phase

  services.AddDbContext<T>(o => o.UseSqlServer(...).UseStrongTypes());
                                // Unwrap translator plugin, DI phase

These can't be collapsed: converters have to be registered as pre-
conventions on ModelConfigurationBuilder (before property discovery, or
reference-typed wrappers get inferred as owned entity types), while the
translator plugin lives in EF's internal service provider and has to go
through IDbContextOptionsExtension.

Integration tests restructured by kind:

  Tests/
    ApiTests/           (HTTP round-trip tests, one per strong type)
      Numeric/
      Strings/
    ConverterTests/     (direct DbContext LINQ tests for Unwrap)
      Numeric/
      Strings/

HTTP route/body helpers moved from IntegrationTestBase into the
EntityTests base that actually uses them, so ConverterTests don't carry
an unused RoutePrefix.

Core StrongTypes package bumps to 0.3.0 with release notes covering the
generic numeric wrappers (source-generated in this cycle) and Unwrap().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn analyzer shipped inside Kalicz.StrongTypes under
analyzers/dotnet/cs/ — no separate package to reference.

Fires when:
- the project references Microsoft.EntityFrameworkCore
- it doesn't reference Kalicz.StrongTypes.EfCore
- a DbContext-derived class has a DbSet<T> whose entity T carries a
  StrongTypes wrapper property (NonEmptyString / Positive<T> / … )

That triple is the combination that otherwise blows up at model-build
time with a "no suitable constructor for NonEmptyString" error because
EF infers the wrapper as an owned entity type. The diagnostic points at
the DbSet property and links to the nuget.org listing.

Auto-install code fix deferred — that requires IDE-specific APIs
(NuGet.VisualStudio.IVsPackageInstaller or Rider equivalent) and is
tracked as the remaining checkbox on #17. IDEs' built-in "unresolved
symbol → install package" fix covers the common case once a user types
UseStrongTypes() or a converter type name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## One-call registration

optionsBuilder.UseStrongTypes() now wires everything — value converters
AND the Unwrap translator — in one place. No ConfigureConventions
override needed on consumer DbContexts.

The convention trick: register an IEntityTypeAddedConvention at index 0
of the convention set. It runs before PropertyDiscoveryConvention
decides which members become scalar properties vs complex types. We
pre-declare each strong-type member as a scalar property with its
ValueConverter attached, so when property discovery walks the members
they already exist as scalars — EF never falls through to complex-type
inference and the "no suitable constructor for NonEmptyString" failure
can't happen.

Nullable<> wrappers (Positive<int>?) are unwrapped in the converter
lookup so numeric wrappers work in both mapped slots.

## Analyzer ST0001 now fires on three locations

Previously only the DbSet<T> declaration. Now we also mark:
- each strong-type property on the mapped entity (developer reading
  the entity definition sees the hint)
- the DbContext class itself (that's where UseStrongTypes() will be
  called once the package is installed)

## Auto-install code fix

AddEfCorePackageCodeFixProvider reads the project's csproj, adds
<PackageReference Include="Kalicz.StrongTypes.EfCore" Version="0.3.0" />
to an existing PackageReference ItemGroup when possible, and saves.
The IDE picks up the csproj change on its own.

File IO is intrinsic to the fix (PackageReference lives outside the
Roslyn document model), so RS1035 is suppressed at the two necessary
call sites with explanation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-50efaf

# Conflicts:
#	src/StrongTypes/Strings/NonEmptyStringExtensions.cs
Integration tests share one DB across the collection, so the Api suites
had already seeded int.MaxValue into PositiveIntEntities. SQL Server
doesn't short-circuit the ID-filter branch of the WHERE clause, so
CAST(Value AS int) * 2 ran across every row and overflowed before the
ID predicate narrowed the set. Casting Unwrap() to long forces the
arithmetic into bigint space, which can't overflow for any 32-bit seed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The package was shipping the repo-root readme, which talks about
StrongTypes-the-library and never mentions EF Core. Replace it with
an EfCore-focused guide: install, register via UseStrongTypes(), model
entities, and filter — including equality/ordering directly on wrappers,
Unwrap() for string operators and arithmetic, and EF.Functions.Like.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives the analyzer directly through CompilationWithAnalyzers — no
Microsoft.CodeAnalysis.Testing dependency — so cases stay tight and
portable across Roslyn / xUnit versions. Covers:
 - fires when EF Core is referenced without Kalicz.StrongTypes.EfCore,
 - silent when the EfCore package is referenced (either assembly name),
 - silent when EF Core isn't referenced at all,
 - silent when the mapped entity has no StrongTypes wrapper properties,
 - detects nullable and generic numeric wrappers,
 - reports at all three location kinds (DbSet, entity property, DbContext),
 - reports each wrapper property on the entity exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fix writes to the csproj on disk rather than producing a Solution
diff, so `CSharpCodeFixTest` can't observe it. Instead, materialize a
real csproj in a temp directory, point an `AdhocWorkspace` at it, invoke
the registered `CodeAction`, and inspect the XML afterwards.

Cases covered:
 - drops the PackageReference into an existing ItemGroup when one holds
   other PackageReferences (matches `dotnet add package` layout),
 - creates a new ItemGroup when none is suitable,
 - idempotent when the package is already referenced (no duplicate, byte-
   for-byte unchanged file),
 - case-insensitive match on the existing package id,
 - no-ops safely when the csproj path doesn't exist on disk,
 - advertises the expected diagnostic id, fix-all provider, and title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Assert NotEmpty before Assert.All in the ST0001 fires-case (Assert.All
  is vacuously true on empty, so the order matters).
- New-ItemGroup test now asserts exactly one ItemGroup exists in the
  csproj after the fix runs (the original had none, so this proves a
  fresh ItemGroup was created instead of relying on the tautological
  PackageReference.Parent.Name == "ItemGroup" check).
- Same test now asserts the written Version matches the package
  constant. Promoted EfCorePackageVersion to public to keep the version
  a single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors AsNonEmpty() on string?: one generic extension per invariant
over INumber<T> so any numeric primitive can be wrapped fluently.
Surfaces the helpers in the README next to the existing factory snippet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ changed the title Server-side filtering on NonEmptyString via Unwrap (closes #21) v0.3.0: EF Core package, Unwrap filtering, ST0001 analyzer, numeric As* helpers Apr 19, 2026
KaliCZ and others added 2 commits April 19, 2026 11:47
…ions

Parallel to the nullable-returning As* helpers: the To* variants delegate
to Create and throw ArgumentException on invariant violation. Same
relationship as TryCreate vs Create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the numeric As*/To* split: each As* helper on string? and
NonEmptyString now has a To* sibling that delegates to the framework's
Parse methods and throws on failure. ToNonEmpty throws ArgumentException
when the input is null/empty/whitespace.

StringExtensions_Old (and its two test files) is deleted — its surface
is fully replaced by the As*/To* pair on StringExtensions, and keeping
it would create name clashes with the new To* methods.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ merged commit 2415e48 into main Apr 19, 2026
2 checks passed
@KaliCZ KaliCZ deleted the claude/kind-blackburn-50efaf branch April 19, 2026 09:53
KaliCZ added a commit that referenced this pull request Apr 19, 2026
…s* helpers (#27)

* Translate NonEmptyString.Unwrap() to column access so EF can filter server-side

Closes #21. Adds a NonEmptyString.Unwrap() extension plus an EF Core method-
call translator that rewrites it as a pass-through to the underlying string
column (re-typed with a plain string mapping so downstream LIKE/Contains
parameter binding doesn't hit the NonEmptyString value converter). New
filter endpoints on the NonEmptyString controller and integration tests on
both providers cover equality, null-checks, ordering, Contains/StartsWith/
EndsWith, and a dedicated EF.Functions.Like case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Extend Unwrap to every strong type; test via DbContext, not HTTP

Unwrap is a marker for EF translation, not an in-memory API. The numeric
generator now emits Unwrap on every wrapper's extensions class, and the
translator matches all five variants (the hand-written string one plus
the four generated numeric ones) by method definition.

Filter tests now query the DbContext directly — HTTP wasn't exercising
anything the translator cares about, and the scaffolding (controller filter
endpoints, protected DbSet, route helpers in the base class) is gone.
Numeric coverage added: one arithmetic predicate per wrapper shape through
Unwrap(), on both providers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Extract StrongTypes.EfCore package (closes #16), v0.3.0

EF Core value converters and the Unwrap LINQ translator now live in a
separate Kalicz.StrongTypes.EfCore NuGet package, shipped at 0.3.0. Core
library stays EF-free.

Consumer API splits in two clear touchpoints (same verb):

  protected override void ConfigureConventions(ModelConfigurationBuilder b)
      => b.UseStrongTypes();    // value converters, pre-convention phase

  services.AddDbContext<T>(o => o.UseSqlServer(...).UseStrongTypes());
                                // Unwrap translator plugin, DI phase

These can't be collapsed: converters have to be registered as pre-
conventions on ModelConfigurationBuilder (before property discovery, or
reference-typed wrappers get inferred as owned entity types), while the
translator plugin lives in EF's internal service provider and has to go
through IDbContextOptionsExtension.

Integration tests restructured by kind:

  Tests/
    ApiTests/           (HTTP round-trip tests, one per strong type)
      Numeric/
      Strings/
    ConverterTests/     (direct DbContext LINQ tests for Unwrap)
      Numeric/
      Strings/

HTTP route/body helpers moved from IntegrationTestBase into the
EntityTests base that actually uses them, so ConverterTests don't carry
an unused RoutePrefix.

Core StrongTypes package bumps to 0.3.0 with release notes covering the
generic numeric wrappers (source-generated in this cycle) and Unwrap().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add ST0001 analyzer nudging users to install StrongTypes.EfCore (#17)

Roslyn analyzer shipped inside Kalicz.StrongTypes under
analyzers/dotnet/cs/ — no separate package to reference.

Fires when:
- the project references Microsoft.EntityFrameworkCore
- it doesn't reference Kalicz.StrongTypes.EfCore
- a DbContext-derived class has a DbSet<T> whose entity T carries a
  StrongTypes wrapper property (NonEmptyString / Positive<T> / … )

That triple is the combination that otherwise blows up at model-build
time with a "no suitable constructor for NonEmptyString" error because
EF infers the wrapper as an owned entity type. The diagnostic points at
the DbSet property and links to the nuget.org listing.

Auto-install code fix deferred — that requires IDE-specific APIs
(NuGet.VisualStudio.IVsPackageInstaller or Rider equivalent) and is
tracked as the remaining checkbox on #17. IDEs' built-in "unresolved
symbol → install package" fix covers the common case once a user types
UseStrongTypes() or a converter type name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Collapse StrongTypes.EfCore to a single touchpoint + expand ST0001

## One-call registration

optionsBuilder.UseStrongTypes() now wires everything — value converters
AND the Unwrap translator — in one place. No ConfigureConventions
override needed on consumer DbContexts.

The convention trick: register an IEntityTypeAddedConvention at index 0
of the convention set. It runs before PropertyDiscoveryConvention
decides which members become scalar properties vs complex types. We
pre-declare each strong-type member as a scalar property with its
ValueConverter attached, so when property discovery walks the members
they already exist as scalars — EF never falls through to complex-type
inference and the "no suitable constructor for NonEmptyString" failure
can't happen.

Nullable<> wrappers (Positive<int>?) are unwrapped in the converter
lookup so numeric wrappers work in both mapped slots.

## Analyzer ST0001 now fires on three locations

Previously only the DbSet<T> declaration. Now we also mark:
- each strong-type property on the mapped entity (developer reading
  the entity definition sees the hint)
- the DbContext class itself (that's where UseStrongTypes() will be
  called once the package is installed)

## Auto-install code fix

AddEfCorePackageCodeFixProvider reads the project's csproj, adds
<PackageReference Include="Kalicz.StrongTypes.EfCore" Version="0.3.0" />
to an existing PackageReference ItemGroup when possible, and saves.
The IDE picks up the csproj change on its own.

File IO is intrinsic to the fix (PackageReference lives outside the
Roslyn document model), so RS1035 is suppressed at the two necessary
call sites with explanation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Cast Unwrap() to long in arithmetic predicates to prevent SQL overflow

Integration tests share one DB across the collection, so the Api suites
had already seeded int.MaxValue into PositiveIntEntities. SQL Server
doesn't short-circuit the ID-filter branch of the WHERE clause, so
CAST(Value AS int) * 2 ran across every row and overflowed before the
ID predicate narrowed the set. Casting Unwrap() to long forces the
arithmetic into bigint space, which can't overflow for any 32-bit seed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Updated solution structure

* Updated release notes

* Give StrongTypes.EfCore its own README

The package was shipping the repo-root readme, which talks about
StrongTypes-the-library and never mentions EF Core. Replace it with
an EfCore-focused guide: install, register via UseStrongTypes(), model
entities, and filter — including equality/ordering directly on wrappers,
Unwrap() for string operators and arithmetic, and EF.Functions.Like.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add tests for MissingEfCorePackageAnalyzer (ST0001)

Drives the analyzer directly through CompilationWithAnalyzers — no
Microsoft.CodeAnalysis.Testing dependency — so cases stay tight and
portable across Roslyn / xUnit versions. Covers:
 - fires when EF Core is referenced without Kalicz.StrongTypes.EfCore,
 - silent when the EfCore package is referenced (either assembly name),
 - silent when EF Core isn't referenced at all,
 - silent when the mapped entity has no StrongTypes wrapper properties,
 - detects nullable and generic numeric wrappers,
 - reports at all three location kinds (DbSet, entity property, DbContext),
 - reports each wrapper property on the entity exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add tests for AddEfCorePackageCodeFixProvider

The fix writes to the csproj on disk rather than producing a Solution
diff, so `CSharpCodeFixTest` can't observe it. Instead, materialize a
real csproj in a temp directory, point an `AdhocWorkspace` at it, invoke
the registered `CodeAction`, and inspect the XML afterwards.

Cases covered:
 - drops the PackageReference into an existing ItemGroup when one holds
   other PackageReferences (matches `dotnet add package` layout),
 - creates a new ItemGroup when none is suitable,
 - idempotent when the package is already referenced (no duplicate, byte-
   for-byte unchanged file),
 - case-insensitive match on the existing package id,
 - no-ops safely when the csproj path doesn't exist on disk,
 - advertises the expected diagnostic id, fix-all provider, and title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tighten analyzer + code fix tests

- Assert NotEmpty before Assert.All in the ST0001 fires-case (Assert.All
  is vacuously true on empty, so the order matters).
- New-ItemGroup test now asserts exactly one ItemGroup exists in the
  csproj after the fix runs (the original had none, so this proves a
  fresh ItemGroup was created instead of relying on the tautological
  PackageReference.Parent.Name == "ItemGroup" check).
- Same test now asserts the written Version matches the package
  constant. Promoted EfCorePackageVersion to public to keep the version
  a single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add AsPositive/AsNonNegative/AsNegative/AsNonPositive extensions

Mirrors AsNonEmpty() on string?: one generic extension per invariant
over INumber<T> so any numeric primitive can be wrapped fluently.
Surfaces the helpers in the README next to the existing factory snippet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add ToPositive/ToNonNegative/ToNegative/ToNonPositive throwing extensions

Parallel to the nullable-returning As* helpers: the To* variants delegate
to Create and throw ArgumentException on invariant violation. Same
relationship as TryCreate vs Create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add throwing To* string parsers; drop StringExtensions_Old

Mirrors the numeric As*/To* split: each As* helper on string? and
NonEmptyString now has a To* sibling that delegates to the framework's
Parse methods and throws on failure. ToNonEmpty throws ArgumentException
when the input is null/empty/whitespace.

StringExtensions_Old (and its two test files) is deleted — its surface
is fully replaced by the As*/To* pair on StringExtensions, and keeping
it would create name clashes with the new To* methods.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integration tests: SQL converter filtering and Unwrap for LINQ translations Extract EF Core converters into StrongTypes.EFCore package

1 participant