dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.69k stars 9.81k forks source link

[Blazor] Runtime APIs to support fingerprinting #56076

Closed javiercn closed 2 days ago

javiercn commented 1 week ago

Description

These set of APIs allow Blazor, MVC and Razor pages to map well-known URLs to content-specific (fingerprinted URLs).

Scenarios

Startup/Program APIs

The list of assets is defined on a per-endpoint basis. MVC and RazorPages opt-in to the feature via a call to WithResourceCollection following the call to MapRazorPages() or MapControllers().

This call adds a ResourceAssetCollection to the Metadata collection of the endpoints, which contains the mapping between human-readable URLs and content-specific URLs.

From there, other components can retrieve the mapping from endpoint metadata.

app.MapStaticAssets();

app.MapRazorPages()
+   .WithResourceCollection();

Since we can have calls to map different manifests, WithResourceCollection takes a parameter to provide the Id for the matching MatchStaticAssets.

app.MapStaticAssets("WebassemblyApp1.Client");
app.MapStaticAssets("WebassemblyApp2.Client");

app.MapRazorPages()
+   .WithResourceCollection("WebassemblyApp1.Client");

Consuming the mapped assets

The assets are consumed indirectly from MVC and Razor Pages via existing Script, Image, Link, and Url tag helpers.

In Blazor, the assets are consumed via the Assets property in ComponentBase which exposes an indexer to resolve the fingerprinted url for a given asset.

<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["BlazorWeb-CSharp.styles.css"]" />

This in the future will be cleaner with a compiler feature similar to the Url tag helper in MVC that transforms "~/app.css" into the above code inside the href attribute.

In addition to this, there are two built-in components to generate an importmap for scripts. See here for details.

TL;DR: Creates a mapping for calls to import and script type="module" that optionally includes integrity information. A sample:

<script type="importmap">
  {
    "imports": {
      "square": "./module/shapes/square.js"
    },
    "integrity": {
      "./module/shapes/square.js": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
    }
  }
</script>

There is an ImportMap blazor component

<ImportMap />

and in MVC we extended the script tag helper to support it

<script type="importmap"></script>

Layering

I included this section to explain the reasons for some of the APIs below.

flowchart LR
    subgraph AspNet[ASP.NET Framework reference]
        StaticAssets[Static Assets]
        Endpoints[Components Endpoints SSR]
        MVCRp[Mvc and Razor Pages]
    end
    Components[Components]
    ComponentsWasm[Components Webassembly]
    ComponentsWebView[Components WebView]
    ComponentsWasm-->Components
    ComponentsWebView-->Components
    Endpoints-->Components
    Endpoints-->StaticAssets
    MVCRp-->Endpoints

Two important things:

With this in mind, information for the assets is exposed differently from StaticAssets and Components

flowchart TD
    subgraph AspNet[ASP.NET Framework reference]
        subgraph StaticAssets[Static Assets]
            StaticAssetDescriptor
            StaticAssetSelector
            StaticAssetProperty
            StaticAssetResponseHeader
        end
        Endpoints[Components Endpoints SSR]
        MVCRp[Mvc and Razor Pages]
    end
    subgraph Components[Components]
       ResourceAssetCollection
       ResourceAsset
       ResourceAssetProperty
       ImportMapDefinition
    end
    ComponentsWasm[Components Webassembly]
    ComponentsWebView[Components WebView]
    ComponentsWasm-->Components
    ComponentsWebView-->Components
    Endpoints-->Components
    Endpoints-->StaticAssets
    MVCRp-->Endpoints

The Component endpoints assembly is the one responsible for mapping the StaticAssetDescriptors into the ResourceAssetCollection so that Blazor and MVC can consume them.

Microsoft.AspNetCore.Components.dll

namespace Microsoft.AspNetCore.Components;

public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
+    protected ResourceAssetCollection Assets { get; }
}

public readonly struct RenderHandle
{
+    public ResourceAssetCollection Assets { get; }
}

public abstract partial class Renderer : IDisposable, IAsyncDisposable
{
+    protected internal virtual ResourceAssetCollection Assets { get; } = ResourceAssetCollection.Empty;
}

+public class ResourceAssetCollection : IReadOnlyList<ResourceAsset>
+{
+   public static readonly ResourceAssetCollection Empty = new([]);
+   public ResourceAssetCollection(IReadOnlyList<ResourceAsset> resources);
+   public string this[string key];
+   public bool IsContentSpecificUrl(string path);
+}

+public class ResourceAsset(string url, IReadOnlyList<ResourceAssetProperty>? properties)
+{
+   public string Url { get; } = url;
+   public IReadOnlyList<ResourceAssetProperty>? Properties { get; } = properties;
+}

+public class ResourceAssetProperty(string name, string value)
+{
+   public string Name { get; } = name;
+   public string Value { get; } = value;
+}

Microsoft.AspNetCore.Components.Endpoints

namespace Microsoft.AspNetCore.Components;

+public class ImportMap : IComponent
+{
+    [CascadingParameter] public HttpContext? HttpContext { get; set; } = null;
+    [Parameter] public ImportMapDefinition? ImportMapDefinition { get; set; }
+}

+public class ImportMapDefinition
+{
+   public ImportMapDefinition(
+        IReadOnlyDictionary<string, string>? imports,
+        IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>>? scopes,
+        IReadOnlyDictionary<string, string>? integrity)

+   public static ImportMapDefinition FromResourceCollection(ResourceAssetCollection assets);
+   public static ImportMapDefinition Combine(params ImportMapDefinition[] sources);

+   public IReadOnlyDictionary<string, string>? Imports { get; }
+   public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>>? Scopes { get; }
+   public IReadOnlyDictionary<string, string>? Integrity { get; }
+   public override string ToString();
+}

public static class RazorComponentsEndpointConventionBuilderExtensions
{
+    public static RazorComponentsEndpointConventionBuilder WithResourceCollection(
+    this RazorComponentsEndpointConventionBuilder builder,
+    string? manifestPath = null);
}

Assembly Microsoft.AspNetCore.WebUtilities

namespace Microsoft.AspNetCore.WebUtilities;

public class WebEncoders
{
+    Base64UrlEncode(System.ReadOnlySpan<byte> input, System.Span<char> output)
}

Assembly Microsoft.AspNetCore.Mvc.RazorPages

namespace Microsoft.AspNetCore.Builder;

+public static class PageActionEndpointConventionBuilderResourceCollectionExtensions
+{
+    static WithResourceCollection(this Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder! builder, string? manifestPath = null): Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder!
+}

Microsoft.AspNetCore.Mvc.ViewFeatures

namespace Microsoft.AspNetCore.Builder;

+public static class ControllerActionEndpointConventionBuilderResourceCollectionExtensions
+{
+    static WithResourceCollection(this Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder! builder, string? manifestPath = null): Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder!
+}

Assembly Microsoft.AspNetCore.Mvc.TagHelpers.dll

namespace Microsoft.AspNetCore.Mvc.TagHelpers;

public class ScriptTagHelper
{
+    public string Type { get; set; }
+    public ImportMapDefinition ImportMap { get; set; }
}

Assembly Microsoft.AspNetCore.StaticAssets.dll

namespace Microsoft.AspNetCore.StaticAssets;

+public sealed class StaticAssetDescriptor
+{
+    public required string Route { get; set; }
+    public required string AssetPath { get; set; }
+    public IReadOnlyList<StaticAssetSelector> Selectors { get; set; }
+    public IReadOnlyList<StaticAssetProperty> Properties { get; set; }
+    public IReadOnlyList<StaticAssetResponseHeader> ResponseHeaders { get; set; }
+}

+public sealed class StaticAssetResponseHeader(string name, string value)
+{
+    public string Name { get; } = name;
+    public string Value { get; } = value;
+}

+public sealed class StaticAssetProperty(string name, string value)
+{
+    public string Name { get; } = name;
+    public string Value { get; } = value;
+}

+public sealed class StaticAssetSelector(string name, string value, string quality)
+{
+   public string Name { get; } = name;
+   public string Value { get; } = value;
+    public string Quality { get; } = quality;
+}
namespace Microsoft.AspNetCore.StaticAssets.Infrastructure;

public static class StaticAssetsEndpointDataSourceHelper
{
+    public static bool HasStaticAssetsDataSource(IEndpointRouteBuilder builder, string? staticAssetsManifestPath = null);

+    public static IReadOnlyList<StaticAssetDescriptor> ResolveStaticAssetDescriptors(
        IEndpointRouteBuilder endpointRouteBuilder,
        string? manifestPath)
}
dotnet-policy-service[bot] commented 1 week ago

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

amcasey commented 1 week ago

[API Review]

Current status is that we need to figure out a good name for the main public API. We can't review the other pieces properly until we understand that.

amcasey commented 4 days ago

[API Review]