DMS-1264: HTTP If-None-Match support (conditional GET 304 + write create-guard)#1095
DMS-1264: HTTP If-None-Match support (conditional GET 304 + write create-guard)#1095stephenfuqua wants to merge 20 commits into
Conversation
ReviewFindings
Simplicity Notes
Open Question Jira says |
|
If it's intentional to have no entity-tag list support, then the ticket/design needs updating. |
There was a problem hiding this comment.
Pull request overview
Adds full If-None-Match support to the DMS HTTP pipeline: conditional GET now returns 304 Not Modified with ETag when appropriate, and writes honor If-None-Match as a create-guard (including wildcard * semantics and precedence vs If-Match). This builds on the existing ContentVersion-derived ETag work and extends behavior consistently across relational and descriptor write paths, including the deferred-authorization branch.
Changes:
- Implement conditional GET for GET-by-id (
If-None-Match→304+ETag, no body) using full served-tag comparison. - Implement write-side create-guard semantics for
If-None-Match(wildcard and specific tag) across relational + descriptor paths, with centralized evaluation logic. - Add unit/integration/E2E coverage for read + write behavior across PostgreSQL and SQL Server, including deferred-authorization scenarios.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/dms/core/EdFi.DataManagementService.Core/Utilities/EtagValue.cs | Adds parsing helper for If-None-Match (weak W/ + quoted/unquoted tolerance). |
| src/dms/core/EdFi.DataManagementService.Core/Handler/GetByIdHandler.cs | Emits ETag on 200 and short-circuits to 304 on satisfied If-None-Match. |
| src/dms/core/EdFi.DataManagementService.Core/Backend/WritePreconditionFactory.cs | Adds If-None-Match precondition parsing with If-Match precedence. |
| src/dms/core/EdFi.DataManagementService.Core.External/Backend/WritePrecondition.cs | Introduces WritePrecondition.IfNoneMatch record. |
| src/dms/backend/EdFi.DataManagementService.Backend/Etag/EtagPreconditionEvaluator.cs | Centralizes proceed-vs-412 evaluation for If-Match and If-None-Match. |
| src/dms/backend/EdFi.DataManagementService.Backend/RelationalWriteExecutionStateResolver.cs | Broadens precondition gating to include If-None-Match (incl. deferred path safety). |
| src/dms/backend/EdFi.DataManagementService.Backend/RelationalCurrentEtagPreconditionChecker.cs | Generalizes checker to accept WritePrecondition and returns IsSatisfied. |
| src/dms/backend/EdFi.DataManagementService.Backend/DefaultRelationalWriteExecutor.cs | Ensures If-None-Match participates in before/deferred precondition flow and CreateNew behavior. |
| src/dms/backend/EdFi.DataManagementService.Backend/DescriptorWriteHandler.cs | Adds If-None-Match parity for descriptor POST/PUT (and explicitly ignores on DELETE). |
| src/dms/core/EdFi.DataManagementService.Core.Tests.Unit/Utilities/EtagValueTests.cs | Unit tests for conditional-tag parsing (quoted/unquoted/weak/wildcard). |
| src/dms/core/EdFi.DataManagementService.Core.Tests.Unit/Handler/GetByIdHandlerTests.cs | Unit tests for 200 + ETag and conditional-GET 304 behavior variants. |
| src/dms/core/EdFi.DataManagementService.Core.Tests.Unit/Backend/WritePreconditionFactoryTests.cs | Unit tests for If-None-Match parsing and precedence vs If-Match. |
| src/dms/frontend/EdFi.DataManagementService.Frontend.AspNetCore.Tests.Unit/AspNetCoreFrontendResponseHeaderTests.cs | Verifies frontend mapping for 304 + quoted ETag and no body writes. |
| src/dms/backend/EdFi.DataManagementService.Backend.Tests.Unit/Etag/EtagPreconditionEvaluatorTests.cs | Unit tests for If-Match/If-None-Match satisfaction polarity and null-etag paths. |
| src/dms/backend/EdFi.DataManagementService.Backend.Tests.Unit/RelationalCurrentEtagPreconditionCheckerTests.cs | Extends checker tests to cover If-None-Match satisfied/unsatisfied conditions. |
| src/dms/backend/EdFi.DataManagementService.Backend.Tests.Unit/RelationalWriteExecutorResultsTests.cs | Locks behavior for stale guarded no-op outcomes under If-None-Match. |
| src/dms/backend/EdFi.DataManagementService.Backend.Tests.Unit/DefaultRelationalWriteExecutorTests.cs | Adds execution-path tests for If-None-Match across create/update/missing/deferred cases. |
| src/dms/backend/EdFi.DataManagementService.Backend.Tests.Unit/DescriptorWriteHandlerPreconditionTests.cs | Adds descriptor parity tests for If-None-Match create-guard and missing-target behavior. |
| src/dms/backend/EdFi.DataManagementService.Backend.Tests.Unit/DescriptorWriteHandlerNamespaceAuthorizationTests.cs | Ensures proposed-namespace authorization still precedes insert on If-None-Match create path. |
| src/dms/backend/EdFi.DataManagementService.Backend.Tests.Unit/RelationalDocumentStoreRepositoryTests.cs | Confirms DELETE ignores If-None-Match. |
| src/dms/backend/EdFi.DataManagementService.Backend.Postgresql.Tests.Integration/PostgresqlRelationalWriteAuthoritativeSampleStudentSchoolAssociationSmokeTests.cs | Updates assertions for renamed precondition result flag (IsSatisfied). |
| src/dms/backend/EdFi.DataManagementService.Backend.Mssql.Tests.Integration/MssqlRelationalWriteAuthoritativeSampleStudentSchoolAssociationSmokeTests.cs | Updates assertions for renamed precondition result flag (IsSatisfied). |
| src/dms/tests/EdFi.DataManagementService.Tests.Integration/Scenarios/ConditionalGetIfNoneMatchScenario.cs | Integration scenario coverage for conditional GET behaviors. |
| src/dms/tests/EdFi.DataManagementService.Tests.Integration/Scenarios/WriteCreateGuardIfNoneMatchScenario.cs | Integration scenario coverage for write create-guard behaviors (incl. deferred auth path). |
| src/dms/tests/EdFi.DataManagementService.Tests.Integration/Tests/Postgresql/Given_Postgresql_ConditionalGetIfNoneMatch.cs | Wires PostgreSQL integration tests to conditional GET scenarios. |
| src/dms/tests/EdFi.DataManagementService.Tests.Integration/Tests/Postgresql/Given_Postgresql_WriteCreateGuardIfNoneMatch.cs | Wires PostgreSQL integration tests to write create-guard scenarios (incl. deferred auth). |
| src/dms/tests/EdFi.DataManagementService.Tests.Integration/Tests/Mssql/Given_Mssql_ConditionalGetIfNoneMatch.cs | Wires SQL Server integration tests to conditional GET scenarios. |
| src/dms/tests/EdFi.DataManagementService.Tests.Integration/Tests/Mssql/Given_Mssql_WriteCreateGuardIfNoneMatch.cs | Wires SQL Server integration tests to write create-guard scenarios (incl. deferred auth). |
| src/dms/tests/EdFi.DataManagementService.Tests.E2E/StepDefinitions/StepDefinitions.cs | Adds E2E steps to send GET/PUT with If-None-Match. |
| src/dms/tests/EdFi.DataManagementService.Tests.E2E/Features/Resources/etag.feature | Adds E2E scenarios for conditional GET and write create-guard. |
| reference/adr-etag-from-content-version.md | Documents the If-None-Match amendment and related RFC updates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Although the ODS/API does not support the entity-tag list, this is a good opportunity for DMS to get it right. Good call out. |
65f2c45 to
0bc8bd1
Compare
…ility finding Add "Amendment (2026-07-06): If-None-Match support" specifying full RFC 9110 If-None-Match handling: conditional GET returning 304 (full served-tag, weak comparison) and a write create-guard returning 412 (state-significant projection), with the inverted wildcard, If-Match precedence, and DELETE / If-Modified-Since / If-Unmodified-Since / If-Range out of scope. Record the ODS-6853 finding: legacy ODS/API returns 304 only for an unquoted If-None-Match, making DMS unquoted-acceptance a compatibility requirement. Marked Proposed pending team sign-off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The If-None-Match amendment's "Scope of the code change" described the read path recomposing the served tag via EtagComposer; the shipped handler instead echoes the already-composed body _etag (functionally identical, cannot drift from read materialization). Note the intentional divergence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…valuator Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… gates Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on-matching retries) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds API-level scenarios for the write-side If-None-Match create-guard: wildcard/specific-tag PUT and POST-upsert against existing targets (412), wildcard POST-insert (201), wildcard PUT to a missing target (404), and If-Match-governs-when-both-present. Also adds a deferred-authorization integration scenario (RelationshipsWithEdOrgsOnly Update boundary) proving the create-guard still 412s on the post-proposed-authorization branch, matching the existing backend unit regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…g ADR Document that If-None-Match accepts a comma-separated list of entity-tags (RFC 9110 13.1.2), matching if any tag matches, and clarify the weak comparison function (a W/-prefixed tag is compared by opaque value only). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement RFC 9110 13.1.2 entity-tag list support for the If-None-Match header. The header may now carry a comma-separated list of entity-tags (e.g. "t1", "t2", W/"t3"); the precondition triggers when ANY tag matches: 304 on conditional GET, 412 on the write create-guard. Per-tag weak comparison (RFC 9110 2.1) treats W/"1" the same as stored "1". A bare * remains the wildcard only as the sole value. Production changes: - WritePrecondition.IfNoneMatch now holds an ordered list of values with ordinal SequenceEqual equality plus a single-value convenience ctor. - EtagValue.ParseConditionalTagList splits and normalizes each list element (W/ strip, quote strip, unquoted tolerance). - WritePreconditionFactory builds the list precondition; If-Match precedence and bare-* wildcard detection preserved. - EtagPreconditionEvaluator: write is unsatisfied when any tag's projection matches. - GetByIdHandler: conditional GET returns 304 when any listed tag equals the full served etag. Tests: - Unit coverage for list parsing, weak-in-list, quoted/unquoted mix, whitespace/empty handling, and evaluator any-match cases. - PostgreSQL + SQL Server integration scenarios for multi-tag conditional GET and write create-guard, wired into all fixtures. - Fixed two pre-existing test compile errors (missing ComposedWriteResultEtag argument; IsMatch -> IsSatisfied) so the backend unit test project builds. Documents the behavior recorded in reference/adr-etag-from-content-version.md (2026-07-06 amendment). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Implements HTTP
If-None-Matchsupport (DMS-1264), building on the etag→ContentVersionwork (DMS-1252). Two parts:ETagresponse header and returns304 Not Modifiedwhen the client'sIf-None-Matchmatches the current representation. Comparison is against the full served tag (representation-sensitive: a different profile / format / links mode does not match — deliberately distinct from the write-side projection). Weak (W/) and unquoted values are accepted; a bare*returns304when the resource exists.If-None-Match: *is insert-only — a POST resolving to an insert proceeds; a POST resolving to an existing document, or a PUT against an existing resource, returns412; a PUT to a missing target returns404(not412). A specific tag returns412when it matches the current representation and proceeds otherwise (state-significant projection).If-Matchtakes precedence when both headers are present. Honored on both the relational and descriptor paths, on both the before-authorization and deferred-post-authorization routes;DELETEignoresIf-None-Match.Behavior is specified in the ADR amendment "Amendment (2026-07-06): If-None-Match support" (
reference/adr-etag-from-content-version.md) and reflects the ODS-6853 legacy finding: legacy ODS/API returns304only for an unquotedIf-None-Match, so unquoted acceptance is a compatibility requirement (the server still emits quoted strong ETags).Base branch
Targets
etag-content-version, notmain, so reviewers can merge the base etag work with or without thisIf-None-Matchfeature.Test plan
304(10) + write create-guard (14), including the deferred-authorization write path on both enginesOut of scope
If-Modified-Since,If-Unmodified-Since, andIf-Rangeremain unimplemented (date-based validators are deferred in favor of the strongContentVersion-derived entity tag;If-Rangeneeds HTTP range support the DMS does not provide).AI-use disclosure
This feature was implemented and reviewed with substantial AI assistance (Claude Code). Each task passed a two-stage (spec-compliance + code-quality) review, plus a whole-feature final review for each of the read and write parts. Human review is required before merge; accountability for the final result rests with the development team.
🤖 Generated with Claude Code