orchardcore-media
Skill for managing media in Orchard Core. Covers media library configuration, media profiles, image processing, media field usage, and storage providers.
SKILL.mdsrc/CrestApps.AgentSkills/orchardcore/orchardcore-media/SKILL.md
GitHubOrchard Core Media
Guidelines
OrchardCore.Mediaprovides the media library, theIMediaFileStore, on-demand image resizing, media profiles andMediaField. Files are stored inApp_Data/Sites/{tenant}/Mediaand served from/media.- The admin UI is the Vue 3 Media Gallery backed by the
api/mediaMedia API; seeorchardcore-media-galleryfor the gallery, the API, TUS uploads, SignalR and multi-instance setup. - Only one storage provider is active at a time: file system (default), Azure Blob (
orchardcore-azure-media) or Amazon S3 (orchardcore-amazon-s3-media). - Images are resized by the built-in middleware and the NetVips engine (
IImageProcessingEngine). Orchard Core no longer depends on ImageSharp. - Use
resize_url,AssetUrlor theimg-*tag helper attributes instead of hand-written query strings: withUseTokenizedQueryString(default) the parameters are HMAC-signed per tenant. - Reference permissions through
OrchardCore.Media.MediaPermissions.
Features
| Feature ID | Purpose |
|---|---|
OrchardCore.Media |
Media library, resizing, profiles, MediaField, Media API. |
OrchardCore.Media.Cache |
Local cache of remote (Azure/S3) files; enabled by the cloud storage features. |
OrchardCore.Media.Security |
Secure Media, folder-level view permissions. |
OrchardCore.Media.Slugify |
SEO-friendly file and folder names (orchardcore-media-slugify). |
OrchardCore.Media.Tus |
Resumable TUS uploads. |
OrchardCore.Media.SignalR |
Real-time gallery updates. |
OrchardCore.Media.Indexing.Text, .Pdf, .OpenXML |
Extract .txt/.md, .pdf, .docx/.pptx text from files referenced by media fields. |
{
"steps": [
{
"name": "Feature",
"enable": [
"OrchardCore.Media",
"OrchardCore.Media.Indexing.Pdf"
],
"disable": []
}
]
}
Configuration (OrchardCore:Media)
{
"OrchardCore": {
"Media": {
"MaxFileSize": 30000000,
"CdnBaseUrl": "https://cdn.example.com",
"AssetsRequestPath": "/media",
"AssetsPath": "Media",
"UseTokenizedQueryString": true,
"SupportedSizes": [ 16, 32, 50, 100, 160, 240, 480, 600, 1024, 2048 ],
"MaxBrowserCacheDays": 30,
"MaxSecureFilesBrowserCacheDays": 0,
"MaxCacheDays": 365,
"AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".gif", ".webp", ".pdf", ".docx", ".mp4" ],
"RestrictedFileExtensions": [ ".css", ".js", ".svg" ],
"ContentSecurityPolicy": "default-src 'self'; style-src 'unsafe-inline'",
"MaxUploadChunkSize": 104857600,
"TemporaryFileLifetime": "01:00:00",
"DisableThumbnails": false
}
}
}
- Environment variable form:
OrchardCore__Media__MaxFileSize=50000000. AllowedFileExtensionsandRestrictedFileExtensionsare case-insensitive, must not overlap (tenant startup fails), and replace the defaults, so list every extension you keep. Extensions in neither list are rejected.- Extensions in
RestrictedFileExtensions(default.css,.js,.svg) also need the security-criticalUploadRestrictedMediapermission (Administrator and Editor by default). The check covers regular, API, TUS, copy/rename and remote publishing uploads. Recipe imports may use either list. MaxFileSizeis enforced client-side in the gallery, pickers and editors, and on the server. Also raise Kestrel/IIS/form limits (see below).AssetsPathis relative to the tenant data folder. Absolute paths, drive prefixes (includingC:Media), empty segments and segments ending in a dot or space (.,..) fail options validation. To relocate tenant data, set the host app data location (ORCHARD_APP_DATA) instead.- In-progress chunked and TUS uploads go to
OrchardCore:TempDirectory:Path(seeorchardcore-temp-file-storage). CdnBaseUrlprefixes generated media URLs; query strings on the asset path (such as?v=or resize parameters) are preserved.DisableThumbnailsforces the gallery list view (seeorchardcore-media-gallery).- Fine-tune the media
StaticFileOptionswithservices.PostConfigure<MediaOptions>(o => ...); modulewwwrootfiles useservices.Configure<StaticFileOptions>(...).
Upload size limits
services.Configure<KestrelServerOptions>(options => options.Limits.MaxRequestBodySize = 50_000_000);
services.Configure<IISServerOptions>(options => options.MaxRequestBodySize = 50_000_000);
services.Configure<FormOptions>(options => options.MultipartBodyLengthLimit = 50_000_000);
On IIS also set <requestLimits maxAllowedContentLength="50000000" /> in
web.config.
Image Processing
MediaImageProcessingMiddlewarevalidates the request, parses it intoImageProcessingCommands(Width,Height,ResizeMode,Format,Quality,BackgroundColor,FocalPointX/Y,AutoOrient) and callsIImageProcessingEngine.ProcessAsync.- The default
VipsImageProcessingEngineoutputsjpg,png,gifandwebp; any other format falls back to JPEG (bmpandtgaare not supported). - Native libvips binaries come from
NetVips.Native. For a runtime it does not cover, add the matchingNetVips.Native.<rid>package to the web project. - Resized images are cached through
IResizedImageCache; the default writes towwwroot/{tenant}/media-cache. Azure and S3 can hold this cache (OrchardCore.Media.Azure.ImageCache,OrchardCore.Media.AmazonS3.ImageCache).
Custom image processing engine
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using OrchardCore.Media.Processing;
using OrchardCore.Modules;
public sealed class MyImageProcessingEngine : IImageProcessingEngine
{
public async Task<ImageProcessingResult> ProcessAsync(
Stream input,
ImageProcessingCommands commands,
CancellationToken cancellationToken = default)
{
var output = new MemoryStream();
// Resize "input" with your imaging library using commands.Width, commands.Height,
// commands.ResizeMode, commands.Quality, commands.BackgroundColor and the focal point.
await input.CopyToAsync(output, cancellationToken);
output.Position = 0;
return new ImageProcessingResult
{
Output = output,
ContentType = "image/jpeg",
};
}
}
[RequireFeatures("OrchardCore.Media")]
public sealed class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton<IImageProcessingEngine, MyImageProcessingEngine>());
}
}
A custom IResizedImageCache implements GetAsync(cacheKey, fileExtension),
SetAsync, ClearAsync and ClearStaleAsync, and is registered the same way.
Media Profiles
Create profiles under Media → Profiles (ManageMediaProfiles) or with the
MediaProfiles step. Mode is Max, Crop, Pad, BoxPad, Min or
Stretch; Format is Undefined, Jpg, Png, Gif or WebP.
{
"steps": [
{
"name": "MediaProfiles",
"MediaProfiles": {
"thumbnail": {
"Hint": "Square thumbnail for listings",
"Width": 240,
"Height": 240,
"Mode": "Crop",
"Format": "WebP",
"Quality": 80,
"BackgroundColor": "",
"AutoOrient": true
}
}
}
]
}
Profiles are exported with the All Media Profiles deployment step.
Rendering Media
Liquid
{{ Model.ContentItem.Content.Article.Image.Paths.first | asset_url | img_tag: alt: 'Cover' }}
{{ 'animals/kittens.jpg' | asset_url | resize_url: width: 400, height: 300, mode: 'crop' | img_tag }}
{{ 'animals/kittens.jpg' | asset_url | resize_url: width: 800, format: 'webp', quality: 75 }}
{{ 'animals/kittens.jpg' | asset_url | resize_url: profile: 'thumbnail' }}
{{ 'animals/kittens.jpg' | asset_url | append_version }}
{% assign anchor = Model.ContentItem.Content.Article.Image.Anchors.first %}
{{ 'animals/kittens.jpg' | asset_url | resize_url: width: 100, height: 240, mode: 'crop', anchor: anchor }}
Named and positional resize_url arguments cannot be mixed. bgcolor applies
to pad/boxpad.
Razor
Reference the OrchardCore.Media package from the theme or module and add
@addTagHelper *, OrchardCore.Media to _ViewImports.cshtml.
<img src="@Orchard.AssetUrl(Model.Paths[0], width: 400, height: 300, resizeMode: ResizeMode.Crop)" alt="" />
<img src="@await Orchard.AssetProfileUrlAsync(Model.Paths[0], "thumbnail")" alt="" />
<img asset-src="@Model.Paths[0]" alt="@Model.MediaTexts[0]" img-width="800" img-format="WebP" img-quality="75" asp-append-version="true" />
<img asset-src="@Model.Paths[0]" img-profile="thumbnail" img-anchor="@Model.Field.GetAnchors()[0]" alt="" />
<a asset-href="docs/brochure.pdf" target="_blank">Download</a>
MediaTexts[] and Anchors[] are index-aligned with Paths[].
Media Fields
MediaFieldSettings: Multiple, AllowMediaText, AllowAnchors,
AllowedExtensions (can only narrow the global lists), plus Hint/Required.
| Editor | Behavior |
|---|---|
| Standard (default) | Picks existing library files; Secure Media folder permissions apply. |
Gallery |
Same storage as Standard, with card/list views and drag-and-drop reordering. |
Attached |
Uploads into mediafields/{ContentType}/{ContentItemId}/ with hash-based names; requires ManageAttachedMediaFieldsFolder; with Secure Media, files follow the item's ViewContent. Supports TUS. |
await _contentDefinitionManager.AlterPartDefinitionAsync("Article", part => part
.WithField("Attachments", field => field
.OfType("MediaField")
.WithDisplayName("Attachments")
.WithEditor("Attached")
.WithSettings(new MediaFieldSettings
{
Multiple = true,
AllowMediaText = true,
AllowedExtensions = [".pdf", ".docx"],
})));
After switching existing fields to Attached, move the referenced files:
{
"steps": [
{
"name": "move-attached-media-fields",
"ContentTypes": [ "Article", "Report" ]
}
]
}
Omit ContentTypes to process every type with an Attached media field. The
step runs in a background job and saves in batches of 50.
Importing Files (media step)
{
"steps": [
{
"name": "media",
"Files": [
{ "TargetPath": "images/logo.png", "SourcePath": "../wwwroot/images/logo.png" },
{ "TargetPath": "images/banner.jpg", "SourceUrl": "https://example.com/banner.jpg" },
{ "TargetPath": "docs/readme.txt", "Base64": "SGVsbG8=" }
]
}
]
}
TargetPath (alias Path) must be a relative path inside the tenant media
store; rooted paths, drive prefixes, empty segments and ./.. segments are
rejected before anything is read or overwritten.
Permissions
MediaPermissions member |
Name | Notes |
|---|---|---|
ManageMediaFolder |
ManageMediaFolder |
Broadest; implies all others below it and the Secure Media view permissions. |
ManageOthersMedia |
ManageOthersMediaContent |
Implied by ManageMediaFolder. |
ManageOwnMedia |
ManageOwnMediaContent |
Implied by ManageOthersMedia. |
ManageAttachedMediaFieldsFolder |
ManageAttachedMediaFieldsFolder |
mediafields/ folder. |
ManageMedia |
ManageMediaContent |
Minimum to open the library. |
UploadRestrictedMedia |
UploadRestrictedMedia |
Restricted extensions. |
ManageMediaProfiles |
ManageMediaProfiles |
Profiles. |
ViewMediaOptions |
ViewMediaOptions |
Settings → Media → Options page. |
ManageMediaApiSettings |
ManageMediaApiSettings |
Media API authentication scheme. |
ManageAssetCache |
ManageAssetCache |
Remote media cache purge. |
ViewMedia, ViewRootMedia, ViewOthersMedia, ViewOwnMedia |
ViewMediaContent, ViewRootMediaContent, ViewOthersMediaContent, ViewOwnMediaContent |
Only with Secure Media. |
if (!await _authorizationService.AuthorizeAsync(User, MediaPermissions.ManageMediaFolder, (object)"images/products"))
{
return Forbid();
}
Secure Media (OrchardCore.Media.Security)
- Adds
ViewMediaContent_{folder}permissions for the root and each first-level folder, plus own/others view permissions on top ofManageMediaContent. - Unauthorized and anonymous requests get
404. Secure files are sent withCache-Control: no-storeunlessMaxSecureFilesBrowserCacheDaysis set. - Enabling it changes access immediately: only Administrators view all media, and Authenticated gets
ViewOwnMediaContent. The Anonymous role has no view permission by default, so grantViewMediaContent(or folder permissions) to Anonymous for public media. An admin notification reminds you to review roles.
Media Events
using OrchardCore.Media.Events;
public sealed class MediaQuotaEventHandler : IMediaEventHandler
{
public Task MediaPermittedStorageAsync(MediaPermittedStorageContext context)
{
context.Constrain(10L * 1024 * 1024 * 1024); // smallest limit wins
return Task.CompletedTask;
}
public Task MediaCreatedFileAsync(MediaCreatedContext context)
{
// React to uploads, e.g. audit or notify.
return Task.CompletedTask;
}
}
public sealed class StripMetadataHandler : IMediaCreatingEventHandler
{
public Task<Stream> MediaCreatingAsync(MediaCreatingContext context, Stream creatingStream)
=> Task.FromResult(creatingStream); // return a transformed stream to replace the upload
}
Register with services.AddScoped<IMediaEventHandler, MediaQuotaEventHandler>()
and services.AddScoped<IMediaCreatingEventHandler, StripMetadataHandler>().
IMediaEventHandler has default implementations for every method (deleting,
moving, copying, directory events). The plain
IMediaFileStore.CreateFileFromStreamAsync(path, stream) only runs
IMediaCreatingEventHandler. To also run the shared IFileEventHandler
pipeline (which orchardcore-antivirus uses to scan files), call the
CreateFileFromStreamAsync(fileCreationService, path, stream, ...) overload
from MediaFileStoreExtensions; see orchardcore-file-upload-security.
Media Indexing
Tick Include this element in the index on a media field; the index then gets
{Part}.{Field}.MediaText and, with an extraction feature, {Part}.{Field}.FileText.
Standalone library files that no media field references are not indexed.
Upgrading from 3.x
OrchardCore.Media.ImageSharpV3and the Media ImageSharp Image Processing feature were removed. Remove the package reference and the feature from recipes; NetVips processes images (tenants that had it enabled keep working). To keep ImageSharp, implementIImageProcessingEnginein your project and replace the registration as shown above.OrchardCore.Media.Permissions,SecureMediaPermissions.View*andMediaCachePermissions.ManageAssetCachemembers were removed; useMediaPermissions.*(role claim names are unchanged).- The Anonymous role no longer gets
ViewMediaContentfrom Secure Media; grant it explicitly if needed. .css,.jsand.svgmoved toRestrictedFileExtensions; grantUploadRestrictedMediato roles that upload them.MediaOptions.TusTempPathwas removed; useOrchardCore:TempDirectory:Path.- Rename
OrchardCore_MediatoMediain configuration (env varOrchardCore__Media__...); the old name is deprecated but still read. - The Media
AdminControllerAJAX endpoints were replaced by theapi/mediaMedia API (orchardcore-media-gallery). - Coming from 2.x: the
is-cachefolder becamemedia-cache, and the Azure/S3ImageSharpImageCachefeature IDs becameImageCache.