Skip to content

Remove server-side byte-range emission surface#522

Merged
xavierjohn merged 6 commits into
mainfrom
byte-range-removal
May 23, 2026
Merged

Remove server-side byte-range emission surface#522
xavierjohn merged 6 commits into
mainfrom
byte-range-removal

Conversation

@xavierjohn
Copy link
Copy Markdown
Owner

Issue

Trellis is positioned for general business web services. The server-side byte-range emission surface (PartialContent*, RangeRequestEvaluator, RangeOutcome, HttpResponseOptionsBuilder<T>.WithRange, .WithAcceptRanges, RepresentationMetadata.AcceptRanges) duplicates Microsoft.AspNetCore.Http.Results.File(enableRangeProcessing: true) and adds no Trellis-specific value. Consumers who need byte-range responses should call ASP.NET Core directly.

Fix

Delete the server-side range-evaluation / 206 helpers and their tests, docs, and examples. The client-side typed-error vocabulary stays — HttpError.RangeNotSatisfiable continues to surface inbound 416s, and ResponseFailureWriter still writes a 416 + Content-Range: bytes */N companion header when such a fault propagates through a Result chain.

Breaking API removals

  • PartialContentHttpResult, PartialContentResult, RangeRequestEvaluator, RangeOutcome
  • HttpResponseOptionsBuilder<T>.WithRange(...) (both overloads), .WithAcceptRanges(string)
  • HttpResponseOptions<T>.RangeSelector / .StaticRange / .AcceptRanges
  • RepresentationMetadata.AcceptRanges / Builder.SetAcceptRanges(string)
  • The Status206PartialContent ProducesResponseTypeMetadata entry on TrellisHttpResult<TDomain, TBody>

Migration. For binary downloads, call ASP.NET Core's Results.File(stream, enableRangeProcessing: true) directly. For advisory headers (e.g. Accept-Ranges: none), set the header on HttpContext.Response.Headers from middleware or the endpoint handler.

Trellis serves general business web services, not media servers. The
server-side byte-range emission helpers (PartialContentHttpResult,
PartialContentResult, RangeRequestEvaluator, RangeOutcome,
HttpResponseOptionsBuilder<T>.WithRange / .WithAcceptRanges,
RepresentationMetadata.AcceptRanges) duplicate
Microsoft.AspNetCore.Http.Results.File(enableRangeProcessing: true)
and add no Trellis-specific value.

Consumers who need byte-range responses should call ASP.NET Core
directly. The client-side typed-error vocabulary
HttpError.RangeNotSatisfiable, the 416 incoming mapping in
HttpResponseExtensions, and the Content-Range companion-header
emission in ResponseFailureWriter remain so inbound 416 responses
still round-trip through Result chains.

Breaking API removals: PartialContentHttpResult, PartialContentResult,
RangeRequestEvaluator, RangeOutcome, HttpResponseOptionsBuilder<T>.WithRange,
HttpResponseOptionsBuilder<T>.WithAcceptRanges, HttpResponseOptions<T>.RangeSelector,
HttpResponseOptions<T>.StaticRange, HttpResponseOptions<T>.AcceptRanges,
RepresentationMetadata.AcceptRanges, RepresentationMetadata.Builder.SetAcceptRanges,
the Status206PartialContent ProducesResponseTypeMetadata entry on
TrellisHttpResult<TDomain, TBody>.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 23, 2026

Test Results

6 100 tests   - 29   6 085 ✅  - 29   5m 1s ⏱️ -43s
   23 suites ± 0      15 💤 ± 0 
   23 files   ± 0       0 ❌ ± 0 

Results for commit 9c45782. ± Comparison against base commit 65ec76f.

This pull request removes 30 and adds 1 tests. Note that renamed tests count towards both.
Trellis.Asp.Tests.HttpPartialContentResultTest ‑ InitializesStatusCodeAndValue(value: "Test string")
Trellis.Asp.Tests.HttpPartialContentResultTest ‑ InitializesStatusCodeAndValue(value: Person { Id = 274, Name = "George" })
Trellis.Asp.Tests.HttpPartialContentResultTest ‑ InitializesStatusCodeAndValue(value: null)
Trellis.Asp.Tests.HttpPartialContentResultTest ‑ SetContentRangeHeader(rangeStart: 0, rangeEnd: 3, totalLength: 10, expected: "items 0-3/10")
Trellis.Asp.Tests.HttpPartialContentResultTest ‑ SetContentRangeHeader(rangeStart: 2, rangeEnd: 5, totalLength: null, expected: "items 2-5/*")
Trellis.Asp.Tests.HttpResponseOptionsBuilderTests ‑ Both_WithRange_overloads_are_chainable
Trellis.Asp.Tests.HttpResponseOptionsBuilderTests ‑ WithRange_selector_throws_on_null
Trellis.Asp.Tests.RangeRequestEvaluatorTests ‑ Evaluate_MultipleRanges_ReturnsFull
Trellis.Asp.Tests.RangeRequestEvaluatorTests ‑ Evaluate_NoRangeHeader_ReturnsFull
Trellis.Asp.Tests.RangeRequestEvaluatorTests ‑ Evaluate_NonBytesUnit_ReturnsFull
…
Trellis.Asp.Tests.TrellisHttpResultExtraTests ‑ Response_includes_LastModified_ContentLanguage_and_ContentLocation_headers

♻️ This comment has been updated with latest results.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.17%. Comparing base (65ec76f) to head (9c45782).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #522      +/-   ##
==========================================
+ Coverage   86.16%   86.17%   +0.01%     
==========================================
  Files         322      319       -3     
  Lines       13271    13145     -126     
  Branches     2845     2820      -25     
==========================================
- Hits        11435    11328     -107     
+ Misses       1210     1195      -15     
+ Partials      626      622       -4     
Files with missing lines Coverage Δ
...lis.Asp/src/Response/HttpResponseOptionsBuilder.cs 95.12% <ø> (-0.44%) ⬇️
Trellis.Asp/src/Response/TrellisHttpResult.cs 93.29% <ø> (+0.47%) ⬆️
...llis.Asp/src/Response/TrellisWriteOutcomeResult.cs 98.07% <ø> (-0.08%) ⬇️
Trellis.Core/src/Pagination/Page.cs 100.00% <ø> (ø)
...is.Http.Abstractions/src/RepresentationMetadata.cs 95.83% <100.00%> (-0.47%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Issue: The CHANGELOG had no entry for the breaking removal of the
server-side byte-range emission surface, and three articles still
listed 206 in positive descriptions of Trellis-emitted status codes
(Cache-Control table row, ProducesResponseType union, VaryForActor
cache list).

Fix: Add an Unreleased breaking-change entry listing the removed
public API, the migration to Results.File(enableRangeProcessing: true)
and direct HttpContext.Response.Headers writes, and the preserved
client-side HttpError.RangeNotSatisfiable round-trip. Remove 206
from the three status lists.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 removes Trellis’s server-side byte-range emission surface area (206 helpers, range evaluation, and related options/metadata) to avoid duplicating ASP.NET Core’s built-in Results.File(..., enableRangeProcessing: true) behavior, while preserving the client-side 416 typed-error vocabulary and server-side 416 companion header emission.

Changes:

  • Removed WithRange / WithAcceptRanges, RangeRequestEvaluator / RangeOutcome, and PartialContent* results from Trellis.Asp, plus updated response metadata to no longer advertise 206.
  • Removed AcceptRanges from RepresentationMetadata and stopped emitting the Accept-Ranges header from Trellis response writers.
  • Updated docs/samples/cookbook recipe/ADR to direct consumers to ASP.NET Core for byte-range responses.

Reviewed changes

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

Show a summary per file
File Description
Trellis.Http.Abstractions/tests/RepresentationMetadataTests.cs Removes AcceptRanges assertions from metadata tests.
Trellis.Http.Abstractions/src/RepresentationMetadata.cs Drops AcceptRanges from the metadata bag and builder.
Trellis.Core/src/Pagination/Page.cs Updates guidance to use ASP.NET Core Results.File(... enableRangeProcessing: true) for binary range downloads.
Trellis.Asp/tests/WithCacheControlTests.cs Removes 206/range-specific cache-control test.
Trellis.Asp/tests/TrellisWriteOutcomeResultTests.cs Removes Accept-Ranges expectations and builder usage.
Trellis.Asp/tests/TrellisHttpResultMetadataTests.cs Updates produced-response-type status set to exclude 206.
Trellis.Asp/tests/TrellisHttpResultExtraTests.cs Removes range-related test coverage and adjusts header tests.
Trellis.Asp/tests/RangeRequestEvaluatorTests.cs Deletes tests for the removed range evaluator.
Trellis.Asp/tests/HttpResponseOptionsBuilderTests.cs Removes tests for WithRange and WithAcceptRanges.
Trellis.Asp/tests/HttpPartialContentResultTest.cs Deletes tests for the removed MVC 206 result.
Trellis.Asp/src/Response/TrellisWriteOutcomeResult.cs Stops emitting Accept-Ranges from metadata/options.
Trellis.Asp/src/Response/TrellisHttpResult.cs Removes range evaluation/206 execution path and drops 206 from endpoint metadata.
Trellis.Asp/src/Response/HttpResponseOptionsBuilder.cs Removes WithRange / WithAcceptRanges and related option fields.
Trellis.Asp/src/RangeRequestEvaluator.cs Deletes the range evaluation API and outcome types.
Trellis.Asp/src/PartialContentResult.cs Deletes MVC PartialContentResult implementation.
Trellis.Asp/src/PartialContentHttpResult.cs Deletes Minimal API PartialContentHttpResult implementation.
Trellis.Asp/SAMPLES.md Removes sample section demonstrating Content-Range/206 pagination.
Examples/CookbookSnippets/Recipe06_ConditionalGet.cs Updates recipe snippet to remove byte-range example and focus on conditional GET.
docs/docfx_project/articles/integration-aspnet.md Removes range entries/sections from the integration guide (but still needs follow-up wording cleanup per comments).
docs/docfx_project/articles/asp-tohttpresponse.md Removes range references from the ToHttpResponse guide (but still needs a Cache-Control row tweak per comments).
docs/docfx_project/api_reference/trellis-api-http-abstractions.md Updates RepresentationMetadata docs to remove Accept-Ranges.
docs/docfx_project/api_reference/trellis-api-cookbook.md Updates Recipe 6 to remove range support and document ASP.NET Core Results.File for byte ranges.
docs/docfx_project/api_reference/trellis-api-asp.md Removes range surface documentation and adds explicit “byte ranges out of scope” note.
docs/docfx_project/adr/ADR-002-v2-redesign-plan.md Updates ADR narrative to reflect full removal of server-side byte-range emission surface.
.github/copilot-instructions.md Updates contributor guidance to remove “range handling” mentions for ASP docs.

Comment on lines 127 to 131
b.WithETag(_ => "x")
.WithLastModified(_ => DateTimeOffset.UtcNow)
.Vary("Accept")
.WithContentLanguage("en")
.WithContentLocation(_ => "/x")
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e2f5099 — unused import removed.

Comment on lines 13 to 17
/// <summary>
/// Outcome-level coverage for <c>ToHttpResponse</c> branches not exercised by the higher-level
/// <c>ToHttpResponseTests</c>: metadata headers, range handling, conditional-request decisions,
/// <c>ToHttpResponseTests</c>: metadata headers, conditional-request decisions,
/// location resolution, and per-call error-mapping overrides. Every test drives the public
/// extension and asserts on what a caller observes (status code, headers, body bytes).
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e2f5099 — unused import removed.

Comment on lines 59 to 63
| `Vary(params string[])` | Appends to `Vary` (preserves existing values; case-insensitive dedupe). |
| `WithContentLanguage(params string[])` / `WithContentLocation(Func<T, string>)` / `WithAcceptRanges(string)` | Sets the matching response header. |
| `WithCacheControl(CacheControlHeaderValue)` | Sets `Cache-Control` on success (200 / 201 / 204 / 206 / 304 / WriteOutcome / paged) **and on failure** responses, so a sensitive endpoint declaring `WithCacheControl(CacheControl.NoStore())` protects 404 / 403 / 412 / 422 from intermediate-cache leakage just as much as the 200. Throws `ArgumentNullException` on null. Use the [`CacheControl`](../api_reference/trellis-api-asp.md#cachecontrol) presets (`NoStore()`, `NoCache()`, `Public(TimeSpan)`, `Private(TimeSpan)`, `Immutable(TimeSpan)`) for common shapes. |
| `WithContentLanguage(params string[])` / `WithContentLocation(Func<T, string>)` | Sets the matching response header. |
| `WithCacheControl(CacheControlHeaderValue)` | Sets `Cache-Control` on success (200 / 201 / 204 / 304 / WriteOutcome / paged) **and on failure** responses, so a sensitive endpoint declaring `WithCacheControl(CacheControl.NoStore())` protects 404 / 403 / 412 / 422 from intermediate-cache leakage just as much as the 200. Throws `ArgumentNullException` on null. Use the [`CacheControl`](../api_reference/trellis-api-asp.md#cachecontrol) presets (`NoStore()`, `NoCache()`, `Public(TimeSpan)`, `Private(TimeSpan)`, `Immutable(TimeSpan)`) for common shapes. |
| `WithCacheControl(Func<T, CacheControlHeaderValue?>)` | Per-domain selector — success path only (failures carry no domain value, and no-payload write outcomes like `UpdatedNoContent` / `AcceptedNoContent` also skip the selector since they carry no `T`). Returning `null` from the selector skips the per-domain header; when the static-value overload is also configured, the static value remains in place. |
| `Created(string literal)` / `Created(Func<T, string>)` | `201 Created` with literal or value-derived `Location`. |
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already addressed in 30957bb — Cache-Control row no longer lists 206.

Comment on lines 23 to 27
| Conditional `GET`/`HEAD` (`If-None-Match`, `If-Modified-Since`) | `opts.WithETag(...).EvaluatePreconditions()` | [Conditional requests](#conditional-requests) |
| Honor `Prefer: return=minimal` / `return=representation` | `opts.HonorPrefer()` on a `WriteOutcome` response | [Prefer header](#prefer-header) |
| Emit `201 Created` with a `Location` header | `opts.CreatedAtRoute(name, values)` (AOT-safe) / `Created(...)` / `CreatedAtAction(...)` | [Created responses](#created-responses) |
| Return paginated JSON + RFC 8288 `Link` header | `Result<Page<T>>.ToHttpResponse(nextUrlBuilder, body)` | [Pagination](#pagination) |
| Return `206 Partial Content` for byte / item ranges | `opts.WithRange(from, to, total)` / `opts.WithRange(selector)` | [Range responses](#range-responses) |
| Validate scalar value objects (route, query, JSON body) | `AddScalarValueValidation` + `UseScalarValueValidation` + `WithScalarValueValidation` | [Scalar value validation](#scalar-value-validation) |
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed across two commits: the line-563 status union and the asp-authorization.md:748 cache-list were fixed in 30957bb; the intro (line 11) and the audience bullet (line 35) are fixed in e2f5099.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.

/// <c>Content-Location</c>, and <c>Accept-Ranges</c> response headers consistently
/// across MVC and Minimal API responses (200, 201, 206, 304).
/// and <c>Content-Location</c> response headers consistently
/// across MVC and Minimal API responses (200, 201, 304).
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e2f5099 — summary now reads (200, 201, 204, 304).

// Domain-dependent selectors (ETag / Last-Modified / Content-Location / Cache-Control)
// must short-circuit in that case — invoking them with null would NPE on the first
// member access. Non-domain headers (Vary / ContentLanguage / AcceptRanges) still apply.
// Non-domain headers (Vary / ContentLanguage) still apply.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e2f5099 — comment rewritten to be complete (…would NPE on the first member access) and Cache-Control dropped from the selector list since ApplyMetadata does not apply it.

Comment thread CHANGELOG.md Outdated
- `Trellis.Asp.PartialContentResult` (MVC `ObjectResult`)
- `Trellis.Asp.RangeRequestEvaluator` and the `RangeOutcome` closed union (`FullRepresentation` / `PartialContent` / `NotSatisfiable`)
- `HttpResponseOptionsBuilder<T>.WithRange(Func<T, ContentRangeHeaderValue>)`, `WithRange(long, long, long)`, `WithAcceptRanges(string)`
- `HttpResponseOptions<T>.RangeSelector`, `StaticRange`, `AcceptRanges`
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e2f5099 — internal-type bullet removed; the public-facing WithRange / WithAcceptRanges builder bullet covers the user-visible removal.

Issue: The first byte-range removal commits left small inconsistencies:
two test files retained an unused System.Net.Http.Headers import;
the RepresentationMetadata XML summary omitted 204 from the list of
target responses; the ApplyMetadata comment in TrellisHttpResult was
truncated mid-sentence and still referenced Cache-Control even though
this method does not apply it; the CHANGELOG listed the internal
HttpResponseOptions<T> properties under removed public API; and
integration-aspnet.md still introduced Trellis.Asp as handling ranges
and Range semantics.

Fix: Remove the unused usings; add 204 to RepresentationMetadata's
summary; rewrite the ApplyMetadata comment to be complete and to drop
Cache-Control from the list; drop the internal-type bullet from the
CHANGELOG entry; remove range/ranges from integration-aspnet.md's
intro and audience bullet.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.

Comment on lines 14 to 16
public static void Map(IEndpointRouteBuilder app) =>
app.MapGet("/blobs/{id:guid}", async (System.Guid id, HttpRequest req, IBlobRepository repo, CancellationToken ct) =>
app.MapGet("/blobs/{id:guid}", async (System.Guid id, IBlobRepository repo, CancellationToken ct) =>
{
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 2b9837e — unused import removed.

Issue: After the Recipe 06 rewrite dropped the HttpRequest parameter
and the byte-range branch, the Microsoft.AspNetCore.Http using
remained even though no symbols from that namespace are referenced.

Fix: Drop the unused using.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.

**Problem.** Serve a resource with strong-ETag conditional GET so clients can revalidate cheaply.

```csharp
using Microsoft.AspNetCore.Http;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in f4b3604 — unused using Microsoft.AspNetCore.Http; removed from the cookbook snippet.

Comment thread CHANGELOG.md Outdated
- `Trellis.Asp.PartialContentResult` (MVC `ObjectResult`)
- `Trellis.Asp.RangeRequestEvaluator` and the `RangeOutcome` closed union (`FullRepresentation` / `PartialContent` / `NotSatisfiable`)
- `HttpResponseOptionsBuilder<T>.WithRange(Func<T, ContentRangeHeaderValue>)`, `WithRange(long, long, long)`, `WithAcceptRanges(string)`
- `RepresentationMetadata.AcceptRanges` and `RepresentationMetadata.Builder.SetAcceptRanges(string)` (the constructor and builder still accept every other parameter unchanged)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in f4b3604 — reworded to (every other RepresentationMetadata member is unchanged) so the note no longer references a non-existent public constructor.

Issue: The Recipe 6 cookbook snippet still imported
Microsoft.AspNetCore.Http even though no symbols from that namespace
are referenced. The CHANGELOG bullet for the AcceptRanges removal
described a non-existent public constructor on RepresentationMetadata.

Fix: Drop the unused using from the snippet so it mirrors the
underlying code file. Reword the AcceptRanges bullet to state that
every other RepresentationMetadata member is unchanged, without
implying a public constructor.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.

Comment thread Trellis.Asp/SAMPLES.md
@@ -311,94 +311,6 @@ public class JobsController : ControllerBase
}
```

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 9c45782 — TOC entry removed. The deleted Pagination Support section documented the old item-range wire shape (206 Partial Content with Content-Range: items 0-49/1000), which this PR intentionally eliminates. Current pagination guidance lives in trellis-api-cookbook.md Recipe 9 and integration-aspnet.md (Result<Page<T>>.ToHttpResponse(nextUrlBuilder, body) + RFC 8288 Link header), so SAMPLES.md does not need to duplicate it.

Issue: The SAMPLES.md Table of Contents still linked to a Pagination
Support section whose body was removed in this PR (it documented
emitting 206 Partial Content with Content-Range: items, which is the
anti-pattern this PR eliminates). The TOC link became a dangling
anchor.

Fix: Drop the Pagination Support TOC entry. The current pagination
guidance (Result<Page<T>>.ToHttpResponse(nextUrlBuilder, body) +
RFC 8288 Link header) lives in trellis-api-cookbook.md (Recipe 9)
and integration-aspnet.md; SAMPLES.md does not need to duplicate it.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.

@xavierjohn xavierjohn merged commit c4b73bb into main May 23, 2026
6 checks passed
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.

2 participants