Runic.Assets
0.10.0
See the version list below for details.
dotnet add package Runic.Assets --version 0.10.0
NuGet\Install-Package Runic.Assets -Version 0.10.0
<PackageReference Include="Runic.Assets" Version="0.10.0" />
<PackageVersion Include="Runic.Assets" Version="0.10.0" />
<PackageReference Include="Runic.Assets" />
paket add Runic.Assets --version 0.10.0
#r "nuget: Runic.Assets, 0.10.0"
#:package Runic.Assets@0.10.0
#addin nuget:?package=Runic.Assets&version=0.10.0
#tool nuget:?package=Runic.Assets&version=0.10.0
Runic.Assets
Runic.Assets gives a .NET application one portable, validated description of
its static files. Embed a Vite build for NativeAOT-friendly deployment, use
explicit assembly resources for a small bundle, or use a refreshable directory
while developing—then attach the same IAssetSource to the host
adapter you choose.
Runic.Assets or the Views www/ folder
Runic has two ways to ship a frontend, and they do not share settings:
- Runic.Application.Views applications build
RunicApplicationFrontendDirectoryand copy itsdist/as loose files towww/next to the executable; the Views host serves that folder. This is the default for Runic Views apps, anddotnet runic devworks with it. Without a Vite or Angular development server,devhas the Runic Desktop host servedist/itself throughDevelopmentDirectoryAssetSource, beforewww/, and reload its windows whendist/changes. - Runic.Assets packs
RunicAssetsDistinto one validated archive and embeds it in the assembly. Use it for ASP.NET Core and for Runic Desktop applications without Views, or wherever a single-file or NativeAOT deployment must carry its frontend inside the executable.Runic.Assets.AspNetCoreandRunic.Assets.Desktopserve it.
A Views application can serve an embedded archive instead of www/ by setting
RunicDesktopHost.SurfaceOptions to a DesktopSurfaceOptions whose Content
is new DesktopContent.Handler(assets.ToDesktopContentHandler()). The archive
must then hold everything the build copies to www/, including the host's
runic-views.js client, and dotnet runic dev then serves it instead of the
live dist/. A build property that connects the two pipelines is planned for a
later 1.x release.
Install
dotnet add package Runic.Assets --prerelease
The package targets .NET 10, has no UI or web-framework dependency, and is
currently in preview. Install Runic.Assets.AspNetCore or
Runic.Assets.Desktop only when you also need that delivery integration.
Embed a Vite build
After your Vite project's npm run build creates dist, add this to the .NET
application project:
<PropertyGroup>
<RunicAssetsDist>../Client.Web/dist</RunicAssetsDist>
</PropertyGroup>
dotnet build incrementally creates a canonical archive and embeds it through
the packer's trusted generated-output snapshot mode. It works on Linux,
Windows, and macOS, rejects observed symbolic links and reparse points, and
assumes its frontend output is not concurrently hostilely mutated. Load
the default Runic.Assets.StaticFiles resource at runtime:
using System.Reflection;
using Runic.Assets;
AssetArchiveSource assets = AssetArchive.ReadEmbedded(
Assembly.GetExecutingAssembly());
The default entry point is index.html. Set RunicAssetsEntryPoint for a
different entry file; set RunicAssetsDistExclude to omit files such as build
statistics (by default runic-assets.zip; the value replaces that default, and
an empty <RunicAssetsDistExclude></RunicAssetsDistExclude> in the project file
packs every file). To use an existing canonical archive, set
RunicAssetsEmbeddedArchive instead of RunicAssetsDist. Set
RunicAssetsEmbeddedResourceName if you need a resource name other than
Runic.Assets.StaticFiles, and supply that name to ReadEmbedded.
To produce an archive in a separate step, run the packaged packer directly; see
the packer README.
Directory archives mark a file Immutable (public, max-age=31536000, immutable) only when its name carries a content hash, such as Vite's
index-Cf3tzbYH.js, SvelteKit's start.D2kX0q3e.js or webpack's
main.<hex>.js. Every other file, including .html/.htm documents and
unhashed public/ files such as favicon.ico, robots.txt or sw.js, uses
Revalidate with its strong entity tag.
Small explicit bundles
For a few hand-authored files, declare assembly resources in your application project and map them explicitly:
<ItemGroup>
<EmbeddedResource Include="Assets/index.html" />
<EmbeddedResource Include="Assets/app.css" />
</ItemGroup>
using Runic.Assets;
var assets = new EmbeddedAssetSource(
typeof(Program).Assembly,
[
new("index.html", "MyApp.Assets.index.html", IsEntryPoint: true),
new("assets/app.css", "MyApp.Assets.app.css",
CacheMode: AssetCacheMode.Immutable),
]);
Use DevelopmentDirectoryAssetSource for a development inner loop on Linux,
Windows or macOS. dotnet runic dev uses it when no Vite or Angular
development server runs: the Runic Desktop window serves the frontend output
directory and reloads when it changes
(dotnet-runic).
The source refreshes local files and marks them no-store. No path component
below the root may be a symbolic link or reparse point. On Linux and macOS the
source pins its root with one no-follow directory handle and opens every file
relative to it, so replacing the root or an ancestor cannot redirect a
published source. Windows checks each path component for a reparse point
before it opens a file, but does not pin the root: a directory moved into the
root's place is read from then on. That is enough for a development source that
serves your own build output. Entries whose names are not valid asset paths (for example
containing :, ?, # or a backslash) are skipped and listed in
SkippedEntries instead of failing the scan.
using var source = new DevelopmentDirectoryAssetSource(frontendDirectory, "index.html");
source.Changed += (_, change) => Reload(change.Current);
source.StartWatching(new AssetWatchOptions { DebounceDelay = TimeSpan.FromMilliseconds(100) });
// Dispose the source to stop watching.
Subscribe to Changed (IAssetSourceChangeNotifier) first, then call
StartWatching once: it coalesces filesystem signals into source-owned
refreshes and runs until the source is disposed, which is the only way to stop
it; a second call throws InvalidOperationException. RefreshAsync scans
the directory on demand and returns the current manifest; refreshes run one at
a time. The source is the single publisher and each event contains immutable
previous/current manifest snapshots. Notifications are serialized and coalesced
after refresh, subscriber failures are isolated, and disposal stops future
refreshes. The watcher uses one fixed 8 KiB native event buffer and retries from
the authoritative directory scan after overflow, or after a scan that fails
because a build left the directory half written, with at most three retries per
change burst by default (AssetWatchOptions.MaxRetryAttempts). Reads materialize a verified temporary-file snapshot, so content that
changes after publication is rejected rather than delivered under the old digest.
Use an embedded source or an archive for a
reproducible deployed bundle.
Sources
Every source implements IAssetSource: its immutable Manifest,
ValidateAsync, and OpenAsync(relativePath), which returns an
AssetReadSnapshot. The snapshot's Descriptor and Content stream are one
owned unit, so response entity tags and range metadata describe exactly the
bytes delivered; dispose the snapshot to close the stream. A custom source
implements these three members. OpenAsync throws FileNotFoundException for a
path the manifest does not declare.
Guarantees and limits
AssetPath rejects rooted, traversal, encoded, ambiguous, and
control-character paths. AssetPath.TryNormalize, AssetManifest.TryGetAsset
and AssetManifest.TryResolveRequestPath return false for such paths instead
of throwing, so request handlers can pass untrusted paths to them directly.
AssetManifest is immutable and ordinally ordered.
Each entry carries media type, length, SHA-256 digest, strong entity tag,
Subresource Integrity token, and cache policy. CSP is host policy rather than
archive content: the archive cannot safely invent an application's script or
connect origins. ASP.NET Core delivery supports RFC conditional requests and
single byte ranges directly from this metadata. Archive reads validate declared content and can be bounded with
AssetArchiveReadOptions when input is not trusted.
AssetArchive.Inspect returns an archive's schema version, its manifest and the
SHA-256 digest of the canonical manifest bytes. Readers accept only the current
runic.assets.archive/1 schema and reject incompatible archive versions.
For application integration, consume IAssetSource.Manifest directly
for asset identity, SHA-256, entity tags, and cache metadata. Do not recreate a
parallel manifest in a hosting adapter.
This package deliberately does not expose HTTP endpoints or a Desktop handler; choose a host adapter for that responsibility.
Documentation and support
| 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
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Runic.Assets:
| Package | Downloads |
|---|---|
|
Runic.Assets.Desktop
Request-scoped Runic Desktop delivery for Runic Assets. |
|
|
Runic.Assets.AspNetCore
ASP.NET Core endpoint integration for Runic Assets. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.13.0 | 1,805 | 10/29/2026 |
| 0.12.1 | 1,066 | 10/27/2026 |
| 0.12.0 | 302 | 10/27/2026 |
| 0.11.0 | 3,167 | 10/25/2026 |
| 0.10.0 | 4,640 | 10/22/2026 |
| 0.9.1 | 454 | 10/21/2026 |
| 0.9.0 | 143 | 10/21/2026 |
| 0.8.0-preview.6 | 456 | 10/19/2026 |
| 0.8.0-preview.5 | 174 | 10/17/2026 |
| 0.8.0-preview.4 | 409 | 10/17/2026 |
| 0.8.0-preview.3 | 414 | 10/15/2026 |
| 0.8.0-preview.2 | 94 | 10/15/2026 |
| 0.8.0-preview.1 | 117 | 10/15/2026 |
| 0.7.0-preview.9 | 350 | 10/14/2026 |
| 0.7.0-preview.8 | 96 | 10/12/2026 |
| 0.7.0-preview.7 | 63 | 10/12/2026 |
| 0.7.0-preview.6 | 363 | 10/9/2026 |
| 0.7.0-preview.5 | 133 | 10/9/2026 |
| 0.7.0-preview.4 | 87 | 10/8/2026 |
| 0.7.0-preview.3 | 70 | 10/8/2026 |