I/O, Streams & HTTP · Lesson 14

URI composition, query values and trailing slashes

Compose structure separately from escaped data and verify the resulting address.

Separate address structure from query data

A URI contains structural separators whose meaning changes when data is inserted into the wrong place. The slash separates path segments, the question mark starts a query, the ampersand separates common query pairs and the hash introduces a fragment. The sample composes trusted address structure with Uri and UriBuilder, then escapes one query value as data. It performs no request. This distinction makes the resulting strings easy to inspect before introducing HttpClient, credentials, redirects or a server. Composition correctness is a useful prerequisite for transport correctness, but it does not establish authorization to contact an address.

Explain all four resolved addresses

The first base ends in /api/, so resolving items produces https://offline.invalid/api/items. The second base ends in /api without a trailing slash; resolving the same relative reference replaces that final path segment and produces https://offline.invalid/items. The third reference begins with /items, so it replaces the path from the root even though the base ends in /api/. Finally the query value C# & .NET is escaped and assigned to the builder, producing q=C%23%20%26%20.NET on the /api/items address. Each line exposes a separate structural decision.

Let Uri and UriBuilder own composition

Uri resolves the relationship between a base and a relative reference. UriBuilder supplies a writable representation of the selected components and exposes a Uri when composition is complete. EscapeDataString is applied to the query value, leaving the deliberately supplied q= syntax outside that call. If the entire address were escaped as one data string, structural separators would lose their intended role. If the raw value were appended directly, its hash and ampersand could change which component or pair receives the text. Keep ownership of syntax and ownership of data visibly separate in the code.

Escaping a value is not permission to request a host

The example contains one query pair and does not implement a general query parser or a multi-value query collection. It also does not make path traversal safe, validate an allowed host, authorize a resource or neutralize server-side request forgery. Resolving an absolute reference can replace the base entirely. Those are application policy questions beyond percent encoding. Even the displayed path is only an address: no observation here proves that /api/items exists. Inspect the composed scheme, host and path at the boundary where your application chooses its permitted destination.

Practice: vary the path boundary deliberately

Add cases resolving items/7, ../health and an absolute https address against the same directory base. Predict each AbsoluteUri before running. Explain which part of the base is retained and which part is replaced, rather than memorizing the resulting strings. Then create a caller policy that accepts only the intended scheme and host after resolution. The hint is to evaluate the final Uri properties, not to trust that every reference supplied with a base stays under that base. Do not send requests as part of this exercise.

Practice: round-trip a query value without double encoding

Use a new query value containing a literal percent sign, an ampersand and non-ASCII text. Escape it once and inspect the resulting AbsoluteUri. Decode only that encoded value with the corresponding URI unescaping API and compare the restored text using ordinal equality. Repeat after accidentally escaping the already escaped value, and identify why the extra percent encoding changes the round trip. This lab concerns one value; it should not decode a whole query before separating its pairs, because encoded delimiters can be legitimate data inside a value.

Review the address before adding transport

The four original lines make two contracts visible: how relative references resolve and how one query value is represented without becoming syntax. A trailing slash and a leading slash are meaningful address choices, not formatting preferences. Preserve those expectations in small address-composition tests before attaching a transport adapter. For production code, use a query-construction API appropriate to your framework when multiple values or repeated keys are required, and keep destination allowlists and request authorization as explicit policies. A correctly escaped URI alone cannot supply those policies.

  • A directory-like base needs its intended trailing slash.
  • Encode each data value once; validate the resolved destination separately.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

var directoryBase = new Uri("https://offline.invalid/api/");
Console.WriteLine(new Uri(directoryBase, "items").AbsoluteUri);
Console.WriteLine(new Uri(new Uri("https://offline.invalid/api"), "items").AbsoluteUri);
Console.WriteLine(new Uri(directoryBase, "/items").AbsoluteUri);
var builder = new UriBuilder(new Uri(directoryBase, "items")) { Query = "q=" + Uri.EscapeDataString("C# & .NET") };
Console.WriteLine(builder.Uri.AbsoluteUri);

Expected output

https://offline.invalid/api/items
https://offline.invalid/items
https://offline.invalid/items
https://offline.invalid/api/items?q=C%23%20%26%20.NET
Try this
A trailing slash and an origin-rooted relative path affect resolution; escape the parameter value, not the separators.

Further reading