The cache saves the server's work, but the shop still sends the whole product list every time, even to a browser that got the very same list a minute ago. An ETag fixes that: a short fingerprint of an answer, sent in a header next to it. The same content always gives the same ETag; change one price and it is completely different.
A browser that has a copy sends its ETag back in the If-None-Match header, meaning "send it only if it isn't this one". If the fingerprint still matches, the server answers 304 Not Modified with an empty body, and the browser shows its own copy. A few bytes travel instead of the whole list.
One place for every route: ASP.NET Core runs a middleware, added with app.Use, around every request. The code above await next() runs before the route, the code below it runs after. For example, this one adds a header to every answer:
app.Use(async (context, next) => {
context.Response.Headers["X-Shop"] = "AlgoShop";
await next();
});
A route writes its answer straight into context.Response.Body, a stream to the browser. To hash an answer, the middleware swaps that stream for a MemoryStream, lets the route write into it, then copies the bytes to the real stream itself. Your code must give these answers, in this order. The 304s save the trip, not the query: the cache still does that job, so the count stays at 4.
GET /products → 200 ETag: "d62f8998b9c6175bf98ed96c8fa6de98" [all 8 products, Mug at 12]
GET /products If-None-Match: "d62f8998b9c6175bf98ed96c8fa6de98" → 304 ETag: "d62f8998b9c6175bf98ed96c8fa6de98" (empty body)
PATCH /products/1 {"price": 13} → 200 {"id": 1, "price": 13} (no ETag)
GET /products If-None-Match: "d62f8998b9c6175bf98ed96c8fa6de98" → 200 ETag: "426305b11dfff0b0d6b5151683164898" [all 8 products, Mug at 13]
GET /products If-None-Match: "426305b11dfff0b0d6b5151683164898" → 304 ETag: "426305b11dfff0b0d6b5151683164898" (empty body)
GET /products/99 → 404 {"error": "Product not found"} (no ETag)
GET /debug/queries → 200 {"queries": 4}
Your task: below RowToDict, add a middleware with app.Use. Catch each answer in a MemoryStream, put the real body stream back after the route, and copy the bytes to it at the end. For a GET that answered 200, hash the bytes with MD5, write the hash as lowercase hex between double quotes, and send it in the ETag header. If the browser's If-None-Match holds that same ETag, answer 304 and return before the copy, so the body stays empty.
Assignment
Follow the Coding Tutorial to add ETags and 304 answers to the shop's successful GET requests.
Hintbuffer.ToArray() gives the answer's bytes, MD5.HashData(...) hashes them and Convert.ToHexString(...).ToLower() turns the hash into text; using System.Security.Cryptography; is already at the top. Read the browser's header with context.Request.Headers["If-None-Match"].
Free plan: 30 tutor messages. PRO is unlimited.
