SimpleAspImageKit.EntityFrameworkCore 1.1.0

dotnet add package SimpleAspImageKit.EntityFrameworkCore --version 1.1.0
                    
NuGet\Install-Package SimpleAspImageKit.EntityFrameworkCore -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SimpleAspImageKit.EntityFrameworkCore" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SimpleAspImageKit.EntityFrameworkCore" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="SimpleAspImageKit.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SimpleAspImageKit.EntityFrameworkCore --version 1.1.0
                    
#r "nuget: SimpleAspImageKit.EntityFrameworkCore, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SimpleAspImageKit.EntityFrameworkCore@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SimpleAspImageKit.EntityFrameworkCore&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=SimpleAspImageKit.EntityFrameworkCore&version=1.1.0
                    
Install as a Cake Tool

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-Type header. SVG is rejected outright: it is a script execution vector.
  • Output: WebP by default. With Normalize = false the source format is kept, except GIF and BMP, which become PNG because Skia cannot encode them.
  • Caching: strong ETag, 304 on If-None-Match, and Cache-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 IImageService in 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/images is served twice. In the default configuration files also resolve directly at /images/{hash}.webp via static files, bypassing the ETag and cache headers above. Set options.Path to a folder outside wwwroot if 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 119 8/9/2026
1.0.0 107 8/9/2026