Clear unused routing params. Fixes #7419 (#12407)

This commit is contained in:
Steve Sanderson 2019-07-21 20:00:03 -07:00 committed by GitHub
parent 6a24db5543
commit 2c3a44371a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 150 additions and 48 deletions

View File

@ -34,36 +34,32 @@ else
</tbody> </tbody>
</table> </table>
<p> <p>
<a href="fetchdata/@StartDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left"> <a href="fetchdata/@startDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
◀ Previous ◀ Previous
</a> </a>
<a href="fetchdata/@StartDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right"> <a href="fetchdata/@startDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
Next ▶ Next ▶
</a> </a>
</p> </p>
} }
@code { @code {
[Parameter] public DateTime StartDate { get; set; } [Parameter] public DateTime? StartDate { get; set; }
WeatherForecast[] forecasts; WeatherForecast[] forecasts;
DateTime startDate;
public override Task SetParametersAsync(ParameterCollection parameters)
{
StartDate = DateTime.Now;
return base.SetParametersAsync(parameters);
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
startDate = StartDate.GetValueOrDefault(DateTime.Now);
forecasts = await Http.GetJsonAsync<WeatherForecast[]>( forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
$"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, // 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. // fake the DateFormatted values here. This would not apply in a real app.
for (var i = 0; i < forecasts.Length; i++) for (var i = 0; i < forecasts.Length; i++)
{ {
forecasts[i].DateFormatted = StartDate.AddDays(i).ToShortDateString(); forecasts[i].DateFormatted = startDate.AddDays(i).ToShortDateString();
} }
} }

View File

@ -51,7 +51,16 @@ namespace Microsoft.AspNetCore.Components.Reflection
} }
public void SetValue(object target, object value) 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);
}
}
} }
} }
} }

View File

@ -8,14 +8,17 @@ namespace Microsoft.AspNetCore.Components.Routing
{ {
internal class RouteEntry internal class RouteEntry
{ {
public RouteEntry(RouteTemplate template, Type handler) public RouteEntry(RouteTemplate template, Type handler, string[] unusedRouteParameterNames)
{ {
Template = template; Template = template;
UnusedRouteParameterNames = unusedRouteParameterNames;
Handler = handler; Handler = handler;
} }
public RouteTemplate Template { get; } public RouteTemplate Template { get; }
public string[] UnusedRouteParameterNames { get; }
public Type Handler { get; } public Type Handler { get; }
internal void Match(RouteContext context) internal void Match(RouteContext context)
@ -45,6 +48,18 @@ namespace Microsoft.AspNetCore.Components.Routing
} }
} }
// 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<string, object>(StringComparer.Ordinal);
for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
{
parameters[UnusedRouteParameterNames[i]] = null;
}
}
context.Parameters = parameters; context.Parameters = parameters;
context.Handler = Handler; context.Handler = Handler;
} }

View File

@ -34,19 +34,38 @@ namespace Microsoft.AspNetCore.Components
internal static RouteTable Create(IEnumerable<Type> componentTypes) internal static RouteTable Create(IEnumerable<Type> componentTypes)
{ {
var routes = new List<RouteEntry>(); var templatesByHandler = new Dictionary<Type, string[]>();
foreach (var type in componentTypes) foreach (var componentType in componentTypes)
{ {
// We're deliberately using inherit = false here. // We're deliberately using inherit = false here.
// //
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an // 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. // ambiguity. You end up with two components (base class and derived class) with the same route.
var routeAttributes = type.GetCustomAttributes<RouteAttribute>(inherit: false); var routeAttributes = componentType.GetCustomAttributes<RouteAttribute>(inherit: false);
foreach (var routeAttribute in routeAttributes) var templates = routeAttributes.Select(t => t.Template).ToArray();
templatesByHandler.Add(componentType, templates);
}
return Create(templatesByHandler);
}
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler)
{
var routes = new List<RouteEntry>();
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 unusedRouteParameterNames = allRouteParameterNames
var entry = new RouteEntry(template, type); .Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase)
.ToArray();
var entry = new RouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames);
routes.Add(entry); routes.Add(entry);
} }
} }
@ -54,6 +73,14 @@ namespace Microsoft.AspNetCore.Components
return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray()); 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();
}
/// <summary> /// <summary>
/// Route precedence algorithm. /// Route precedence algorithm.
/// We collect all the routes and sort them from most specific to /// We collect all the routes and sort them from most specific to

View File

@ -358,6 +358,24 @@ namespace Microsoft.AspNetCore.Components.Test
ex.Message); 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 class HasInstanceProperties
{ {

View File

@ -268,8 +268,9 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
{ {
// Arrange // Arrange
var routeTable = new TestRouteTableBuilder() var routeTable = new TestRouteTableBuilder()
.AddRoute("/an/awesome/path") .AddRoute("/an/awesome/path", typeof(TestHandler1))
.AddRoute("/{some}/awesome/{route}/").Build(); .AddRoute("/{some}/awesome/{route}/", typeof(TestHandler2))
.Build();
var context = new RouteContext("/an/awesome/path"); var context = new RouteContext("/an/awesome/path");
// Act // Act
@ -346,9 +347,58 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
Assert.Equal(expectedMessage, exception.Message); 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<string>(), 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<string>(), route.UnusedRouteParameterNames);
});
Assert.Same(typeof(TestHandler1), context.Handler);
Assert.Equal(new Dictionary<string, object>
{
{ "param1", 456 },
{ "param2", null },
}, context.Parameters);
}
private class TestRouteTableBuilder 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); Type _handler = typeof(object);
public TestRouteTableBuilder AddRoute(string template, Type handler = null) public TestRouteTableBuilder AddRoute(string template, Type handler = null)
@ -361,10 +411,10 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
{ {
try try
{ {
return new RouteTable(_routeTemplates var templatesByHandler = _routeTemplates
.Select(rt => new RouteEntry(TemplateParser.ParseTemplate(rt.Item1), rt.Item2)) .GroupBy(rt => rt.Handler)
.OrderBy(id => id, RouteTableFactory.RoutePrecedence) .ToDictionary(group => group.Key, group => group.Select(g => g.Template).ToArray());
.ToArray()); return RouteTableFactory.Create(templatesByHandler);
} }
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException) catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
{ {
@ -373,5 +423,8 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
} }
} }
} }
class TestHandler1 { }
class TestHandler2 { }
} }
} }

View File

@ -226,10 +226,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
AssertHighlightedLinks("With parameters", "With more parameters"); AssertHighlightedLinks("With parameters", "With more parameters");
// Can remove parameters while remaining on same page // 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(); app.FindElement(By.LinkText("With parameters")).Click();
Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text); Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("With parameters"); AssertHighlightedLinks("With parameters");

View File

@ -8,12 +8,4 @@
[Parameter] public string FirstName { get; set; } [Parameter] public string FirstName { get; set; }
[Parameter] public string LastName { 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);
}
} }

View File

@ -34,28 +34,24 @@ else
</tbody> </tbody>
</table> </table>
<p> <p>
<a href="fetchdata/@StartDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left"> <a href="fetchdata/@startDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
◀ Previous ◀ Previous
</a> </a>
<a href="fetchdata/@StartDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right"> <a href="fetchdata/@startDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
Next ▶ Next ▶
</a> </a>
</p> </p>
} }
@code { @code {
[Parameter] public DateTime StartDate { get; set; } [Parameter] public DateTime? StartDate { get; set; }
WeatherForecast[] forecasts; WeatherForecast[] forecasts;
DateTime startDate;
public override Task SetParametersAsync(ParameterCollection parameters)
{
StartDate = DateTime.Now;
return base.SetParametersAsync(parameters);
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
forecasts = await ForecastService.GetForecastAsync(StartDate); startDate = StartDate.GetValueOrDefault(DateTime.Now);
forecasts = await ForecastService.GetForecastAsync(startDate);
} }
} }