Uploads
Serenity has a complete upload infrastructure for handling file and image uploads in your row-based services. It covers storing the file, validating it, optionally processing images (scaling/thumbnails), moving the temporary file to its permanent location on save, deleting old files, and serving the file back to the client.
How Uploads Work
Uploads follow a two-phase flow so that a file isn't committed to permanent storage until the row is actually saved:
- Temporary upload. The client-side upload editor posts the file to a temporary upload endpoint (
/File/TemporaryUpload). The endpoint uses IUploadProcessor to validate the file and write it to the temporary folder, returning a temporary path (prefixedtemporary/). The row field is set to this temporary path. - Save. When the row is saved, the FileUploadBehavior moves the temporary file to its permanent location, writes the permanent path into the field, and deletes (or archives) the previous file. Old temporary files are purged periodically.
Files are served back through IUploadFileResponder, which exposes files at /upload/{path}.
Declaring an Upload Field
Decorating a String field on your row (not the form) with an upload editor attribute is enough to enable uploads. The attribute provides the editor type for the client and the options used by FileUploadBehavior.
[DisplayName("User Image"), Size(100)]
[ImageUploadEditor(FilenameFormat = "UserImage/~", CopyToHistory = true)]
public string UserImage { get => fields.UserImage[this]; set => fields.UserImage[this] = value; }
The Northwind sample uses the same pattern:
[DisplayName("Product Image"), Size(100)]
[ImageUploadEditor(FilenameFormat = "ProductImage/~", CopyToHistory = true)]
public string ProductImage { get => fields.ProductImage[this]; set => fields.ProductImage[this] = value; }
Upload editor attributes
| Attribute | Editor type | Multiple? | Description |
|---|---|---|---|
| ImageUploadEditor | ImageUpload |
No | Allows only image files by default |
| FileUploadEditor | ImageUpload |
No | Allows image and non-image files |
| MultipleImageUploadEditor | MultipleImageUpload |
Yes | Multiple images; stores a JSON array |
| MultipleFileUploadEditor | MultipleImageUpload |
Yes | Multiple files; stores a JSON array |
Important: put the attribute in
Row.cs, notForm.cs. TheFileUploadBehavioronly runs when the attribute is on the field, so if you only put it on the form your files will stay in the temporary folder.
Common options
All upload editor attributes derive from BaseUploadEditorAttribute, which exposes options such as:
FilenameFormat— folder/file naming, e.g."UserImage/~".OriginalNameProperty— the name of another field in the row that stores the original file name.MaxSize/MinSize(bytes),MaxWidth/MaxHeight/MinWidth/MinHeight.AllowNonImage,ImageExtensions,AllowedExtensions.ScaleWidth/ScaleHeight,ScaleSmaller,ScaleMode,ScaleQuality,ScaleBackColor.ThumbWidth/ThumbHeight,ThumbSizes,ThumbMode,ThumbQuality,ThumbBackColor.CopyToHistory— archive the previous file instead of deleting it.JsonEncodeValue— multiple editors store the file list as a JSON array.
Upload Storage
IUploadStorage abstracts where files live. It provides methods to write, read, delete, and copy files, get file URLs and sizes, and manage file metadata.
The default implementation, DefaultUploadStorage, is disk-based and writes files under App_Data/upload/. Related implementations include DiskUploadStorage, TempUploadStorage, and CombinedUploadStorage.
UploadPathHelper contains path utilities (thumbnail names, security checks), and UploadStorageExtensions provides helpers such as CopyTemporaryFile and GetThumbnailUrl.
Storage is registered by AddUploadStorage():
public static IServiceCollection AddUploadStorage(this IServiceCollection collection)
{
collection.TryAddSingleton<IFilenameFormatSanitizer, DefaultFilenameFormatSanitizer>();
collection.TryAddSingleton<IUploadStorage, DefaultUploadStorage>();
collection.TryAddSingleton<IUploadValidator, DefaultUploadValidator>();
collection.TryAddSingleton<IImageProcessor, DefaultImageProcessor>();
collection.TryAddSingleton<IUploadProcessor, DefaultUploadProcessor>();
collection.TryAddSingleton<IUploadFileResponder, DefaultUploadFileResponder>();
return collection;
}
Processing and Validation
The pipeline is handled by IUploadProcessor, whose default implementation does the following when a file is posted:
- Security check on the file name (via
UploadPathHelper.CheckFileNameSecurity). - Optional antivirus scan through IUploadAVScanner.
- Validation with IUploadValidator — checks file size and allowed extensions, and image constraints (dimensions).
- Write the temporary file to storage.
- Image processing with IImageProcessor — scale the main image and generate thumbnails if options specify sizes.
- Returns a
ProcessedUploadInfowhoseTemporaryFileis the new temporary path.
If anything fails, the partially written temporary file is cleaned up and the exception is thrown (so the client shows a validation error).
Image processing
DefaultImageProcessor uses ImageChecker to validate image content and ThumbnailGenerator to create thumbnails. The scaling options (ScaleWidth, ScaleHeight, ThumbWidth, etc.) on the editor attribute control what happens.
Configuration
UploadSettings are read from the UploadSettings section of appsettings.json:
{
"UploadSettings": {
"Path": "App_Data/upload/",
"Url": "~/upload/",
"ExtensionBlacklistInclude": ".exe;.dll;",
"ExtensionWhitelistExclude": ".zip;"
}
}
Path— the root folder for uploads (defaultApp_Data/upload/).Url— the public URL prefix (default~/upload/).ExtensionBlacklist/ExtensionWhitelist(plus*Include/*Excludevariants) — control which file extensions are allowed.EditableMetadataKeys— metadata keys clients are allowed to set.
Deleting and Archiving Files
When a field value changes, FileUploadBehavior registers the old file for deletion through FilesToDelete (via UnitOfWork.RegisterFilesToDelete), so the file is removed only if the transaction commits. If CopyToHistory is set, the old file is archived instead of deleted.