v0.3.0: EF Core package, Unwrap filtering, ST0001 analyzer, numeric As* helpers#27
Merged
v0.3.0: EF Core package, Unwrap filtering, ST0001 analyzer, numeric As* helpers#27
Conversation
…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>
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>
# Conflicts: # StrongTypes.slnx
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>
…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
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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch accumulated several related pieces that ship together as v0.3.0:
NonEmptyStringviaUnwrap()(closes Integration tests: SQL converter filtering and Unwrap for LINQ translations #21) — addsNonEmptyString.Unwrap()plus an EF CoreIMethodCallTranslatorplugin that rewrites it as a pass-through to the underlying string column, re-typed with a plainstringmapping so downstream operators (Contains,StartsWith,EndsWith,EF.Functions.Like) translate to server-side SQL on both providers. Extended to every strong type, not justNonEmptyString.StrongTypes.EfCorepackage extraction (closes Extract EF Core converters into StrongTypes.EFCore package #16) — EF-Core integration now ships as a separateKalicz.StrongTypes.EfCoreNuGet package with its own README, collapsed to a singleUseStrongTypes()touchpoint.ST0001analyzer + 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 theStrongTypes.EfCorepackage installed, with a one-click "Add package" code fix. Comes with a full analyzer test suite.As*helpers —AsPositive(),AsNonNegative(),AsNegative(),AsNonPositive()extensions on anyINumber<T>, mirroringAsNonEmpty()onstring?. Surfaced in the root README.Why the Unwrap machinery
A naive
DbFunctionpass-through (HasDbFunction(...).HasTranslation(args => args[0])) fails for string operators becauseargs[0]is the column expression with itsNonEmptyStringValueConvertermapping still attached. Downstream translators pipe their string literals through that converter at SQL-parameter bind time and throwInvalidCastException. The plugin strips the mapping by converting tostringwith a freshRelationalTypeMapping.Arithmetic predicates that fold into SQL get an
(long)cast onUnwrap()to avoid server-side overflow onintcolumns.Test plan
dotnet test src/StrongTypes.Tests— all tests pass, including the newNumberExtensionsTestsproperty coverage for the numericAs*helpers and theNonEmptyStringUnwrapTests.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