Throw unhandled exceptions during prerendering

Fixes: #8609

Currently exceptions thrown during prerendering are simply logged. This
change uses the existing *unhandled exception* mechanism of the
renderer/circuit to throw these. The result is that the developer
exception page just works for prerendering.
This commit is contained in:
Ryan Nowak 2019-03-17 20:14:44 -07:00
parent 2a08c6e54d
commit b743ba2f66
10 changed files with 522 additions and 77 deletions

View File

@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Components
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<float?> setter, float? existingValue) { throw null; }
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<float> setter, float existingValue) { throw null; }
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<string> setter, string existingValue) { throw null; }
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<T> setter, T existingValue) where T : System.Enum { throw null; }
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<T> setter, T existingValue) where T : struct, System.Enum { throw null; }
}
public static partial class EventCallbackFactoryUIEventArgsExtensions
{

View File

@ -11,74 +11,263 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static class EventCallbackFactoryBinderExtensions
{
private delegate bool BindConverter<T>(object obj, out T value);
// Perf: conversion delegates are written as static funcs so we can prevent
// allocations for these simple cases.
private static Func<object, string> ConvertToString = (obj) => (string)obj;
private readonly static BindConverter<string> ConvertToString = ConvertToStringCore;
private static Func<object, bool> ConvertToBool = (obj) => (bool)obj;
private static Func<object, bool?> ConvertToNullableBool = (obj) => (bool?)obj;
private static Func<object, int> ConvertToInt = (obj) => int.Parse((string)obj);
private static Func<object, int?> ConvertToNullableInt = (obj) =>
private static bool ConvertToStringCore(object obj, out string value)
{
if (int.TryParse((string)obj, out var value))
// We expect the input to already be a string.
value = (string)obj;
return true;
}
private static BindConverter<bool> ConvertToBool = ConvertToBoolCore;
private static BindConverter<bool?> ConvertToNullableBool = ConvertToNullableBoolCore;
private static bool ConvertToBoolCore(object obj, out bool value)
{
// We expect the input to already be a bool.
value = (bool)obj;
return true;
}
private static bool ConvertToNullableBoolCore(object obj, out bool? value)
{
// We expect the input to already be a bool.
value = (bool?)obj;
return true;
}
private static BindConverter<int> ConvertToInt = ConvertToIntCore;
private static BindConverter<int?> ConvertToNullableInt = ConvertToNullableIntCore;
private static bool ConvertToIntCore(object obj, out int value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
return value;
value = default;
return false;
}
return null;
};
private static Func<object, long> ConvertToLong = (obj) => long.Parse((string)obj);
private static Func<object, long?> ConvertToNullableLong = (obj) =>
{
if (long.TryParse((string)obj, out var value))
if (!int.TryParse(text, out var converted))
{
return value;
value = default;
return false;
}
return null;
};
value = converted;
return true;
}
private static Func<object, float> ConvertToFloat = (obj) => float.Parse((string)obj);
private static Func<object, float?> ConvertToNullableFloat = (obj) =>
private static bool ConvertToNullableIntCore(object obj, out int? value)
{
if (float.TryParse((string)obj, out var value))
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
return value;
value = default;
return true;
}
return null;
};
private static Func<object, double> ConvertToDouble = (obj) => double.Parse((string)obj);
private static Func<object, double?> ConvertToNullableDouble = (obj) =>
{
if (double.TryParse((string)obj, out var value))
if (!int.TryParse(text, out var converted))
{
return value;
value = default;
return false;
}
return null;
};
value = converted;
return true;
}
private static Func<object, decimal> ConvertToDecimal = (obj) => decimal.Parse((string)obj);
private static Func<object, decimal?> ConvertToNullableDecimal = (obj) =>
private static BindConverter<long> ConvertToLong = ConvertToLongCore;
private static BindConverter<long?> ConvertToNullableLong = ConvertToNullableLongCore;
private static bool ConvertToLongCore(object obj, out long value)
{
if (decimal.TryParse((string)obj, out var value))
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
return value;
value = default;
return false;
}
return null;
};
private static class EnumConverter<T> where T : Enum
{
public static Func<object, T> Convert = (obj) =>
if (!long.TryParse(text, out var converted))
{
return (T)Enum.Parse(typeof(T), (string)obj);
};
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableLongCore(object obj, out long? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!long.TryParse(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<float> ConvertToFloat = ConvertToFloatCore;
private static BindConverter<float?> ConvertToNullableFloat = ConvertToNullableFloatCore;
private static bool ConvertToFloatCore(object obj, out float value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!float.TryParse(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableFloatCore(object obj, out float? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!float.TryParse(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<double> ConvertToDouble = ConvertToDoubleCore;
private static BindConverter<double?> ConvertToNullableDouble = ConvertToNullableDoubleCore;
private static bool ConvertToDoubleCore(object obj, out double value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!double.TryParse(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableDoubleCore(object obj, out double? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!double.TryParse(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static BindConverter<decimal> ConvertToDecimal = ConvertToDecimalCore;
private static BindConverter<decimal?> ConvertToNullableDecimal = ConvertToNullableDecimalCore;
private static bool ConvertToDecimalCore(object obj, out decimal value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return false;
}
if (!decimal.TryParse(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static bool ConvertToNullableDecimalCore(object obj, out decimal? value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!decimal.TryParse(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
private static class EnumConverter<T> where T : struct, Enum
{
public static readonly BindConverter<T> Convert = ConvertCore;
public static bool ConvertCore(object obj, out T value)
{
var text = (string)obj;
if (string.IsNullOrEmpty(text))
{
value = default;
return true;
}
if (!Enum.TryParse<T>(text, out var converted))
{
value = default;
return false;
}
value = converted;
return true;
}
}
/// <summary>
@ -330,7 +519,22 @@ namespace Microsoft.AspNetCore.Components
// when a format is used.
Action<UIChangeEventArgs> callback = (e) =>
{
setter(ConvertDateTime(e.Value, format: null));
DateTime value = default;
var converted = false;
try
{
value = ConvertDateTime(e.Value, format: null);
converted = true;
}
catch
{
}
// See comments in CreateBinderCore
if (converted)
{
setter(value);
}
};
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
@ -355,7 +559,22 @@ namespace Microsoft.AspNetCore.Components
// when a format is used.
Action<UIChangeEventArgs> callback = (e) =>
{
setter(ConvertDateTime(e.Value, format));
DateTime value = default;
var converted = false;
try
{
value = ConvertDateTime(e.Value, format);
converted = true;
}
catch
{
}
// See comments in CreateBinderCore
if (converted)
{
setter(value);
}
};
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
@ -373,7 +592,7 @@ namespace Microsoft.AspNetCore.Components
this EventCallbackFactory factory,
object receiver,
Action<T> setter,
T existingValue) where T : Enum
T existingValue) where T : struct, Enum
{
return CreateBinderCore<T>(factory, receiver, setter, EnumConverter<T>.Convert);
}
@ -399,11 +618,27 @@ namespace Microsoft.AspNetCore.Components
this EventCallbackFactory factory,
object receiver,
Action<T> setter,
Func<object, T> converter)
BindConverter<T> converter)
{
Action<UIChangeEventArgs> callback = e =>
{
setter(converter(e.Value));
T value = default;
var converted = false;
try
{
converted = converter(e.Value, out value);
}
catch
{
}
// We only invoke the setter if the conversion didn't throw. This is valuable because it allows us to attempt
// to process invalid input but avoid dirtying the state of the component if can't be converted. Imagine if
// we assigned default(T) on failure - this would result in trouncing the user's typed in value.
if (converted)
{
setter(value);
}
};
return factory.Create<UIChangeEventArgs>(receiver, callback);
}

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components
public class EventCallbackFactoryBinderExtensionsTest
{
[Fact]
public async Task CreateBinder_ThrowsConversionException()
public async Task CreateBinder_SwallowsConversionException()
{
// Arrange
var value = 17;
@ -20,12 +20,61 @@ namespace Microsoft.AspNetCore.Components
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
// Act
await Assert.ThrowsAsync<FormatException>(() =>
await binder.InvokeAsync(new UIChangeEventArgs() { Value = "not-an-integer!", });
Assert.Equal(17, value); // Setter not called
Assert.Equal(1, component.Count);
}
[Fact]
public async Task CreateBinder_ThrowsSetterException()
{
// Arrange
var component = new EventCountingComponent();
Action<int> setter = (_) => { throw new InvalidTimeZoneException(); };
var binder = EventCallback.Factory.CreateBinder(component, setter, 17);
// Act
await Assert.ThrowsAsync<InvalidTimeZoneException>(() =>
{
return binder.InvokeAsync(new UIChangeEventArgs() { Value = "not-an-integer!", });
return binder.InvokeAsync(new UIChangeEventArgs() { Value = "18", });
});
Assert.Equal(17, value);
Assert.Equal(1, component.Count);
}
[Fact]
public async Task CreateBinder_BindsEmpty_DoesNotCallSetter()
{
// Arrange
var value = 17;
var component = new EventCountingComponent();
Action<int> setter = (_) => value = _;
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
// Act
await binder.InvokeAsync(new UIChangeEventArgs() { Value = "not-an-integer!", });
Assert.Equal(17, value); // Setter not called
Assert.Equal(1, component.Count);
}
[Fact]
public async Task CreateBinder_BindsEmpty_CallsSetterForNullable()
{
// Arrange
var value = (int?)17;
var component = new EventCountingComponent();
Action<int?> setter = (_) => value = _;
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
// Act
await binder.InvokeAsync(new UIChangeEventArgs() { Value = "", });
Assert.Null(value); // Setter called
Assert.Equal(1, component.Count);
}

View File

@ -1,7 +1,9 @@
// 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.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
@ -26,8 +28,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
GetFullUri(context.Request),
GetFullBaseUri(context.Request));
// We don't need to unsubscribe because the circuit host object is scoped to this call.
circuitHost.UnhandledException += CircuitHost_UnhandledException;
// For right now we just do prerendering and dispose the circuit. In the future we will keep the circuit around and
// reconnect to it from the ComponentsHub.
// reconnect to it from the ComponentsHub. If we keep the circuit/renderer we also need to unsubscribe this error
// handler.
try
{
return await circuitHost.PrerenderComponentAsync(
@ -40,6 +46,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
}
}
private void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
// Throw all exceptions encountered during pre-rendering so the default developer
// error page can respond.
ExceptionDispatchInfo.Capture((Exception)e.ExceptionObject).Throw();
}
private string GetFullUri(HttpRequest request)
{
return UriHelper.BuildAbsolute(

View File

@ -94,6 +94,8 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
{
Log.UnhandledExceptionRenderingComponent(_logger, exception);
}
UnhandledException?.Invoke(this, exception);
}
/// <inheritdoc />

View File

@ -199,8 +199,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("-42", boundValue.Text);
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Modify target; value is not updated because it's not convertable.
target.Clear();
Browser.Equal("-42", () => boundValue.Text);
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.SendKeys("42\t");
Browser.Equal("42", () => boundValue.Text);
Assert.Equal("42", mirrorValue.GetAttribute("value"));
@ -218,6 +222,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// 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
target.SendKeys("-42\t");
Browser.Equal("-42", () => boundValue.Text);
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
@ -245,8 +253,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("3000000000", boundValue.Text);
Assert.Equal("3000000000", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Modify target; value is not updated because it's not convertable.
target.Clear();
Browser.Equal("3000000000", () => boundValue.Text);
Assert.Equal("3000000000", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.SendKeys("-3000000000\t");
Browser.Equal("-3000000000", () => boundValue.Text);
Assert.Equal("-3000000000", mirrorValue.GetAttribute("value"));
@ -264,6 +276,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// 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
target.SendKeys("3000000000\t");
Browser.Equal("3000000000", () => boundValue.Text);
Assert.Equal("3000000000", mirrorValue.GetAttribute("value"));
@ -291,8 +307,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("3.141", boundValue.Text);
Assert.Equal("3.141", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Modify target; value is not updated because it's not convertable.
target.Clear();
Browser.Equal("3.141", () => boundValue.Text);
Assert.Equal("3.141", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.SendKeys("-3.141\t");
Browser.Equal("-3.141", () => boundValue.Text);
Assert.Equal("-3.141", mirrorValue.GetAttribute("value"));
@ -310,6 +330,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// 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
target.SendKeys("3.141\t");
Browser.Equal("3.141", () => boundValue.Text);
Assert.Equal("3.141", mirrorValue.GetAttribute("value"));
@ -337,8 +361,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("3.14159265359", boundValue.Text);
Assert.Equal("3.14159265359", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Modify target; value is not updated because it's not convertable.
target.Clear();
Browser.Equal("3.14159265359", () => boundValue.Text);
Assert.Equal("3.14159265359", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.SendKeys("-3.14159265359\t");
Browser.Equal("-3.14159265359", () => boundValue.Text);
Assert.Equal("-3.14159265359", mirrorValue.GetAttribute("value"));
@ -363,6 +391,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// 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
target.SendKeys("3.14159265359\t");
Browser.Equal("3.14159265359", () => boundValue.Text);
Assert.Equal("3.14159265359", mirrorValue.GetAttribute("value"));
@ -397,9 +429,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("0.0000000000000000000000000001", boundValue.Text);
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
// Modify target; value is not updated because it's not convertable.
target.Clear();
Browser.Equal("0.0000000000000000000000000001", () => boundValue.Text);
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Decimal should preserve trailing zeros
target.Clear();
target.SendKeys("0.010\t");
Browser.Equal("0.010", () => boundValue.Text);
Assert.Equal("0.010", mirrorValue.GetAttribute("value"));
@ -417,6 +453,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// 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
target.SendKeys("0.0000000000000000000000000001\t");
Browser.Equal("0.0000000000000000000000000001", () => boundValue.Text);
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
@ -434,5 +474,69 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// This tests what happens you put invalid (unconvertable) input in. This is separate from the
// other tests because it requires type="text" - the other tests use type="number"
[Fact]
public void CanBindTextbox_Decimal_InvalidInput()
{
var target = Browser.FindElement(By.Id("textbox-decimal-invalid"));
var boundValue = Browser.FindElement(By.Id("textbox-decimal-invalid-value"));
var mirrorValue = Browser.FindElement(By.Id("textbox-decimal-invalid-mirror"));
Assert.Equal("0.0000000000000000000000000001", target.GetAttribute("value"));
Assert.Equal("0.0000000000000000000000000001", boundValue.Text);
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("0.01\t");
Browser.Equal("0.01", () => boundValue.Text);
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
// Modify target to something invalid - the invalid value is preserved in the input, the other displays
// don't change and still have the last value valid.
target.SendKeys("A\t");
Browser.Equal("0.01", () => boundValue.Text);
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
Assert.Equal("0.01A", target.GetAttribute("value"));
// Modify target to something valid.
target.SendKeys(Keys.Backspace);
target.SendKeys("1\t");
Browser.Equal("0.011", () => boundValue.Text);
Assert.Equal("0.011", mirrorValue.GetAttribute("value"));
}
// This tests what happens you put invalid (unconvertable) input in. This is separate from the
// other tests because it requires type="text" - the other tests use type="number"
[Fact]
public void CanBindTextbox_NullableDecimal_InvalidInput()
{
var target = Browser.FindElement(By.Id("textbox-nullable-decimal-invalid"));
var boundValue = Browser.FindElement(By.Id("textbox-nullable-decimal-invalid-value"));
var mirrorValue = Browser.FindElement(By.Id("textbox-nullable-decimal-invalid-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();
target.SendKeys("0.01\t");
Browser.Equal("0.01", () => boundValue.Text);
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
// Modify target to something invalid - the invalid value is preserved in the input, the other displays
// don't change and still have the last value valid.
target.SendKeys("A\t");
Browser.Equal("0.01", () => boundValue.Text);
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
Assert.Equal("0.01A", target.GetAttribute("value"));
// Modify target to something valid.
target.SendKeys(Keys.Backspace);
target.SendKeys("1\t");
Browser.Equal("0.011", () => boundValue.Text);
Assert.Equal("0.011", mirrorValue.GetAttribute("value"));
}
}
}

View File

@ -75,6 +75,18 @@
<span id="textbox-nullable-decimal-value">@textboxNullableDecimalValue</span>
<input id="textbox-nullable-decimal-mirror" bind="textboxNullableDecimalValue" readonly />
</p>
<p>
decimal (invalid-input):
<input id="textbox-decimal-invalid" bind="textboxDecimalInvalidValue" />
<span id="textbox-decimal-invalid-value">@textboxDecimalInvalidValue</span>
<input id="textbox-decimal-invalid-mirror" bind="textboxDecimalInvalidValue" readonly />
</p>
<p>
Nullable decimal (invalid-input):
<input id="textbox-nullable-decimal-invalid" bind="textboxNullableDecimalInvalidValue" />
<span id="textbox-nullable-decimal-invalid-value">@textboxNullableDecimalInvalidValue</span>
<input id="textbox-nullable-decimal-invalid-mirror" bind="textboxNullableDecimalInvalidValue" readonly />
</p>
<h2>Text Area</h2>
<p>
@ -116,7 +128,7 @@
<option value=@SelectableValue.Third>Third choice</option>
@if (includeFourthOption)
{
<option value=@SelectableValue.Fourth>Fourth choice</option>
<option value=@SelectableValue.Fourth>Fourth choice</option>
}
</select>
<span id="select-box-value">@selectValue</span>
@ -144,6 +156,8 @@
double? textboxNullableDoubleValue = null;
decimal textboxDecimalValue = 0.0000000000000000000000000001M;
decimal? textboxNullableDecimalValue = null;
decimal textboxDecimalInvalidValue = 0.0000000000000000000000000001M;
decimal? textboxNullableDecimalInvalidValue = null;
bool includeFourthOption = false;
enum SelectableValue { First, Second, Third, Fourth }

View File

@ -1,12 +1,14 @@
// 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.Net;
using System.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Parser.Html;
using BasicWebSite;
using BasicWebSite.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@ -17,18 +19,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public ComponentRenderingFunctionalTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
{
Factory = fixture;
Client = Client ?? CreateClient(fixture);
}
public HttpClient Client { get; }
public MvcTestFixture<StartupWithoutEndpointRouting> Factory { get; }
[Fact]
public async Task Renders_BasicComponent()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/components");
var client = CreateClient(Factory);
var response = await client.GetAsync("http://localhost/components");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -41,9 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer()
{
// Arrange & Act
var client = Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
.CreateClient();
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var response = await client.GetAsync("http://localhost/components");
@ -58,7 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task Renders_RoutingComponent()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/components/routable");
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var response = await client.GetAsync("http://localhost/components/routable");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -71,9 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer()
{
// Arrange & Act
var client = Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
.CreateClient();
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var response = await client.GetAsync("http://localhost/components/routable");
@ -84,6 +83,21 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
}
[Fact]
public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerrenderer()
{
// Arrange & Act
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var response = await client.GetAsync("http://localhost/components/throws");
// Assert
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("InvalidTimeZoneException: test", content);
}
[Fact]
public async Task Renders_AsyncComponent()
{
@ -138,8 +152,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
</table>
";
var response = await Client.GetAsync("http://localhost/components");
var client = CreateClient(Factory);
var response = await client.GetAsync("http://localhost/components");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -164,12 +178,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
}
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture, Action<IWebHostBuilder> configure = null)
{
var loopHandler = new LoopHttpHandler();
var client = fixture
.WithWebHostBuilder(builder => builder.ConfigureServices(ConfigureTestWeatherForecastService))
.WithWebHostBuilder(builder =>
{
configure?.Invoke(builder);
builder.ConfigureServices(ConfigureTestWeatherForecastService);
})
.CreateClient();
// We configure the inner handler with a handler to this TestServer instance so that calls to the

View File

@ -51,7 +51,7 @@ namespace BasicWebSite.Controllers
};
[HttpGet("/components")]
[HttpGet("/components/routable")]
[HttpGet("/components/{component}")]
public IActionResult Index()
{
return View();

View File

@ -0,0 +1,10 @@
@page "/components/throws"
@* This is expected to throw and result in a 500 *@
@functions {
protected override async Task OnInitAsync()
{
await base.OnInitAsync();
throw new InvalidTimeZoneException("test");
}
}