[Blazor] Render Blazor Webassembly components from MVC (#25203)

* Server pieces

* Changes for prerendering

* Discover client components

* tmp

* Cleanup

* Cleanups

* Undo changes

* Remove unwanted changes

* Move interop class to its own file

* Cleanup unwanted changes

* Add test rendering multiple client-side components

* Unit tests and E2E tests

* Cleanups

* Addressed feedback

* Rename Client to WebAssembly in RenderMode

* Update generated js files

* Cleaned up JS and addressed feedback

* Client->WebAssembly and other feedback

* Unify component discovery code and use webassembly instead of 'client'

* Update js files

* Fix tests
This commit is contained in:
Javier Calvarro Nelson 2020-08-25 18:30:24 +02:00 committed by GitHub
parent 78a587b02e
commit 402dc41d33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1301 additions and 278 deletions

View File

@ -59,13 +59,13 @@ namespace Microsoft.AspNetCore.Components.Server
{
private readonly IDataProtector _dataProtector;
private readonly ILogger<ServerComponentDeserializer> _logger;
private readonly ServerComponentTypeCache _rootComponentTypeCache;
private readonly RootComponentTypeCache _rootComponentTypeCache;
private readonly ComponentParameterDeserializer _parametersDeserializer;
public ServerComponentDeserializer(
IDataProtectionProvider dataProtectionProvider,
ILogger<ServerComponentDeserializer> logger,
ServerComponentTypeCache rootComponentTypeCache,
RootComponentTypeCache rootComponentTypeCache,
ComponentParameterDeserializer parametersDeserializer)
{
// When we protect the data we use a time-limited data protector with the

View File

@ -57,7 +57,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<StaticFileOptions>, ConfigureStaticFilesOptions>());
services.TryAddSingleton<CircuitFactory>();
services.TryAddSingleton<ServerComponentDeserializer>();
services.TryAddSingleton<ServerComponentTypeCache>();
services.TryAddSingleton<RootComponentTypeCache>();
services.TryAddSingleton<ComponentParameterDeserializer>();
services.TryAddSingleton<ComponentParametersTypeCache>();
services.TryAddSingleton<CircuitIdFactory>();

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Description>Runtime server features for ASP.NET Core Components.</Description>
@ -54,6 +54,8 @@
<Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="Circuits" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
<Compile Include="..\..\Shared\src\BrowserNavigationManagerInterop.cs" />
<Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />

View File

@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return new ServerComponentDeserializer(
_ephemeralDataProtectionProvider,
NullLogger<ServerComponentDeserializer>.Instance,
new ServerComponentTypeCache(),
new RootComponentTypeCache(),
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
}

View File

@ -9,7 +9,7 @@ using System.Reflection;
namespace Microsoft.AspNetCore.Components
{
// A cache for root component types
internal class ServerComponentTypeCache
internal class RootComponentTypeCache
{
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
@ -39,14 +39,14 @@ namespace Microsoft.AspNetCore.Components
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
}
private struct Key : IEquatable<Key>
private readonly struct Key : IEquatable<Key>
{
public Key(string assembly, string type) =>
(Assembly, Type) = (assembly, type);
public string Assembly { get; set; }
public string Assembly { get; }
public string Type { get; set; }
public string Type { get; }
public override bool Equals(object obj) => Equals((Key)obj);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,11 +7,12 @@ import { shouldAutoStart } from './BootCommon';
import { RenderQueue } from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel, Logger } from './Platform/Logging/Logger';
import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
import { discoverComponents, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
let renderingFailed = false;
let started = false;
@ -29,7 +30,7 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
const components = discoverComponents(document);
const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
const circuit = new CircuitDescriptor(components);
const initialConnection = await initializeConnection(options, logger, circuit);

View File

@ -2,7 +2,7 @@ import { DotNet } from '@microsoft/dotnet-js-interop';
import './GlobalExports';
import * as Environment from './Environment';
import { monoPlatform } from './Platform/Mono/MonoPlatform';
import { renderBatch, getRendererer } from './Rendering/Renderer';
import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer';
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
import { shouldAutoStart } from './BootCommon';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
@ -11,6 +11,8 @@ import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
import { BootConfigResult } from './Platform/BootConfig';
import { Pointer } from './Platform/Platform';
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
import { discoverComponents, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
let started = false;
@ -71,8 +73,31 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
const environment = options?.environment;
// Fetch the resources and prepare the Mono runtime
const bootConfigResult = await BootConfigResult.initAsync(environment);
const bootConfigPromise = BootConfigResult.initAsync(environment);
// Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on
// the document.
const discoveredComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
const componentAttacher = new WebAssemblyComponentAttacher(discoveredComponents);
window['Blazor']._internal.registeredComponents = {
getRegisteredComponentsCount: () => componentAttacher.getCount(),
getId: (index) => componentAttacher.getId(index),
getAssembly: (id) => BINDING.js_string_to_mono_string(componentAttacher.getAssembly(id)),
getTypeName: (id) => BINDING.js_string_to_mono_string(componentAttacher.getTypeName(id)),
getParameterDefinitions: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterDefinitions(id) || ''),
getParameterValues: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterValues(id) || ''),
};
window['Blazor']._internal.attachRootComponentToElement = (selector, componentId, rendererId) => {
const element = componentAttacher.resolveRegisteredElement(selector);
if (!element) {
attachRootComponentToElement(selector, componentId, rendererId);
} else {
attachRootComponentToLogicalElement(rendererId, element, componentId);
}
};
const bootConfigResult = await bootConfigPromise;
const [resourceLoader] = await Promise.all([
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);

View File

@ -8,7 +8,6 @@ window['Blazor'] = {
navigateTo,
_internal: {
attachRootComponentToElement,
navigationManager: navigationManagerInternalFunctions,
domWrapper: domFunctions,
Virtualize,

View File

@ -1,12 +1,13 @@
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
import { ServerComponentDescriptor } from '../../Services/ComponentDescriptorDiscovery';
export class CircuitDescriptor {
public circuitId?: string;
public components: ComponentDescriptor[];
public components: ServerComponentDescriptor[];
public constructor(components: ComponentDescriptor[]) {
public constructor(components: ServerComponentDescriptor[]) {
this.circuitId = undefined;
this.components = components;
}
@ -54,221 +55,3 @@ export class CircuitDescriptor {
}
}
interface ComponentMarker {
type: string;
sequence: number;
descriptor: string;
}
export class ComponentDescriptor {
public type: string;
public start: Node;
public end?: Node;
public sequence: number;
public descriptor: string;
public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
this.type = type;
this.start = start;
this.end = end;
this.sequence = sequence;
this.descriptor = descriptor;
}
public toRecord(): ComponentMarker {
const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
return result;
}
}
export function discoverComponents(document: Document): ComponentDescriptor[] {
const componentComments = resolveComponentComments(document);
const discoveredComponents: ComponentDescriptor[] = [];
for (let i = 0; i < componentComments.length; i++) {
const componentComment = componentComments[i];
const entry = new ComponentDescriptor(
componentComment.type,
componentComment.start,
componentComment.end,
componentComment.sequence,
componentComment.descriptor,
);
discoveredComponents.push(entry);
}
return discoveredComponents.sort((a, b) => a.sequence - b.sequence);
}
interface ComponentComment {
type: 'server';
sequence: number;
descriptor: string;
start: Node;
end?: Node;
prerenderId?: string;
}
function resolveComponentComments(node: Node): ComponentComment[] {
if (!node.hasChildNodes()) {
return [];
}
const result: ComponentComment[] = [];
const childNodeIterator = new ComponentCommentIterator(node.childNodes);
while (childNodeIterator.next() && childNodeIterator.currentElement) {
const componentComment = getComponentComment(childNodeIterator);
if (componentComment) {
result.push(componentComment);
} else {
const childResults = resolveComponentComments(childNodeIterator.currentElement);
for (let j = 0; j < childResults.length; j++) {
const childResult = childResults[j];
result.push(childResult);
}
}
}
return result;
}
const blazorCommentRegularExpression = /\W*Blazor:[^{]*(.*)$/;
function getComponentComment(commentNodeIterator: ComponentCommentIterator): ComponentComment | undefined {
const candidateStart = commentNodeIterator.currentElement;
if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
return;
}
if (candidateStart.textContent) {
const componentStartComment = new RegExp(blazorCommentRegularExpression);
const definition = componentStartComment.exec(candidateStart.textContent);
const json = definition && definition[1];
if (json) {
try {
return createComponentComment(json, candidateStart, commentNodeIterator);
} catch (error) {
throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
}
} else {
return;
}
}
}
function createComponentComment(json: string, start: Node, iterator: ComponentCommentIterator): ComponentComment {
const payload = JSON.parse(json) as ComponentComment;
const { type, sequence, descriptor, prerenderId } = payload;
if (type !== 'server') {
throw new Error(`Invalid component type '${type}'.`);
}
if (!descriptor) {
throw new Error('descriptor must be defined when using a descriptor.');
}
if (sequence === undefined) {
throw new Error('sequence must be defined when using a descriptor.');
}
if (!Number.isInteger(sequence)) {
throw new Error(`Error parsing the sequence '${sequence}' for component '${json}'`);
}
if (!prerenderId) {
return {
type,
sequence: sequence,
descriptor,
start,
};
} else {
const end = getComponentEndComment(prerenderId, iterator);
if (!end) {
throw new Error(`Could not find an end component comment for '${start}'`);
}
return {
type,
sequence,
descriptor,
start,
prerenderId,
end,
};
}
}
function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
while (iterator.next() && iterator.currentElement) {
const node = iterator.currentElement;
if (node.nodeType !== Node.COMMENT_NODE) {
continue;
}
if (!node.textContent) {
continue;
}
const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
const json = definition && definition[1];
if (!json) {
continue;
}
validateEndComponentPayload(json, prerenderedId);
return node;
}
return undefined;
}
function validateEndComponentPayload(json: string, prerenderedId: string): void {
const payload = JSON.parse(json) as ComponentComment;
if (Object.keys(payload).length !== 1) {
throw new Error(`Invalid end of component comment: '${json}'`);
}
const prerenderedEndId = payload.prerenderId;
if (!prerenderedEndId) {
throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
}
if (prerenderedEndId !== prerenderedId) {
throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
}
}
class ComponentCommentIterator {
private childNodes: NodeListOf<ChildNode>;
private currentIndex: number;
private length: number;
public currentElement: ChildNode | undefined;
public constructor(childNodes: NodeListOf<ChildNode>) {
this.childNodes = childNodes;
this.currentIndex = -1;
this.length = childNodes.length;
}
public next(): boolean {
this.currentIndex++;
if (this.currentIndex < this.length) {
this.currentElement = this.childNodes[this.currentIndex];
return true;
} else {
this.currentElement = undefined;
return false;
}
}
}

View File

@ -0,0 +1,51 @@
import { LogicalElement, toLogicalRootCommentElement } from '../Rendering/LogicalElements';
import { WebAssemblyComponentDescriptor } from '../Services/ComponentDescriptorDiscovery';
export class WebAssemblyComponentAttacher {
public preregisteredComponents: WebAssemblyComponentDescriptor[];
private componentsById: { [index: number]: WebAssemblyComponentDescriptor };
public constructor(components: WebAssemblyComponentDescriptor[]) {
this.preregisteredComponents = components;
const componentsById = {};
for (let index = 0; index < components.length; index++) {
const component = components[index];
componentsById[component.id] = component;
}
this.componentsById = componentsById;
}
public resolveRegisteredElement(id: string): LogicalElement | undefined {
const parsedId = Number.parseInt(id);
if (!Number.isNaN(parsedId)) {
return toLogicalRootCommentElement(this.componentsById[parsedId].start as Comment, this.componentsById[parsedId].end as Comment);
} else {
return undefined;
}
}
public getParameterValues(id: number): string | undefined {
return this.componentsById[id].parameterValues;
}
public getParameterDefinitions(id: number): string | undefined {
return this.componentsById[id].parameterDefinitions;
}
public getTypeName(id: number): string {
return this.componentsById[id].typeName;
}
public getAssembly(id: number): string {
return this.componentsById[id].assembly;
}
public getId(index: number): number {
return this.preregisteredComponents[index].id;
}
public getCount(): number {
return this.preregisteredComponents.length;
}
}

View File

@ -0,0 +1,354 @@
export function discoverComponents(document: Document, type: 'webassembly' | 'server'): ServerComponentDescriptor[] | WebAssemblyComponentDescriptor[] {
switch (type){
case 'webassembly':
return discoverWebAssemblyComponents(document);
case 'server':
return discoverServerComponents(document);
}
}
function discoverServerComponents(document: Document): ServerComponentDescriptor[] {
const componentComments = resolveComponentComments(document, 'server') as ServerComponentComment[];
const discoveredComponents: ServerComponentDescriptor[] = [];
for (let i = 0; i < componentComments.length; i++) {
const componentComment = componentComments[i];
const entry = new ServerComponentDescriptor(
componentComment.type,
componentComment.start,
componentComment.end,
componentComment.sequence,
componentComment.descriptor,
);
discoveredComponents.push(entry);
}
return discoveredComponents.sort((a, b): number => a.sequence - b.sequence);
}
function discoverWebAssemblyComponents(document: Document): WebAssemblyComponentDescriptor[] {
const componentComments = resolveComponentComments(document, 'webassembly') as WebAssemblyComponentDescriptor[];
const discoveredComponents: WebAssemblyComponentDescriptor[] = [];
for (let i = 0; i < componentComments.length; i++) {
const componentComment = componentComments[i];
const entry = new WebAssemblyComponentDescriptor(
componentComment.type,
componentComment.start,
componentComment.end,
componentComment.assembly,
componentComment.typeName,
componentComment.parameterDefinitions,
componentComment.parameterValues,
);
discoveredComponents.push(entry);
}
return discoveredComponents.sort((a, b): number => a.id - b.id);
}
interface ComponentComment {
type: 'server' | 'webassembly';
prerenderId?: string;
}
interface ServerComponentComment {
type: 'server';
sequence: number;
descriptor: string;
start: Node;
end?: Node;
prerenderId?: string;
}
interface WebAssemblyComponentComment {
type: 'webassembly';
typeName: string;
assembly: string;
parameterDefinitions?: string;
parameterValues?: string;
prerenderId?: string;
start: Node;
end?: Node;
}
function resolveComponentComments(node: Node, type: 'webassembly' | 'server'): ComponentComment[] {
if (!node.hasChildNodes()) {
return [];
}
const result: ComponentComment[] = [];
const childNodeIterator = new ComponentCommentIterator(node.childNodes);
while (childNodeIterator.next() && childNodeIterator.currentElement) {
const componentComment = getComponentComment(childNodeIterator, type);
if (componentComment) {
result.push(componentComment);
} else {
const childResults = resolveComponentComments(childNodeIterator.currentElement, type);
for (let j = 0; j < childResults.length; j++) {
const childResult = childResults[j];
result.push(childResult);
}
}
}
return result;
}
const blazorCommentRegularExpression = /\W*Blazor:[^{]*(?<descriptor>.*)$/;
function getComponentComment(commentNodeIterator: ComponentCommentIterator, type: 'webassembly' | 'server'): ComponentComment | undefined {
const candidateStart = commentNodeIterator.currentElement;
if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
return;
}
if (candidateStart.textContent) {
const componentStartComment = new RegExp(blazorCommentRegularExpression);
const definition = componentStartComment.exec(candidateStart.textContent);
const json = definition && definition.groups && definition.groups['descriptor'];
if (json) {
try {
const componentComment = parseCommentPayload(json);
switch (type) {
case 'webassembly':
return createWebAssemblyComponentComment(componentComment as WebAssemblyComponentComment, candidateStart, commentNodeIterator);
case 'server':
return createServerComponentComment(componentComment as ServerComponentComment, candidateStart, commentNodeIterator);
}
} catch (error) {
throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
}
} else {
return;
}
}
}
function parseCommentPayload(json: string): ComponentComment {
const payload = JSON.parse(json) as ComponentComment;
const { type } = payload;
if (type !== 'server' && type !== 'webassembly') {
throw new Error(`Invalid component type '${type}'.`);
}
return payload;
}
function createServerComponentComment(payload: ServerComponentComment, start: Node, iterator: ComponentCommentIterator): ServerComponentComment | undefined {
const { type, descriptor, sequence, prerenderId } = payload;
if (type !== 'server') {
return undefined;
}
if (!descriptor) {
throw new Error('descriptor must be defined when using a descriptor.');
}
if (sequence === undefined) {
throw new Error('sequence must be defined when using a descriptor.');
}
if (!Number.isInteger(sequence)) {
throw new Error(`Error parsing the sequence '${sequence}' for component '${JSON.stringify(payload)}'`);
}
if (!prerenderId) {
return {
type,
sequence: sequence,
descriptor,
start,
};
} else {
const end = getComponentEndComment(prerenderId, iterator);
if (!end) {
throw new Error(`Could not find an end component comment for '${start}'`);
}
return {
type,
sequence,
descriptor,
start,
prerenderId,
end,
};
}
}
function createWebAssemblyComponentComment(payload: WebAssemblyComponentComment, start: Node, iterator: ComponentCommentIterator): WebAssemblyComponentComment | undefined {
const { type, assembly, typeName, parameterDefinitions, parameterValues, prerenderId } = payload;
if (type !== 'webassembly') {
return undefined;
}
if (!assembly) {
throw new Error('assembly must be defined when using a descriptor.');
}
if (!typeName) {
throw new Error('typeName must be defined when using a descriptor.');
}
if (!prerenderId) {
return {
type,
assembly,
typeName,
// Parameter definitions and values come Base64 encoded from the server, since they contain random data and can make the
// comment invalid. We could unencode them in .NET Code, but that would be slower to do and we can leverage the fact that
// JS provides a native function that will be much faster and that we are doing this work while we are fetching
// blazor.boot.json
parameterDefinitions: parameterDefinitions && atob(parameterDefinitions),
parameterValues: parameterValues && atob(parameterValues),
start,
};
} else {
const end = getComponentEndComment(prerenderId, iterator);
if (!end) {
throw new Error(`Could not find an end component comment for '${start}'`);
}
return {
type,
assembly,
typeName,
// Same comment as above.
parameterDefinitions: parameterDefinitions && atob(parameterDefinitions),
parameterValues: parameterValues && atob(parameterValues),
start,
prerenderId,
end,
};
}
}
function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
while (iterator.next() && iterator.currentElement) {
const node = iterator.currentElement;
if (node.nodeType !== Node.COMMENT_NODE) {
continue;
}
if (!node.textContent) {
continue;
}
const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
const json = definition && definition[1];
if (!json) {
continue;
}
validateEndComponentPayload(json, prerenderedId);
return node;
}
return undefined;
}
function validateEndComponentPayload(json: string, prerenderedId: string): void {
const payload = JSON.parse(json) as ComponentComment;
if (Object.keys(payload).length !== 1) {
throw new Error(`Invalid end of component comment: '${json}'`);
}
const prerenderedEndId = payload.prerenderId;
if (!prerenderedEndId) {
throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
}
if (prerenderedEndId !== prerenderedId) {
throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
}
}
class ComponentCommentIterator {
private childNodes: NodeListOf<ChildNode>;
private currentIndex: number;
private length: number;
public currentElement: ChildNode | undefined;
public constructor(childNodes: NodeListOf<ChildNode>) {
this.childNodes = childNodes;
this.currentIndex = -1;
this.length = childNodes.length;
}
public next(): boolean {
this.currentIndex++;
if (this.currentIndex < this.length) {
this.currentElement = this.childNodes[this.currentIndex];
return true;
} else {
this.currentElement = undefined;
return false;
}
}
}
interface ServerComponentMarker {
type: string;
sequence: number;
descriptor: string;
}
export class ServerComponentDescriptor {
public type: string;
public start: Node;
public end?: Node;
public sequence: number;
public descriptor: string;
public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
this.type = type;
this.start = start;
this.end = end;
this.sequence = sequence;
this.descriptor = descriptor;
}
public toRecord(): ServerComponentMarker {
const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
return result;
}
}
export class WebAssemblyComponentDescriptor {
private static globalId = 1;
public type: 'webassembly';
public typeName: string;
public assembly: string;
public parameterDefinitions?: string;
public parameterValues?: string;
public id: number;
public start: Node;
public end?: Node;
public constructor(type: 'webassembly', start: Node, end: Node | undefined, assembly: string, typeName: string, parameterDefinitions?: string, parameterValues?: string) {
this.id = WebAssemblyComponentDescriptor.globalId++;
this.type = type;
this.assembly = assembly;
this.typeName = typeName;
this.parameterDefinitions = parameterDefinitions;
this.parameterValues = parameterValues;
this.start = start;
this.end = end;
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
{
internal class RegisteredComponentsInterop
{
private static readonly string Prefix = "Blazor._internal.registeredComponents.";
public static readonly string GetRegisteredComponentsCount = Prefix + "getRegisteredComponentsCount";
public static readonly string GetId = Prefix + "getId";
public static readonly string GetAssembly = Prefix + "getAssembly";
public static readonly string GetTypeName = Prefix + "getTypeName";
public static readonly string GetParameterDefinitions = Prefix + "getParameterDefinitions";
public static readonly string GetParameterValues = Prefix + "getParameterValues";
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// and <paramref name="selector"/>.
/// </summary>
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector.</param>
/// <param name="selector">The DOM element selector or component registration id for the component.</param>
public RootComponentMapping(Type componentType, string selector)
{
if (componentType is null)
@ -38,6 +39,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
ComponentType = componentType;
Selector = selector;
Parameters = ParameterView.Empty;
}
/// <summary>
/// Creates a new instance of <see cref="RootComponentMapping"/> with the provided <paramref name="componentType"/>
/// and <paramref name="selector"/>.
/// </summary>
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector or registration id for the component.</param>
/// <param name="parameters">The parameters to pass to the component.</param>
public RootComponentMapping(Type componentType, string selector, ParameterView parameters) : this(componentType, selector)
{
Parameters = parameters;
}
/// <summary>
@ -49,5 +63,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// Gets the DOM element selector.
/// </summary>
public string Selector { get; }
/// <summary>
/// Gets the parameters to pass to the root component.
/// </summary>
public ParameterView Parameters { get; }
}
}

View File

@ -34,6 +34,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector.</param>
public void Add(Type componentType, string selector)
{
Add(componentType, selector, ParameterView.Empty);
}
/// <summary>
/// Adds a component mapping to the collection.
/// </summary>
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector.</param>
/// <param name="parameters">The parameters to the root component.</param>
public void Add(Type componentType, string selector, ParameterView parameters)
{
if (componentType is null)
{
@ -45,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
throw new ArgumentNullException(nameof(selector));
}
Add(new RootComponentMapping(componentType, selector));
Add(new RootComponentMapping(componentType, selector, parameters));
}
/// <summary>

View File

@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
for (var i = 0; i < rootComponents.Length; i++)
{
var rootComponent = rootComponents[i];
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector);
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector, rootComponent.Parameters);
}
await tcs.Task;

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
public sealed class WebAssemblyHostBuilder
{
private Func<IServiceProvider> _createServiceProvider;
private RootComponentTypeCache _rootComponentCache;
/// <summary>
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
// Retrieve required attributes from JSRuntimeInvoker
InitializeNavigationManager(jsRuntimeInvoker);
InitializeRegisteredRootComponents(jsRuntimeInvoker);
InitializeDefaultServices();
var hostEnvironment = InitializeEnvironment(jsRuntimeInvoker);
@ -68,6 +70,38 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
};
}
private void InitializeRegisteredRootComponents(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
{
var componentsCount = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, int>(RegisteredComponentsInterop.GetRegisteredComponentsCount, null, null, null);
if (componentsCount == 0)
{
return;
}
var registeredComponents = new WebAssemblyComponentMarker[componentsCount];
for (var i = 0; i < componentsCount; i++)
{
var id = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, int>(RegisteredComponentsInterop.GetId, i, null, null);
var assembly = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetAssembly, id, null, null);
var typeName = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetTypeName, id, null, null);
var serializedParameterDefinitions = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetParameterDefinitions, id, null, null);
var serializedParameterValues = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetParameterValues, id, null, null);
registeredComponents[i] = new WebAssemblyComponentMarker(WebAssemblyComponentMarker.ClientMarkerType, assembly, typeName, serializedParameterDefinitions, serializedParameterValues, id.ToString());
}
var componentDeserializer = WebAssemblyComponentParameterDeserializer.Instance;
foreach (var registeredComponent in registeredComponents)
{
_rootComponentCache = new RootComponentTypeCache();
var componentType = _rootComponentCache.GetRootComponent(registeredComponent.Assembly, registeredComponent.TypeName);
var definitions = componentDeserializer.GetParameterDefinitions(registeredComponent.ParameterDefinitions);
var values = componentDeserializer.GetParameterValues(registeredComponent.ParameterValues);
var parameters = componentDeserializer.DeserializeParameters(definitions, values);
RootComponents.Add(componentType, registeredComponent.PrerenderId, parameters);
}
}
private void InitializeNavigationManager(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
{
var baseUri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetBaseUri, null, null, null);
@ -190,7 +224,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
Services.AddLogging(builder => {
Services.AddLogging(builder =>
{
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
});
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@ -15,13 +15,7 @@
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.JSInterop.WebAssembly" />
<ProjectReference
Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj"
ReferenceOutputAssemblies="false"
SkipGetTargetFrameworkProperties="true"
UndefineProperties="TargetFramework"
Private="false"
Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
<ProjectReference Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj" ReferenceOutputAssemblies="false" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework" Private="false" Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
<SuppressBaselineReference Include="Microsoft.AspNetCore.Components.WebAssembly.HttpHandler" />
</ItemGroup>
@ -31,6 +25,11 @@
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\WebEventData.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" Link="Prerendering/WebAssemblyComponentSerializationSettings.cs" />
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" Link="Prerendering/WebAssemblyComponentMarker.cs" />
<Compile Include="$(SharedSourceRoot)Components\ComponentParameter.cs" Link="Prerendering/ComponentParameter.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,88 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.AspNetCore.Components
{
internal class WebAssemblyComponentParameterDeserializer
{
private readonly ComponentParametersTypeCache _parametersCache;
public WebAssemblyComponentParameterDeserializer(
ComponentParametersTypeCache parametersCache)
{
_parametersCache = parametersCache;
}
public static WebAssemblyComponentParameterDeserializer Instance { get; } = new WebAssemblyComponentParameterDeserializer(new ComponentParametersTypeCache());
public ParameterView DeserializeParameters(IList<ComponentParameter> parametersDefinitions, IList<object> parameterValues)
{
var parametersDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (parameterValues.Count != parametersDefinitions.Count)
{
// Mismatched number of definition/parameter values.
throw new InvalidOperationException($"The number of parameter definitions '{parametersDefinitions.Count}' does not match the number parameter values '{parameterValues.Count}'.");
}
for (var i = 0; i < parametersDefinitions.Count; i++)
{
var definition = parametersDefinitions[i];
if (definition.Name == null)
{
throw new InvalidOperationException("The name is missing in a parameter definition.");
}
if (definition.TypeName == null && definition.Assembly == null)
{
parametersDictionary[definition.Name] = null;
}
else if (definition.TypeName == null || definition.Assembly == null)
{
throw new InvalidOperationException($"The parameter definition for '{definition.Name}' is incomplete: Type='{definition.TypeName}' Assembly='{definition.Assembly}'.");
}
else
{
var parameterType = _parametersCache.GetParameterType(definition.Assembly, definition.TypeName);
if (parameterType == null)
{
throw new InvalidOperationException($"The parameter '{definition.Name} with type '{definition.TypeName}' in assembly '{definition.Assembly}' could not be found.");
}
try
{
var value = (JsonElement)parameterValues[i];
var parameterValue = JsonSerializer.Deserialize(
value.GetRawText(),
parameterType,
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
parametersDictionary[definition.Name] = parameterValue;
}
catch (Exception e)
{
throw new InvalidOperationException("Could not parse the parameter value for parameter '{definition.Name}' of type '{definition.TypeName}' and assembly '{definition.Assembly}'.", e);
}
}
}
return ParameterView.FromDictionary(parametersDictionary);
}
public ComponentParameter[] GetParameterDefinitions(string parametersDefinitions)
{
return JsonSerializer.Deserialize<ComponentParameter[]>(parametersDefinitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
}
public IList<object> GetParameterValues(string parameterValues)
{
return JsonSerializer.Deserialize<IList<object>>(parameterValues, WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
}
}
}

View File

@ -47,13 +47,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
/// </summary>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
/// <param name="parameters">The parameters for the component.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
/// <remarks>
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
/// want to await the rendering of the added component.
/// </remarks>
public Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : IComponent
=> AddComponentAsync(typeof(TComponent), domElementSelector);
public Task AddComponentAsync<TComponent>(string domElementSelector, ParameterView parameters) where TComponent : IComponent
=> AddComponentAsync(typeof(TComponent), domElementSelector, parameters);
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
@ -61,12 +62,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
/// <param name="parameters">The list of root component parameters.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
/// <remarks>
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
/// want to await the rendering of the added component.
/// </remarks>
public Task AddComponentAsync(Type componentType, string domElementSelector)
public Task AddComponentAsync(Type componentType, string domElementSelector, ParameterView parameters)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
@ -83,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
componentId,
_webAssemblyRendererId);
return RenderRootComponentAsync(componentId);
return RenderRootComponentAsync(componentId, parameters);
}
/// <inheritdoc />

View File

@ -29,6 +29,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
case "Blazor._internal.navigationManager.getUnmarshalledLocationHref":
var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
return (TResult)(object)testHref;
case "Blazor._internal.registeredComponents.getRegisteredComponentsCount":
return (TResult)(object)0;
default:
throw new NotImplementedException($"{nameof(TestWebAssemblyJSRuntimeInvoker)} has no implementation for '{identifier}'.");
}

View File

@ -56,6 +56,8 @@
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\WebAssemblyComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\WebAssemblyComponentMarker.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,95 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETests.Tests
{
public class ClientRenderingMultpleComponentsTest : ServerTestBase<BasicTestAppServerSiteFixture<MultipleComponents>>
{
private const string MarkerPattern = ".*?<!--Blazor:(.*?)-->.*?";
public ClientRenderingMultpleComponentsTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<MultipleComponents> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
public DateTime LastLogTimeStamp { get; set; } = DateTime.MinValue;
public override async Task InitializeAsync()
{
await base.InitializeAsync();
// Capture the last log timestamp so that we can filter logs when we
// check for duplicate connections.
var lastLog = Browser.Manage().Logs.GetLog(LogType.Browser).LastOrDefault();
if (lastLog != null)
{
LastLogTimeStamp = lastLog.Timestamp;
}
}
[Fact]
public void CanRenderMultipleRootComponents()
{
Navigate("/Client/multiple-components");
var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(7, greets.Length); // 1 statically rendered + 5 prerendered + 1 server prerendered
Assert.DoesNotContain("Hello Red fish", greets);
Assert.Single(greets, "Hello John");
Assert.Single(greets, "Hello Abraham");
Assert.Equal(2, greets.Where(g => g == "Hello Blue fish").Count());
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 server prerendered without parameters
var content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
var markers = ReadMarkers(content);
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
Assert.Equal(13, componentSequence.Length);
// Once the app starts, output changes
BeginInteractivity();
Browser.Exists(By.CssSelector("h3.interactive"));
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(7, updatedGreets.Where(g => string.Equals("Hello Alfred", g)).Count());
Assert.Equal(2, updatedGreets.Where(g => g == "Hello Red fish").Count());
Assert.Equal(2, updatedGreets.Where(g => g == "Hello Blue fish").Count());
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Albert", g)));
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Abraham", g)));
}
private (WebAssemblyComponentMarker, WebAssemblyComponentMarker)[] ReadMarkers(string content)
{
content = content.Replace("\r\n", "");
var matches = Regex.Matches(content, MarkerPattern);
var markers = matches.Select(s => JsonSerializer.Deserialize<WebAssemblyComponentMarker>(
s.Groups[1].Value,
WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
var prerenderMarkers = markers.Where(m => m.PrerenderId != null).GroupBy(p => p.PrerenderId).Select(g => (g.First(), g.Skip(1).First())).ToArray();
var nonPrerenderMarkers = markers.Where(m => m.PrerenderId == null).Select(g => (g, (WebAssemblyComponentMarker)default)).ToArray();
return prerenderMarkers.Concat(nonPrerenderMarkers).ToArray();
}
private void BeginInteractivity()
{
Browser.FindElement(By.Id("load-boot-script")).Click();
}
}
}

View File

@ -32,6 +32,18 @@ namespace TestServer
app.UseDeveloperExceptionPage();
}
app.Map("/Client/multiple-components", app =>
{
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapFallbackToPage("/Client/MultipleComponents");
});
});
app.Map("/multiple-components", app =>
{
app.UseStaticFiles();

View File

@ -0,0 +1,29 @@
@page "/multiple-components"
@using BasicTestApp.MultipleComponents;
@{
Layout = "./MultipleComponentsLayout.cshtml";
}
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.WebAssemblyPrerendered))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.WebAssembly))
<component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
<div id="container">
<p>Some content before</p>
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
<p>Some content between</p>
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" />
<p>Some content after</p>
<div id="nested-an-extra-level">
<p>Some content before</p>
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" />
<p>Some content after</p>
</div>
</div>
<div id="container">
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Albert"' />
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Abraham"' />
</div>

View File

@ -0,0 +1,45 @@
@using BasicTestApp.MultipleComponents;
<!DOCTYPE html>
<html>
<head>
<title>Multiple component entry points</title>
@* We need to make sure base is set to "/" so that the libraries load correctly *@
<base href="~/" />
@* This page is used to validate the ability to render multiple root components in a blazor webassembly application.
*@
</head>
<body>
<div id="test-container">
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Red fish"' />
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Blue fish"' />
@RenderBody()
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Red fish"' />
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Blue fish"' />
</div>
@*
So that E2E tests can make assertions about both the prerendered and
interactive states, we only load the .js file when told to.
*@
<hr />
<script>
// Used unconditionally on Program.cs, so needs to be defined here to avoid the test failing
function getCurrentUrl() {
return location.href;
}
</script>
<button id="load-boot-script" onclick="start()">Load boot script</button>
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
</script>
</body>
</html>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -66,6 +66,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
case RenderMode.Server:
case RenderMode.ServerPrerendered:
case RenderMode.Static:
case RenderMode.WebAssembly:
case RenderMode.WebAssemblyPrerendered:
_renderMode = value;
break;

View File

@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
// See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
internal class WebAssemblyComponentSerializer
{
public WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, bool prerendered)
{
var assembly = type.Assembly.GetName().Name;
var typeFullName = type.FullName;
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
// We need to serialize and Base64 encode parameters separately since they can contain arbitrary data that might
// cause the HTML comment to be invalid (like if you serialize a string that contains two consecutive dashes "--").
var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues) :
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues);
}
internal IEnumerable<string> GetPreamble(WebAssemblyComponentMarker record)
{
var serializedStartRecord = JsonSerializer.Serialize(
record,
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
if (record.PrerenderId != null)
{
return PrerenderedStart(serializedStartRecord);
}
else
{
return NonPrerenderedSequence(serializedStartRecord);
}
static IEnumerable<string> PrerenderedStart(string startRecord)
{
yield return "<!--Blazor:";
yield return startRecord;
yield return "-->";
}
static IEnumerable<string> NonPrerenderedSequence(string record)
{
yield return "<!--Blazor:";
yield return record;
yield return "-->";
}
}
internal IEnumerable<string> GetEpilogue(WebAssemblyComponentMarker record)
{
var serializedStartRecord = JsonSerializer.Serialize(
record.GetEndRecord(),
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
return PrerenderEnd(serializedStartRecord);
static IEnumerable<string> PrerenderEnd(string endRecord)
{
yield return "<!--Blazor:";
yield return endRecord;
yield return "-->";
}
}
}
}

View File

@ -177,6 +177,9 @@ namespace Microsoft.Extensions.DependencyInjection
// Component services for Blazor server-side interop
services.TryAddSingleton<ServerComponentSerializer>();
// Component services for Blazor webassembly interop
services.TryAddSingleton<WebAssemblyComponentSerializer>();
//
// View Components
//

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
@ -31,7 +31,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" />
<Compile Include="$(SharedSourceRoot)Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" />
<Compile Include="$(SharedSourceRoot)Components\ServerComponentMarker.cs" />
<Compile Include="$(SharedSourceRoot)Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />

View File

@ -77,6 +77,8 @@ Microsoft.AspNetCore.Mvc.Rendering.RenderMode
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Server = 2 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.ServerPrerendered = 3 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Static = 1 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.WebAssembly = 4 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.WebAssemblyPrerendered = 5 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
Microsoft.AspNetCore.Mvc.Rendering.SelectList
Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup
Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup.Disabled.get -> bool

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -15,13 +15,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private static readonly object ComponentSequenceKey = new object();
private readonly StaticComponentRenderer _staticComponentRenderer;
private readonly ServerComponentSerializer _serverComponentSerializer;
private readonly WebAssemblyComponentSerializer _WebAssemblyComponentSerializer;
public ComponentRenderer(
StaticComponentRenderer staticComponentRenderer,
ServerComponentSerializer serverComponentSerializer)
ServerComponentSerializer serverComponentSerializer,
WebAssemblyComponentSerializer WebAssemblyComponentSerializer)
{
_staticComponentRenderer = staticComponentRenderer;
_serverComponentSerializer = serverComponentSerializer;
_WebAssemblyComponentSerializer = WebAssemblyComponentSerializer;
}
public async Task<IHtmlContent> RenderComponentAsync(
@ -55,6 +58,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
RenderMode.WebAssembly => NonPrerenderedWebAssemblyComponent(context, componentType, parameterView),
RenderMode.WebAssemblyPrerendered => await PrerenderedWebAssemblyComponentAsync(context, componentType, parameterView),
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
};
}
@ -99,12 +104,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
_serverComponentSerializer.GetEpilogue(currentInvocation));
}
private async Task<IHtmlContent> PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var currentInvocation = _WebAssemblyComponentSerializer.SerializeInvocation(
type,
parametersCollection,
prerendered: true);
var result = await _staticComponentRenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
_WebAssemblyComponentSerializer.GetPreamble(currentInvocation),
result,
_WebAssemblyComponentSerializer.GetEpilogue(currentInvocation));
}
private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation));
}
private IHtmlContent NonPrerenderedWebAssemblyComponent(HttpContext context, Type type, ParameterView parametersCollection)
{
var currentInvocation = _WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(_WebAssemblyComponentSerializer.GetPreamble(currentInvocation));
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Type componentType)
{
InitializeStandardComponentServices(httpContext);
var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof (ILoggerFactory));
var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof(ILoggerFactory));
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode))
{
ComponentRenderedText result = default;
@ -71,15 +71,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
_initialized = true;
var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService<NavigationManager>();
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request));
var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
if (authenticationStateProvider != null)
{
var authenticationState = new AuthenticationState(httpContext.User);
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
}
var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService<NavigationManager>();
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request));
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Rendering
@ -30,5 +30,16 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
ServerPrerendered = 3,
/// <summary>
/// Renders a marker for a Blazor webassembly application. This doesn't include any output from the component.
/// When the user-agent starts, it uses this marker to bootstrap a blazor client-side application.
/// </summary>
WebAssembly = 4,
/// <summary>
/// Renders the component into static HTML and includes a marker for a Blazor webassembly application.
/// When the user-agent starts, it uses this marker to bootstrap a blazor client-side application.
/// </summary>
WebAssemblyPrerendered = 5,
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;
@ -26,13 +27,239 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public class ComponentRendererTest
{
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
private const string PrerenderedComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
private const string ComponentPattern = "^<!--Blazor:(.*?)-->$";
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
private readonly ComponentRenderer renderer = GetComponentRenderer();
[Fact]
public async Task CanRender_ParameterlessComponent_ClientMode()
{
// Arrange
var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssembly, null);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Null(marker.PrerenderId);
Assert.Equal("webassembly", marker.Type);
Assert.Equal(typeof(TestComponent).Assembly.GetName().Name, marker.Assembly);
Assert.Equal(typeof(TestComponent).FullName, marker.TypeName);
}
[Fact]
public async Task CanPrerender_ParameterlessComponent_ClientMode()
{
// Arrange
var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssemblyPrerendered, null);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.Equal("webassembly", preambleMarker.Type);
Assert.Equal(typeof(TestComponent).Assembly.GetName().Name, preambleMarker.Assembly);
Assert.Equal(typeof(TestComponent).FullName, preambleMarker.TypeName);
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<h1>Hello world!</h1>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Assembly);
Assert.Null(epilogueMarker.TypeName);
Assert.Null(epilogueMarker.Type);
Assert.Null(epilogueMarker.ParameterDefinitions);
Assert.Null(epilogueMarker.ParameterValues);
}
[Fact]
public async Task CanRender_ComponentWithParameters_ClientMode()
{
// Arrange
var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
RenderMode.WebAssembly,
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Null(marker.PrerenderId);
Assert.Equal("webassembly", marker.Type);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, marker.Assembly);
Assert.Equal(typeof(GreetingComponent).FullName, marker.TypeName);
var parameterDefinition = Assert.Single(
JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(marker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
Assert.Equal("Name", parameterDefinition.Name);
Assert.Equal("System.String", parameterDefinition.TypeName);
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(marker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
var rawValue = Assert.IsType<JsonElement>(value);
Assert.Equal("Daniel", rawValue.GetString());
}
[Fact]
public async Task CanRender_ComponentWithNullParameters_ClientMode()
{
// Arrange
var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
RenderMode.WebAssembly,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Null(marker.PrerenderId);
Assert.Equal("webassembly", marker.Type);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, marker.Assembly);
Assert.Equal(typeof(GreetingComponent).FullName, marker.TypeName);
var parameterDefinition = Assert.Single(JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(marker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
Assert.Equal("Name", parameterDefinition.Name);
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(marker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
Assert.Null(value);
}
[Fact]
public async Task CanPrerender_ComponentWithParameters_ClientMode()
{
// Arrange
var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
RenderMode.WebAssemblyPrerendered,
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.Equal("webassembly", preambleMarker.Type);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, preambleMarker.Assembly);
Assert.Equal(typeof(GreetingComponent).FullName, preambleMarker.TypeName);
var parameterDefinition = Assert.Single(JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(preambleMarker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
Assert.Equal("Name", parameterDefinition.Name);
Assert.Equal("System.String", parameterDefinition.TypeName);
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(preambleMarker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
var rawValue = Assert.IsType<JsonElement>(value);
Assert.Equal("Daniel", rawValue.GetString());
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Hello Daniel!</p>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Assembly);
Assert.Null(epilogueMarker.TypeName);
Assert.Null(epilogueMarker.Type);
Assert.Null(epilogueMarker.ParameterDefinitions);
Assert.Null(epilogueMarker.ParameterValues);
}
[Fact]
public async Task CanPrerender_ComponentWithNullParameters_ClientMode()
{
// Arrange
var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
RenderMode.WebAssemblyPrerendered,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.Equal("webassembly", preambleMarker.Type);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, preambleMarker.Assembly);
Assert.Equal(typeof(GreetingComponent).FullName, preambleMarker.TypeName);
var parameterDefinition = Assert.Single(JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(preambleMarker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
Assert.Equal("Name", parameterDefinition.Name);
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(preambleMarker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
Assert.Null(value);
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Hello (null)!</p>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Assembly);
Assert.Null(epilogueMarker.TypeName);
Assert.Null(epilogueMarker.Type);
Assert.Null(epilogueMarker.ParameterDefinitions);
Assert.Null(epilogueMarker.ParameterValues);
}
[Fact]
public async Task CanRender_ParameterlessComponent()
{
@ -60,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
var match = Regex.Match(content, ComponentPattern);
// Assert
Assert.True(match.Success);
@ -89,7 +316,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
@ -130,11 +357,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Act
var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult);
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
var firstMatch = Regex.Match(firstComponent, PrerenderedComponentPattern, RegexOptions.Multiline);
var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
var secondMatch = Regex.Match(secondComponent, ComponentPattern);
// Assert
Assert.True(firstMatch.Success);
@ -186,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
var match = Regex.Match(content, ComponentPattern);
// Assert
Assert.True(match.Success);
@ -225,7 +452,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null });
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
var match = Regex.Match(content, ComponentPattern);
// Assert
Assert.True(match.Success);
@ -264,7 +491,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
@ -315,7 +542,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Act
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
@ -554,7 +781,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
</table>";
// Act
var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null);
var result = await renderer.RenderComponentAsync(viewContext, typeof(AsyncComponent), RenderMode.Static, null);
var content = HtmlContentUtilities.HtmlContentToString(result);
// Assert
@ -564,7 +791,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private static ComponentRenderer GetComponentRenderer() =>
new ComponentRenderer(
new StaticComponentRenderer(HtmlEncoder.Default),
new ServerComponentSerializer(_dataprotectorProvider));
new ServerComponentSerializer(_dataprotectorProvider),
new WebAssemblyComponentSerializer());
private static ViewContext GetViewContext(HttpContext context = null, Action<IServiceCollection> configureServices = null)
{

View File

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Components
{
internal struct WebAssemblyComponentMarker
{
public const string ClientMarkerType = "webassembly";
public WebAssemblyComponentMarker(string type, string assembly, string typeName, string parameterDefinitions, string parameterValues, string prereenderId) =>
(Type, Assembly, TypeName, ParameterDefinitions, ParameterValues, PrerenderId) = (type, assembly, typeName, parameterDefinitions, parameterValues, prereenderId);
public string Type { get; set; }
public string Assembly { get; set; }
public string TypeName { get; set; }
public string ParameterDefinitions { get; set; }
public string ParameterValues { get; set; }
public string PrerenderId { get; set; }
internal static WebAssemblyComponentMarker NonPrerendered(string assembly, string typeName, string parameterDefinitions, string parameterValues) =>
new WebAssemblyComponentMarker(ClientMarkerType, assembly, typeName, parameterDefinitions, parameterValues, null);
internal static WebAssemblyComponentMarker Prerendered(string assembly, string typeName, string parameterDefinitions, string parameterValues) =>
new WebAssemblyComponentMarker(ClientMarkerType, assembly, typeName, parameterDefinitions, parameterValues, Guid.NewGuid().ToString("N"));
public WebAssemblyComponentMarker GetEndRecord()
{
if (PrerenderId == null)
{
throw new InvalidOperationException("Can't get an end record for non-prerendered components.");
}
return new WebAssemblyComponentMarker(null, null, null, null, null, PrerenderId);
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
namespace Microsoft.AspNetCore.Components
{
internal static class WebAssemblyComponentSerializationSettings
{
public static readonly JsonSerializerOptions JsonSerializationOptions =
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
IgnoreNullValues = true
};
}
}

View File

@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v5.0", FrameworkDisplayName = "")]

View File

@ -0,0 +1,28 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyMetadataAttribute("SourceCommitUrl", "https://github.com/dotnet/aspnetcore/tree/")]
[assembly: System.Reflection.AssemblyMetadataAttribute("Serviceable", "True")]
[assembly: System.Reflection.AssemblyCompanyAttribute("Microsoft Corporation")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyCopyrightAttribute("© Microsoft Corporation. All rights reserved.")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("42.42.42.42424")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("5.0.0-dev")]
[assembly: System.Reflection.AssemblyProductAttribute("Microsoft ASP.NET Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("KitchenSink")]
[assembly: System.Reflection.AssemblyVersionAttribute("5.0.0.0")]
[assembly: System.Reflection.AssemblyMetadataAttribute("RepositoryUrl", "https://github.com/dotnet/aspnetcore")]
[assembly: System.Resources.NeutralResourcesLanguageAttribute("en-US")]
// Generated by the MSBuild WriteCodeFragment class.

View File

@ -0,0 +1 @@
5426b0432f23f134c800ff0d43b24a23681aac3d