To resize images on the fly in ASP.NET Core, add an image middleware in front of the static files, so that /photos/cat.jpg?width=400 returns a 400-pixel version, made on the first request and cached on disk after that. OneImage.Web is that middleware: two lines in Program.cs, URL commands for size, crop, format and quality, signed URLs and presets for public sites, and pure .NET image code underneath, with no native libraries. It also accepts ImageSharp.Web’s query spellings, so existing URLs keep working. Every sample was run against OneImage.Web 1.10.2 on .NET 10, and the requests shown were made with curl.
dotnet add package RecursiveAI.OneImage.WebSet up the middleware
using OneImage.Web; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOneImage(); var app = builder.Build(); app.UseOneImage(); // before UseStaticFiles, so it sees image requests first app.UseStaticFiles(); app.Run();
Put UseOneImage() before UseStaticFiles(), or the static file middleware answers image requests before the resizer sees them. Every image under wwwroot can now be requested in another size or format. With a 3000×3002 photo at wwwroot/photos/photo.jpg:
| Request | Response |
|---|---|
/photos/photo.jpg?width=400 | 400×400 JPEG, 31 KB |
/photos/photo.jpg?w=300&h=300&fit=cover | 300×300 JPEG, cropped around the center |
/photos/photo.jpg?width=800&format=webp&q=80 | 800×801 WebP, 94 KB |
/photos/photo.jpg | the original file, served by the static file middleware |
/photos/photo.jpg?width=abc | 400 Bad Request |
A request without commands isn’t touched. Invalid values fail with 400 instead of being guessed at, and unknown parameters such as utm_source are ignored. Results carry an ETag, Last-Modified and Cache-Control: public, max-age=604800 (7 days), and a revalidation with If-None-Match got a 304 without touching the image.
URL commands
| Command | Values |
|---|---|
width, height (w, h) | Pixels. With one of them, the other follows the aspect ratio. |
fit | contain (default), cover, stretch, pad |
anchor, focus | Which part a crop keeps: an edge, a corner, smart, or a focal point such as focus=0.3,0.6 |
format | jpg, png, webp, gif, bmp |
quality (q) | 1 to 100, for JPEG and WebP |
bg | Padding color, and the color transparency is flattened onto |
upscale | Allow enlarging past the source size. Off by default. |
preset | A named command string defined in code |
The OneImage.Web documentation lists every command and alias. Each result goes through OneImage’s thumbnail pipeline: a large JPEG is decoded at 1/2, 1/4 or 1/8 scale when the target size allows, resampling happens in linear light, phone photos are turned upright from their EXIF orientation, and EXIF, XMP and IPTC metadata, which can hold GPS positions, are removed. Our test phone photo, stored sideways, came back upright at 300×450 with no EXIF data.
Caching
Processed images are written to oneimage-cache in the content root, outside wwwroot, so they’re reachable only through the middleware. The cache key is the path plus the normalized commands, so ?w=400, ?width=400 and ?width=400&utm_source=x share one entry. When the source file changes, its entry is rebuilt on the next request. Several identical requests that arrive together are processed once, and the others wait for that result. Cached files can be deleted at any time and are recreated when they’re asked for.
Presets for public sites
On a public site, free-form commands let anyone ask for any size, and every distinct size becomes a cache entry. Presets limit the set of results to the ones you define:
using OneImage.Web; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOneImage(o => { o.Presets["thumb"] = "width=200&height=200&fit=cover&format=webp"; o.Presets["hero"] = "width=1600&format=webp&quality=80"; o.PresetsOnly = true; // ?preset=thumb works; ?width=123 is ignored }) .ConfigureCache(c => c.CacheRootPath = Path.Combine(builder.Environment.ContentRootPath, "image-cache")); var app = builder.Build(); app.UseOneImage(); app.UseStaticFiles(); app.Run();
?preset=thumb returned a 200×200 WebP and ?preset=hero a 1600×1601 WebP. ?width=123 was ignored and the original was served, and an unknown preset got a 400. The cache moved to image-cache. Output sizes are capped in any case: MaxWidth and MaxHeight default to 4096, so ?width=5000 got a 400. Source files over 64 MiB, or over the decoding limits, are refused before they’re decoded.
Signed URLs
Signed URLs keep free-form commands but accept only URLs your own pages generated:
using OneImage.Web; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOneImage(o => { // At least 32 random bytes, kept out of source control. o.HmacSecretKey = Convert.FromBase64String(builder.Configuration["Images:Key"]!); }); var app = builder.Build(); app.UseOneImage(); app.UseStaticFiles(); // Pages build their image URLs with the signer. app.MapGet("/", (ImageUrlSigner signer) => { string src = signer.Sign("/photos/photo.jpg?width=400&format=webp"); return Results.Content($"<img src=\"{src}\" alt=\"The Earth\">", "text/html"); }); app.Run();
The page’s <img> got /photos/photo.jpg?width=400&format=webp&hmac=…, and that URL returned the WebP. The same URL without the token, or with the width changed, returned 403. The token is an HMAC-SHA256 of the path and query, compared in constant time. In Razor views, @addTagHelper *, OneImage.Web lets you write <img src="~/photos/photo.jpg" oi-width="400" oi-format="webp" /> and signs the URL for you.
Serve WebP to browsers that accept it
using OneImage.Web; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOneImage(o => { // Serve WebP to browsers that accept it, unless the URL names a format. o.OnParseCommandsAsync = context => { string accept = context.HttpContext.Request.Headers.Accept.ToString(); if (!context.Commands.ContainsKey("format") && accept.Contains("image/webp")) { context.Commands["format"] = "webp"; } return Task.CompletedTask; }; // Caches between you and the browser must keep the two versions apart. o.OnPrepareResponseAsync = httpContext => { httpContext.Response.Headers.Vary = "Accept"; return Task.CompletedTask; }; }); var app = builder.Build(); app.UseOneImage(); app.UseStaticFiles(); app.Run();
A request with a browser-style Accept: image/avif,image/webp,… header got WebP, and one whose Accept header didn’t list WebP got JPEG, both with Vary: Accept. A URL that names a format keeps it. The two versions are separate cache entries, because the format is part of the cache key. The same hooks can snap widths to a fixed list, add headers, or add a watermark to every image.
Images outside wwwroot
Providers decide where source images come from. wwwroot is the default. PhysicalFileImageProvider serves any folder under a URL prefix, FileProviderImageProvider takes any IFileProvider, and your own IImageProvider can read from a database or blob storage, given a modified time and a length for the cache. ProcessingBehavior.All on a provider also processes requests without commands, for example to re-encode or watermark every image it serves.
Moving from ImageSharp.Web
using OneImage.Web; var builder = WebApplication.CreateBuilder(args); // Was: builder.Services.AddImageSharp(); builder.Services.AddOneImage(o => o.UseImageSharpDefaults()); var app = builder.Build(); // Was: app.UseImageSharp(); app.UseOneImage(); // before UseStaticFiles, as with ImageSharp.Web app.UseStaticFiles(); app.Run();
The two commented calls are the setup from ImageSharp.Web’s getting-started page. OneImage.Web accepts ImageSharp.Web 3.2.0’s query spellings alongside its own, so stored URLs keep working. UseImageSharpDefaults() keeps ImageSharp.Web’s behavior of cropping, and allowing enlargement, when a width and height come without a mode. With it, against the same photo:
| Request | Response |
|---|---|
?width=400&height=250 | 400×250, cropped |
?width=400&height=250&rmode=max | 250×250, fitted inside the box |
?width=400&height=250&rmode=pad&bgcolor=white | 400×250, padded with white |
?width=400&rsampler=bicubic | 400×400 |
logo.png?bgcolor=white&format=jpg&quality=85 | a JPEG on white |
Three things change. Signed URLs need new tokens, because the HMAC tokens aren’t interchangeable. Custom IImageWebProcessor classes move into the OnBeforeSaveAsync hook, which receives the image before it’s encoded. And rmode=manual isn’t supported: it returns 400, so search your templates for it. ImageSharp.Web 3.2.0 and 4.0.2 both ship under the Six Labors Split License 1.0, according to the license file in their NuGet packages, and in our test the current release, 4.0.2, stopped the build with “No Six Labors license found” and asked for a license key. Our page on the ImageSharp license explains who needs a paid license and the key, and the ImageSharp migration guide covers the rest of the API.
Resize at upload time instead
On-the-fly resizing suits sites that need many sizes of the same images, or sizes that change with the design. When you know the sizes up front, resizing once when the file is uploaded is simpler and needs no cache. Our guide to resizing images in C# has an upload endpoint, and compressing images in C# covers quality settings. For everything else, see the OneImage overview.