Skip to content

Release v0.5.0 — source-generated AOT accessors + CI hardening - #221

Merged
Chris-Wolfgang merged 53 commits into
mainfrom
vNext
Aug 8, 2026
Merged

Release v0.5.0 — source-generated AOT accessors + CI hardening#221
Chris-Wolfgang merged 53 commits into
mainfrom
vNext

Conversation

@Chris-Wolfgang

Copy link
Copy Markdown
Owner

v0.5.0

Headline — Native AOT support via source-generated accessors (ADR 0006, #95)

Mark a record [BulkCopyable] and the bundled source generator emits its property getters, enum→underlying converters, and a full type descriptor at compile time; the loader prefers these over the runtime Expression.Compile getter, keeping the marked type's hot path free of runtime IL emission (net5.0+). Opt-in and additive — unmarked types are unchanged, and the generator ships inside the existing package (no second NuGet). Reflection remains the fallback (quarantined behind [RequiresUnreferencedCode]), and a DescriptorConformanceTests suite asserts the generated map == the reflection map.

CI / quality hardening (thorough-review tier)

Release metadata

  • Version 0.4.0 → 0.5.0; AssemblyVersion/FileVersion 0.5.0 (per-minor pin, 0.x policy).
  • PublicAPI Unshipped → Shipped; ADR 0006 Accepted (supersedes 0004); CHANGELOG finalized.

Notes

Closes #220

Chris-Wolfgang and others added 30 commits July 16, 2026 21:50
Enables the SDK's package validation (Microsoft.DotNet.ApiCompat, integrated)
so `dotnet pack` compares the public surface against the last published release
and fails on a binary/source break a non-MAJOR bump forbids.

- EnablePackageValidation=true, PackageValidationBaselineVersion=0.4.0.
- Runs in release.yaml's existing Pack job — no new hand-rolled download/ApiCompat
  step needed (the SDK downloads the baseline package itself).
- Intentional MAJOR-release breaks recorded via
  -p:GenerateCompatibilitySuppressionFile=true (writes CompatibilitySuppressions.xml).

Verified: clean `dotnet pack` downloads the 0.4.0 baseline and passes (0 CP
breaks; vNext has no API change yet). Baseline must track the last PUBLISHED
release — bump it right after each release cut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the canonical SECURITY.md section (structure from Etl-DbClient #240 /
ETL-FixedWidth) with this repo's specifics: OIDC Trusted Publishing release
path, no fallback (OIDC identity Chris-Wolfgang/ETL-SqlBulkCopy), owner, leaf-
library downstream scope, and package coordinates for unlisting. No placeholders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/Wolfgang.Etl.SqlBulkCopy.Tests.DocExamples (net8.0) — a Roslyn-based
guard against XML-doc example rot. It walks up from the test binary directory
to locate src/ (avoids [CallerFilePath], which resolves to a deterministic
'/_/' path under CI), extracts every <example><code> block, un-escapes the XML
entities, wraps each in a synthetic harness (a Person record + an open
connection + an async source + a token), and compiles it with
Microsoft.CodeAnalysis.CSharp. A snippet that no longer compiles fails the test.

Single-TFM project on purpose — sidesteps the main suite's 13-TFM matrix (Roslyn
doesn't need net462). Runs in CI via the solution test on net8.0. Extend the
harness as new snippets introduce new contextual symbols.

Verified: passes on the current example; a negative test (injecting a call to a
non-existent method) correctly fails with 'does not compile' (CS1061).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…evalidation

build: API/ABI compatibility gate via PackageValidation (#88)
…-path

docs(security): Release path & compromise scope appendix (#103)
test: compile every XML-doc <example> snippet (#93)
Records the 0.5.0+ decision to move property-getter emission from runtime
Expression.Compile to a compile-time Roslyn source generator: keeps the
compiled-getter throughput while making the hot path Native-AOT-clean.
Generated metadata is the preferred provider, reflection remains the runtime
fallback, one package, rule contract single-sourced via conformance tests.
Corrects ADR 0004's inaccurate 'throws under AOT' claim (Expression.Compile
falls back to the interpreter). Tracked by #95.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(adr): 0006 source-generated property accessors (AOT)
Adds the runtime seam the source-generator pilot plugs into: a
GeneratedAccessorRegistry that generated code registers compile-time getters
into, and ColumnMap now prefers a registered generated getter over the runtime
Expression.Compile'd one (falling back to it when none is registered). No
generator yet and no packaging change — this is the preferred-provider /
reflection-fallback seam from ADR 0006, unit-tested by hand-registering a
sentinel getter.

The Register entry point is public (generated code runs in the consumer
assembly, no InternalsVisibleTo) and marked [EditorBrowsable(Never)] as infra.

Refs #95.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…seam

feat: source-generated accessor seam (ADR 0006, #95, PR-A)
Adds the incremental source generator that fills the PR-A seam. A type marked
[BulkCopyable] gets strongly-typed getters emitted at compile time and
registered with GeneratedAccessorRegistry from a module initializer, so its
bulk-copy hot path reads values with no runtime Expression.Compile — the
Native-AOT-clean path from ADR 0006. Reflection stays the fallback for every
un-marked or compile-time-invisible type.

- New src/Wolfgang.Etl.SqlBulkCopy.SourceGenerator (netstandard2.0, Roslyn
  4.11.0), packed as an analyzer asset INTO the existing nupkg
  (analyzers/dotnet/cs) — one package, no second NuGet.
- Public [BulkCopyable] opt-in attribute.
- Registration is emitted under #if NET5_0_OR_GREATER (module initializers);
  legacy TFMs compile it away and fall back to reflection — no polyfill,
  no collision.
- End-to-end proof: the generator is referenced as an analyzer by Tests.Unit;
  a [BulkCopyable] fixture's accessors are asserted registered + correct
  (net5.0+). net462 builds clean (guard compiles to nothing); pack verified to
  contain exactly one analyzer dll.

Known follow-up (PR-C): the enum-underlying converter is a second
Expression.Compile site not yet source-generated; conformance test + PublishAot
smoke also land in PR-C.

Refs #95. Docs: ADR 0006.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, PR-C)

Closes the second Expression.Compile site. The enum->underlying converter
(ColumnMap._enumConverter) now prefers a source-generated converter registered
via GeneratedAccessorRegistry.RegisterEnumConverter, falling back to
ReflectionHelpers.CompileEnumToUnderlyingConverter. The generator emits a
ConvertEnum_<T>(object)=>(object)(TUnderlying)(TEnum)boxed per distinct enum
(incl. nullable-enum) column on a [BulkCopyable] type. With this, a marked type
with enum columns has NO runtime Expression.Compile on its hot path.

Adds the ADR 0006 single-sourcing guard: a conformance test asserting the
generated getters AND enum converters produce identical results to the
reflection path across a corpus of [BulkCopyable] fixtures (int/string/enum/
byte-enum/nullable-enum) — so the generated fast path can't silently diverge
from the reflection fallback.

268 unit tests green (net10.0); net462 builds clean (conformance is net5.0+).

Refs #95. Docs: ADR 0006. PublishAot smoke follows as PR-D.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a console that publishes with <PublishAot>true</PublishAot> and reads
values through a [BulkCopyable] type's source-generated getters and enum
converter, plus a Linux CI job that publishes it natively and runs it.

The smoke reaches only TypeMap/ColumnMap (via InternalsVisibleTo), never
SqlBulkCopyLoader, so Microsoft.Data.SqlClient is trimmed out of the reachable
graph — keeping the AOT signal about THIS library's code, not SqlClient's
separate AOT story (ADR 0006). The CI job asserts the native publish emits no
IL2xxx/IL3xxx referencing Wolfgang.Etl.SqlBulkCopy and that the binary runs
(exit 0).

Verified locally: JIT run prints ok=True (exit 0); AOT publish reaches ILC with
zero IL warnings (SqlClient trimmed) — only the native linker is unavailable on
this Windows box (no C++ Desktop workload), which is why the native compile+run
runs on Linux CI.

Refs #95. Docs: ADR 0006. Completes the 3-part pilot (seam/generator/enum) with
the empirical AOT proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a 'Native AOT & trimming' section to the README (what [BulkCopyable] does,
opt-in/additive, one-package, net5.0+ caveat, and the SqlClient external-gate
note) plus a Features-table row, and a [Unreleased] CHANGELOG entry for the new
public surface (BulkCopyableAttribute + GeneratedAccessorRegistry).

Refs #95. Docs: ADR 0006.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SourceLink workflow built every src/**/*.csproj with -f net10.0, which
NETSDK1005-fails on the netstandard2.0-only source generator (Roslyn analyzers
can't target net10.0). Skip projects whose csproj has no net10.0 target — they
produce no net10.0 PDB to verify anyway.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat: source generator for compile-time accessors (ADR 0006, #95, PR-B)
…rmance

feat: source-generate enum converters + conformance test (ADR 0006, #95, PR-C)
test: Native AOT smoke consumer + CI (ADR 0006, #95, PR-D)
docs: document [BulkCopyable] Native AOT support (ADR 0006, #95)
Previously the Native AOT smoke only ran on PRs targeting main/vNext, so
source-generator work landing directly on vNext wasn't natively verified until
a release PR. Add a push trigger (same path filter) so vNext pushes exercise the
native publish+run. Opening this PR also fires the first real native run,
verifying the merged pilot end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci: run AOT smoke on pushes to vNext (+ first native verification)
… (ADR 0006, #95)

Option B for the AOT-mapping gap the smoke surfaced (IL2070 in TypeMap's
reflection). The generator now emits, per flat [BulkCopyable] type, a full
GeneratedTypeDescriptor (schema/table/column metadata) mirroring TypeMap's
rules; TypeMap.Create prefers it and builds the map reflection-free (delegates
from the PR-B/C accessor registry). The reflection path is quarantined behind
[RequiresUnreferencedCode] (+ netFx/netstandard polyfills) with a justified
suppression on Create, so a [BulkCopyable] type's mapping no longer reflects
over the type — clearing IL2070 under Native AOT.

Guarded by a descriptor conformance test: the generated map equals the
reflection map of a structurally-identical plain type across flat / enum /
[Table]+[Column] / per-load-override cases. 273 unit tests green; all 5 TFMs
build clean.

Types with nested-table properties still use the reflection path (recursive
nested descriptors are the next increment). Refs #95. Docs: ADR 0006.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ement 2)

Closes the nested-table gap. The generator now emits a GeneratedNestedTableDescriptor
per nested collection and includes it in the type descriptor — but only when the
WHOLE graph is [BulkCopyable] (recursive IsFullyGeneratable eligibility check;
cyclic or partly-unmarked graphs fall back to reflection, keeping AOT honest).
TypeMap.BuildFromDescriptor resolves each child via Create (its own descriptor),
and NestedTableMap gained a reflection-free ctor pulling the collection getter
from the accessor registry — so a fully-marked graph maps with zero reflection.

Conformance extended: generated nested tables (property, child table, child
columns) match the reflection map of a plain twin graph. The AOT smoke now
includes a nested [BulkCopyable] child, so the native run proves the nested
descriptor path is IL2070-clean too. 275 unit tests green; all 5 TFMs build clean.

Refs #95. Docs: ADR 0006.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/Wolfgang.Etl.SqlBulkCopy.Tests.Fuzz (net10.0, CsCheck 4.0.0) with two
mapping invariants asserted over large numbers of generated inputs:
- QualifiedTableName bracket-quoting round-trips for arbitrary schema/table
  identifiers (un-quoting recovers the original — catches any escaping bug that
  lets an identifier break out of its brackets).
- Every mapped column's getter returns exactly what reflection reads from the
  same property, across random record values (exercises the compiled /
  source-generated getter path).

Plus .github/workflows/fuzz.yaml (weekly cron + workflow_dispatch, CsCheck case
count from CsCheck_Iterations, reports uploaded as an artifact), mirroring the
ETL-DbClient fuzz pattern. Both properties hold at 20k local iterations.

Refs #81.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds pr-benchmarks.yaml (canonical fleet workflow, adapted): runs the DB-free
micro-benchmarks (PropertyGetter, SliceList) on the PR HEAD and on the
merge-base with the target branch, computes per-benchmark time+allocation
deltas, and posts a single PR comment that's replaced (not appended) on each
push. Informational gate for now; threshold-based failure is a follow-up once
runs have sized the normal noise. Uses the repo's current action pins.

Refs #106.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chris-Wolfgang and others added 19 commits August 5, 2026 09:03
Adds an attest-build-provenance job to release.yaml (canonical fleet pattern):
after publish, it downloads the packed nuget-packages artifact and attests the
.nupkg/.snupkg provenance via GitHub OIDC + Sigstore keyless signing — proving
they were built by this workflow at this commit. Requires NO code-signing cert.

SECURITY.md gains a consumer-side verification section (gh attestation verify),
and explicitly documents that NuGet author-signing (which needs a cert) is out
of scope — the SLSA attestation provides the equivalent build-integrity
guarantee. SBOM (CycloneDX) was already emitted by release.yaml.

Refs #90 (signing sub-task intentionally deferred; needs a cert).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/Wolfgang.Etl.SqlBulkCopy.Tests.Concurrency (net10.0, Microsoft.Coyote
1.7.11) driving TypeMap's ConcurrentDictionary cache through Coyote's
TestingEngine: under any interleaving of N concurrent TypeMap.Create callers for
the same type, every caller must receive the SAME cached instance (GetOrAdd
race-freedom). Runs at 100 iterations per-PR (Category=Concurrency) and 10,000
on the weekly concurrency.yaml + workflow_dispatch (COYOTE_ITERATIONS).

Coyote explores schedules in-process, so no SQL/Docker is needed for the cache
races; the realistic sustained-load angle is covered by gc-profile.yaml (#94).
Passes locally at 3,000 iterations (0 bugs).

Refs #89 (24h self-hosted soak intentionally dropped, matching the fleet's
canonical Coyote-only approach).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…log line

- Gate the aot-consumer job on needs.pack-and-validate.outputs.has-packages so it
  is skipped (not failed) in repos with no packages, matching publish-nuget.
- Replace the always-zero "exited $?" line (set -e aborts on failure) with a
  static success message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Chris-Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com>
Address review feedback on BulkCopyAccessorGenerator.cs: the repeated
IsStatic/IsIndexer/GetMethod-null(/[NotMapped]) or-chains are factored into
intention-revealing predicates, mirroring TypeMap.IsReadableInstanceProperty on
the reflection path so both providers filter identically:

- IsReadableInstanceProperty  — instance property with a getter
- IsMappableProperty          — readable + not [NotMapped] (was duplicated verbatim)
- IsAccessorEmittableProperty — readable + non-ref-like + reachable getter

Each of the four call sites collapses to one named line. Behaviour is unchanged
(DescriptorConformanceTests: generated map == reflection map, 276 tests green).

Also reflow the two .Add(string.Join(...)) blocks to the Allman multi-line
argument style (opening paren on its own line) per review feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Raise the four Option-B descriptor types to 100% line coverage so the
per-module 90% gate passes at the vNext->main release:

- ColumnMap: descriptor ctor throws when no generated getter is registered,
  and when an enum column type has no registered converter.
- NestedTableMap: descriptor ctor throws when no generated getter is registered.
- GeneratedAccessorRegistry.RegisterEnumConverter / GeneratedTypeMapRegistry.Register:
  ArgumentNullException guards.

Adds TestModels/UnregisteredProbeEnum (never [BulkCopyable], never registered)
to drive the enum-converter defensive branch. +7 tests (283 total, all green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Version 0.4.0 -> 0.5.0 (Version / AssemblyVersion / FileVersion; AssemblyVersion
  tracks the minor in 0.x per the netfx binding policy).
- Move the source-gen AOT surface from PublicAPI.Unshipped -> Shipped
  (BulkCopyableAttribute, GeneratedAccessorRegistry, and the descriptor types).
- Finalize CHANGELOG [0.5.0]: AOT source-generated accessors + reflection-free
  descriptors (Added); SLSA build-provenance attestation on release (Security).
- ADR 0006 Proposed -> Accepted (0.5.0); ADR 0004 -> Superseded by 0006.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat: source-generate type descriptors; quarantine reflection for AOT (ADR 0006, #95)
test: continuous property-based fuzz via CsCheck (#81)
ci: per-PR benchmark delta vs merge-base (#106)
…ifferential

ci: cross-platform / multi-arch differential (#91)
ci: sustained-load GC / allocation profiling via Docker SQL (#94)
ci(release): gate NuGet publish on the Native AOT smoke
ci: SLSA build-provenance attestation on release (#90, minus signing)
- pr-benchmarks.yaml: match BenchmarkDotNet's *-report-full-compressed.json
  output (was *-report.json → empty delta); full base-branch fetch instead of
  --depth=100 (could miss merge-base on long PRs); --paginate the marker-comment
  lookup so it updates instead of duplicating past page 1.
- gc-profile.yaml: fail the job on a workload crash (after stopping counters)
  instead of swallowing the exit code, so the scheduled signal is reliable.
- fuzz.yaml, aot-smoke.yaml: pin actions/checkout to the repo-standard SHA
  (3d3c42e) for supply-chain consistency.

Workflow-only; no library/test/package change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`dotnet test ... | tee reports/*.log` reported tee's zero exit, masking a
failing fuzz/Coyote run and leaving the scheduled workflow green. Add
`set -eo pipefail` so a test failure propagates through the pipe and fails
the step. (aot-smoke.yaml and release.yaml already set pipefail on their tee
pipes; no other `| tee` needs it.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 8, 2026 13:12
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR benchmark delta

No benchmarks ran on either side.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prepares the v0.5.0 release by adding Native AOT / trimming support to the type-mapping hot path via bundled source generation, and by hardening the repo’s quality/CI posture with new test suites (AOT smoke, doc-example compilation, fuzz, and concurrency) plus updated security and release documentation.

Changes:

  • Introduces [BulkCopyable] plus a bundled Roslyn incremental source generator that registers property getters, enum converters, and (when eligible) full type descriptors to avoid runtime IL emission/reflection on net5.0+.
  • Updates runtime mapping to prefer generated descriptors/accessors, and adds conformance + end-to-end tests (descriptor parity, generator registration, fuzz, Coyote concurrency, Native AOT smoke, XML-doc snippet compilation).
  • Bumps package version to 0.5.0, updates PublicAPI tracking + ADRs + README/SECURITY/CHANGELOG.

Reviewed changes

Copilot reviewed 49 out of 49 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tools/GcProfileWorkload/Widget.cs Adds workload entity type used by GC profiling harness.
tools/GcProfileWorkload/Program.cs Adds sustained-load SQL Server/Testcontainers workload driver for GC profiling.
tools/GcProfileWorkload/GcProfileWorkload.csproj Adds standalone workload project referencing runtime + Testcontainers/SqlClient.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/Wolfgang.Etl.SqlBulkCopy.Tests.Unit.csproj References source generator as analyzer for end-to-end generator testing.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/TestModels/UnregisteredProbeEnum.cs Adds enum fixture to exercise defensive “no generated converter” branch.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/TestModels/GeneratedAccessorProbeRecord.cs Adds isolated model for registry tests to avoid cross-test leakage.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/TestModels/DescriptorConformanceFixtures.cs Adds paired BulkCopyable/plain fixtures for descriptor-vs-reflection parity tests.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/TestModels/BulkCopyableFixture.cs Adds simple [BulkCopyable] fixture for generator registration tests.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/TestModels/BulkCopyableEnumFixture.cs Adds enum-bearing [BulkCopyable] fixture for enum converter coverage.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/NestedTableMapTests.cs Adds negative test for descriptor ctor when getter is missing.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/GeneratedTypeMapRegistryTests.cs Adds argument-null tests for generated type map registry.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/GeneratedAccessorRegistryTests.cs Adds tests verifying generated accessor preference + null-guard behavior.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/DescriptorConformanceTests.cs Adds tests asserting generated descriptors match reflection-built maps.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/ColumnMapTests.cs Adds negative tests for missing generated getter/enum converter in descriptor ctor.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/BulkCopyAccessorGeneratorTests.cs Adds end-to-end generator registration assertions for [BulkCopyable] fixtures.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Unit/BulkCopyAccessorConformanceTests.cs Adds conformance checks: generated getters/converters match reflection.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Fuzz/Wolfgang.Etl.SqlBulkCopy.Tests.Fuzz.csproj Adds CsCheck-driven fuzz test project (net10.0).
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Fuzz/MappingFuzz.cs Adds property-based fuzz over qualified table quoting + getter correctness.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Fuzz/FuzzRecord.cs Adds fixed record shape used by fuzz properties.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.DocExamples/Wolfgang.Etl.SqlBulkCopy.Tests.DocExamples.csproj Adds Roslyn-based doc-example compilation test project (net8.0).
tests/Wolfgang.Etl.SqlBulkCopy.Tests.DocExamples/DocExampleTests.cs Extracts and compiles <example><code> snippets to prevent doc rot.
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Concurrency/Wolfgang.Etl.SqlBulkCopy.Tests.Concurrency.csproj Adds Coyote systematic concurrency test project (net10.0).
tests/Wolfgang.Etl.SqlBulkCopy.Tests.Concurrency/TypeMapCacheConcurrencyTests.cs Adds Coyote-driven cache race test for TypeMap.Create caching invariants.
tests/Wolfgang.Etl.SqlBulkCopy.AotSmoke/Wolfgang.Etl.SqlBulkCopy.AotSmoke.csproj Adds Native AOT publishable smoke consumer project (net10.0).
tests/Wolfgang.Etl.SqlBulkCopy.AotSmoke/Program.cs Implements AOT smoke program exercising generated getters, enum converter, nested descriptor.
src/Wolfgang.Etl.SqlBulkCopy/Wolfgang.Etl.SqlBulkCopy.csproj Bumps version to 0.5.0; enables package validation; embeds generator into package.
src/Wolfgang.Etl.SqlBulkCopy/TypeMap.cs Prefers generated type descriptors; annotates reflection path for trimming/AOT guidance.
src/Wolfgang.Etl.SqlBulkCopy/PublicAPI.Shipped.txt Ships new public surface for generator/descriptor infrastructure + attribute.
src/Wolfgang.Etl.SqlBulkCopy/Properties/AssemblyInfo.cs Extends InternalsVisibleTo for new test/tool assemblies.
src/Wolfgang.Etl.SqlBulkCopy/Polyfills/UnconditionalSuppressMessageAttribute.cs Adds polyfill for trim-warning suppression on pre-net5 targets.
src/Wolfgang.Etl.SqlBulkCopy/Polyfills/RequiresUnreferencedCodeAttribute.cs Adds polyfill for trimming annotation on pre-net5 targets.
src/Wolfgang.Etl.SqlBulkCopy/NestedTableMap.cs Adds descriptor-based ctor using generated getters; refactors enumeration logic.
src/Wolfgang.Etl.SqlBulkCopy/GeneratedTypeMapRegistry.cs Adds registry for generated type descriptors (public register, internal query).
src/Wolfgang.Etl.SqlBulkCopy/GeneratedTypeDescriptor.cs Adds descriptor model for schema/table/columns/nested tables.
src/Wolfgang.Etl.SqlBulkCopy/GeneratedNestedTableDescriptor.cs Adds nested-table descriptor model.
src/Wolfgang.Etl.SqlBulkCopy/GeneratedColumnDescriptor.cs Adds column descriptor model.
src/Wolfgang.Etl.SqlBulkCopy/GeneratedAccessorRegistry.cs Adds registry for generated getters + enum converters (public register, internal query).
src/Wolfgang.Etl.SqlBulkCopy/ColumnMap.cs Prefers generated getters/converters; adds descriptor-based ctor.
src/Wolfgang.Etl.SqlBulkCopy/BulkCopyableAttribute.cs Adds opt-in attribute driving generation.
src/Wolfgang.Etl.SqlBulkCopy.SourceGenerator/Wolfgang.Etl.SqlBulkCopy.SourceGenerator.csproj Adds netstandard2.0 incremental source generator project.
src/Wolfgang.Etl.SqlBulkCopy.SourceGenerator/Polyfills/IsExternalInit.cs Adds IsExternalInit polyfill for records/init in netstandard2.0 generator.
src/Wolfgang.Etl.SqlBulkCopy.SourceGenerator/BulkCopyAccessorGenerator.cs Implements generator: emits getters, enum converters, optional descriptors + module initializer.
SECURITY.md Documents supply-chain verification (SBOM + SLSA provenance verification steps).
README.md Documents Native AOT/trimming guidance and [BulkCopyable] usage.
ETL-SqlBulkCopy.slnx Adds new generator + test projects to solution.
docs/adr/index.md Adds ADR 0006 entry and marks 0004 as superseded.
docs/adr/0006-source-generated-property-accessors.md Adds ADR describing the new source-generated AOT approach.
docs/adr/0004-compiled-property-getters-and-native-aot.md Marks ADR 0004 as superseded by ADR 0006.
CHANGELOG.md Adds v0.5.0 release notes documenting generator/AOT and SLSA additions.
Suppressed comments (1)

src/Wolfgang.Etl.SqlBulkCopy.SourceGenerator/BulkCopyAccessorGenerator.cs:152

  • EncodeDescriptor's XML doc lists "nested-table properties" as making a type ineligible for descriptor generation, but the implementation supports nested-table descriptors (and only rejects nested graphs when they are not fully generatable). This mismatch makes it harder to understand when a descriptor will be emitted.
    /// Encodes the type's generated descriptor, or returns an empty string when
    /// the type is not eligible for descriptor generation in this pass (has
    /// nested-table properties, inherits mapped properties, has no mappable
    /// columns, has duplicate column names, or is <c>[NotMapped]</c>). Those
    /// types fall back to the reflection path, which produces the identical map.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Wolfgang.Etl.SqlBulkCopy/ColumnMap.cs
Comment thread tools/GcProfileWorkload/Program.cs
@Chris-Wolfgang

Copy link
Copy Markdown
Owner Author

All four review findings are fixed as a stacked series on vNext (merge these before this release PR):

Resolving these threads; the fixes land in this PR once #222#225 merge into vNext.

@Chris-Wolfgang
Chris-Wolfgang merged commit 94ad09f into main Aug 8, 2026
19 checks passed
@Chris-Wolfgang
Chris-Wolfgang deleted the vNext branch August 8, 2026 15:48
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.

ci: harden thorough-review workflows (pr-benchmarks glob, gc-profile exit code, checkout pin)

3 participants