Skip to content
Edgar Mesquita edited this page Feb 9, 2026 · 3 revisions

Asset Management System

eQuantic.UI provides a declarative asset dependency system that allows components to declare their required scripts and stylesheets. Assets are automatically collected during SSR, deduplicated, and injected into the page's <head>.

Overview

The system solves a common problem: components that depend on external libraries (Prism.js, Chart.js, etc.) need their scripts/CSS loaded, but shouldn't embed <script> tags inline in their rendered HTML. Instead, they declare dependencies, and the framework handles injection.

Component Tree Walk → Collect IRequireAssets → Collect IComponentAssetProvider<T> → Deduplicate → Inject into <head>

Core Types

All types are in eQuantic.UI.Core.Assets.

IAsset

Base interface for all asset types.

public interface IAsset
{
    string Key { get; }      // Unique key for deduplication
    string? Id { get; }      // Optional HTML id for client-side manipulation
    string Render();         // Renders as HTML tag
}

Asset Implementations

Type Output Key Format
ScriptAsset <script src="..." defer></script> script:{Src}
InlineScriptAsset <script>...</script> inline-script:{hash}
StylesheetAsset <link rel="stylesheet" href="..."> stylesheet:{Href}
InlineStyleAsset <style>...</style> inline-style:{hash}

All types support an optional Id parameter for client-side DOM manipulation:

new StylesheetAsset("https://cdn.example.com/theme.css", Id: "theme-css")
// Renders: <link rel="stylesheet" href="https://cdn.example.com/theme.css" id="theme-css">

AssetBuilder

Fluent builder passed to components for declaring dependencies:

public class AssetBuilder
{
    AssetBuilder AddScript(string src, bool defer = true, string? id = null);
    AssetBuilder AddInlineScript(string content, string? id = null);
    AssetBuilder AddStylesheet(string href, string? id = null);
    AssetBuilder AddInlineStyle(string content, string? id = null);
}

AssetCollection

Collects and deduplicates assets. Rendering order is optimized: CSS first, then JS (proper render-blocking order).

StylesheetAsset → InlineStyleAsset → ScriptAsset → InlineScriptAsset

Deduplication uses TryAdd with the asset's Key — first registration wins.

Two Patterns

Pattern 1: IRequireAssets (Self-Declaring)

For components that own their dependencies. The component itself declares what it needs.

public class CodeBlock : StatelessComponent, IRequireAssets
{
    public void ConfigureAssets(AssetBuilder assets)
    {
        assets.AddStylesheet(
            "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
            id: "prism-theme");
        assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js");
    }
}

When to use:

  • Framework components (CodeBlock, charts, etc.)
  • Components where dependencies are intrinsic
  • The developer using the component doesn't need to know about the underlying libraries

Pattern 2: IComponentAssetProvider<T> (External Provider)

For associating assets with components externally — typically third-party components that don't implement IRequireAssets.

public class ChartJsAssetProvider : IComponentAssetProvider<ChartCanvas>
{
    public void ConfigureAssets(AssetBuilder assets)
    {
        assets.AddScript("https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js");
    }
}

Registration (choose one):

// Option 1: Auto-scan (discovered automatically in scanned assemblies)
options.ScanAssembly(typeof(Program).Assembly);

// Option 2: Explicit registration via UIOptions
options.WithAssetProvider<ChartJsAssetProvider>();

// Option 3: Manual DI registration
services.AddSingleton<IComponentAssetProvider<ChartCanvas>, ChartJsAssetProvider>();

When to use:

  • Third-party components that you can't modify
  • App-level overrides for framework component assets
  • Components from external packages

Priority

When both patterns exist for the same component type:

  1. IRequireAssets executes first (component default)
  2. IComponentAssetProvider<T> executes second (external provider)

Deduplication is by Key with first-wins semantics. If you need the provider to override a component's default asset, use a different Key (e.g., different URL).

How It Works (SSR Pipeline)

During Server-Side Rendering, ServerRenderingService walks the component tree:

RenderPageAsync()
    ├── Create AssetCollection
    ├── CollectAssets(rootComponent, assets, services, visited)
    │     ├── Check IRequireAssets → ConfigureAssets()
    │     ├── Check DI for IComponentAssetProvider<T> → ConfigureAssets()
    │     ├── Recurse into component.Children
    │     └── For StatelessComponent → Build() → recurse result
    ├── Render HTML
    └── Return ServerRenderResult with Assets

Assets are then merged into HtmlShellOptions.HeadTags before serving the page.

Key behaviors:

  • Per-type deduplication: Each component type is processed once (via HashSet<Type>)
  • Per-asset deduplication: Each asset key is registered once (via Dictionary.TryAdd)
  • Zero overhead: Pages without asset-requiring components get no extra tags
  • Graceful fallback: If Build() fails (e.g., missing DI), the component is skipped

Auto-Registration

IComponentAssetProvider<T> implementations are auto-discovered during AddUI():

builder.Services.AddUI(options =>
{
    options.ScanAssembly(typeof(Program).Assembly);  // Auto-finds providers here
});

The scan looks for all non-abstract classes implementing IComponentAssetProvider<T> in the scanned assemblies and registers them as singletons via TryAddSingleton.

Explicit registrations via WithAssetProvider<T>() take priority over auto-scanned ones (registered first).

Real-World Example: CodeBlock

The CodeBlock component demonstrates the full pattern:

public class CodeBlock : StatelessComponent, IRequireAssets
{
    public void ConfigureAssets(AssetBuilder assets)
    {
        // Stylesheet with id for dynamic theme switching
        assets.AddStylesheet(
            "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
            id: "prism-theme");

        // Core Prism.js scripts
        assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js");
        assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js");

        // Utility functions + dark/light theme auto-switching
        assets.AddInlineScript(
            "function copyToClipboard(id){...}" +
            "function toggleCodeBlock(id){...}" +
            "(function(){" +
                "function updateTheme(){...}" +    // Switches between prism.css and prism-tomorrow.css
                "updateTheme();" +
                "new MutationObserver(function(){updateTheme()})" +
                    ".observe(document.documentElement,{attributes:true,attributeFilter:['class']});" +
            "})();"
        );
    }
}

The developer just uses new CodeBlock(code, "csharp") — Prism.js scripts, CSS, theme switching, and utility functions are all handled automatically.

Clone this wiki locally