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>
</table>
<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
</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 ▶
</a>
</p>
}
@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<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,
// 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();
}
}

View File

@ -51,7 +51,16 @@ namespace Microsoft.AspNetCore.Components.Reflection
}
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
{
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 @@ 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.Handler = Handler;
}

View File

@ -34,19 +34,38 @@ namespace Microsoft.AspNetCore.Components
internal static RouteTable Create(IEnumerable<Type> componentTypes)
{
var routes = new List<RouteEntry>();
foreach (var type in componentTypes)
var templatesByHandler = new Dictionary<Type, string[]>();
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<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 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 @@ namespace Microsoft.AspNetCore.Components
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>
/// Route precedence algorithm.
/// 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);
}
[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
{

View File

@ -268,8 +268,9 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
{
// 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 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
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
{
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 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
{
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 @@ 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");
// 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");

View File

@ -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);
}
}

View File

@ -34,28 +34,24 @@ else
</tbody>
</table>
<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
</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 ▶
</a>
</p>
}
@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);
}
}