Merge branch 'release/3.1' -> 'master'

This commit is contained in:
Doug Bunting 2019-10-14 09:01:59 -07:00
commit 0dedfa95d4
No known key found for this signature in database
GPG Key ID: EE41520987982C03
111 changed files with 1854 additions and 462 deletions

View File

@ -229,7 +229,7 @@
<CastleCorePackageVersion>4.2.1</CastleCorePackageVersion>
<FSharpCorePackageVersion>4.2.1</FSharpCorePackageVersion>
<GoogleProtobufPackageVersion>3.8.0</GoogleProtobufPackageVersion>
<GrpcAspNetCorePackageVersion>2.23.1</GrpcAspNetCorePackageVersion>
<GrpcAspNetCorePackageVersion>2.23.2</GrpcAspNetCorePackageVersion>
<IdentityServer4AspNetIdentityPackageVersion>3.0.0</IdentityServer4AspNetIdentityPackageVersion>
<IdentityServer4EntityFrameworkPackageVersion>3.0.0</IdentityServer4EntityFrameworkPackageVersion>
<IdentityServer4PackageVersion>3.0.0</IdentityServer4PackageVersion>

View File

@ -21,6 +21,8 @@
<PropertyGroup>
<!-- Working around https://github.com/NuGet/Home/issues/8467 -->
<NoWarn>$(NoWarn);NU5131</NoWarn>
<!-- Workaround until https://github.com/aspnet/AspNetCore-Internal/issues/3103 is resolved -->
<NoWarn>$(NoWarn);NU5048</NoWarn>
</PropertyGroup>
<!-- Workaround https://github.com/dotnet/roslyn/issues/27975 -->

View File

@ -96,6 +96,25 @@ app {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@media (max-width: 767.98px) {
.main .top-row {
display: none;

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
@ -8,9 +9,16 @@
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<!-- Intentionally targeting netcoreapp3.0 because we do not want to update the Benchmark client to use 3.1 -->
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>true</IsPackable>
<IsShippingPackage>false</IsShippingPackage>
<HasReferenceAssembly>false</HasReferenceAssembly>

View File

@ -1,4 +1,4 @@
@page "/"
@page "/"
<h1>Hello, world!</h1>

View File

@ -13,9 +13,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<script src="_framework/blazor.server.js"></script>
</body>

View File

@ -1,4 +1,4 @@
@inherits LayoutComponentBase
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
@ -12,4 +12,10 @@
<div class="content px-4">
@Body
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a class="reload">Reload</a>
<a class="dismiss">X</a>
</div>

View File

@ -111,6 +111,23 @@ app {
color: red;
}
#blazor-error-ui {
background: lightyellow;
position: fixed;
border: "1px solid";
border-color: black;
width: 100%;
bottom: 0;
left: 0;
z-index: 1000;
}
#blazor-error-ui .dismiss {
position: absolute;
right: 5px;
top: 5px;
}
@media (max-width: 767.98px) {
.main .top-row {
display: none;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,7 @@ import '@dotnet/jsinterop';
import './GlobalExports';
import * as signalR from '@aspnet/signalr';
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
import { showErrorNotification } from './BootErrors';
import { shouldAutoStart } from './BootCommon';
import { RenderQueue } from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
@ -106,6 +107,7 @@ async function initializeConnection(options: BlazorOptions, logger: Logger, circ
connection.on('JS.Error', error => {
renderingFailed = true;
unhandledError(connection, error, logger);
showErrorNotification();
});
window['Blazor']._internal.forceCloseConnection = () => connection.stop();

View File

@ -0,0 +1,30 @@
let hasFailed = false;
export async function showErrorNotification() {
let errorUi = document.querySelector('#blazor-error-ui') as HTMLElement;
if (errorUi) {
errorUi.style.display = 'block';
}
if (!hasFailed) {
hasFailed = true;
const errorUiReloads = document.querySelectorAll<HTMLElement>('#blazor-error-ui .reload');
errorUiReloads.forEach(reload => {
reload.onclick = function (e) {
location.reload();
e.preventDefault();
};
});
let errorUiDismiss = document.querySelectorAll<HTMLElement>('#blazor-error-ui .dismiss');
errorUiDismiss.forEach(dismiss => {
dismiss.onclick = function (e) {
const errorUi = document.querySelector<HTMLElement>('#blazor-error-ui');
if (errorUi) {
errorUi.style.display = 'none';
}
e.preventDefault();
};
});
}
}

View File

@ -1,6 +1,7 @@
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform';
import { getFileNameFromUrl } from '../Url';
import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
import { showErrorNotification } from '../../BootErrors';
const assemblyHandleCache: { [assemblyName: string]: number } = {};
const typeHandleCache: { [fullyQualifiedTypeName: string]: number } = {};
@ -232,7 +233,11 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
const suppressMessages = ['DEBUGGING ENABLED'];
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(`WASM: ${line}`));
module.printErr = line => console.error(`WASM: ${line}`);
module.printErr = line => {
console.error(`WASM: ${line}`);
showErrorNotification();
};
module.preRun = [];
module.postRun = [];
module.preloadPlugins = [];

View File

@ -341,9 +341,18 @@ export class BrowserRenderer {
}
}
private tryApplyValueProperty(batch: RenderBatch, element: Element, attributeFrame: RenderTreeFrame | null) {
private tryApplyValueProperty(batch: RenderBatch, element: Element, attributeFrame: RenderTreeFrame | null): boolean {
// Certain elements have built-in behaviour for their 'value' property
const frameReader = batch.frameReader;
if (element.tagName === 'INPUT' && element.getAttribute('type') === 'time' && !element.getAttribute('step')) {
const timeValue = attributeFrame ? frameReader.attributeValue(attributeFrame) : null;
if (timeValue) {
element['value'] = timeValue.substring(0, 5);
return true;
}
}
switch (element.tagName) {
case 'INPUT':
case 'SELECT':

View File

@ -1,13 +1,19 @@
export class EventForDotNet<TData extends UIEventArgs> {
constructor(public readonly type: EventArgsType, public readonly data: TData) {
public constructor(public readonly type: EventArgsType, public readonly data: TData) {
}
static fromDOMEvent(event: Event): EventForDotNet<UIEventArgs> {
public static fromDOMEvent(event: Event): EventForDotNet<UIEventArgs> {
const element = event.target as Element;
switch (event.type) {
case 'input':
case 'change': {
if (isTimeBasedInput(element)) {
const normalizedValue = normalizeTimeBasedValue(element);
return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: normalizedValue });
}
const targetIsCheckbox = isCheckbox(element);
const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: newValue });
@ -36,7 +42,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'keydown':
case 'keyup':
case 'keypress':
return new EventForDotNet<UIKeyboardEventArgs>('keyboard', parseKeyboardEvent(<KeyboardEvent>event));
return new EventForDotNet<UIKeyboardEventArgs>('keyboard', parseKeyboardEvent(event as KeyboardEvent));
case 'contextmenu':
case 'click':
@ -46,10 +52,10 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'mousedown':
case 'mouseup':
case 'dblclick':
return new EventForDotNet<UIMouseEventArgs>('mouse', parseMouseEvent(<MouseEvent>event));
return new EventForDotNet<UIMouseEventArgs>('mouse', parseMouseEvent(event as MouseEvent));
case 'error':
return new EventForDotNet<UIErrorEventArgs>('error', parseErrorEvent(<ErrorEvent>event));
return new EventForDotNet<UIErrorEventArgs>('error', parseErrorEvent(event as ErrorEvent));
case 'loadstart':
case 'timeout':
@ -57,7 +63,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'load':
case 'loadend':
case 'progress':
return new EventForDotNet<UIProgressEventArgs>('progress', parseProgressEvent(<ProgressEvent>event));
return new EventForDotNet<UIProgressEventArgs>('progress', parseProgressEvent(event as ProgressEvent));
case 'touchcancel':
case 'touchend':
@ -65,7 +71,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'touchenter':
case 'touchleave':
case 'touchstart':
return new EventForDotNet<UITouchEventArgs>('touch', parseTouchEvent(<TouchEvent>event));
return new EventForDotNet<UITouchEventArgs>('touch', parseTouchEvent(event as TouchEvent));
case 'gotpointercapture':
case 'lostpointercapture':
@ -77,11 +83,11 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'pointerout':
case 'pointerover':
case 'pointerup':
return new EventForDotNet<UIPointerEventArgs>('pointer', parsePointerEvent(<PointerEvent>event));
return new EventForDotNet<UIPointerEventArgs>('pointer', parsePointerEvent(event as PointerEvent));
case 'wheel':
case 'mousewheel':
return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(<WheelEvent>event));
return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(event as WheelEvent));
default:
return new EventForDotNet<UIEventArgs>('unknown', { type: event.type });
@ -204,8 +210,38 @@ function parseMouseEvent(event: MouseEvent) {
};
}
function isCheckbox(element: Element | null) {
return element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
function isCheckbox(element: Element | null): boolean {
return !!element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
}
const timeBasedInputs = [
'date',
'datetime-local',
'month',
'time',
'week',
];
function isTimeBasedInput(element: Element): element is HTMLInputElement {
return timeBasedInputs.indexOf(element.getAttribute('type')!) !== -1;
}
function normalizeTimeBasedValue(element: HTMLInputElement): string {
const value = element.value;
const type = element.type;
switch (type) {
case 'date':
case 'datetime-local':
case 'month':
return value;
case 'time':
return value.length === 5 ? value + ':00' : value; // Convert hh:mm to hh:mm:00
case 'week':
// For now we are not going to normalize input type week as it is not trivial
return value;
}
throw new Error(`Invalid element type '${type}'.`);
}
// The following interfaces must be kept in sync with the UIEventArgs C# classes

View File

@ -172,9 +172,15 @@ namespace Microsoft.AspNetCore.Components.Web
[Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", "value", "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", null, "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", "value", "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", null, "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", "value", "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", null, "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)]
public static partial class BindAttributes

View File

@ -172,9 +172,15 @@ namespace Microsoft.AspNetCore.Components.Web
[Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", "value", "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", null, "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", "value", "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", null, "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", "value", "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", null, "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)]
public static partial class BindAttributes

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.Forms
// of it for us. We will only get asked to parse the T for nonempty inputs.
var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue);
if (targetType == typeof(int) ||
targetType == typeof(long) ||
targetType == typeof(float) ||
targetType == typeof(double) ||
targetType == typeof(decimal))

View File

@ -30,6 +30,20 @@ namespace Microsoft.AspNetCore.Components.Web
[BindInputElement("date", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")]
[BindInputElement("date", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")]
// type="datetime-local" is invariant culture with a specific format.
// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for details.
[BindInputElement("datetime-local", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-ddTHH:mm:ss")]
[BindInputElement("datetime-local", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-ddTHH:mm:ss")]
// type="month" is invariant culture with a specific format.
// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for details.
[BindInputElement("month", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM")]
[BindInputElement("month", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM")]
// type="time" is invariant culture with a specific format.
[BindInputElement("time", null, "value", "onchange", isInvariantCulture: true, format: "HH:mm:ss")]
[BindInputElement("time", "value", "value", "onchange", isInvariantCulture: true, format: "HH:mm:ss")]
[BindElement("select", null, "value", "onchange")]
[BindElement("textarea", null, "value", "onchange")]
public static class BindAttributes

View File

@ -0,0 +1,60 @@
// 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.E2ETest.ServerExecutionTests
{
public class ComponentWithParametersTest : ServerTestBase<BasicTestAppServerSiteFixture<PrerenderedStartup>>
{
public ComponentWithParametersTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<PrerenderedStartup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
[Fact]
public void PassingParametersToComponentsFromThePageWorks()
{
Navigate("/prerendered/componentwithparameters?QueryValue=testQueryValue");
BeginInteractivity();
Browser.Exists(By.CssSelector(".interactive"));
var parameter1 = Browser.FindElement(By.CssSelector(".Param1"));
Assert.Equal(100, parameter1.FindElements(By.CssSelector("li")).Count);
Assert.Equal("99 99", parameter1.FindElement(By.CssSelector("li:last-child")).Text);
// The assigned value is of a more derived type than the declared model type. This check
// verifies we use the actual model type during round tripping.
var parameter2 = Browser.FindElement(By.CssSelector(".Param2"));
Assert.Equal("Value Derived-Value", parameter2.Text);
// This check verifies CaptureUnmatchedValues works
var parameter3 = Browser.FindElements(By.CssSelector(".Param3 li"));
Assert.Collection(
parameter3,
p => Assert.Equal("key1 testQueryValue", p.Text),
p => Assert.Equal("key2 43", p.Text));
}
private void BeginInteractivity()
{
Browser.FindElement(By.Id("load-boot-script")).Click();
}
}
}

View File

@ -174,6 +174,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Browser.Equal(9000.ToString(cultureInfo), () => display.Text);
Browser.Equal(9000.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
// long
input = Browser.FindElement(By.Id("inputnumber_long"));
display = Browser.FindElement(By.Id("inputnumber_long_value"));
Browser.Equal(4200.ToString(cultureInfo), () => display.Text);
Browser.Equal(4200.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
input.Clear();
input.SendKeys(90000000000.ToString(CultureInfo.InvariantCulture));
input.SendKeys("\t");
Browser.Equal(90000000000.ToString(cultureInfo), () => display.Text);
Browser.Equal(90000000000.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
// decimal
input = Browser.FindElement(By.Id("inputnumber_decimal"));
display = Browser.FindElement(By.Id("inputnumber_decimal_value"));

View File

@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using Moq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
@ -1018,5 +1020,263 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal(expected.DateTime, () => DateTimeOffset.Parse(boundValue.Text).DateTime);
Assert.Equal(expected.DateTime, DateTimeOffset.Parse(mirrorValue.GetAttribute("value")).DateTime);
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindDateTimeLocalTextboxDateTime()
{
var target = Browser.FindElement(By.Id("datetime-local-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("datetime-local-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("datetime-local-textbox-datetime-mirror"));
var expected = new DateTime(1985, 3, 4);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 01/01/0001 because that's the default
target.Clear();
expected = default;
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#datetime-local-textbox-datetime", "2000-01-02T04:05:06");
expected = new DateTime(2000, 1, 2, 04, 05, 06);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindDateTimeLocalTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#datetime-local-textbox-nullable-datetime", "2000-01-02T04:05:06");
var expected = new DateTime(2000, 1, 2, 04, 05, 06);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindMonthTextboxDateTime()
{
var target = Browser.FindElement(By.Id("month-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("month-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("month-textbox-datetime-mirror"));
var expected = new DateTime(1985, 3, 1);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
// When the value gets displayed the first time it gets truncated to the 1st day,
// until there is no change the bound value doesn't get updated.
Assert.Equal(expected.AddDays(3), DateTime.Parse(boundValue.Text));
Assert.Equal(expected.AddDays(3), DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 01/01/0001 because that's the default
target.Clear();
expected = default;
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#month-textbox-datetime", "2000-02");
expected = new DateTime(2000, 2, 1);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindMonthTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("month-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("month-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("month-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#month-textbox-nullable-datetime", "2000-02");
var expected = new DateTime(2000, 2, 1);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeTextboxDateTime()
{
var target = Browser.FindElement(By.Id("time-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("time-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-textbox-datetime-mirror"));
var expected = DateTime.Now.Date.AddHours(8).AddMinutes(5);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(default, DateTime.Parse(boundValue.Text));
Assert.Equal(default, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-textbox-datetime", "04:05");
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("time-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("time-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-textbox-nullable-datetime", "05:06");
var expected = DateTime.Now.Date.Add(new TimeSpan(05, 06, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeStepTextboxDateTime()
{
var target = Browser.FindElement(By.Id("time-step-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("time-step-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-step-textbox-datetime-mirror"));
var expected = DateTime.Now.Date.Add(new TimeSpan(8, 5, 30));
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(default, DateTime.Parse(boundValue.Text));
Assert.Equal(default, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-step-textbox-datetime", "04:05:06");
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 6));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeStepTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-step-textbox-nullable-datetime", "05:06");
var expected = DateTime.Now.Date.Add(new TimeSpan(05, 06, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// Applies an input through javascript to datetime-local/month/time controls.
private void ApplyInputValue(string cssSelector, string value)
{
// It's very difficult to enter an invalid value into an <input type=date>, because
// most combinations of keystrokes get normalized to something valid. Additionally,
// using Selenium's SendKeys interacts unpredictably with this normalization logic,
// most likely based on timings. As a workaround, use JS to apply the values. This
// should only be used when strictly necessary, as it doesn't represent actual user
// interaction as authentically as SendKeys in other cases.
var javascript = (IJavaScriptExecutor)Browser;
javascript.ExecuteScript(
$"var elem = document.querySelector('{cssSelector}');"
+ $"elem.value = '{value}';"
+ "elem.dispatchEvent(new KeyboardEvent('change'));");
}
}
}

View File

@ -0,0 +1,67 @@
// 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 BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
public class ErrorNotificationClientSideTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
{
public ErrorNotificationClientSideTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
Browser.MountTestComponent<ErrorComponent>();
Browser.Exists(By.Id("blazor-error-ui"));
Browser.Exists(By.TagName("button"));
}
[Fact]
public void ShowsErrorNotification_OnError_Dismiss()
{
var errorUi = Browser.FindElement(By.Id("blazor-error-ui"));
Assert.Equal("none", errorUi.GetCssValue("display"));
var causeErrorButton = Browser.FindElement(By.TagName("button"));
causeErrorButton.Click();
Browser.Exists(By.CssSelector("#blazor-error-ui[style='display: block;']"), TimeSpan.FromSeconds(10));
var reload = Browser.FindElement(By.ClassName("reload"));
reload.Click();
Browser.DoesNotExist(By.TagName("button"));
}
[Fact]
public void ShowsErrorNotification_OnError_Reload()
{
var causeErrorButton = Browser.Exists(By.TagName("button"));
var errorUi = Browser.FindElement(By.Id("blazor-error-ui"));
Assert.Equal("none", errorUi.GetCssValue("display"));
causeErrorButton.Click();
Browser.Exists(By.CssSelector("#blazor-error-ui[style='display: block;']"));
var dismiss = Browser.FindElement(By.ClassName("dismiss"));
dismiss.Click();
Browser.Exists(By.CssSelector("#blazor-error-ui[style='display: none;']"));
}
}
}

View File

@ -0,0 +1,25 @@
// 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 BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
public class ErrorNotificationServerSideTest : ErrorNotificationClientSideTest
{
public ErrorNotificationServerSideTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}
}
}

View File

@ -11,6 +11,7 @@ using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
@ -68,6 +69,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["testDtoAsync"] = "Same",
["returnPrimitiveAsync"] = "123",
["returnArrayAsync"] = "first,second",
["syncGenericInstanceMethod"] = @"""Initial value""",
["asyncGenericInstanceMethod"] = @"""Updated value 1""",
};
var expectedSyncValues = new Dictionary<string, string>
@ -102,6 +105,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["testDtoSync"] = "Same",
["returnPrimitive"] = "123",
["returnArray"] = "first,second",
["genericInstanceMethod"] = @"""Updated value 2""",
};
// Include the sync assertions only when running under WebAssembly
@ -132,13 +136,17 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Assert
foreach (var expectedValue in expectedValues)
{
var actualValue = actualValues[expectedValue.Key];
if (expectedValue.Key.Contains("Exception"))
{
Assert.StartsWith(expectedValue.Value, actualValues[expectedValue.Key]);
Assert.StartsWith(expectedValue.Value, actualValue);
}
else
{
Assert.Equal(expectedValue.Value, actualValues[expectedValue.Key]);
if (expectedValue.Value != actualValue)
{
throw new AssertActualExpectedException(expectedValue.Value, actualValue, $"Scenario '{expectedValue.Key}' failed. Expected '{expectedValue.Value}, Actual {actualValue}");
}
}
}
}

View File

@ -254,6 +254,63 @@
<span id="select-markup-box-value">@selectMarkupValue</span>
</p>
<h2>Time related inputs (datetime-local, month, time)</h2>
<h3>datetime-local</h3>
<p>
DateTime:
<input id="datetime-local-textbox-datetime" @bind="dateTimeLocalTextboxDateTimeValue" type="datetime-local" />
<span id="datetime-local-textbox-datetime-value">@dateTimeLocalTextboxDateTimeValue</span>
<input id="datetime-local-textbox-datetime-mirror" @bind="dateTimeLocalTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="datetime-local-textbox-nullable-datetime" @bind-value="dateTimeLocalTextboxNullableDateTimeValue" type="datetime-local" />
<span id="datetime-local-textbox-nullable-datetime-value">@dateTimeLocalTextboxNullableDateTimeValue</span>
<input id="datetime-local-textbox-nullable-datetime-mirror" @bind="dateTimeLocalTextboxNullableDateTimeValue" readonly />
</p>
<h3>month</h3>
<p>
DateTime:
<input id="month-textbox-datetime" @bind="monthTextboxDateTimeValue" type="month" />
<span id="month-textbox-datetime-value">@monthTextboxDateTimeValue</span>
<input id="month-textbox-datetime-mirror" @bind="monthTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="month-textbox-nullable-datetime" @bind-value="monthTextboxNullableDateTimeValue" type="month" />
<span id="month-textbox-nullable-datetime-value">@monthTextboxNullableDateTimeValue</span>
<input id="month-textbox-nullable-datetime-mirror" @bind="monthTextboxNullableDateTimeValue" readonly />
</p>
<h3>time</h3>
<p>
DateTime:
<input id="time-textbox-datetime" @bind="timeTextboxDateTimeValue" type="time" />
<span id="time-textbox-datetime-value">@timeTextboxDateTimeValue</span>
<input id="time-textbox-datetime-mirror" @bind="timeTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="time-textbox-nullable-datetime" @bind-value="timeTextboxNullableDateTimeValue" type="time" />
<span id="time-textbox-nullable-datetime-value">@timeTextboxNullableDateTimeValue</span>
<input id="time-textbox-nullable-datetime-mirror" @bind="timeTextboxNullableDateTimeValue" readonly />
</p>
<h3>time with step (supports seconds)</h3>
<p>
DateTime:
<input id="time-step-textbox-datetime" @bind="timeStepTextboxDateTimeValue" type="time" step="1" />
<span id="time-step-textbox-datetime-value">@timeStepTextboxDateTimeValue</span>
<input id="time-step-textbox-datetime-mirror" @bind="timeStepTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="time-step-textbox-nullable-datetime" @bind-value="timeStepTextboxNullableDateTimeValue" type="time" step="1" />
<span id="time-step-textbox-nullable-datetime-value">@timeStepTextboxNullableDateTimeValue</span>
<input id="time-step-textbox-nullable-datetime-mirror" @bind="timeStepTextboxNullableDateTimeValue" readonly />
</p>
@code {
string textboxInitiallyBlankValue = null;
string textboxInitiallyPopulatedValue = "Hello";
@ -297,6 +354,18 @@
DateTime textboxDateTimeFormatInvalidValue = new DateTime(1985, 3, 4);
DateTimeOffset? textboxNullableDateTimeOffsetFormatInvalidValue = null;
DateTime dateTimeLocalTextboxDateTimeValue = new DateTime(1985, 3, 4);
DateTime? dateTimeLocalTextboxNullableDateTimeValue = null;
DateTime monthTextboxDateTimeValue = new DateTime(1985, 3, 4);
DateTime? monthTextboxNullableDateTimeValue = null;
DateTime timeTextboxDateTimeValue = DateTime.Now.Date.Add(new TimeSpan(8, 5, 0));
DateTime? timeTextboxNullableDateTimeValue = null;
DateTime timeStepTextboxDateTimeValue = DateTime.Now.Date.Add(new TimeSpan(8, 5, 30));
DateTime? timeStepTextboxNullableDateTimeValue = null;
bool includeFourthOption = false;
enum SelectableValue { First, Second, Third, Fourth }
SelectableValue selectValue = SelectableValue.Second;

View File

@ -0,0 +1,48 @@
<h3 class="interactive">Component With Parameters</h3>
<ul class="Param1">
@foreach (var value in Param1)
{
<li>@value.StringProperty @value.IntProperty</li>
}
</ul>
@* Making sure polymorphism works *@
<div class="Param2">@DerivedParam2.StringProperty @DerivedParam2.DerivedProperty</div>
@* Making sure CaptureUnmatchedValues works *@
<ul class="Param3">
@foreach (var value in Param3.OrderBy(kvp => kvp.Key))
{
<li>@value.Key @value.Value</li>
}
</ul>
@code
{
[Parameter] public List<TestModel> Param1 { get; set; }
[Parameter] public TestModel Param2 { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> Param3 { get; set; }
private DerivedModel DerivedParam2 => (DerivedModel)Param2;
public static List<TestModel> TestModelValues => Enumerable.Range(0, 100).Select(c => new TestModel { StringProperty = c.ToString(), IntProperty = c }).ToList();
public static DerivedModel DerivedModelValue = new DerivedModel { StringProperty = "Value", DerivedProperty = "Derived-Value" };
public class TestModel
{
public string StringProperty { get; set; }
public int IntProperty { get; set; }
}
public class DerivedModel : TestModel
{
public string DerivedProperty { get; set; }
}
}

View File

@ -0,0 +1,16 @@
<div>
<h2>Error throwing button</h2>
<p>
<button @onclick="@(IncrementCount)">Click me</button>
</p>
</div>
@code {
int currentCount = 0;
void IncrementCount()
{
currentCount++;
throw new NotImplementedException("Doing crazy things!");
}
}

View File

@ -62,6 +62,10 @@
int: <InputNumber id="inputnumber_int" @bind-Value="inputNumberInt" />
<span id="inputnumber_int_value">@inputNumberInt</span>
</div>
<div>
long: <InputNumber id="inputnumber_long" @bind-Value="inputNumberLong" />
<span id="inputnumber_long_value">@inputNumberLong</span>
</div>
<div>
decimal: <InputNumber id="inputnumber_decimal" @bind-Value="inputNumberDecimal" />
<span id="inputnumber_decimal_value">@inputNumberDecimal</span>
@ -99,8 +103,18 @@
DateTimeOffset inputTypeDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
int inputNumberInt = 42;
long inputNumberLong = 4200;
decimal inputNumberDecimal = 4.2m;
DateTime inputDateDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeDateTimeLocalDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeDateTimeLocalDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeMonthDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeMonthDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeTimeDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeTimeDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
}

View File

@ -1,6 +1,6 @@
@using Microsoft.AspNetCore.Components.Rendering
<div id="test-selector">
Select test:
Select test:
<select id="test-selector-select" @bind=SelectedComponentTypeName>
<option value="none">Choose...</option>
<option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option>
@ -20,6 +20,7 @@
<option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option>
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
<option value="BasicTestApp.ErrorComponent">Error throwing</option>
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
@ -82,6 +83,12 @@
@((RenderFragment)RenderSelectedComponent)
</app>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href class='reload'>Reload</a>
<a class='dismiss' style="cursor: pointer;">🗙</a>
</div>
@code {
string SelectedComponentTypeName { get; set; } = "none";

View File

@ -70,13 +70,15 @@
var shouldSupportSyncInterop = RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"));
var testDTOTOPassByRef = new TestDTO(nonSerializedValue: 123);
var instanceMethodsTarget = new JavaScriptInterop();
var genericType = new JavaScriptInterop.GenericType<string> { Value = "Initial value" };
Console.WriteLine("Starting interop invocations.");
await JSRuntime.InvokeVoidAsync(
"jsInteropTests.invokeDotNetInteropMethodsAsync",
shouldSupportSyncInterop,
DotNetObjectReference.Create(testDTOTOPassByRef),
DotNetObjectReference.Create(instanceMethodsTarget));
DotNetObjectReference.Create(instanceMethodsTarget),
DotNetObjectReference.Create(genericType));
if (shouldSupportSyncInterop)
{

View File

@ -462,5 +462,25 @@ namespace BasicTestApp.InteropTest
public DotNetObjectReference<TestDTO> OutgoingByRef { get; set; }
}
public class GenericType<TValue>
{
public TValue Value { get; set; }
[JSInvokable]
public TValue Update(TValue newValue)
{
var oldValue = Value;
Value = newValue;
return oldValue;
}
[JSInvokable]
public async Task<TValue> UpdateAsync(TValue newValue)
{
await Task.Yield();
return Update(newValue);
}
}
}
}

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Basic test app</title>
@ -9,6 +10,7 @@
<!-- Used by ExternalContentPackage -->
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head>
<body>
<root>Loading...</root>
@ -31,4 +33,5 @@
<!-- Used by ExternalContentPackage -->
<script src="_content/TestContentPackage/prompt.js"></script>
</body>
</html>

View File

@ -2,7 +2,7 @@
var results = {};
var assemblyName = 'BasicTestApp';
function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectByRef, instanceMethodsTarget) {
async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectByRef, instanceMethodsTarget, genericDotNetObjectByRef) {
if (shouldSupportSyncInterop) {
console.log('Invoking void sync methods.');
DotNet.invokeMethod(assemblyName, 'VoidParameterless');
@ -15,6 +15,7 @@ function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectB
DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8, dotNetObjectByRef));
console.log('Invoking returning sync methods.');
results['result1'] = DotNet.invokeMethod(assemblyName, 'ReturnArray');
results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1, dotNetObjectByRef));
@ -40,75 +41,73 @@ function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectB
}
console.log('Invoking void async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync')
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(() => {
console.log('Invoking returning async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync')
.then(r => results['result1Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef)))
.then(r => results['result2Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef)))
.then(r => results['result3Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef)))
.then(r => results['result4Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef)))
.then(r => results['result5Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef)))
.then(r => results['result6Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef)))
.then(r => results['result7Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef)))
.then(r => results['result8Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(r => results['result9Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync'))
.then(r => DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', r['Some async instance']))
.then(r => {
results['resultReturnDotNetObjectByRefAsync'] = r;
})
.then(() => instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
}))
.then(r => {
results['instanceMethodThisTypeNameAsync'] = r.thisTypeName;
results['instanceMethodStringValueUpperAsync'] = r.stringValueUpper;
results['instanceMethodIncomingByRefAsync'] = r.incomingByRef;
return DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', r.outgoingByRef);
}).then(r => {
results['instanceMethodOutgoingByRefAsync'] = r;
})
})
.then(() => {
console.log('Invoking methods that throw exceptions');
try {
shouldSupportSyncInterop && DotNet.invokeMethod(assemblyName, 'ThrowException');
} catch (e) {
results['ThrowException'] = e.message;
}
return DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowSyncException')
.catch(e => {
results['AsyncThrowSyncException'] = e.message;
await DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync');
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef));
return DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowAsyncException');
}).catch(e => {
results['AsyncThrowAsyncException'] = e.message;
console.log('Invoking returning async methods.');
results['result1Async'] = await DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync');
results['result2Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef));
results['result3Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef));
results['result4Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef));
results['result5Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef));
results['result6Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef));
results['result7Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef));
results['result8Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef));
results['result9Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef));
console.log('Done invoking interop methods');
});
});
const returnDotNetObjectByRefAsync = await DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync');
results['resultReturnDotNetObjectByRefAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefAsync['Some async instance']);
const instanceMethodAsync = await instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
});
results['instanceMethodThisTypeNameAsync'] = instanceMethodAsync.thisTypeName;
results['instanceMethodStringValueUpperAsync'] = instanceMethodAsync.stringValueUpper;
results['instanceMethodIncomingByRefAsync'] = instanceMethodAsync.incomingByRef;
results['instanceMethodOutgoingByRefAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', instanceMethodAsync.outgoingByRef);
console.log('Invoking generic type instance methods.');
results['syncGenericInstanceMethod'] = await genericDotNetObjectByRef.invokeMethodAsync('Update', 'Updated value 1');
results['asyncGenericInstanceMethod'] = await genericDotNetObjectByRef.invokeMethodAsync('UpdateAsync', 'Updated value 2');
if (shouldSupportSyncInterop) {
results['genericInstanceMethod'] = genericDotNetObjectByRef.invokeMethod('Update', 'Updated Value 3');
}
console.log('Invoking methods that throw exceptions');
try {
shouldSupportSyncInterop && DotNet.invokeMethod(assemblyName, 'ThrowException');
} catch (e) {
results['ThrowException'] = e.message;
}
try {
await DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowSyncException');
} catch (e) {
results['AsyncThrowSyncException'] = e.message;
}
try {
await DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowAsyncException');
} catch (e) {
results['AsyncThrowAsyncException'] = e.message;
}
console.log('Done invoking interop methods');
}
function createArgumentList(argumentNumber, dotNetObjectByRef){
function createArgumentList(argumentNumber, dotNetObjectByRef) {
const array = new Array(argumentNumber);
if (argumentNumber === 0) {
return [];
@ -149,7 +148,7 @@ function createArgumentList(argumentNumber, dotNetObjectByRef){
source: `Some random text with at least ${i} characters`,
start: argumentNumber + 1,
length: argumentNumber + 1
}
};
break;
default:
console.log(i);
@ -169,7 +168,7 @@ window.jsInteropTests = {
returnPrimitive: returnPrimitive,
returnPrimitiveAsync: returnPrimitiveAsync,
receiveDotNetObjectByRef: receiveDotNetObjectByRef,
receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync,
receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync
};
function returnPrimitive() {
@ -211,9 +210,9 @@ function asyncFunctionThrowsAsyncException() {
}
function asyncFunctionTakesLongerThanDefaultTimeoutToResolve() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(undefined), 5000);
});
return new Promise((resolve, reject) => {
setTimeout(() => resolve(undefined), 5000);
});
}
function collectInteropResults() {

View File

@ -6,6 +6,14 @@
outline: 1px solid red;
}
#blazor-error-ui {
display: none;
}
#blazor-error-ui dismiss {
cursor: pointer;
}
.validation-message {
color: red;
}

View File

@ -12,7 +12,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<App>(RenderMode.Server))</app>
<component type="typeof(App)" render-mode="Server" />
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({

View File

@ -0,0 +1,37 @@
@page
<component type="typeof(ComponentWithParameters)"
render-mode="ServerPrerendered"
param-Param1="ComponentWithParameters.TestModelValues"
param-Param2="ComponentWithParameters.DerivedModelValue"
param-key1="QueryValue"
param-key2="43" />
@*
So that E2E tests can make assertions about both the prerendered and
interactive states, we only load the .js file when told to.
*@
<hr />
<button id="load-boot-script" onclick="start()">Load boot script</button>
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
</script>
@functions
{
[BindProperty(SupportsGet = true)]
public string QueryValue { get; set; }
}

View File

@ -12,24 +12,24 @@
<div id="test-container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<div id="container">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<p>Some content between</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p>
<div id="nested-an-extra-level">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p>
</div>
</div>
<div id="container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
<component type="typeof(GreeterComponent)" render-mode="Server" param-name='"Albert"' />
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" param-name='"Abraham"' />
</div>
</div>

View File

@ -1,5 +1,6 @@
@page
@using BasicTestApp.RouterTest
<!DOCTYPE html>
<html>
<head>
@ -7,7 +8,7 @@
<base href="~/" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<TestRouter>(RenderMode.ServerPrerendered))</app>
<app><component type="typeof(TestRouter)" render-mode="ServerPrerendered" /></app>
@*
So that E2E tests can make assertions about both the prerendered and

View File

@ -1,4 +1,5 @@
@page ""
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
<!DOCTYPE html>
<html>
<head>
@ -11,7 +12,7 @@
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head>
<body>
<root>@(await Html.RenderComponentAsync<BasicTestApp.Index>(RenderMode.Server))</root>
<root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>
<!-- Used for testing interop scenarios between JS and .NET -->
<script src="js/jsinteroptests.js"></script>

View File

@ -0,0 +1,3 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using BasicTestApp

View File

@ -3221,6 +3221,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public partial class ValidationVisitor
{
public ValidationVisitor(Microsoft.AspNetCore.Mvc.ActionContext actionContext, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IModelValidatorProvider validatorProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache validatorCache, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationStateDictionary validationState) { }
[System.ObsoleteAttribute("This property is deprecated and is no longer used by the runtime.")]
public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
protected Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache Cache { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
protected object Container { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

View File

@ -1894,7 +1894,10 @@ namespace Microsoft.AspNetCore.Mvc
detail: detail,
instance: instance);
return new ObjectResult(problemDetails);
return new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status
};
}
/// <summary>
@ -1970,7 +1973,10 @@ namespace Microsoft.AspNetCore.Mvc
return new BadRequestObjectResult(validationProblem);
}
return new ObjectResult(validationProblem);
return new ObjectResult(validationProblem)
{
StatusCode = validationProblem.Status
};
}
/// <summary>

View File

@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// </summary>
/// <value>The default value is <see langword="true"/>.</value>
/// <remarks>This property is currently ignored.</remarks>
[Obsolete("This property is deprecated and is no longer used by the runtime.")]
public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent { get; set; } = true;
/// <summary>

View File

@ -2316,6 +2316,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(actionResult);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequestResult.Value);
Assert.Equal(400, badRequestResult.StatusCode);
Assert.Equal(400, problemDetails.Status);
Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
@ -2348,6 +2349,48 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(detail, problemDetails.Detail);
}
[Fact]
public void ValidationProblemDetails_UsesSpecifiedStatusCode()
{
// Arrange
var options = GetApiBehaviorOptions();
var controller = new TestableController
{
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
};
// Act
var actionResult = controller.ValidationProblem(statusCode: 405);
// Assert
var objectResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ValidationProblemDetails>(objectResult.Value);
Assert.Equal(405, objectResult.StatusCode);
Assert.Equal(405, problemDetails.Status);
}
[Fact]
public void ValidationProblemDetails_StatusCode400_ReturnsBadRequestObjectResultFor2xCompatibility()
{
// Arrange
var options = GetApiBehaviorOptions();
var controller = new TestableController
{
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
};
// Act
var actionResult = controller.ValidationProblem(statusCode: 400);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(actionResult);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequestResult.Value);
Assert.Equal(400, badRequestResult.StatusCode);
Assert.Equal(400, problemDetails.Status);
}
[Fact]
public void ProblemDetails_Works()
{
@ -2371,6 +2414,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
Assert.Equal(500, actionResult.StatusCode);
Assert.Equal(500, problemDetails.Status);
Assert.Equal("An error occured while processing your request.", problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
@ -2396,6 +2440,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
Assert.Equal(500, actionResult.StatusCode);
Assert.Equal(500, problemDetails.Status);
Assert.Equal(title, problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
@ -2419,6 +2464,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
Assert.Equal(422, actionResult.StatusCode);
Assert.Equal(422, problemDetails.Status);
Assert.Equal("Unprocessable entity.", problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type);

View File

@ -12,7 +12,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters
@ -462,6 +464,30 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Single(formatterContext.ModelState["Person.Name"].Errors);
}
[Fact]
public async Task ReadAsync_DoesNotDisposeBufferedReadStream()
{
// Arrange
var formatter = GetInputFormatter();
var content = "{\"name\": \"Test\"}";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var testBufferedReadStream = new Mock<FileBufferingReadStream>(httpContext.Request.Body, 1024) { CallBase = true };
httpContext.Request.Body = testBufferedReadStream.Object;
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
var userModel = Assert.IsType<ComplexModel>(result.Model);
Assert.Equal("Test", userModel.Name);
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
}
internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; }
internal abstract string JsonFormatter_EscapedKeys_Expected { get; }
@ -517,7 +543,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
protected sealed class ComplexPoco
{
public int Id { get; set; }
public Person Person{ get; set; }
public Person Person { get; set; }
}
protected sealed class Person

View File

@ -118,6 +118,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var request = context.HttpContext.Request;
Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering)
{
@ -135,6 +136,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
await readStream.DrainAsync(CancellationToken.None);
readStream.Seek(0L, SeekOrigin.Begin);
disposeReadStream = true;
}
try
@ -162,9 +165,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
finally
{
if (readStream is FileBufferingReadStream fileBufferingReadStream)
if (disposeReadStream)
{
await fileBufferingReadStream.DisposeAsync();
await readStream.DisposeAsync();
}
}
}

View File

@ -99,6 +99,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var request = context.HttpContext.Request;
Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering)
{
// XmlSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
@ -115,6 +117,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
await readStream.DrainAsync(CancellationToken.None);
readStream.Seek(0L, SeekOrigin.Begin);
disposeReadStream = true;
}
try
@ -155,9 +158,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
finally
{
if (readStream is FileBufferingReadStream fileBufferingReadStream)
if (disposeReadStream)
{
await fileBufferingReadStream.DisposeAsync();
await readStream.DisposeAsync();
}
}
}

View File

@ -12,6 +12,7 @@ using System.Xml;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Moq;
using Xunit;
@ -166,6 +167,39 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
Assert.Equal(expectedString, model.sampleString);
}
[Fact]
public async Task ReadAsync_DoesNotDisposeBufferedStreamIfItDidNotCreateIt()
{
// Arrange
var expectedInt = 10;
var expectedString = "TestString";
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
"<sampleString>" + expectedString + "</sampleString></TestLevelOne>";
var formatter = new XmlDataContractSerializerInputFormatter(new MvcOptions());
var contentBytes = Encoding.UTF8.GetBytes(input);
var httpContext = new DefaultHttpContext();
var testBufferedReadStream = new Mock<FileBufferingReadStream>(new MemoryStream(contentBytes), 1024) { CallBase = true };
httpContext.Request.Body = testBufferedReadStream.Object;
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
// Act
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
}
[Fact]
public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody()
{

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.WebUtilities;
using Moq;
using Xunit;
@ -622,6 +623,39 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), model.SampleDate);
}
[Fact]
public async Task ReadAsync_DoesNotDisposeBufferedStreamIfItDidNotCreateIt()
{
// Arrange
var expectedInt = 10;
var expectedString = "TestString";
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
"<sampleString>" + expectedString + "</sampleString></TestLevelOne>";
var formatter = new XmlSerializerInputFormatter(new MvcOptions());
var contentBytes = Encoding.UTF8.GetBytes(input);
var httpContext = new DefaultHttpContext();
var testBufferedReadStream = new Mock<FileBufferingReadStream>(new MemoryStream(contentBytes), 1024) { CallBase = true };
httpContext.Request.Body = testBufferedReadStream.Object;
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
// Act
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
}
private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType)
{
var httpContext = GetHttpContext(contentBytes);

View File

@ -129,6 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var suppressInputFormatterBuffering = _options.SuppressInputFormatterBuffering;
var readStream = request.Body;
var disposeReadStream = false;
if (!request.Body.CanSeek && !suppressInputFormatterBuffering)
{
// JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously
@ -145,6 +146,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
await readStream.DrainAsync(CancellationToken.None);
readStream.Seek(0L, SeekOrigin.Begin);
disposeReadStream = true;
}
var successful = true;
@ -170,9 +173,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
jsonSerializer.Error -= ErrorHandler;
ReleaseJsonSerializer(jsonSerializer);
if (readStream is FileBufferingReadStream fileBufferingReadStream)
if (disposeReadStream)
{
await fileBufferingReadStream.DisposeAsync();
await readStream.DisposeAsync();
}
}
}

View File

@ -23,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<None Include="build\$(DefaultNetCoreTargetFramework)\*" Pack="true" PackagePath="build\$(DefaultNetCoreTargetFramework)" />
<None Include="targets\$(PackageId).targets" Pack="true" PackagePath="build\$(DefaultNetCoreTargetFramework)\$(PackageId).targets" />
<None Include="targets\$(PackageId).targets" Pack="true" PackagePath="buildTransitive\$(DefaultNetCoreTargetFramework)\$(PackageId).targets" />
</ItemGroup>
</Project>

View File

@ -105,6 +105,22 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
public CacheTagHelperOptions() { }
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type", TagStructure=Microsoft.AspNetCore.Razor.TagHelpers.TagStructure.WithoutEndTag)]
public sealed partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
public ComponentTagHelper() { }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")]
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")]
public System.Collections.Generic.IDictionary<string, object> Parameters { get { throw null; } set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; }
}
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
{

View File

@ -0,0 +1,80 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// A <see cref="TagHelper"/> that renders a Razor component.
/// </summary>
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
public sealed class ComponentTagHelper : TagHelper
{
private const string ComponentParameterName = "params";
private const string ComponentParameterPrefix = "param-";
private const string ComponentTypeName = "type";
private const string RenderModeName = "render-mode";
private IDictionary<string, object> _parameters;
/// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
/// <summary>
/// Gets or sets values for component parameters.
/// </summary>
[HtmlAttributeName(ComponentParameterName, DictionaryAttributePrefix = ComponentParameterPrefix)]
public IDictionary<string, object> Parameters
{
get
{
_parameters ??= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
return _parameters;
}
set => _parameters = value;
}
/// <summary>
/// Gets or sets the component type. This value is required.
/// </summary>
[HtmlAttributeName(ComponentTypeName)]
public Type ComponentType { get; set; }
/// <summary>
/// Gets or sets the <see cref="Rendering.RenderMode"/>
/// </summary>
[HtmlAttributeName(RenderModeName)]
public RenderMode RenderMode { get; set; }
/// <inheritdoc />
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);
// Reset the TagName. We don't want `component` to render.
output.TagName = null;
output.Content.SetHtmlContent(result);
}
}
}

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.IO.Pipes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class ComponentTagHelperTest
{
[Fact]
public async Task ProcessAsync_RendersComponent()
{
// Arrange
var tagHelper = new ComponentTagHelper
{
ViewContext = GetViewContext(),
};
var context = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
var content = HtmlContentUtilities.HtmlContentToString(output.Content);
Assert.Equal("Hello world", content);
Assert.Null(output.TagName);
}
private static TagHelperContext GetTagHelperContext()
{
return new TagHelperContext(
"component",
new TagHelperAttributeList(),
new Dictionary<object, object>(),
Guid.NewGuid().ToString("N"));
}
private static TagHelperOutput GetTagHelperOutput()
{
return new TagHelperOutput(
"component",
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
}
private ViewContext GetViewContext()
{
var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
var renderer = Mock.Of<IComponentRenderer>(c =>
c.RenderComponentAsync(It.IsAny<ViewContext>(), It.IsAny<Type>(), It.IsAny<RenderMode>(), It.IsAny<object>()) == Task.FromResult<IHtmlContent>(htmlContent));
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceCollection().AddSingleton<IComponentRenderer>(renderer).BuildServiceProvider(),
};
return new ViewContext
{
HttpContext = httpContext,
};
}
}
}

View File

@ -330,8 +330,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
}
public static partial class HtmlHelperComponentExtensions
{
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, System.Type componentType, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) { throw null; }
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
}
public static partial class HtmlHelperDisplayExtensions

View File

@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
@ -206,8 +205,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<SaveTempDataFilter>();
//
// Component prerendering
// Component rendering
//
services.TryAddScoped<IComponentRenderer, ComponentRenderer>();
services.TryAddScoped<StaticComponentRenderer>();
services.TryAddScoped<NavigationManager, HttpNavigationManager>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();

View File

@ -1,127 +0,0 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentExtensions
{
private static readonly object ComponentSequenceKey = new object();
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(renderMode, null);
}
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
RenderMode renderMode,
object parameters) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
var context = htmlHelper.ViewContext.HttpContext;
return renderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.Static => await StaticComponentAsync(context, typeof(TComponent), GetParametersCollection(parameters)),
_ => throw new ArgumentException("Invalid render mode", nameof(renderMode)),
};
}
private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
}
return (ServerComponentInvocationSequence)result;
}
private static ParameterView GetParametersCollection(object parameters) => parameters == null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
private static async Task<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(result);
}
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
invocationSerializer.GetPreamble(currentInvocation),
result,
invocationSerializer.GetEpilogue(currentInvocation));
}
private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
}
}
}

View File

@ -0,0 +1,110 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class ComponentRenderer : IComponentRenderer
{
private static readonly object ComponentSequenceKey = new object();
private readonly StaticComponentRenderer _staticComponentRenderer;
private readonly ServerComponentSerializer _serverComponentSerializer;
public ComponentRenderer(
StaticComponentRenderer staticComponentRenderer,
ServerComponentSerializer serverComponentSerializer)
{
_staticComponentRenderer = staticComponentRenderer;
_serverComponentSerializer = serverComponentSerializer;
}
public async Task<IHtmlContent> RenderComponentAsync(
ViewContext viewContext,
Type componentType,
RenderMode renderMode,
object parameters)
{
if (viewContext is null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException(Resources.FormatTypeMustDeriveFromType(componentType, typeof(IComponent)));
}
var context = viewContext.HttpContext;
var parameterView = parameters is null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
return renderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
};
}
private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
}
return (ServerComponentInvocationSequence)result;
}
private async Task<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var result = await _staticComponentRenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(result);
}
private async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var currentInvocation = _serverComponentSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await _staticComponentRenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
_serverComponentSerializer.GetPreamble(currentInvocation),
result,
_serverComponentSerializer.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));
}
}
}

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.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal interface IComponentRenderer
{
Task<IHtmlContent> RenderComponentAsync(
ViewContext viewContext,
Type componentType,
RenderMode renderMode,
object parameters);
}
}

View File

@ -11,11 +11,10 @@ using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class StaticComponentRenderer
{

View File

@ -2,7 +2,6 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop;

View File

@ -0,0 +1,70 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentExtensions
{
/// <summary>
/// Renders the <typeparamref name="TComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
=> RenderComponentAsync<TComponent>(htmlHelper, renderMode, parameters: null);
/// <summary>
/// Renders the <typeparamref name="TComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
RenderMode renderMode,
object parameters) where TComponent : IComponent
=> RenderComponentAsync(htmlHelper, typeof(TComponent), renderMode, parameters);
/// <summary>
/// Renders the specified <paramref name="componentType"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="componentType">The component type.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
public static Task<IHtmlContent> RenderComponentAsync(
this IHtmlHelper htmlHelper,
Type componentType,
RenderMode renderMode,
object parameters)
{
if (htmlHelper is null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
var viewContext = htmlHelper.ViewContext;
var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
return componentRenderer.RenderComponentAsync(viewContext, componentType, renderMode, parameters);
}
}
}

View File

@ -295,4 +295,7 @@
<data name="TempData_CannotDeserializeType" xml:space="preserve">
<value>Unsupported data type '{0}'.</value>
</data>
<data name="UnsupportedRenderMode" xml:space="preserve">
<value>Unsupported RenderMode '{0}'.</value>
</data>
</root>

View File

@ -13,7 +13,7 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -22,24 +22,26 @@ using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public class HtmlHelperComponentExtensionsTests
public class ComponentRendererTest
{
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
private readonly ComponentRenderer renderer = GetComponentRenderer();
[Fact]
public async Task CanRender_ParameterlessComponent()
{
// Arrange
var helper = CreateHelper();
var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Static);
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, null);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@ -51,15 +53,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ParameterlessComponent_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
// Assert
@ -82,15 +82,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanPrerender_ParameterlessComponent_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
@ -125,21 +123,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRenderMultipleServerComponents()
{
// Arrange
var helper = CreateHelper();
var firstWriter = new StringWriter();
var secondWriter = new StringWriter();
var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var firstResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered);
firstResult.WriteTo(firstWriter, HtmlEncoder.Default);
var firstComponent = firstWriter.ToString();
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 secondResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server);
secondResult.WriteTo(secondWriter, HtmlEncoder.Default);
var secondComponent = secondWriter.ToString();
var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
// Assert
@ -171,20 +165,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithParametersObject()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Static,
new
{
Name = "Steve"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, new { Name = "Steve" });
// Assert
var content = HtmlContentUtilities.HtmlContentToString(result);
Assert.Equal("<p>Hello Steve!</p>", content);
}
@ -192,20 +179,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server,
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
// Assert
@ -237,20 +217,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithNullParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
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);
// Assert
@ -274,28 +248,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);;
var value = Assert.Single(serverComponent.ParameterValues); ;
Assert.Null(value);
}
[Fact]
public async Task CanPrerender_ComponentWithParameters_ServerMode()
public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode()
{
// Arrange
var helper = CreateHelper();
var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.ServerPrerendered,
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
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);
// Assert
@ -336,23 +304,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
}
[Fact]
public async Task CanPrerender_ComponentWithNullParameters_ServerMode()
public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode()
{
// Arrange
var helper = CreateHelper();
var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.ServerPrerendered,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
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);
// Assert
@ -396,39 +358,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task ComponentWithInvalidRenderMode_Throws()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
// Act & Assert
var result = await Assert.ThrowsAsync<ArgumentException>(() => helper.RenderComponentAsync<GreetingComponent>(
default,
new
{
Name = "Steve"
}));
Assert.Equal("renderMode", result.ParamName);
var ex = await ExceptionAssert.ThrowsArgumentAsync(
() => renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), default, new { Name = "Daniel" }),
"renderMode",
$"Unsupported RenderMode '{(RenderMode)default}'");
}
[Fact]
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
// Act
var state = new OnAfterRenderState();
var result = await helper.RenderComponentAsync<OnAfterRenderComponent>(
RenderMode.Static,
new
{
State = state
});
result.WriteTo(writer, HtmlEncoder.Default);
var result = await renderer.RenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, new { state });
// Assert
Assert.Equal("<p>Hello</p>", writer.ToString());
var content = HtmlContentUtilities.HtmlContentToString(result);
Assert.Equal("<p>Hello</p>", content);
Assert.False(state.OnAfterRenderRan);
}
@ -436,10 +387,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanCatch_ComponentWithSynchronousException()
{
// Arrange
var helper = CreateHelper();
var viewContext = GetViewContext();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static,
new
{
@ -454,10 +407,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanCatch_ComponentWithAsynchronousException()
{
// Arrange
var helper = CreateHelper();
var viewContext = GetViewContext();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static,
new
{
@ -472,10 +427,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task Rendering_ComponentWithJsInteropThrows()
{
// Arrange
var helper = CreateHelper();
var viewContext = GetViewContext();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static,
new
{
@ -503,11 +460,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var responseMock = new Mock<IHttpResponseFeature>();
responseMock.Setup(r => r.HasStarted).Returns(true);
ctx.Features.Set(responseMock.Object);
var helper = CreateHelper(ctx);
var writer = new StringWriter();
var viewContext = GetViewContext(ctx);
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(RedirectComponent),
RenderMode.Static,
new
{
@ -515,8 +473,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
}));
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
exception.Message);
}
@ -530,10 +488,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
ctx.Request.PathBase = "/base";
ctx.Request.Path = "/path";
ctx.Request.QueryString = new QueryString("?query=value");
var helper = CreateHelper(ctx);
var viewContext = GetViewContext(ctx);
// Act
await helper.RenderComponentAsync<RedirectComponent>(
await renderer.RenderComponentAsync(
viewContext,
typeof(RedirectComponent),
RenderMode.Static,
new
{
@ -549,8 +509,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_AsyncComponent()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var viewContext = GetViewContext();
var expectedContent = @"<table>
<thead>
<tr>
@ -595,29 +554,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
</table>";
// Act
var result = await helper.RenderComponentAsync<AsyncComponent>(RenderMode.Static);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null);
var content = HtmlContentUtilities.HtmlContentToString(result);
// Assert
Assert.Equal(expectedContent.Replace("\r\n", "\n"), content);
}
private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action<IServiceCollection> configureServices = null)
private static ComponentRenderer GetComponentRenderer() =>
new ComponentRenderer(
new StaticComponentRenderer(HtmlEncoder.Default),
new ServerComponentSerializer(_dataprotectorProvider));
private static ViewContext GetViewContext(HttpContext context = null, Action<IServiceCollection> configureServices = null)
{
var services = new ServiceCollection();
services.AddSingleton(HtmlEncoder.Default);
services.AddSingleton<ServerComponentSerializer>();
services.AddSingleton(_dataprotectorProvider);
services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.AddSingleton<NavigationManager, HttpNavigationManager>();
services.AddSingleton<StaticComponentRenderer>();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
configureServices?.Invoke(services);
var helper = new Mock<IHtmlHelper>();
var context = ctx ?? new DefaultHttpContext();
context ??= new DefaultHttpContext();
context.RequestServices = services.BuildServiceProvider();
context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost");
@ -625,12 +584,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
context.Request.Path = "/path";
context.Request.QueryString = QueryString.FromUriComponent("?query=value");
helper.Setup(h => h.ViewContext)
.Returns(new ViewContext()
{
HttpContext = context
});
return helper.Object;
return new ViewContext { HttpContext = context };
}
private class TestComponent : IComponent

View File

@ -6,13 +6,11 @@ using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorComponents
namespace Microsoft.AspNetCore.Components.Rendering
{
public class HtmlRendererTest
{

View File

@ -0,0 +1,56 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public class HtmlHelperComponentExtensionsTest
{
[Fact]
public async Task RenderComponentAsync_Works()
{
// Arrange
var viewContext = GetViewContext();
var htmlHelper = Mock.Of<IHtmlHelper>(h => h.ViewContext == viewContext);
// Act
var result = await HtmlHelperComponentExtensions.RenderComponentAsync<TestComponent>(htmlHelper, RenderMode.Static);
// Assert
Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(result));
}
private static ViewContext GetViewContext()
{
var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
var renderer = Mock.Of<IComponentRenderer>(c =>
c.RenderComponentAsync(It.IsAny<ViewContext>(), It.IsAny<Type>(), It.IsAny<RenderMode>(), It.IsAny<object>()) == Task.FromResult<IHtmlContent>(htmlContent));
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceCollection().AddSingleton<IComponentRenderer>(renderer).BuildServiceProvider(),
};
var viewContext = new ViewContext { HttpContext = httpContext };
return viewContext;
}
private class TestComponent : IComponent
{
public void Attach(RenderHandle renderHandle)
{
}
public Task SetParametersAsync(ParameterView parameters) => null;
}
}
}

View File

@ -1,2 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(MvcSandbox.Startup).Assembly" FallbackComponent="@typeof(NotFound)" />
@using MvcSandbox.Components.Shared
<Router AppAssembly="@typeof(MvcSandbox.Startup).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@ -1,4 +0,0 @@
@using MvcSandbox.Components.Shared
@layout MainLayout
<h1>Not Found</h1>
<h2>Sorry, nothing was found.</h2>

View File

@ -1,5 +1,4 @@
@page
@model MvcSandbox.Pages.ComponentsModel
@{
Layout = null;
}
@ -15,8 +14,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<MvcSandbox.Components.App>(RenderMode.Static))</app>
<script src="_framework/components.server.js"></script>
<component type="typeof(MvcSandbox.Components.App)" render-mode="Static" />
<script src="_framework/blazor.server.js"></script>
</body>
</html>

View File

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MvcSandbox.Pages
{
public class ComponentsModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -19,6 +19,9 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Components">Components</a>
</li>
</ul>
</div>
</div>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<DefineConstants>$(DefineConstants)</DefineConstants>
<!-- We have tests that test runtime view compilation. -->
<RazorCompileOnBuild>false</RazorCompileOnBuild>
<IsTestAssetProject>true</IsTestAssetProject>
@ -21,4 +20,6 @@
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
</ItemGroup>
<Import Project="..\..\..\Mvc.Razor.RuntimeCompilation\src\targets\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.targets" />
</Project>

View File

@ -11,7 +11,7 @@
<!--#if (IndividualLocalAuth && !UseLocalDB) -->
<ItemGroup>
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" />
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup>
<!--#endif -->

View File

@ -13,7 +13,7 @@
<!--#endif -->
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" />
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup>
<!--#if (IndividualB2CAuth || IndividualLocalAuth || OrganizationalAuth) -->

View File

@ -13,7 +13,7 @@
<!--#endif -->
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" />
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup>
<!--#if (IndividualB2CAuth || IndividualLocalAuth || OrganizationalAuth) -->

View File

@ -14,9 +14,20 @@
</head>
<body>
<app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -111,6 +111,25 @@ app {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@media (max-width: 767.98px) {
.main .top-row {
display: none;

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -1,7 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},

View File

@ -2,7 +2,7 @@ syntax = "proto3";
option csharp_namespace = "GrpcService_CSharp";
package Greet;
package greet;
// The greeting service definition.
service Greeter {

View File

@ -1,7 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -1,7 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -1,7 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" />
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->

View File

@ -24,7 +24,7 @@
</ItemGroup>
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" />
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
////#if (IndividualLocalAuth)
// },

View File

@ -10,7 +10,9 @@
////#endif
"Logging": {
"LogLevel": {
"Default": "Warning"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
////#if (IndividualLocalAuth)

View File

@ -1,9 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
////#if (IndividualLocalAuth)
// },

View File

@ -10,7 +10,9 @@
////#endif
"Logging": {
"LogLevel": {
"Default": "Warning"
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
////#if (IndividualLocalAuth)

Some files were not shown because too many files have changed in this diff Show More