diff --git a/src/Components/Blazor/testassets/StandaloneApp/Pages/FetchData.razor b/src/Components/Blazor/testassets/StandaloneApp/Pages/FetchData.razor
index 9a741bddfbbe..c640fe9fbc07 100644
--- a/src/Components/Blazor/testassets/StandaloneApp/Pages/FetchData.razor
+++ b/src/Components/Blazor/testassets/StandaloneApp/Pages/FetchData.razor
@@ -34,36 +34,32 @@ else
-
+
◀ Previous
-
+
Next ▶
}
@code {
- [Parameter] public DateTime StartDate { get; set; }
+ [Parameter] public DateTime? StartDate { get; set; }
WeatherForecast[] forecasts;
-
- public override Task SetParametersAsync(ParameterCollection parameters)
- {
- StartDate = DateTime.Now;
- return base.SetParametersAsync(parameters);
- }
+ DateTime startDate;
protected override async Task OnParametersSetAsync()
{
+ startDate = StartDate.GetValueOrDefault(DateTime.Now);
forecasts = await Http.GetJsonAsync(
- $"sample-data/weather.json?date={StartDate.ToString("yyyy-MM-dd")}");
+ $"sample-data/weather.json?date={startDate.ToString("yyyy-MM-dd")}");
// Because StandaloneApp doesn't really have a server endpoint to get dynamic data from,
// fake the DateFormatted values here. This would not apply in a real app.
for (var i = 0; i < forecasts.Length; i++)
{
- forecasts[i].DateFormatted = StartDate.AddDays(i).ToShortDateString();
+ forecasts[i].DateFormatted = startDate.AddDays(i).ToShortDateString();
}
}
diff --git a/src/Components/Components/src/Reflection/MemberAssignment.cs b/src/Components/Components/src/Reflection/MemberAssignment.cs
index 46d2b8056944..0ab288cedcbe 100644
--- a/src/Components/Components/src/Reflection/MemberAssignment.cs
+++ b/src/Components/Components/src/Reflection/MemberAssignment.cs
@@ -51,7 +51,16 @@ public PropertySetter(MethodInfo setMethod)
}
public void SetValue(object target, object value)
- => _setterDelegate((TTarget)target, (TValue)value);
+ {
+ if (value == null)
+ {
+ _setterDelegate((TTarget)target, default);
+ }
+ else
+ {
+ _setterDelegate((TTarget)target, (TValue)value);
+ }
+ }
}
}
}
diff --git a/src/Components/Components/src/Routing/RouteEntry.cs b/src/Components/Components/src/Routing/RouteEntry.cs
index cff9420bd9c0..3870ef2245ff 100644
--- a/src/Components/Components/src/Routing/RouteEntry.cs
+++ b/src/Components/Components/src/Routing/RouteEntry.cs
@@ -8,14 +8,17 @@ namespace Microsoft.AspNetCore.Components.Routing
{
internal class RouteEntry
{
- public RouteEntry(RouteTemplate template, Type handler)
+ public RouteEntry(RouteTemplate template, Type handler, string[] unusedRouteParameterNames)
{
Template = template;
+ UnusedRouteParameterNames = unusedRouteParameterNames;
Handler = handler;
}
public RouteTemplate Template { get; }
+ public string[] UnusedRouteParameterNames { get; }
+
public Type Handler { get; }
internal void Match(RouteContext context)
@@ -45,6 +48,18 @@ internal void Match(RouteContext context)
}
}
+ // In addition to extracting parameter values from the URL, each route entry
+ // also knows which other parameters should be supplied with null values. These
+ // are parameters supplied by other route entries matching the same handler.
+ if (UnusedRouteParameterNames.Length > 0)
+ {
+ parameters ??= new Dictionary(StringComparer.Ordinal);
+ for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
+ {
+ parameters[UnusedRouteParameterNames[i]] = null;
+ }
+ }
+
context.Parameters = parameters;
context.Handler = Handler;
}
diff --git a/src/Components/Components/src/Routing/RouteTableFactory.cs b/src/Components/Components/src/Routing/RouteTableFactory.cs
index eae29a7530b4..e0601bc95a9e 100644
--- a/src/Components/Components/src/Routing/RouteTableFactory.cs
+++ b/src/Components/Components/src/Routing/RouteTableFactory.cs
@@ -34,19 +34,38 @@ public static RouteTable Create(Assembly appAssembly)
internal static RouteTable Create(IEnumerable componentTypes)
{
- var routes = new List();
- foreach (var type in componentTypes)
+ var templatesByHandler = new Dictionary();
+ foreach (var componentType in componentTypes)
{
// We're deliberately using inherit = false here.
//
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
// ambiguity. You end up with two components (base class and derived class) with the same route.
- var routeAttributes = type.GetCustomAttributes(inherit: false);
+ var routeAttributes = componentType.GetCustomAttributes(inherit: false);
+
+ var templates = routeAttributes.Select(t => t.Template).ToArray();
+ templatesByHandler.Add(componentType, templates);
+ }
+ return Create(templatesByHandler);
+ }
- foreach (var routeAttribute in routeAttributes)
+ internal static RouteTable Create(Dictionary templatesByHandler)
+ {
+ var routes = new List();
+ foreach (var keyValuePair in templatesByHandler)
+ {
+ var parsedTemplates = keyValuePair.Value.Select(v => TemplateParser.ParseTemplate(v)).ToArray();
+ var allRouteParameterNames = parsedTemplates
+ .SelectMany(GetParameterNames)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ foreach (var parsedTemplate in parsedTemplates)
{
- var template = TemplateParser.ParseTemplate(routeAttribute.Template);
- var entry = new RouteEntry(template, type);
+ var unusedRouteParameterNames = allRouteParameterNames
+ .Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+ var entry = new RouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames);
routes.Add(entry);
}
}
@@ -54,6 +73,14 @@ internal static RouteTable Create(IEnumerable componentTypes)
return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
}
+ private static string[] GetParameterNames(RouteTemplate routeTemplate)
+ {
+ return routeTemplate.Segments
+ .Where(s => s.IsParameter)
+ .Select(s => s.Value)
+ .ToArray();
+ }
+
///
/// Route precedence algorithm.
/// We collect all the routes and sort them from most specific to
diff --git a/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs b/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs
index 131697490f42..987673ae744a 100644
--- a/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs
+++ b/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs
@@ -358,6 +358,24 @@ public void DeclaredParameterClashesWithInheritedParameter_Throws()
ex.Message);
}
+ [Fact]
+ public void SupplyingNullWritesDefaultForType()
+ {
+ // Arrange
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { nameof(HasInstanceProperties.IntProp), null },
+ { nameof(HasInstanceProperties.StringProp), null },
+ }.Build();
+ var target = new HasInstanceProperties { IntProp = 123, StringProp = "Hello" };
+
+ // Act
+ parameterCollection.SetParameterProperties(target);
+
+ // Assert
+ Assert.Equal(0, target.IntProp);
+ Assert.Null(target.StringProp);
+ }
class HasInstanceProperties
{
diff --git a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs
index f6733d8ceb1d..8250fab16bc3 100644
--- a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs
+++ b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs
@@ -268,8 +268,9 @@ public void PrefersLiteralTemplateOverTemplateWithParameters()
{
// Arrange
var routeTable = new TestRouteTableBuilder()
- .AddRoute("/an/awesome/path")
- .AddRoute("/{some}/awesome/{route}/").Build();
+ .AddRoute("/an/awesome/path", typeof(TestHandler1))
+ .AddRoute("/{some}/awesome/{route}/", typeof(TestHandler2))
+ .Build();
var context = new RouteContext("/an/awesome/path");
// Act
@@ -346,9 +347,58 @@ public void DetectsAmbiguousRoutes(string left, string right)
Assert.Equal(expectedMessage, exception.Message);
}
+ [Fact]
+ public void SuppliesNullForUnusedHandlerParameters()
+ {
+ // Arrange
+ var routeTable = new TestRouteTableBuilder()
+ .AddRoute("/", typeof(TestHandler1))
+ .AddRoute("/products/{param1:int}", typeof(TestHandler1))
+ .AddRoute("/products/{param2}/{PaRam1}", typeof(TestHandler1))
+ .AddRoute("/{unrelated}", typeof(TestHandler2))
+ .Build();
+ var context = new RouteContext("/products/456");
+
+ // Act
+ routeTable.Route(context);
+
+ // Assert
+ Assert.Collection(routeTable.Routes,
+ route =>
+ {
+ Assert.Same(typeof(TestHandler1), route.Handler);
+ Assert.Equal("/", route.Template.TemplateText);
+ Assert.Equal(new[] { "param1", "param2" }, route.UnusedRouteParameterNames);
+ },
+ route =>
+ {
+ Assert.Same(typeof(TestHandler2), route.Handler);
+ Assert.Equal("{unrelated}", route.Template.TemplateText);
+ Assert.Equal(Array.Empty(), route.UnusedRouteParameterNames);
+ },
+ route =>
+ {
+ Assert.Same(typeof(TestHandler1), route.Handler);
+ Assert.Equal("products/{param1:int}", route.Template.TemplateText);
+ Assert.Equal(new[] { "param2" }, route.UnusedRouteParameterNames);
+ },
+ route =>
+ {
+ Assert.Same(typeof(TestHandler1), route.Handler);
+ Assert.Equal("products/{param2}/{PaRam1}", route.Template.TemplateText);
+ Assert.Equal(Array.Empty(), route.UnusedRouteParameterNames);
+ });
+ Assert.Same(typeof(TestHandler1), context.Handler);
+ Assert.Equal(new Dictionary
+ {
+ { "param1", 456 },
+ { "param2", null },
+ }, context.Parameters);
+ }
+
private class TestRouteTableBuilder
{
- IList<(string, Type)> _routeTemplates = new List<(string, Type)>();
+ IList<(string Template, Type Handler)> _routeTemplates = new List<(string, Type)>();
Type _handler = typeof(object);
public TestRouteTableBuilder AddRoute(string template, Type handler = null)
@@ -361,10 +411,10 @@ public RouteTable Build()
{
try
{
- return new RouteTable(_routeTemplates
- .Select(rt => new RouteEntry(TemplateParser.ParseTemplate(rt.Item1), rt.Item2))
- .OrderBy(id => id, RouteTableFactory.RoutePrecedence)
- .ToArray());
+ var templatesByHandler = _routeTemplates
+ .GroupBy(rt => rt.Handler)
+ .ToDictionary(group => group.Key, group => group.Select(g => g.Template).ToArray());
+ return RouteTableFactory.Create(templatesByHandler);
}
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
{
@@ -373,5 +423,8 @@ public RouteTable Build()
}
}
}
+
+ class TestHandler1 { }
+ class TestHandler2 { }
}
}
diff --git a/src/Components/test/E2ETest/Tests/RoutingTest.cs b/src/Components/test/E2ETest/Tests/RoutingTest.cs
index ce0dc725e69f..8a44d704c71f 100644
--- a/src/Components/test/E2ETest/Tests/RoutingTest.cs
+++ b/src/Components/test/E2ETest/Tests/RoutingTest.cs
@@ -226,10 +226,6 @@ public void CanFollowLinkToPageWithParameters()
AssertHighlightedLinks("With parameters", "With more parameters");
// Can remove parameters while remaining on same page
- // WARNING: This only works because the WithParameters component overrides SetParametersAsync
- // and explicitly resets its parameters to default when each new set of parameters arrives.
- // Without that, the page would retain the old value.
- // See https://github.com/aspnet/AspNetCore/issues/6864 where we reverted the logic to auto-reset.
app.FindElement(By.LinkText("With parameters")).Click();
Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("With parameters");
diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/WithParameters.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/WithParameters.razor
index 8b242ccbd296..a353a87c95fc 100644
--- a/src/Components/test/testassets/BasicTestApp/RouterTest/WithParameters.razor
+++ b/src/Components/test/testassets/BasicTestApp/RouterTest/WithParameters.razor
@@ -8,12 +8,4 @@
[Parameter] public string FirstName { get; set; }
[Parameter] public string LastName { get ; set; }
-
- public override Task SetParametersAsync(ParameterCollection parameters)
- {
- // Manually reset parameters to defaults so we don't retain any from an earlier URL
- FirstName = default;
- LastName = default;
- return base.SetParametersAsync(parameters);
- }
}
diff --git a/src/Components/test/testassets/ComponentsApp.App/Pages/FetchData.razor b/src/Components/test/testassets/ComponentsApp.App/Pages/FetchData.razor
index 239e35cb8c58..fd8493791164 100644
--- a/src/Components/test/testassets/ComponentsApp.App/Pages/FetchData.razor
+++ b/src/Components/test/testassets/ComponentsApp.App/Pages/FetchData.razor
@@ -34,28 +34,24 @@ else
-
+
◀ Previous
-
+
Next ▶
}
@code {
- [Parameter] public DateTime StartDate { get; set; }
+ [Parameter] public DateTime? StartDate { get; set; }
WeatherForecast[] forecasts;
-
- public override Task SetParametersAsync(ParameterCollection parameters)
- {
- StartDate = DateTime.Now;
- return base.SetParametersAsync(parameters);
- }
+ DateTime startDate;
protected override async Task OnParametersSetAsync()
{
- forecasts = await ForecastService.GetForecastAsync(StartDate);
+ startDate = StartDate.GetValueOrDefault(DateTime.Now);
+ forecasts = await ForecastService.GetForecastAsync(startDate);
}
}