---
title: "Resize images on the fly in ASP.NET Core"
url: https://recursiveai.org/oneimage/articles/resize-images-on-the-fly-aspnet-core/
description: "Resize, crop and convert images on request in ASP.NET Core with ?width=400: disk cache, presets, signed URLs, WebP by Accept header, and ImageSharp.Web URLs."
date: 2026-10-26
author: OneImage team
---

# Resize images on the fly in ASP.NET Core

Resize, crop and convert images on request in ASP.NET Core with ?width=400: disk cache, presets, signed URLs, WebP by Accept header, and ImageSharp.Web URLs.

October 26, 2026 · OneImage team

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`.

*Terminal · NuGet*

```
dotnet add package RecursiveAI.OneImage.Web
```

## Set up the middleware

*Program.cs · ASP.NET Core · .NET 10*

```
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](https://github.com/recursive-ai-oss/oneimage-csharp/blob/main/docs/web.md) 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:

*Program.cs · ASP.NET Core · .NET 10*

```
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:

*Program.cs · ASP.NET Core · .NET 10*

```
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

*Program.cs · ASP.NET Core · .NET 10*

```
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](https://recursiveai.org/oneimage/articles/add-watermark-to-image-csharp/).

## 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

*Program.cs · ASP.NET Core · .NET 10*

```
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](https://docs.sixlabors.com/articles/imagesharp.web/gettingstarted.html). 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](https://recursiveai.org/oneimage/articles/imagesharp-license/) explains who needs a paid license and the key, and the [ImageSharp migration guide](https://recursiveai.org/oneimage/articles/migrate-from-imagesharp/) 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#](https://recursiveai.org/oneimage/articles/resize-image-csharp/) has an upload endpoint, and [compressing images in C#](https://recursiveai.org/oneimage/articles/compress-image-csharp/) covers quality settings. For everything else, see the [OneImage overview](https://recursiveai.org/oneimage/).
