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:

  1. 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 (prefixed temporary/). The row field is set to this temporary path.
  2. 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, not Form.cs. The FileUploadBehavior only 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:

  1. Security check on the file name (via UploadPathHelper.CheckFileNameSecurity).
  2. Optional antivirus scan through IUploadAVScanner.
  3. Validation with IUploadValidator — checks file size and allowed extensions, and image constraints (dimensions).
  4. Write the temporary file to storage.
  5. Image processing with IImageProcessor — scale the main image and generate thumbnails if options specify sizes.
  6. Returns a ProcessedUploadInfo whose TemporaryFile is 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 (default App_Data/upload/).
  • Url — the public URL prefix (default ~/upload/).
  • ExtensionBlacklist / ExtensionWhitelist (plus *Include / *Exclude variants) — 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.

See Also