mirror of
https://gitee.com/LongbowEnterprise/BootstrapBlazor.git
synced 2024-12-05 21:50:05 +08:00
!2091 feat(#I4IB5N): tab route upgrade to net6.0
* feat: 升级 Tab 路由支持 net6
This commit is contained in:
parent
239884d793
commit
51755c67d8
@ -1,90 +0,0 @@
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BootstrapBlazor.Components
|
||||
{
|
||||
internal static class RouteTableFactory
|
||||
{
|
||||
[NotNull]
|
||||
private static Routing.IRouteTable? Routes { get; set; }
|
||||
private static readonly HashSet<Assembly> _assemblies = new HashSet<Assembly>();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="assemblies"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
public static RouteContext Create(IEnumerable<Assembly> assemblies, string url)
|
||||
{
|
||||
RefreshRouteTable(assemblies);
|
||||
if (url.IndexOf("?") > 0) url = url.Substring(0, url.IndexOf("?"));
|
||||
var routeContext = new Routing.RouteContext(url);
|
||||
Routes.Route(routeContext);
|
||||
return new RouteContext()
|
||||
{
|
||||
Handler = routeContext.Handler,
|
||||
Parameters = routeContext.Parameters,
|
||||
Segments = routeContext.Segments
|
||||
};
|
||||
}
|
||||
|
||||
private static void RefreshRouteTable(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
var assembliesSet = new HashSet<Assembly>(assemblies);
|
||||
if (!_assemblies.SetEquals(assembliesSet))
|
||||
{
|
||||
Routes = Routing.RouteTableFactory.Create(assemblies);
|
||||
_assemblies.Clear();
|
||||
_assemblies.UnionWith(assembliesSet);
|
||||
}
|
||||
}
|
||||
|
||||
#if SSR
|
||||
#region SSR 模式可用
|
||||
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
|
||||
|
||||
public static RouteContext Create(IEnumerable<Assembly> assemblies, string url)
|
||||
{
|
||||
var routerAssembly = typeof(Microsoft.AspNetCore.Components.Routing.Router).Assembly;
|
||||
var routerTableFactoryType = routerAssembly.GetType("Microsoft.AspNetCore.Components.RouteTableFactory");
|
||||
|
||||
// call RouteTableFactory.Create()
|
||||
var createMethodInfo = routerTableFactoryType?.GetMethod("Create");
|
||||
var routeTableInstance = createMethodInfo?.Invoke(null, new object[] { assemblies });
|
||||
|
||||
var locationPath = url;
|
||||
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
|
||||
|
||||
// new RouteContext
|
||||
var contextType = routerAssembly.GetType("Microsoft.AspNetCore.Components.Routing.RouteContext");
|
||||
var context = Activator.CreateInstance(contextType!, new object[] { locationPath });
|
||||
|
||||
// Call RouteTable.Route(RouteContext)
|
||||
var routeMethodInfo = routeTableInstance?.GetType().GetMethod("Route");
|
||||
routeMethodInfo!.Invoke(routeTableInstance, new object[] { context! });
|
||||
|
||||
// Handler
|
||||
var segments = contextType?.GetProperty("Segments")?.GetValue(context) as string[];
|
||||
var handler = contextType?.GetProperty("Handler")?.GetValue(context) as Type;
|
||||
var parameters = contextType?.GetProperty("Parameters")?.GetValue(context) as IReadOnlyDictionary<string, object>;
|
||||
|
||||
return new RouteContext() { Handler = handler, Parameters = parameters, Segments = segments };
|
||||
}
|
||||
|
||||
private static string StringUntilAny(string str, char[] chars)
|
||||
{
|
||||
var firstIndex = str.IndexOfAny(chars);
|
||||
return firstIndex < 0
|
||||
? str
|
||||
: str.Substring(0, firstIndex);
|
||||
}
|
||||
#endregion
|
||||
#endif
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
/// <summary>
|
||||
/// Provides an abstraction over <see cref="RouteTable"/>.
|
||||
/// the legacy route matching logic is removed.
|
||||
@ -11,4 +13,5 @@ namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
void Route(RouteContext routeContext);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
@ -7,6 +8,7 @@ using System.Globalization;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
internal abstract class RouteConstraint
|
||||
{
|
||||
// note: the things that prevent this cache from growing unbounded is that
|
||||
@ -85,4 +87,37 @@ namespace BootstrapBlazor.Components.Routing
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
internal static class RouteConstraint
|
||||
{
|
||||
public static UrlValueConstraint Parse(string template, string segment, string constraint)
|
||||
{
|
||||
if (string.IsNullOrEmpty(constraint))
|
||||
{
|
||||
throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint.");
|
||||
}
|
||||
|
||||
var targetType = GetTargetType(constraint);
|
||||
if (targetType is null || !UrlValueConstraint.TryGetByTargetType(targetType, out var result))
|
||||
{
|
||||
throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Type? GetTargetType(string constraint) => constraint switch
|
||||
{
|
||||
"bool" => typeof(bool),
|
||||
"datetime" => typeof(DateTime),
|
||||
"decimal" => typeof(decimal),
|
||||
"double" => typeof(double),
|
||||
"float" => typeof(float),
|
||||
"guid" => typeof(Guid),
|
||||
"int" => typeof(int),
|
||||
"long" => typeof(long),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
@ -24,6 +27,9 @@ namespace BootstrapBlazor.Components.Routing
|
||||
|
||||
public string[] Segments { get; }
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
#endif
|
||||
public Type? Handler { get; set; }
|
||||
|
||||
public IReadOnlyDictionary<string, object>? Parameters { get; set; }
|
||||
|
@ -1,14 +1,18 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
#nullable disable warnings
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
[DebuggerDisplay("Handler = {Handler}, Template = {Template}")]
|
||||
internal class RouteEntry
|
||||
{
|
||||
@ -145,4 +149,143 @@ namespace BootstrapBlazor.Components.Routing
|
||||
return segments[^1].IsOptional || segments[^1].IsCatchAll;
|
||||
}
|
||||
}
|
||||
#else
|
||||
[DebuggerDisplay("Handler = {Handler}, Template = {Template}")]
|
||||
internal class RouteEntry
|
||||
{
|
||||
public RouteEntry(RouteTemplate template, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type handler, List<string>? unusedRouteParameterNames)
|
||||
{
|
||||
Template = template;
|
||||
UnusedRouteParameterNames = unusedRouteParameterNames;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
public RouteTemplate Template { get; }
|
||||
|
||||
public List<string>? UnusedRouteParameterNames { get; }
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public Type Handler { get; }
|
||||
|
||||
internal void Match(RouteContext context)
|
||||
{
|
||||
var pathIndex = 0;
|
||||
var templateIndex = 0;
|
||||
Dictionary<string, object> parameters = null;
|
||||
// We will iterate over the path segments and the template segments until we have consumed
|
||||
// one of them.
|
||||
// There are three cases we need to account here for:
|
||||
// * Path is shorter than template ->
|
||||
// * This can match only if we have t-p optional parameters at the end.
|
||||
// * Path and template have the same number of segments
|
||||
// * This can happen when the catch-all segment matches 1 segment
|
||||
// * This can happen when an optional parameter has been specified.
|
||||
// * This can happen when the route only contains literals and parameters.
|
||||
// * Path is longer than template -> This can only match if the parameter has a catch-all at the end.
|
||||
// * We still need to iterate over all the path segments if the catch-all is constrained.
|
||||
// * We still need to iterate over all the template/path segments before the catch-all
|
||||
while (pathIndex < context.Segments.Length && templateIndex < Template.Segments.Length)
|
||||
{
|
||||
var pathSegment = context.Segments[pathIndex];
|
||||
var templateSegment = Template.Segments[templateIndex];
|
||||
|
||||
var matches = templateSegment.Match(pathSegment, out var match);
|
||||
if (!matches)
|
||||
{
|
||||
// A constraint or literal didn't match
|
||||
return;
|
||||
}
|
||||
|
||||
if (!templateSegment.IsCatchAll)
|
||||
{
|
||||
// We were dealing with a literal or a parameter, so just advance both cursors.
|
||||
pathIndex++;
|
||||
templateIndex++;
|
||||
|
||||
if (templateSegment.IsParameter)
|
||||
{
|
||||
parameters ??= new(StringComparer.OrdinalIgnoreCase);
|
||||
parameters[templateSegment.Value] = match;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (templateSegment.Constraints.Length == 0)
|
||||
{
|
||||
|
||||
// Unconstrained catch all, we can stop early
|
||||
parameters ??= new(StringComparer.OrdinalIgnoreCase);
|
||||
parameters[templateSegment.Value] = string.Join('/', context.Segments, pathIndex, context.Segments.Length - pathIndex);
|
||||
|
||||
// Mark the remaining segments as consumed.
|
||||
pathIndex = context.Segments.Length;
|
||||
|
||||
// Catch-alls are always last.
|
||||
templateIndex++;
|
||||
|
||||
// We are done, so break out of the loop.
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For constrained catch-alls, we advance the path index but keep the template index on the catch-all.
|
||||
pathIndex++;
|
||||
if (pathIndex == context.Segments.Length)
|
||||
{
|
||||
parameters ??= new(StringComparer.OrdinalIgnoreCase);
|
||||
parameters[templateSegment.Value] = string.Join('/', context.Segments, templateIndex, context.Segments.Length - templateIndex);
|
||||
|
||||
// This is important to signal that we consumed the entire template.
|
||||
templateIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasRemainingOptionalSegments = templateIndex < Template.Segments.Length &&
|
||||
RemainingSegmentsAreOptional(pathIndex, Template.Segments);
|
||||
|
||||
if ((pathIndex == context.Segments.Length && templateIndex == Template.Segments.Length) || hasRemainingOptionalSegments)
|
||||
{
|
||||
if (hasRemainingOptionalSegments)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
AddDefaultValues(parameters, templateIndex, Template.Segments);
|
||||
}
|
||||
if (UnusedRouteParameterNames?.Count > 0)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < UnusedRouteParameterNames.Count; i++)
|
||||
{
|
||||
parameters[UnusedRouteParameterNames[i]] = null;
|
||||
}
|
||||
}
|
||||
context.Handler = Handler;
|
||||
context.Parameters = parameters;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDefaultValues(Dictionary<string, object> parameters, int templateIndex, TemplateSegment[] segments)
|
||||
{
|
||||
for (var i = templateIndex; i < segments.Length; i++)
|
||||
{
|
||||
var currentSegment = segments[i];
|
||||
parameters[currentSegment.Value] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemainingSegmentsAreOptional(int index, TemplateSegment[] segments)
|
||||
{
|
||||
for (var i = index; index < segments.Length - 1; index++)
|
||||
{
|
||||
if (!segments[i].IsOptional)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return segments[^1].IsOptional || segments[^1].IsCatchAll;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
66
src/BootstrapBlazor/Components/Tab/Route/RouteKey.cs
Normal file
66
src/BootstrapBlazor/Components/Tab/Route/RouteKey.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
internal readonly struct RouteKey : IEquatable<RouteKey>
|
||||
{
|
||||
public readonly Assembly? AppAssembly;
|
||||
public readonly HashSet<Assembly>? AdditionalAssemblies;
|
||||
|
||||
public RouteKey(Assembly? appAssembly, IEnumerable<Assembly>? additionalAssemblies)
|
||||
{
|
||||
AppAssembly = appAssembly;
|
||||
AdditionalAssemblies = additionalAssemblies is null ? null : new HashSet<Assembly>(additionalAssemblies);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is RouteKey other && Equals(other);
|
||||
}
|
||||
|
||||
public bool Equals(RouteKey other)
|
||||
{
|
||||
if (!Equals(AppAssembly, other.AppAssembly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AdditionalAssemblies is null && other.AdditionalAssemblies is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (AdditionalAssemblies is null || other.AdditionalAssemblies is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return AdditionalAssemblies.Count == other.AdditionalAssemblies.Count &&
|
||||
AdditionalAssemblies.SetEquals(other.AdditionalAssemblies);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (AppAssembly is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (AdditionalAssemblies is null)
|
||||
{
|
||||
return AppAssembly.GetHashCode();
|
||||
}
|
||||
|
||||
// Producing a hash code that includes individual assemblies requires it to have a stable order.
|
||||
// We'll avoid the cost of sorting and simply include the number of assemblies instead.
|
||||
return HashCode.Combine(AppAssembly, AdditionalAssemblies.Count);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
@ -1,9 +1,14 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
internal class RouteTable : IRouteTable
|
||||
#else
|
||||
internal class RouteTable
|
||||
#endif
|
||||
{
|
||||
public RouteTable(RouteEntry[] routes)
|
||||
{
|
||||
|
@ -1,16 +1,18 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using IComponent = Microsoft.AspNetCore.Components.IComponent;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
/// <summary>
|
||||
/// Resolves components for an application.
|
||||
/// </summary>
|
||||
@ -236,4 +238,238 @@ namespace BootstrapBlazor.Components.Routing
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// <summary>
|
||||
/// Resolves components for an application.
|
||||
/// </summary>
|
||||
internal static class RouteTableFactory
|
||||
{
|
||||
private static readonly ConcurrentDictionary<RouteKey, RouteTable> Cache = new();
|
||||
public static readonly IComparer<RouteEntry> RoutePrecedence = Comparer<RouteEntry>.Create(RouteComparison);
|
||||
|
||||
public static RouteTable Create(RouteKey routeKey)
|
||||
{
|
||||
if (Cache.TryGetValue(routeKey, out var resolvedComponents))
|
||||
{
|
||||
return resolvedComponents;
|
||||
}
|
||||
|
||||
var componentTypes = GetRouteableComponents(routeKey);
|
||||
var routeTable = Create(componentTypes);
|
||||
Cache.TryAdd(routeKey, routeTable);
|
||||
return routeTable;
|
||||
}
|
||||
|
||||
public static void ClearCaches() => Cache.Clear();
|
||||
|
||||
private static List<Type> GetRouteableComponents(RouteKey routeKey)
|
||||
{
|
||||
var routeableComponents = new List<Type>();
|
||||
if (routeKey.AppAssembly is not null)
|
||||
{
|
||||
GetRouteableComponents(routeableComponents, routeKey.AppAssembly);
|
||||
}
|
||||
|
||||
if (routeKey.AdditionalAssemblies is not null)
|
||||
{
|
||||
foreach (var assembly in routeKey.AdditionalAssemblies)
|
||||
{
|
||||
GetRouteableComponents(routeableComponents, assembly);
|
||||
}
|
||||
}
|
||||
|
||||
return routeableComponents;
|
||||
|
||||
static void GetRouteableComponents(List<Type> routeableComponents, Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.ExportedTypes)
|
||||
{
|
||||
if (typeof(IComponent).IsAssignableFrom(type) && type.IsDefined(typeof(RouteAttribute)))
|
||||
{
|
||||
routeableComponents.Add(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static RouteTable Create(List<Type> 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 = componentType.GetCustomAttributes(typeof(RouteAttribute), inherit: false);
|
||||
var templates = new string[routeAttributes.Length];
|
||||
for (var i = 0; i < routeAttributes.Length; i++)
|
||||
{
|
||||
var attribute = (RouteAttribute)routeAttributes[i];
|
||||
templates[i] = attribute.Template;
|
||||
}
|
||||
|
||||
templatesByHandler.Add(componentType, templates);
|
||||
}
|
||||
return Create(templatesByHandler);
|
||||
}
|
||||
|
||||
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler)
|
||||
{
|
||||
var routes = new List<RouteEntry>();
|
||||
foreach (var (type, templates) in templatesByHandler)
|
||||
{
|
||||
var allRouteParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var parsedTemplates = new (RouteTemplate, HashSet<string>)[templates.Length];
|
||||
for (var i = 0; i < templates.Length; i++)
|
||||
{
|
||||
var parsedTemplate = TemplateParser.ParseTemplate(templates[i]);
|
||||
var parameterNames = GetParameterNames(parsedTemplate);
|
||||
parsedTemplates[i] = (parsedTemplate, parameterNames);
|
||||
|
||||
foreach (var parameterName in parameterNames)
|
||||
{
|
||||
allRouteParameterNames.Add(parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (parsedTemplate, routeParameterNames) in parsedTemplates)
|
||||
{
|
||||
var unusedRouteParameterNames = GetUnusedParameterNames(allRouteParameterNames, routeParameterNames);
|
||||
var entry = new RouteEntry(parsedTemplate, type, unusedRouteParameterNames);
|
||||
routes.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
routes.Sort(RoutePrecedence);
|
||||
return new RouteTable(routes.ToArray());
|
||||
}
|
||||
|
||||
private static HashSet<string> GetParameterNames(RouteTemplate routeTemplate)
|
||||
{
|
||||
var parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var segment in routeTemplate.Segments)
|
||||
{
|
||||
if (segment.IsParameter)
|
||||
{
|
||||
parameterNames.Add(segment.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return parameterNames;
|
||||
}
|
||||
|
||||
private static List<string>? GetUnusedParameterNames(HashSet<string> allRouteParameterNames, HashSet<string> routeParameterNames)
|
||||
{
|
||||
List<string>? unusedParameters = null;
|
||||
foreach (var item in allRouteParameterNames)
|
||||
{
|
||||
if (!routeParameterNames.Contains(item))
|
||||
{
|
||||
unusedParameters ??= new();
|
||||
unusedParameters.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return unusedParameters;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// * A parameter segment with more constraints is more specific than one with fewer constraints
|
||||
/// * 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}
|
||||
/// /Product/{id:int} is more specific than /Product/{id}
|
||||
///
|
||||
/// Routes can be ambiguous 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
|
||||
/// /{parameter:constraint}/literal and /{something:constraint}/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 insensitive) we choose the lexical order
|
||||
/// * For parameters with different numbers of constraints, the one with more wins
|
||||
/// If we get to the end of the comparison routing we've detected an ambiguous pair of routes.
|
||||
/// </summary>
|
||||
internal static int RouteComparison(RouteEntry x, RouteEntry y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var xTemplate = x.Template;
|
||||
var yTemplate = y.Template;
|
||||
var minSegments = Math.Min(xTemplate.Segments.Length, yTemplate.Segments.Length);
|
||||
var currentResult = 0;
|
||||
for (var i = 0; i < minSegments; i++)
|
||||
{
|
||||
var xSegment = xTemplate.Segments[i];
|
||||
var ySegment = yTemplate.Segments[i];
|
||||
|
||||
var xRank = GetRank(xSegment);
|
||||
var yRank = GetRank(ySegment);
|
||||
|
||||
currentResult = xRank.CompareTo(yRank);
|
||||
|
||||
// If they are both literals we can disambiguate
|
||||
if ((xRank, yRank) == (0, 0))
|
||||
{
|
||||
currentResult = StringComparer.OrdinalIgnoreCase.Compare(xSegment.Value, ySegment.Value);
|
||||
}
|
||||
|
||||
if (currentResult != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentResult == 0)
|
||||
{
|
||||
currentResult = xTemplate.Segments.Length.CompareTo(yTemplate.Segments.Length);
|
||||
}
|
||||
|
||||
if (currentResult == 0)
|
||||
{
|
||||
throw new InvalidOperationException($@"The following routes are ambiguous:
|
||||
'{x.Template.TemplateText}' in '{x.Handler.FullName}'
|
||||
'{y.Template.TemplateText}' in '{y.Handler.FullName}'
|
||||
");
|
||||
}
|
||||
|
||||
return currentResult;
|
||||
}
|
||||
|
||||
private static int GetRank(TemplateSegment xSegment)
|
||||
{
|
||||
return xSegment switch
|
||||
{
|
||||
// Literal
|
||||
{ IsParameter: false } => 0,
|
||||
// Parameter with constraints
|
||||
{ IsParameter: true, IsCatchAll: false, Constraints: { Length: > 0 } } => 1,
|
||||
// Parameter without constraints
|
||||
{ IsParameter: true, IsCatchAll: false, Constraints: { Length: 0 } } => 2,
|
||||
// Catch all parameter with constraints
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => 3,
|
||||
// Catch all parameter without constraints
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => 4,
|
||||
// The segment is not correct
|
||||
_ => throw new InvalidOperationException($"Unknown segment definition '{xSegment}.")
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
|
||||
using System.Diagnostics;
|
||||
@ -14,8 +15,24 @@ namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
TemplateText = templateText;
|
||||
Segments = segments;
|
||||
|
||||
#if NET5_0
|
||||
OptionalSegmentsCount = segments.Count(template => template.IsOptional);
|
||||
ContainsCatchAllSegment = segments.Any(template => template.IsCatchAll);
|
||||
#else
|
||||
for (var i = 0; i < segments.Length; i++)
|
||||
{
|
||||
var segment = segments[i];
|
||||
if (segment.IsOptional)
|
||||
{
|
||||
OptionalSegmentsCount++;
|
||||
}
|
||||
if (segment.IsCatchAll)
|
||||
{
|
||||
ContainsCatchAllSegment = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public string TemplateText { get; }
|
||||
|
@ -0,0 +1,70 @@
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
// This is very similar to Microsoft.Extensions.Primitives.StringValues, except it works in terms
|
||||
// of ReadOnlyMemory<char> rather than string, so the querystring handling logic doesn't need to
|
||||
// allocate per-value when tracking things that will be parsed as value types.
|
||||
internal struct StringSegmentAccumulator
|
||||
{
|
||||
private int count;
|
||||
private ReadOnlyMemory<char> _single;
|
||||
private List<ReadOnlyMemory<char>>? _multiple;
|
||||
|
||||
public ReadOnlyMemory<char> this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index >= count)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
return count == 1 ? _single : _multiple![index];
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => count;
|
||||
|
||||
public void SetSingle(ReadOnlyMemory<char> value)
|
||||
{
|
||||
_single = value;
|
||||
|
||||
if (count != 1)
|
||||
{
|
||||
if (count > 1)
|
||||
{
|
||||
_multiple = null;
|
||||
}
|
||||
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(ReadOnlyMemory<char> value)
|
||||
{
|
||||
switch (count++)
|
||||
{
|
||||
case 0:
|
||||
_single = value;
|
||||
break;
|
||||
case 1:
|
||||
_multiple = new();
|
||||
_multiple.Add(_single);
|
||||
_multiple.Add(value);
|
||||
_single = default;
|
||||
break;
|
||||
default:
|
||||
_multiple!.Add(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
|
@ -1,11 +1,13 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
internal class TemplateSegment
|
||||
{
|
||||
public TemplateSegment(string template, string segment, bool isParameter)
|
||||
@ -144,4 +146,144 @@ namespace BootstrapBlazor.Components.Routing
|
||||
_ => throw new InvalidOperationException("Invalid template segment.")
|
||||
};
|
||||
}
|
||||
#else
|
||||
internal class TemplateSegment
|
||||
{
|
||||
public TemplateSegment(string template, string segment, bool isParameter)
|
||||
{
|
||||
IsParameter = isParameter;
|
||||
|
||||
IsCatchAll = isParameter && segment.StartsWith('*');
|
||||
|
||||
if (IsCatchAll)
|
||||
{
|
||||
// Only one '*' currently allowed
|
||||
Value = segment[1..];
|
||||
|
||||
var invalidCharacterIndex = Value.IndexOf('*');
|
||||
if (invalidCharacterIndex != -1)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter may only have one '*' at the beginning of the segment.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = segment;
|
||||
}
|
||||
|
||||
// Process segments that parameters that do not contain a token separating a type constraint.
|
||||
if (IsParameter)
|
||||
{
|
||||
if (Value.IndexOf(':') < 0)
|
||||
{
|
||||
|
||||
// Set the IsOptional flag to true for segments that contain
|
||||
// a parameter with no type constraints but optionality set
|
||||
// via the '?' token.
|
||||
var questionMarkIndex = Value.IndexOf('?');
|
||||
if (questionMarkIndex == Value.Length - 1)
|
||||
{
|
||||
IsOptional = true;
|
||||
Value = Value[0..^1];
|
||||
}
|
||||
// If the `?` optional marker shows up in the segment but not at the very end,
|
||||
// then throw an error.
|
||||
else if (questionMarkIndex >= 0)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name.");
|
||||
}
|
||||
|
||||
Constraints = Array.Empty<UrlValueConstraint>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var tokens = Value.Split(':');
|
||||
if (tokens[0].Length == 0)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list.");
|
||||
}
|
||||
|
||||
Value = tokens[0];
|
||||
IsOptional = tokens[^1].EndsWith('?');
|
||||
if (IsOptional)
|
||||
{
|
||||
tokens[^1] = tokens[^1][0..^1];
|
||||
}
|
||||
|
||||
Constraints = new UrlValueConstraint[tokens.Length - 1];
|
||||
for (var i = 1; i < tokens.Length; i++)
|
||||
{
|
||||
Constraints[i - 1] = RouteConstraint.Parse(template, segment, tokens[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Constraints = Array.Empty<UrlValueConstraint>();
|
||||
}
|
||||
|
||||
if (IsParameter)
|
||||
{
|
||||
if (IsOptional && IsCatchAll)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid segment '{segment}' in route '{template}'. A catch-all parameter cannot be marked optional.");
|
||||
}
|
||||
|
||||
// Moving the check for this here instead of TemplateParser so we can allow catch-all.
|
||||
// We checked for '*' up above specifically for catch-all segments, this one checks for all others
|
||||
if (Value.IndexOf('*') != -1)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. The character '*' in parameter segment '{{{segment}}}' is not allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The value of the segment. The exact text to match when is a literal.
|
||||
// The parameter name when its a segment
|
||||
public string Value { get; }
|
||||
|
||||
public bool IsParameter { get; }
|
||||
|
||||
public bool IsOptional { get; }
|
||||
|
||||
public bool IsCatchAll { get; }
|
||||
|
||||
public UrlValueConstraint[] Constraints { get; }
|
||||
|
||||
public bool Match(string pathSegment, out object? matchedParameterValue)
|
||||
{
|
||||
if (IsParameter)
|
||||
{
|
||||
matchedParameterValue = pathSegment;
|
||||
|
||||
foreach (var constraint in Constraints)
|
||||
{
|
||||
if (!constraint.TryParse(pathSegment, out matchedParameterValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
matchedParameterValue = null;
|
||||
return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => this switch
|
||||
{
|
||||
{ IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: 0 } } => $"{{{Value}}}",
|
||||
{ IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':', (object[])Constraints)}}}",
|
||||
{ IsParameter: true, IsOptional: true, Constraints: { Length: 0 } } => $"{{{Value}?}}",
|
||||
{ IsParameter: true, IsOptional: true, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':', (object[])Constraints)}?}}",
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => $"{{*{Value}}}",
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => $"{{*{Value}:{string.Join(':', (object[])Constraints)}?}}",
|
||||
{ IsParameter: false } => Value,
|
||||
_ => throw new InvalidOperationException("Invalid template segment.")
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
/// <summary>
|
||||
/// A route constraint that requires the value to be parseable as a specified type.
|
||||
/// </summary>
|
||||
@ -48,4 +50,5 @@ namespace BootstrapBlazor.Components.Routing
|
||||
var x => x.Name.ToLowerInvariant()
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
188
src/BootstrapBlazor/Components/Tab/Route/UrlValueConstraint.cs
Normal file
188
src/BootstrapBlazor/Components/Tab/Route/UrlValueConstraint.cs
Normal file
@ -0,0 +1,188 @@
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// Shared logic for parsing tokens from route values and querystring values.
|
||||
/// </summary>
|
||||
internal abstract class UrlValueConstraint
|
||||
{
|
||||
public delegate bool TryParseDelegate<T>(ReadOnlySpan<char> str, [MaybeNullWhen(false)] out T result);
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, UrlValueConstraint> _cachedInstances = new();
|
||||
|
||||
public static bool TryGetByTargetType(Type targetType, [MaybeNullWhen(false)] out UrlValueConstraint result)
|
||||
{
|
||||
if (!_cachedInstances.TryGetValue(targetType, out result))
|
||||
{
|
||||
result = Create(targetType);
|
||||
if (result is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_cachedInstances.TryAdd(targetType, result);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out string result)
|
||||
{
|
||||
result = str.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out DateTime result)
|
||||
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result);
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out DateOnly result)
|
||||
=> DateOnly.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result);
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out TimeOnly result)
|
||||
=> TimeOnly.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result);
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out decimal result)
|
||||
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out double result)
|
||||
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out float result)
|
||||
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out int result)
|
||||
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool TryParse(ReadOnlySpan<char> str, out long result)
|
||||
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static UrlValueConstraint? Create(Type targetType) => targetType switch
|
||||
{
|
||||
var x when x == typeof(string) => new TypedUrlValueConstraint<string>(TryParse),
|
||||
var x when x == typeof(bool) => new TypedUrlValueConstraint<bool>(bool.TryParse),
|
||||
var x when x == typeof(bool?) => new NullableTypedUrlValueConstraint<bool>(bool.TryParse),
|
||||
var x when x == typeof(DateTime) => new TypedUrlValueConstraint<DateTime>(TryParse),
|
||||
var x when x == typeof(DateTime?) => new NullableTypedUrlValueConstraint<DateTime>(TryParse),
|
||||
var x when x == typeof(DateOnly) => new TypedUrlValueConstraint<DateOnly>(TryParse),
|
||||
var x when x == typeof(DateOnly?) => new NullableTypedUrlValueConstraint<DateOnly>(TryParse),
|
||||
var x when x == typeof(TimeOnly) => new TypedUrlValueConstraint<TimeOnly>(TryParse),
|
||||
var x when x == typeof(TimeOnly?) => new NullableTypedUrlValueConstraint<TimeOnly>(TryParse),
|
||||
var x when x == typeof(decimal) => new TypedUrlValueConstraint<decimal>(TryParse),
|
||||
var x when x == typeof(decimal?) => new NullableTypedUrlValueConstraint<decimal>(TryParse),
|
||||
var x when x == typeof(double) => new TypedUrlValueConstraint<double>(TryParse),
|
||||
var x when x == typeof(double?) => new NullableTypedUrlValueConstraint<double>(TryParse),
|
||||
var x when x == typeof(float) => new TypedUrlValueConstraint<float>(TryParse),
|
||||
var x when x == typeof(float?) => new NullableTypedUrlValueConstraint<float>(TryParse),
|
||||
var x when x == typeof(Guid) => new TypedUrlValueConstraint<Guid>(Guid.TryParse),
|
||||
var x when x == typeof(Guid?) => new NullableTypedUrlValueConstraint<Guid>(Guid.TryParse),
|
||||
var x when x == typeof(int) => new TypedUrlValueConstraint<int>(TryParse),
|
||||
var x when x == typeof(int?) => new NullableTypedUrlValueConstraint<int>(TryParse),
|
||||
var x when x == typeof(long) => new TypedUrlValueConstraint<long>(TryParse),
|
||||
var x when x == typeof(long?) => new NullableTypedUrlValueConstraint<long>(TryParse),
|
||||
var x => null
|
||||
};
|
||||
|
||||
public abstract bool TryParse(ReadOnlySpan<char> value, [MaybeNullWhen(false)] out object result);
|
||||
|
||||
public abstract object? Parse(ReadOnlySpan<char> value, string destinationNameForMessage);
|
||||
|
||||
public abstract Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage);
|
||||
|
||||
private class TypedUrlValueConstraint<T> : UrlValueConstraint
|
||||
{
|
||||
private readonly TryParseDelegate<T> _parser;
|
||||
|
||||
public TypedUrlValueConstraint(TryParseDelegate<T> parser)
|
||||
{
|
||||
_parser = parser;
|
||||
}
|
||||
|
||||
public override bool TryParse(ReadOnlySpan<char> value, [MaybeNullWhen(false)] out object result)
|
||||
{
|
||||
if (_parser(value, out var typedResult))
|
||||
{
|
||||
result = typedResult!;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override object? Parse(ReadOnlySpan<char> value, string destinationNameForMessage)
|
||||
{
|
||||
if (!_parser(value, out var parsedValue))
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot parse the value '{value.ToString()}' as type '{typeof(T)}' for '{destinationNameForMessage}'.");
|
||||
}
|
||||
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
public override Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage)
|
||||
{
|
||||
var count = values.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
var result = new T?[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (!_parser(values[i].Span, out result[i]))
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot parse the value '{values[i]}' as type '{typeof(T)}' for '{destinationNameForMessage}'.");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NullableTypedUrlValueConstraint<T> : TypedUrlValueConstraint<T?> where T : struct
|
||||
{
|
||||
public NullableTypedUrlValueConstraint(TryParseDelegate<T> parser)
|
||||
: base(SupportNullable(parser))
|
||||
{
|
||||
}
|
||||
|
||||
private static TryParseDelegate<T?> SupportNullable(TryParseDelegate<T> parser)
|
||||
{
|
||||
return TryParseNullable;
|
||||
|
||||
bool TryParseNullable(ReadOnlySpan<char> value, [MaybeNullWhen(false)] out T? result)
|
||||
{
|
||||
if (value.IsEmpty)
|
||||
{
|
||||
result = default;
|
||||
return true;
|
||||
}
|
||||
else if (parser(value, out var parsedValue))
|
||||
{
|
||||
result = parsedValue;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
89
src/BootstrapBlazor/Router/RouteTableFactory.cs
Normal file
89
src/BootstrapBlazor/Router/RouteTableFactory.cs
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Website: https://www.blazor.zone or https://argozhang.github.io/
|
||||
|
||||
using BootstrapBlazor.Components.Routing;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BootstrapBlazor.Components
|
||||
{
|
||||
#if NET5_0
|
||||
internal static class RouteTableFactory
|
||||
{
|
||||
[NotNull]
|
||||
private static Routing.IRouteTable? Routes { get; set; }
|
||||
private static readonly HashSet<Assembly> _assemblies = new();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="assemblies"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
public static RouteContext Create(IEnumerable<Assembly> assemblies, string url)
|
||||
{
|
||||
RefreshRouteTable(assemblies);
|
||||
if (url.IndexOf("?") > 0) url = url[..url.IndexOf("?")];
|
||||
var routeContext = new Routing.RouteContext(url);
|
||||
Routes.Route(routeContext);
|
||||
return new RouteContext()
|
||||
{
|
||||
Handler = routeContext.Handler,
|
||||
Parameters = routeContext.Parameters,
|
||||
Segments = routeContext.Segments
|
||||
};
|
||||
}
|
||||
|
||||
private static void RefreshRouteTable(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
var assembliesSet = new HashSet<Assembly>(assemblies);
|
||||
if (!_assemblies.SetEquals(assembliesSet))
|
||||
{
|
||||
Routes = Routing.RouteTableFactory.Create(assemblies);
|
||||
_assemblies.Clear();
|
||||
_assemblies.UnionWith(assembliesSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
internal static class RouteTableFactory
|
||||
{
|
||||
[NotNull]
|
||||
private static Routing.RouteTable? Routes { get; set; }
|
||||
private static readonly HashSet<Assembly> _assemblies = new();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="assemblies"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
public static RouteContext Create(IEnumerable<Assembly> assemblies, string url)
|
||||
{
|
||||
RefreshRouteTable(assemblies);
|
||||
if (url.IndexOf("?") > 0) url = url[..url.IndexOf("?")];
|
||||
var routeContext = new Routing.RouteContext(url);
|
||||
Routes.Route(routeContext);
|
||||
return new RouteContext()
|
||||
{
|
||||
Handler = routeContext.Handler,
|
||||
Parameters = routeContext.Parameters,
|
||||
Segments = routeContext.Segments
|
||||
};
|
||||
}
|
||||
|
||||
private static void RefreshRouteTable(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
var assembliesSet = new HashSet<Assembly>(assemblies);
|
||||
if (!_assemblies.SetEquals(assembliesSet))
|
||||
{
|
||||
Routes = Routing.RouteTableFactory.Create(new RouteKey(null, assemblies));
|
||||
_assemblies.Clear();
|
||||
_assemblies.UnionWith(assembliesSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
Loading…
Reference in New Issue
Block a user