SimpleAspImageKit 1.1.0
dotnet add package SimpleAspImageKit --version 1.1.0
NuGet\Install-Package SimpleAspImageKit -Version 1.1.0
<PackageReference Include="SimpleAspImageKit" Version="1.1.0" />
<PackageVersion Include="SimpleAspImageKit" Version="1.1.0" />
<PackageReference Include="SimpleAspImageKit" />
paket add SimpleAspImageKit --version 1.1.0
#r "nuget: SimpleAspImageKit, 1.1.0"
#:package SimpleAspImageKit@1.1.0
#addin nuget:?package=SimpleAspImageKit&version=1.1.0
#tool nuget:?package=SimpleAspImageKit&version=1.1.0
SimpleAspImageKit
Image storage, resizing, cropping and thumbnailing for ASP.NET Core MVC, in one registration call. Ids are content hashes, so every URL is immutable and ETag revalidation comes for free.
Built on SkiaSharp (MIT).
dotnet add package SimpleAspImageKit
dotnet add package SimpleAspImageKit.EntityFrameworkCore # only for database storage
Quick start
builder.Services.AddImageKit();
That is the whole setup. Images are written to wwwroot/images, re-encoded as WebP at their
original dimensions, and served from /api/image/{id} — no pipeline changes needed.
Save image:
public class ProductController(IImageService imageService) : Controller
{
[HttpPost]
public async Task<IActionResult> Upload(IFormFile file)
{
var result = await imageService.SaveImageAsync(file);
if (!result.Success)
{
ModelState.AddModelError("file", result.Error.ToString());
return View();
}
product.ImageId = result.Id; // 64-char hex, also the file name
return RedirectToAction("Index");
}
}
Show image:
<img src="/api/image/@id" alt="" />
<img src="/api/image/thumbnail/@id" alt="" />
Registration options
builder.Services.AddImageKit(options =>
{
options.DefaultWidth = 400;
options.DefaultHeight = 600;
options.Fit = ImageKitFit.Cover;
options.MaxSize = 5_242_880; // tranfered bytes IFormFile.Length
options.MaxPixels = 50_000_000; // decoded pixels (max resolution)
options.Normalize = true; // re-encode everything to WebP
options.Quality = 82;
})
.AddThumbnail(options =>
{
options.DefaultHeight = 48;
options.Fit = ImageKitFit.Cover;
});
Three storage backends, same API:
builder.Services.AddImageKit(); // wwwroot/images
builder.Services.AddImageKit(options => options.Path = @"C:\Images"); // custom folder
builder.Services.AddImageKit<AppDbContext>(options => { /* ... */ }); // database
Path may be absolute or relative. Relative paths resolve against the content root (where the
.csproj lives), not wwwroot.
Custom route
The default prefix is /api/image, with thumbnails at /api/image/thumbnail/{id}. For anything
else, map it explicitly after Build():
app.MapImageKit("/api/assets/images");
// -> /api/assets/images/{id} and /api/assets/images/thumbnail/{id}
Calling MapImageKit also switches off the built-in fallback middleware, so the endpoints stop
answering on the default prefix.
Database storage
Two steps are yours, because this package never touches your schema at runtime:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyImageKitConfiguration();
}
dotnet ef migrations add AddImageTable
dotnet ef database update
The Image table holds one row per image: Hash (PK), ContentType, Content, Thumbnail,
plus dimensions and a timestamp. /api/image/{id} reads Content; /api/image/thumbnail/{id}
reads Thumbnail.
Fit modes
| Fit | Behaviour |
|---|---|
None (default) |
Scales to fit inside the box, preserving the ratio. No crop, no padding. With no dimensions set, no resize at all. |
Contain |
Same geometry as None; clearer intent when both dimensions are given. |
Cover |
Scales to cover the box, then permanently crops the overflow. The trimmed pixels are gone from the stored file. |
Fill |
Stretches to the exact dimensions, ignoring the ratio. With no dimensions given, no resize takes place. |
Pad |
Scales like Contain, then centres on a canvas of exactly the requested size using PadColor. |
Upscaling is off unless AllowUpscale = true. Cover crop position is set by Anchor
(default Center).
Thumbnails
AddThumbnail() generates a thumbnail on every save. It inherits the aspect ratio from the
parent options — or from the source image when the parent has no box — but never the parent's
dimensions. With no dimensions of its own, the thumbnail is 128px tall.
Metadata, formats and caching
- Metadata is always stripped. Every image is re-encoded, which discards EXIF, GPS and the rest. EXIF orientation is applied to the pixels first, so phone photos do not come out sideways.
- Accepted uploads: JPEG, PNG, WebP, GIF (first frame), BMP. Format is detected from the bytes,
not the
Content-Typeheader. SVG is rejected outright: it is a script execution vector. - Output: WebP by default. With
Normalize = falsethe source format is kept, except GIF and BMP, which become PNG because Skia cannot encode them. - Caching: strong
ETag,304onIf-None-Match, andCache-Control: public, max-age=1y, immutable. Safe because the id is a hash of the bytes — a URL's content can never change.
Listing
var images = await imageService.ListImagesAsync(skip: 0, take: 50);
int total = await imageService.CountImagesAsync();
Returns ImageInfo (id, content type, length, created timestamp, whether a thumbnail exists) —
never the bytes. Newest first. take is clamped to 1..500.
This answers "what is in storage", which is not always "what should the gallery show". If images belong to entities, keep the id on the entity and query that; listing the store will also return images whose owning row was deleted. Use it when the gallery genuinely is everything stored, or for admin and orphan-cleanup screens.
On the file store, sorting by date means the directory is walked in full before paging, and the timestamp is the file's creation time, which copies and some filesystems do not preserve. The database store pages in SQL and orders by a real stored column.
Transforms happen on save, not on retrieval
There is no ?w=300&h=200 on the read endpoints, deliberately: an open transform endpoint is a
free cache-buster and a trivial CPU denial-of-service. Decide the sizes when you save, and let the
browser scale from there with width/height attributes or CSS.
Deliberate omissions
- No upload or delete endpoint. Both run through
IImageServicein your own authorized code, so this package can never add an unauthenticated write surface to your app. - Flat directory layout. Fine for thousands of images; NTFS and ext4 both degrade past roughly 100k entries in one folder. Beyond that, reach for an object store.
wwwroot/imagesis served twice. In the default configuration files also resolve directly at/images/{hash}.webpvia static files, bypassing the ETag and cache headers above. Setoptions.Pathto a folder outsidewwwrootif that bothers you.
Result handling
SaveImageAsync never throws on bad input. It returns an ImageResult that converts implicitly
to string?, so the terse form still works:
string? id = await imageService.SaveImageAsync(file); // null on failure
For the reason, use the object: Success, Id, Error, ErrorMessage, Width, Height,
Length, ContentType, AlreadyExisted. Error values are Empty, TooLarge, TooManyPixels,
UnsupportedFormat, CorruptImage, EncodingFailed, StorageFailure, NotFound.
Because the id is a hash of the processed output, saving the same image twice with the same
options returns the same id and skips the write (AlreadyExisted = true).
Linux and Docker
SkiaSharp.NativeAssets.Linux.NoDependencies is referenced for you, so containers work without
installing libfontconfig.
Licence
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- SkiaSharp (>= 3.119.0)
- SkiaSharp.NativeAssets.Linux.NoDependencies (>= 3.119.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on SimpleAspImageKit:
| Package | Downloads |
|---|---|
|
SimpleAspImageKit.EntityFrameworkCore
Entity Framework Core storage backend for SimpleAspImageKit. Stores images and thumbnails in a table on your own DbContext instead of on disk. |
GitHub repositories
This package is not used by any popular GitHub repositories.