This repository has been archived by the owner on Feb 25, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 647
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fixes #220] Support
@page
with custom route template on components
* Updates the router component to scan for components within assemblies. * Parses the templates on `[Route]` in component instances and builds a route table that maps paths to components. * Uses the route table to map paths to components.
- Loading branch information
Showing
22 changed files
with
951 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
<h1>Counter</h1> | ||
@page "/counter" | ||
<h1>Counter</h1> | ||
|
||
<p>Current count: @currentCount</p> | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
@inject HttpClient Http | ||
@page "/fetchdata" | ||
@inject HttpClient Http | ||
|
||
<h1>Weather forecast</h1> | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
<h1>Hello, world!</h1> | ||
@page "/" | ||
<h1>Hello, world!</h1> | ||
|
||
Welcome to your new app. |
73 changes: 73 additions & 0 deletions
73
src/Microsoft.AspNetCore.Blazor/Components/ComponentResolver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// 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.Linq; | ||
using System.Reflection; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Components | ||
{ | ||
/// <summary> | ||
/// Resolves components for an application. | ||
/// </summary> | ||
internal class ComponentResolver | ||
{ | ||
/// <summary> | ||
/// Lists all the types | ||
/// </summary> | ||
/// <param name="appAssembly"></param> | ||
/// <returns></returns> | ||
public static IEnumerable<Type> ResolveComponents(Assembly appAssembly) | ||
{ | ||
var blazorAssembly = typeof(IComponent).Assembly; | ||
|
||
return EnumerateAssemblies(appAssembly.GetName(), blazorAssembly, new HashSet<Assembly>(new AssemblyComparer())) | ||
.SelectMany(a => a.ExportedTypes) | ||
.Where(t => typeof(IComponent).IsAssignableFrom(t)); | ||
} | ||
|
||
private static IEnumerable<Assembly> EnumerateAssemblies( | ||
AssemblyName assemblyName, | ||
Assembly blazorAssembly, | ||
HashSet<Assembly> visited) | ||
{ | ||
var assembly = Assembly.Load(assemblyName); | ||
if (visited.Contains(assembly)) | ||
{ | ||
// Avoid traversing visited assemblies. | ||
yield break; | ||
} | ||
visited.Add(assembly); | ||
var references = assembly.GetReferencedAssemblies(); | ||
if (!references.Any(r => string.Equals(r.FullName, blazorAssembly.FullName, StringComparison.Ordinal))) | ||
{ | ||
// Avoid traversing references that don't point to blazor (like netstandard2.0) | ||
yield break; | ||
} | ||
else | ||
{ | ||
yield return assembly; | ||
|
||
// Look at the list of transitive dependencies for more components. | ||
foreach (var reference in references.SelectMany(r => EnumerateAssemblies(r, blazorAssembly, visited))) | ||
{ | ||
yield return reference; | ||
} | ||
} | ||
} | ||
|
||
private class AssemblyComparer : IEqualityComparer<Assembly> | ||
{ | ||
public bool Equals(Assembly x, Assembly y) | ||
{ | ||
return string.Equals(x?.FullName, y?.FullName, StringComparison.Ordinal); | ||
} | ||
|
||
public int GetHashCode(Assembly obj) | ||
{ | ||
return obj.FullName.GetHashCode(); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// 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; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Routing | ||
{ | ||
internal class RouteContext | ||
{ | ||
private static char[] Separator = new[] { '/' }; | ||
|
||
public RouteContext(string path) | ||
{ | ||
// This is a simplification. We are assuming there are no paths like /a//b/. A proper routing | ||
// implementation would be more sophisticated. | ||
Segments = path.Trim('/').Split(Separator, StringSplitOptions.RemoveEmptyEntries); | ||
} | ||
|
||
public string[] Segments { get; } | ||
|
||
public Type Handler { get; set; } | ||
|
||
public IDictionary<string, string> Parameters { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// 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; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Routing | ||
{ | ||
internal class RouteEntry | ||
{ | ||
public RouteEntry(RouteTemplate template, Type handler) | ||
{ | ||
Template = template; | ||
Handler = handler; | ||
} | ||
|
||
public RouteTemplate Template { get; } | ||
|
||
public Type Handler { get; } | ||
|
||
internal void Match(RouteContext context) | ||
{ | ||
if (Template.Segments.Length != context.Segments.Length) | ||
{ | ||
return; | ||
} | ||
|
||
// Parameters will be lazily initialized. | ||
IDictionary<string, string> parameters = null; | ||
for (int i = 0; i < Template.Segments.Length; i++) | ||
{ | ||
var segment = Template.Segments[i]; | ||
var pathSegment = context.Segments[i]; | ||
if (!segment.Match(pathSegment)) | ||
{ | ||
return; | ||
} | ||
else | ||
{ | ||
if (segment.IsParameter) | ||
{ | ||
GetParameters()[segment.Value] = pathSegment; | ||
} | ||
} | ||
} | ||
|
||
context.Parameters = parameters; | ||
context.Handler = Handler; | ||
|
||
IDictionary<string, string> GetParameters() | ||
{ | ||
if (parameters == null) | ||
{ | ||
parameters = new Dictionary<string, string>(); | ||
} | ||
|
||
return parameters; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// 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.Linq; | ||
using System.Reflection; | ||
using Microsoft.AspNetCore.Blazor.Components; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Routing | ||
{ | ||
internal class RouteTable | ||
{ | ||
public RouteTable(RouteEntry[] routes) | ||
{ | ||
Routes = routes; | ||
} | ||
|
||
public RouteEntry[] Routes { get; set; } | ||
|
||
public static RouteTable Create(IEnumerable<Type> types) | ||
{ | ||
var routes = new List<RouteEntry>(); | ||
foreach (var type in types) | ||
{ | ||
var routeAttributes = type.GetCustomAttributes<RouteAttribute>(); // Inherit: true? | ||
foreach (var routeAttribute in routeAttributes) | ||
{ | ||
var template = TemplateParser.ParseTemplate(routeAttribute.Template); | ||
var entry = new RouteEntry(template, type); | ||
routes.Add(entry); | ||
} | ||
} | ||
|
||
return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray()); | ||
} | ||
|
||
public static IComparer<RouteEntry> RoutePrecedence { get; } = Comparer<RouteEntry>.Create(RouteComparison); | ||
|
||
/// <summary> | ||
/// Route precedence algorithm. | ||
/// We collect all the routes and sort them from most specific to | ||
/// less specific. The specificity of a route is given by the specificity | ||
/// of its segments and the position of those segments in the route. | ||
/// * A literal segment is more specific than a parameter segment. | ||
/// * Segment earlier in the route are evaluated before segments later in the route. | ||
/// For example: | ||
/// /Literal is more specific than /Parameter | ||
/// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters} | ||
/// | ||
/// Routes can be ambigous if: | ||
/// They are composed of literals and those literals have the same values (case insensitive) | ||
/// They are composed of a mix of literals and parameters, in the same relative order and the | ||
/// literals have the same values. | ||
/// For example: | ||
/// * /literal and /Literal | ||
/// /{parameter}/literal and /{something}/literal | ||
/// | ||
/// To calculate the precedence we sort the list of routes as follows: | ||
/// * Shorter routes go first. | ||
/// * A literal wins over a parameter in precedence. | ||
/// * For literals with different values (case insenitive) we choose the lexical order | ||
/// If we get to the end of the comparison routing we've detected an ambigous pair of routes. | ||
internal static int RouteComparison(RouteEntry x, RouteEntry y) | ||
{ | ||
var xTemplate = x.Template; | ||
var yTemplate = y.Template; | ||
if (xTemplate.Segments.Length != y.Template.Segments.Length) | ||
{ | ||
return xTemplate.Segments.Length < y.Template.Segments.Length ? -1 : 1; | ||
} | ||
else | ||
{ | ||
for (int i = 0; i < xTemplate.Segments.Length; i++) | ||
{ | ||
var xSegment = xTemplate.Segments[i]; | ||
var ySegment = yTemplate.Segments[i]; | ||
if (!xSegment.IsParameter && ySegment.IsParameter) | ||
{ | ||
return -1; | ||
} | ||
if (xSegment.IsParameter && !ySegment.IsParameter) | ||
{ | ||
return 1; | ||
} | ||
} | ||
|
||
for (int i = 0; i < xTemplate.Segments.Length; i++) | ||
{ | ||
var xSegment = xTemplate.Segments[i]; | ||
var ySegment = yTemplate.Segments[i]; | ||
if (!xSegment.IsParameter && ySegment.IsParameter) | ||
{ | ||
return -1; | ||
} | ||
if (xSegment.IsParameter && !ySegment.IsParameter) | ||
{ | ||
return 1; | ||
} | ||
|
||
if (!xSegment.IsParameter) | ||
{ | ||
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase); | ||
if (comparison != 0) | ||
{ | ||
return comparison; | ||
} | ||
} | ||
} | ||
|
||
throw new InvalidOperationException($@"The following routes are ambiguous: | ||
'{x.Template.TemplateText}' in '{x.Handler.FullName}' | ||
'{y.Template.TemplateText}' in '{y.Handler.FullName}' | ||
"); | ||
} | ||
} | ||
|
||
internal void Route(RouteContext routeContext) | ||
{ | ||
foreach (var route in Routes) | ||
{ | ||
route.Match(routeContext); | ||
if (routeContext.Handler != null) | ||
{ | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.