mirror of
https://gitee.com/LongbowEnterprise/BootstrapBlazor.git
synced 2024-12-05 21:50:05 +08:00
!2092 feat(#I4IC7G): tab support parameter from query string
* Merge remote-tracking branch 'origin/main' into dev-tab * doc: 更新示例 * doc: 更新示例 * feat: Tab 组件支持 QueryString 参数 * feat: 升级 Tab 路由支持 net6
This commit is contained in:
parent
51755c67d8
commit
5c781ff56b
@ -1,21 +1,32 @@
|
||||
@page "/layout-demo/{text?}"
|
||||
@page "/layout-demo"
|
||||
@page "/layout-demo/{title?}"
|
||||
@layout PageLayout
|
||||
@attribute [TabItemOption(Text = "示例网页", Icon = "fa fa-fw fa-laptop")]
|
||||
|
||||
<p>测试路径:</p>
|
||||
|
||||
<p>
|
||||
<a href="/layout-demo?text=Parameter">layout-demo?text=Parameter</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="/layout-demo/text=Parameter">layout-demo/text=Parameter</a>
|
||||
</p>
|
||||
|
||||
<p>已支持路由参数: @Text</p>
|
||||
|
||||
<p>已支持路由参数: @Title</p>
|
||||
|
||||
<div>
|
||||
精彩从这里开始
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private Tab? TabSet { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
private Layout? Layout { get; set; }
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery()]
|
||||
public string? Text { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Text { get; set; }
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>6.0.3</Version>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
|
||||
|
@ -2,7 +2,7 @@
|
||||
// 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
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
/// <summary>
|
||||
|
@ -6,7 +6,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
internal abstract class RouteConstraint
|
||||
|
@ -7,7 +7,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
internal class RouteContext
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
[DebuggerDisplay("Handler = {Handler}, Template = {Template}")]
|
||||
|
@ -5,7 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
internal readonly struct RouteKey : IEquatable<RouteKey>
|
||||
|
@ -2,7 +2,7 @@
|
||||
// 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
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
internal class RouteTable : IRouteTable
|
||||
|
@ -10,7 +10,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using IComponent = Microsoft.AspNetCore.Components.IComponent;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
/// <summary>
|
||||
|
@ -6,7 +6,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
[DebuggerDisplay("{TemplateText}")]
|
||||
internal class RouteTemplate
|
||||
|
@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
// This is very similar to Microsoft.Extensions.Primitives.StringValues, except it works in terms
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
// This implementation is temporary, in the future we'll want to have
|
||||
// a more performant/properly designed routing set of abstractions.
|
||||
|
@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
internal class TemplateSegment
|
||||
|
@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET5_0
|
||||
/// <summary>
|
||||
|
@ -7,7 +7,7 @@ using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BootstrapBlazor.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <summary>
|
||||
|
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@ -47,6 +48,12 @@ namespace BootstrapBlazor.Components
|
||||
[Inject]
|
||||
private IAuthorizationService? AuthorizationService { get; set; }
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private NavigationManager? NavigationManager { get; set; }
|
||||
#endif
|
||||
|
||||
private bool Authorized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@ -79,12 +86,31 @@ namespace BootstrapBlazor.Components
|
||||
{
|
||||
builder.AddAttribute(index++, kv.Key, kv.Value);
|
||||
}
|
||||
#if NET6_0_OR_GREATER
|
||||
BuildQueryParameters();
|
||||
#endif
|
||||
builder.CloseComponent();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddContent(0, NotAuthorized);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
void BuildQueryParameters()
|
||||
{
|
||||
var queryParameterSupplier = QueryParameterValueSupplier.ForType(RouteContext.Handler);
|
||||
if (queryParameterSupplier is not null)
|
||||
{
|
||||
// Since this component does accept some parameters from query, we must supply values for all of them,
|
||||
// even if the querystring in the URI is empty. So don't skip the following logic.
|
||||
var url = NavigationManager.Uri;
|
||||
var queryStartPos = url.IndexOf('?');
|
||||
var query = queryStartPos < 0 ? default : url.AsMemory(queryStartPos);
|
||||
queryParameterSupplier.RenderParametersFromQueryString(builder, query);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static async Task<bool> IsAuthorizedAsync(Type type, Task<AuthenticationState>? authenticateState, IAuthorizationPolicyProvider? authorizePolicy, IAuthorizationService? authorizeService, object? resource = null)
|
||||
|
352
src/BootstrapBlazor/Misc/Internal/ComponentProperties.cs
Normal file
352
src/BootstrapBlazor/Misc/Internal/ComponentProperties.cs
Normal file
@ -0,0 +1,352 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using static Microsoft.AspNetCore.Internal.LinkerFlags;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Reflection
|
||||
{
|
||||
internal static class ComponentProperties
|
||||
{
|
||||
internal const BindingFlags BindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
|
||||
// Right now it's not possible for a component to define a Parameter and a Cascading Parameter with
|
||||
// the same name. We don't give you a way to express this in code (would create duplicate properties),
|
||||
// and we don't have the ability to represent it in our data structures.
|
||||
private static readonly ConcurrentDictionary<Type, WritersForType> _cachedWritersByType
|
||||
= new ConcurrentDictionary<Type, WritersForType>();
|
||||
|
||||
public static void ClearCache() => _cachedWritersByType.Clear();
|
||||
|
||||
public static void SetProperties(in ParameterView parameters, object target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
var targetType = target.GetType();
|
||||
if (!_cachedWritersByType.TryGetValue(targetType, out var writers))
|
||||
{
|
||||
writers = new WritersForType(targetType);
|
||||
_cachedWritersByType[targetType] = writers;
|
||||
}
|
||||
|
||||
// The logic is split up for simplicity now that we have CaptureUnmatchedValues parameters.
|
||||
if (writers.CaptureUnmatchedValuesWriter == null)
|
||||
{
|
||||
// Logic for components without a CaptureUnmatchedValues parameter
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var parameterName = parameter.Name;
|
||||
if (!writers.TryGetValue(parameterName, out var writer))
|
||||
{
|
||||
// Case 1: There is nowhere to put this value.
|
||||
ThrowForUnknownIncomingParameterName(targetType, parameterName);
|
||||
throw null; // Unreachable
|
||||
}
|
||||
else if (writer.Cascading && !parameter.Cascading)
|
||||
{
|
||||
// We don't allow you to set a cascading parameter with a non-cascading value. Put another way:
|
||||
// cascading parameters are not part of the public API of a component, so it's not reasonable
|
||||
// for someone to set it directly.
|
||||
//
|
||||
// If we find a strong reason for this to work in the future we can reverse our decision since
|
||||
// this throws today.
|
||||
ThrowForSettingCascadingParameterWithNonCascadingValue(targetType, parameterName);
|
||||
throw null; // Unreachable
|
||||
}
|
||||
else if (!writer.Cascading && parameter.Cascading)
|
||||
{
|
||||
// We're giving a more specific error here because trying to set a non-cascading parameter
|
||||
// with a cascading value is likely deliberate (but not supported), or is a bug in our code.
|
||||
ThrowForSettingParameterWithCascadingValue(targetType, parameterName);
|
||||
throw null; // Unreachable
|
||||
}
|
||||
|
||||
SetProperty(target, writer, parameterName, parameter.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Logic with components with a CaptureUnmatchedValues parameter
|
||||
var isCaptureUnmatchedValuesParameterSetExplicitly = false;
|
||||
Dictionary<string, object>? unmatched = null;
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var parameterName = parameter.Name;
|
||||
if (string.Equals(parameterName, writers.CaptureUnmatchedValuesPropertyName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isCaptureUnmatchedValuesParameterSetExplicitly = true;
|
||||
}
|
||||
|
||||
if (writers.TryGetValue(parameterName, out var writer))
|
||||
{
|
||||
if (!writer.Cascading && parameter.Cascading)
|
||||
{
|
||||
// Don't allow an "extra" cascading value to be collected - or don't allow a non-cascading
|
||||
// parameter to be set with a cascading value.
|
||||
//
|
||||
// This is likely a bug in our infrastructure or an attempt to deliberately do something unsupported.
|
||||
ThrowForSettingParameterWithCascadingValue(targetType, parameterName);
|
||||
throw null; // Unreachable
|
||||
}
|
||||
else if (writer.Cascading && !parameter.Cascading)
|
||||
{
|
||||
// Allow unmatched parameters to collide with the names of cascading parameters. This is
|
||||
// valid because cascading parameter names are not part of the public API. There's no
|
||||
// way for the user of a component to know what the names of cascading parameters
|
||||
// are.
|
||||
unmatched ??= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
unmatched[parameterName] = parameter.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetProperty(target, writer, parameterName, parameter.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parameter.Cascading)
|
||||
{
|
||||
// Don't allow an "extra" cascading value to be collected - or don't allow a non-cascading
|
||||
// parameter to be set with a cascading value.
|
||||
//
|
||||
// This is likely a bug in our infrastructure or an attempt to deliberately do something unsupported.
|
||||
ThrowForSettingParameterWithCascadingValue(targetType, parameterName);
|
||||
throw null; // Unreachable
|
||||
}
|
||||
else
|
||||
{
|
||||
unmatched ??= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
unmatched[parameterName] = parameter.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unmatched != null && isCaptureUnmatchedValuesParameterSetExplicitly)
|
||||
{
|
||||
// This has to be an error because we want to allow users to set the CaptureUnmatchedValues
|
||||
// parameter explicitly and ....
|
||||
// 1. We don't ever want to mutate a value the user gives us.
|
||||
// 2. We also don't want to implicitly copy a value the user gives us.
|
||||
//
|
||||
// Either one of those implementation choices would do something unexpected.
|
||||
ThrowForCaptureUnmatchedValuesConflict(targetType, writers.CaptureUnmatchedValuesPropertyName!, unmatched);
|
||||
throw null; // Unreachable
|
||||
}
|
||||
else if (unmatched != null)
|
||||
{
|
||||
// We had some unmatched values, set the CaptureUnmatchedValues property
|
||||
SetProperty(target, writers.CaptureUnmatchedValuesWriter, writers.CaptureUnmatchedValuesPropertyName!, unmatched);
|
||||
}
|
||||
}
|
||||
|
||||
static void SetProperty(object target, PropertySetter writer, string parameterName, object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.SetValue(target, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unable to set property '{parameterName}' on object of " +
|
||||
$"type '{target.GetType().FullName}'. The error was: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties([DynamicallyAccessedMembers(Component)] Type targetType)
|
||||
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, BindablePropertyFlags);
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMembers(Component)] Type targetType,
|
||||
string parameterName)
|
||||
{
|
||||
// We know we're going to throw by this stage, so it doesn't matter that the following
|
||||
// reflection code will be slow. We're just trying to help developers see what they did wrong.
|
||||
var propertyInfo = targetType.GetProperty(parameterName, BindablePropertyFlags);
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
|
||||
$"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// This should not happen
|
||||
throw new InvalidOperationException(
|
||||
$"No writer was cached for the property '{propertyInfo.Name}' on type '{targetType.FullName}'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Object of type '{targetType.FullName}' does not have a property " +
|
||||
$"matching the name '{parameterName}'.");
|
||||
}
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForSettingCascadingParameterWithNonCascadingValue(Type targetType, string parameterName)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
|
||||
$"but it does not have [{nameof(ParameterAttribute)}] applied.");
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForSettingParameterWithCascadingValue(Type targetType, string parameterName)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The property '{parameterName}' on component type '{targetType.FullName}' cannot be set " +
|
||||
$"using a cascading value.");
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForCaptureUnmatchedValuesConflict(Type targetType, string parameterName, Dictionary<string, object> unmatched)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The property '{parameterName}' on component type '{targetType.FullName}' cannot be set explicitly " +
|
||||
$"when also used to capture unmatched values. Unmatched values:" + Environment.NewLine +
|
||||
string.Join(Environment.NewLine, unmatched.Keys));
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForMultipleCaptureUnmatchedValuesParameters([DynamicallyAccessedMembers(Component)] Type targetType)
|
||||
{
|
||||
var propertyNames = new List<string>();
|
||||
foreach (var property in targetType.GetProperties(BindablePropertyFlags))
|
||||
{
|
||||
if (property.GetCustomAttribute<ParameterAttribute>()?.CaptureUnmatchedValues == true)
|
||||
{
|
||||
propertyNames.Add(property.Name);
|
||||
}
|
||||
}
|
||||
|
||||
propertyNames.Sort(StringComparer.Ordinal);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Multiple properties were found on component type '{targetType.FullName}' with " +
|
||||
$"'{nameof(ParameterAttribute)}.{nameof(ParameterAttribute.CaptureUnmatchedValues)}'. Only a single property " +
|
||||
$"per type can use '{nameof(ParameterAttribute)}.{nameof(ParameterAttribute.CaptureUnmatchedValues)}'. Properties:" + Environment.NewLine +
|
||||
string.Join(Environment.NewLine, propertyNames));
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowForInvalidCaptureUnmatchedValuesParameterType(Type targetType, PropertyInfo propertyInfo)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The property '{propertyInfo.Name}' on component type '{targetType.FullName}' cannot be used " +
|
||||
$"with '{nameof(ParameterAttribute)}.{nameof(ParameterAttribute.CaptureUnmatchedValues)}' because it has the wrong type. " +
|
||||
$"The property must be assignable from 'Dictionary<string, object>'.");
|
||||
}
|
||||
|
||||
private class WritersForType
|
||||
{
|
||||
private const int MaxCachedWriterLookups = 100;
|
||||
private readonly Dictionary<string, PropertySetter> _underlyingWriters;
|
||||
private readonly ConcurrentDictionary<string, PropertySetter?> _referenceEqualityWritersCache;
|
||||
|
||||
public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
|
||||
{
|
||||
_underlyingWriters = new Dictionary<string, PropertySetter>(StringComparer.OrdinalIgnoreCase);
|
||||
_referenceEqualityWritersCache = new ConcurrentDictionary<string, PropertySetter?>(ReferenceEqualityComparer.Instance);
|
||||
|
||||
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
|
||||
{
|
||||
var parameterAttribute = propertyInfo.GetCustomAttribute<ParameterAttribute>();
|
||||
var cascadingParameterAttribute = propertyInfo.GetCustomAttribute<CascadingParameterAttribute>();
|
||||
var isParameter = parameterAttribute != null || cascadingParameterAttribute != null;
|
||||
if (!isParameter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propertyName = propertyInfo.Name;
|
||||
if (parameterAttribute != null && (propertyInfo.SetMethod == null || !propertyInfo.SetMethod.IsPublic))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The type '{targetType.FullName}' declares a parameter matching the name '{propertyName}' that is not public. Parameters must be public.");
|
||||
}
|
||||
|
||||
var propertySetter = new PropertySetter(targetType, propertyInfo)
|
||||
{
|
||||
Cascading = cascadingParameterAttribute != null,
|
||||
};
|
||||
|
||||
if (_underlyingWriters.ContainsKey(propertyName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The type '{targetType.FullName}' declares more than one parameter matching the " +
|
||||
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
|
||||
}
|
||||
|
||||
_underlyingWriters.Add(propertyName, propertySetter);
|
||||
|
||||
if (parameterAttribute != null && parameterAttribute.CaptureUnmatchedValues)
|
||||
{
|
||||
// This is an "Extra" parameter.
|
||||
//
|
||||
// There should only be one of these.
|
||||
if (CaptureUnmatchedValuesWriter != null)
|
||||
{
|
||||
ThrowForMultipleCaptureUnmatchedValuesParameters(targetType);
|
||||
}
|
||||
|
||||
// It must be able to hold a Dictionary<string, object> since that's what we create.
|
||||
if (!propertyInfo.PropertyType.IsAssignableFrom(typeof(Dictionary<string, object>)))
|
||||
{
|
||||
ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo);
|
||||
}
|
||||
|
||||
CaptureUnmatchedValuesWriter = new PropertySetter(targetType, propertyInfo);
|
||||
CaptureUnmatchedValuesPropertyName = propertyInfo.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PropertySetter? CaptureUnmatchedValuesWriter { get; }
|
||||
|
||||
public string? CaptureUnmatchedValuesPropertyName { get; }
|
||||
|
||||
public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out PropertySetter writer)
|
||||
{
|
||||
// In intensive parameter-passing scenarios, one of the most expensive things we do is the
|
||||
// lookup from parameterName to writer. Pre-5.0 that was because of the string hashing.
|
||||
// To optimize this, we now have a cache in front of the lookup which is keyed by parameterName's
|
||||
// object identity (not its string hash). So in most cases we can resolve the lookup without
|
||||
// having to hash the string. We only fall back on hashing the string if the cache gets full,
|
||||
// which would only be in very unusual situations because components don't typically have many
|
||||
// parameters, and the parameterName strings usually come from compile-time constants.
|
||||
if (!_referenceEqualityWritersCache.TryGetValue(parameterName, out writer))
|
||||
{
|
||||
_underlyingWriters.TryGetValue(parameterName, out writer);
|
||||
|
||||
// Note that because we're not locking around this, it's possible we might
|
||||
// actually write more than MaxCachedWriterLookups entries due to concurrent
|
||||
// writes. However this won't cause any problems.
|
||||
// Also note that the value we're caching might be 'null'. It's valid to cache
|
||||
// lookup misses just as much as hits, since then we can more quickly identify
|
||||
// incoming values that don't have a corresponding writer and thus will end up
|
||||
// being passed as catch-all parameter values.
|
||||
if (_referenceEqualityWritersCache.Count < MaxCachedWriterLookups)
|
||||
{
|
||||
_referenceEqualityWritersCache.TryAdd(parameterName, writer);
|
||||
}
|
||||
}
|
||||
|
||||
return writer != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
22
src/BootstrapBlazor/Misc/Internal/LinkerFlags.cs
Normal file
22
src/BootstrapBlazor/Misc/Internal/LinkerFlags.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Internal
|
||||
{
|
||||
internal static class LinkerFlags
|
||||
{
|
||||
/// <summary>
|
||||
/// Flags for a member that is JSON (de)serialized.
|
||||
/// </summary>
|
||||
public const DynamicallyAccessedMemberTypes JsonSerialized = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Flags for a component
|
||||
/// </summary>
|
||||
public const DynamicallyAccessedMemberTypes Component = DynamicallyAccessedMemberTypes.All;
|
||||
}
|
||||
}
|
||||
#endif
|
89
src/BootstrapBlazor/Misc/Internal/MemberAssignment.cs
Normal file
89
src/BootstrapBlazor/Misc/Internal/MemberAssignment.cs
Normal file
@ -0,0 +1,89 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Microsoft.AspNetCore.Internal.LinkerFlags;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Reflection
|
||||
{
|
||||
internal class MemberAssignment
|
||||
{
|
||||
public static IEnumerable<PropertyInfo> GetPropertiesIncludingInherited(
|
||||
[DynamicallyAccessedMembers(Component)] Type type,
|
||||
BindingFlags bindingFlags)
|
||||
{
|
||||
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
|
||||
Type? currentType = type;
|
||||
|
||||
while (currentType != null)
|
||||
{
|
||||
var properties = currentType.GetProperties(bindingFlags | BindingFlags.DeclaredOnly);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (!dictionary.TryGetValue(property.Name, out var others))
|
||||
{
|
||||
dictionary.Add(property.Name, property);
|
||||
}
|
||||
else if (!IsInheritedProperty(property, others))
|
||||
{
|
||||
List<PropertyInfo> many;
|
||||
if (others is PropertyInfo single)
|
||||
{
|
||||
many = new List<PropertyInfo> { single };
|
||||
dictionary[property.Name] = many;
|
||||
}
|
||||
else
|
||||
{
|
||||
many = (List<PropertyInfo>)others;
|
||||
}
|
||||
many.Add(property);
|
||||
}
|
||||
}
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
foreach (var item in dictionary)
|
||||
{
|
||||
if (item.Value is PropertyInfo property)
|
||||
{
|
||||
yield return property;
|
||||
continue;
|
||||
}
|
||||
|
||||
var list = (List<PropertyInfo>)item.Value;
|
||||
var count = list.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
yield return list[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInheritedProperty(PropertyInfo property, object others)
|
||||
{
|
||||
if (others is PropertyInfo single)
|
||||
{
|
||||
return single.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition();
|
||||
}
|
||||
|
||||
var many = (List<PropertyInfo>)others;
|
||||
foreach (var other in CollectionsMarshal.AsSpan(many))
|
||||
{
|
||||
if (other.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
62
src/BootstrapBlazor/Misc/Internal/PropertySetter.cs
Normal file
62
src/BootstrapBlazor/Misc/Internal/PropertySetter.cs
Normal file
@ -0,0 +1,62 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Reflection
|
||||
{
|
||||
internal sealed class PropertySetter
|
||||
{
|
||||
private static readonly MethodInfo CallPropertySetterOpenGenericMethod =
|
||||
typeof(PropertySetter).GetMethod(nameof(CallPropertySetter), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
private readonly Action<object, object> _setterDelegate;
|
||||
|
||||
[UnconditionalSuppressMessage(
|
||||
"ReflectionAnalysis",
|
||||
"IL2060:MakeGenericMethod",
|
||||
Justification = "The referenced methods don't have any DynamicallyAccessedMembers annotations. See https://github.com/mono/linker/issues/1727")]
|
||||
public PropertySetter(Type targetType, PropertyInfo property)
|
||||
{
|
||||
if (property.SetMethod == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot provide a value for property " +
|
||||
$"'{property.Name}' on type '{targetType.FullName}' because the property " +
|
||||
"has no setter.");
|
||||
}
|
||||
|
||||
var setMethod = property.SetMethod;
|
||||
|
||||
var propertySetterAsAction =
|
||||
setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(targetType, property.PropertyType));
|
||||
var callPropertySetterClosedGenericMethod =
|
||||
CallPropertySetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
|
||||
_setterDelegate = (Action<object, object>)
|
||||
callPropertySetterClosedGenericMethod.CreateDelegate(typeof(Action<object, object>), propertySetterAsAction);
|
||||
}
|
||||
|
||||
public bool Cascading { get; init; }
|
||||
|
||||
public void SetValue(object target, object value) => _setterDelegate(target, value);
|
||||
|
||||
private static void CallPropertySetter<TTarget, TValue>(
|
||||
Action<TTarget, TValue> setter,
|
||||
object target,
|
||||
object value)
|
||||
where TTarget : notnull
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
setter((TTarget)target, default!);
|
||||
}
|
||||
else
|
||||
{
|
||||
setter((TTarget)target, (TValue)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -0,0 +1,28 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
internal sealed class QueryParameterNameComparer : IComparer<ReadOnlyMemory<char>>, IEqualityComparer<ReadOnlyMemory<char>>
|
||||
{
|
||||
public static readonly QueryParameterNameComparer Instance = new();
|
||||
|
||||
public int Compare(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y)
|
||||
=> x.Span.CompareTo(y.Span, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public bool Equals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y)
|
||||
=> x.Span.Equals(y.Span, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public int GetHashCode([DisallowNull] ReadOnlyMemory<char> obj)
|
||||
=> string.GetHashCode(obj.Span, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
#endif
|
194
src/BootstrapBlazor/Misc/Internal/QueryParameterValueSupplier.cs
Normal file
194
src/BootstrapBlazor/Misc/Internal/QueryParameterValueSupplier.cs
Normal file
@ -0,0 +1,194 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Reflection;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using static Microsoft.AspNetCore.Internal.LinkerFlags;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
internal sealed class QueryParameterValueSupplier
|
||||
{
|
||||
public static void ClearCache() => _cacheByType.Clear();
|
||||
|
||||
private static readonly Dictionary<Type, QueryParameterValueSupplier?> _cacheByType = new();
|
||||
|
||||
// These two arrays contain the same number of entries, and their corresponding positions refer to each other.
|
||||
// Holding the info like this means we can use Array.BinarySearch with less custom implementation.
|
||||
private readonly ReadOnlyMemory<char>[] _queryParameterNames;
|
||||
private readonly QueryParameterDestination[] _destinations;
|
||||
|
||||
public static QueryParameterValueSupplier? ForType([DynamicallyAccessedMembers(Component)] Type componentType)
|
||||
{
|
||||
if (!_cacheByType.TryGetValue(componentType, out var instanceOrNull))
|
||||
{
|
||||
// If the component doesn't have any query parameters, store a null value for it
|
||||
// so we know the upstream code can't try to render query parameter frames for it.
|
||||
var sortedMappings = GetSortedMappings(componentType);
|
||||
instanceOrNull = sortedMappings == null ? null : new QueryParameterValueSupplier(sortedMappings);
|
||||
_cacheByType.TryAdd(componentType, instanceOrNull);
|
||||
}
|
||||
|
||||
return instanceOrNull;
|
||||
}
|
||||
|
||||
private QueryParameterValueSupplier(QueryParameterMapping[] sortedMappings)
|
||||
{
|
||||
_queryParameterNames = new ReadOnlyMemory<char>[sortedMappings.Length];
|
||||
_destinations = new QueryParameterDestination[sortedMappings.Length];
|
||||
for (var i = 0; i < sortedMappings.Length; i++)
|
||||
{
|
||||
ref var mapping = ref sortedMappings[i];
|
||||
_queryParameterNames[i] = mapping.QueryParameterName;
|
||||
_destinations[i] = mapping.Destination;
|
||||
}
|
||||
}
|
||||
|
||||
public void RenderParametersFromQueryString(RenderTreeBuilder builder, ReadOnlyMemory<char> queryString)
|
||||
{
|
||||
// If there's no querystring contents, we can skip renting from the pool
|
||||
if (queryString.IsEmpty)
|
||||
{
|
||||
for (var destinationIndex = 0; destinationIndex < _destinations.Length; destinationIndex++)
|
||||
{
|
||||
ref var destination = ref _destinations[destinationIndex];
|
||||
var blankValue = destination.IsArray ? destination.Parser.ParseMultiple(default, string.Empty) : null;
|
||||
builder.AddAttribute(0, destination.ComponentParameterName, blankValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporary workspace in which we accumulate the data while walking the querystring.
|
||||
var valuesByMapping = ArrayPool<StringSegmentAccumulator>.Shared.Rent(_destinations.Length);
|
||||
|
||||
try
|
||||
{
|
||||
// Capture values by destination in a single pass through the querystring
|
||||
var queryStringEnumerable = new QueryStringEnumerable(queryString);
|
||||
foreach (var suppliedPair in queryStringEnumerable)
|
||||
{
|
||||
var decodedName = suppliedPair.DecodeName();
|
||||
var mappingIndex = Array.BinarySearch(_queryParameterNames, decodedName, QueryParameterNameComparer.Instance);
|
||||
if (mappingIndex >= 0)
|
||||
{
|
||||
var decodedValue = suppliedPair.DecodeValue();
|
||||
|
||||
if (_destinations[mappingIndex].IsArray)
|
||||
{
|
||||
valuesByMapping[mappingIndex].Add(decodedValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
valuesByMapping[mappingIndex].SetSingle(decodedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, emit the parameter attributes by parsing all the string segments and building arrays
|
||||
for (var mappingIndex = 0; mappingIndex < _destinations.Length; mappingIndex++)
|
||||
{
|
||||
ref var destination = ref _destinations[mappingIndex];
|
||||
ref var values = ref valuesByMapping[mappingIndex];
|
||||
|
||||
var parsedValue = destination.IsArray
|
||||
? destination.Parser.ParseMultiple(values, destination.ComponentParameterName)
|
||||
: values.Count == 0
|
||||
? default
|
||||
: destination.Parser.Parse(values[0].Span, destination.ComponentParameterName);
|
||||
|
||||
builder.AddAttribute(0, destination.ComponentParameterName, parsedValue);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<StringSegmentAccumulator>.Shared.Return(valuesByMapping, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static QueryParameterMapping[]? GetSortedMappings([DynamicallyAccessedMembers(Component)] Type componentType)
|
||||
{
|
||||
var candidateProperties = MemberAssignment.GetPropertiesIncludingInherited(componentType, ComponentProperties.BindablePropertyFlags);
|
||||
HashSet<ReadOnlyMemory<char>>? usedQueryParameterNames = null;
|
||||
List<QueryParameterMapping>? mappings = null;
|
||||
|
||||
foreach (var propertyInfo in candidateProperties)
|
||||
{
|
||||
if (!propertyInfo.IsDefined(typeof(ParameterAttribute)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fromQueryAttribute = propertyInfo.GetCustomAttribute<SupplyParameterFromQueryAttribute>();
|
||||
if (fromQueryAttribute is not null)
|
||||
{
|
||||
// Found a parameter that's assignable from querystring
|
||||
var componentParameterName = propertyInfo.Name;
|
||||
var queryParameterName = (string.IsNullOrEmpty(fromQueryAttribute.Name)
|
||||
? componentParameterName
|
||||
: fromQueryAttribute.Name).AsMemory();
|
||||
|
||||
// If it's an array type, capture that info and prepare to parse the element type
|
||||
Type effectiveType = propertyInfo.PropertyType;
|
||||
var isArray = false;
|
||||
if (effectiveType.IsArray)
|
||||
{
|
||||
isArray = true;
|
||||
effectiveType = effectiveType.GetElementType()!;
|
||||
}
|
||||
|
||||
if (!UrlValueConstraint.TryGetByTargetType(effectiveType, out var parser))
|
||||
{
|
||||
throw new NotSupportedException($"Querystring values cannot be parsed as type '{propertyInfo.PropertyType}'.");
|
||||
}
|
||||
|
||||
// Add the destination for this component parameter name
|
||||
usedQueryParameterNames ??= new(QueryParameterNameComparer.Instance);
|
||||
if (usedQueryParameterNames.Contains(queryParameterName))
|
||||
{
|
||||
throw new InvalidOperationException($"The component '{componentType}' declares more than one mapping for the query parameter '{queryParameterName}'.");
|
||||
}
|
||||
usedQueryParameterNames.Add(queryParameterName);
|
||||
|
||||
mappings ??= new();
|
||||
mappings.Add(new QueryParameterMapping
|
||||
{
|
||||
QueryParameterName = queryParameterName,
|
||||
Destination = new QueryParameterDestination(componentParameterName, parser, isArray)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mappings?.Sort((a, b) => QueryParameterNameComparer.Instance.Compare(a.QueryParameterName, b.QueryParameterName));
|
||||
return mappings?.ToArray();
|
||||
}
|
||||
|
||||
private readonly struct QueryParameterMapping
|
||||
{
|
||||
public ReadOnlyMemory<char> QueryParameterName { get; init; }
|
||||
public QueryParameterDestination Destination { get; init; }
|
||||
}
|
||||
|
||||
private readonly struct QueryParameterDestination
|
||||
{
|
||||
public readonly string ComponentParameterName;
|
||||
public readonly UrlValueConstraint Parser;
|
||||
public readonly bool IsArray;
|
||||
|
||||
public QueryParameterDestination(string componentParameterName, UrlValueConstraint parser, bool isArray)
|
||||
{
|
||||
ComponentParameterName = componentParameterName;
|
||||
Parser = parser;
|
||||
IsArray = isArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
219
src/BootstrapBlazor/Misc/Internal/QueryStringEnumerable.cs
Normal file
219
src/BootstrapBlazor/Misc/Internal/QueryStringEnumerable.cs
Normal file
@ -0,0 +1,219 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
#if QueryStringEnumerable_In_WebUtilities
|
||||
namespace Microsoft.AspNetCore.WebUtilities
|
||||
#else
|
||||
namespace Microsoft.AspNetCore.Internal
|
||||
#endif
|
||||
{
|
||||
/// <summary>
|
||||
/// An enumerable that can supply the name/value pairs from a URI query string.
|
||||
/// </summary>
|
||||
#if QueryStringEnumerable_In_WebUtilities
|
||||
public
|
||||
#else
|
||||
internal
|
||||
#endif
|
||||
readonly struct QueryStringEnumerable
|
||||
{
|
||||
private readonly ReadOnlyMemory<char> _queryString;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="QueryStringEnumerable"/>.
|
||||
/// </summary>
|
||||
/// <param name="queryString">The query string.</param>
|
||||
public QueryStringEnumerable(string? queryString)
|
||||
: this(queryString.AsMemory())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="QueryStringEnumerable"/>.
|
||||
/// </summary>
|
||||
/// <param name="queryString">The query string.</param>
|
||||
public QueryStringEnumerable(ReadOnlyMemory<char> queryString)
|
||||
{
|
||||
_queryString = queryString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an object that can iterate through the name/value pairs in the query string.
|
||||
/// </summary>
|
||||
/// <returns>An object that can iterate through the name/value pairs in the query string.</returns>
|
||||
public Enumerator GetEnumerator()
|
||||
=> new Enumerator(_queryString);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single name/value pair extracted from a query string during enumeration.
|
||||
/// </summary>
|
||||
public readonly struct EncodedNameValuePair
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name from this name/value pair in its original encoded form.
|
||||
/// To get the decoded string, call <see cref="DecodeName"/>.
|
||||
/// </summary>
|
||||
public readonly ReadOnlyMemory<char> EncodedName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value from this name/value pair in its original encoded form.
|
||||
/// To get the decoded string, call <see cref="DecodeValue"/>.
|
||||
/// </summary>
|
||||
public readonly ReadOnlyMemory<char> EncodedValue { get; }
|
||||
|
||||
internal EncodedNameValuePair(ReadOnlyMemory<char> encodedName, ReadOnlyMemory<char> encodedValue)
|
||||
{
|
||||
EncodedName = encodedName;
|
||||
EncodedValue = encodedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the name from this name/value pair.
|
||||
/// </summary>
|
||||
/// <returns>Characters representing the decoded name.</returns>
|
||||
public ReadOnlyMemory<char> DecodeName()
|
||||
=> Decode(EncodedName);
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the value from this name/value pair.
|
||||
/// </summary>
|
||||
/// <returns>Characters representing the decoded value.</returns>
|
||||
public ReadOnlyMemory<char> DecodeValue()
|
||||
=> Decode(EncodedValue);
|
||||
|
||||
private static ReadOnlyMemory<char> Decode(ReadOnlyMemory<char> chars)
|
||||
{
|
||||
// If the value is short, it's cheap to check up front if it really needs decoding. If it doesn't,
|
||||
// then we can save some allocations.
|
||||
return chars.Length < 16 && chars.Span.IndexOfAny('%', '+') < 0
|
||||
? chars
|
||||
: Uri.UnescapeDataString(SpanHelper.ReplacePlusWithSpace(chars.Span)).AsMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An enumerator that supplies the name/value pairs from a URI query string.
|
||||
/// </summary>
|
||||
public struct Enumerator
|
||||
{
|
||||
private ReadOnlyMemory<char> _query;
|
||||
|
||||
internal Enumerator(ReadOnlyMemory<char> query)
|
||||
{
|
||||
Current = default;
|
||||
_query = query.IsEmpty || query.Span[0] != '?'
|
||||
? query
|
||||
: query.Slice(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently referenced key/value pair in the query string being enumerated.
|
||||
/// </summary>
|
||||
public EncodedNameValuePair Current { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Moves to the next key/value pair in the query string being enumerated.
|
||||
/// </summary>
|
||||
/// <returns>True if there is another key/value pair, otherwise false.</returns>
|
||||
public bool MoveNext()
|
||||
{
|
||||
while (!_query.IsEmpty)
|
||||
{
|
||||
// Chomp off the next segment
|
||||
ReadOnlyMemory<char> segment;
|
||||
var delimiterIndex = _query.Span.IndexOf('&');
|
||||
if (delimiterIndex >= 0)
|
||||
{
|
||||
segment = _query.Slice(0, delimiterIndex);
|
||||
_query = _query.Slice(delimiterIndex + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
segment = _query;
|
||||
_query = default;
|
||||
}
|
||||
|
||||
// If it's nonempty, emit it
|
||||
var equalIndex = segment.Span.IndexOf('=');
|
||||
if (equalIndex >= 0)
|
||||
{
|
||||
Current = new EncodedNameValuePair(
|
||||
segment.Slice(0, equalIndex),
|
||||
segment.Slice(equalIndex + 1));
|
||||
return true;
|
||||
}
|
||||
else if (!segment.IsEmpty)
|
||||
{
|
||||
Current = new EncodedNameValuePair(segment, default);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Current = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpanHelper
|
||||
{
|
||||
private static readonly SpanAction<char, IntPtr> s_replacePlusWithSpace = ReplacePlusWithSpaceCore;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe string ReplacePlusWithSpace(ReadOnlySpan<char> span)
|
||||
{
|
||||
fixed (char* ptr = &MemoryMarshal.GetReference(span))
|
||||
{
|
||||
return string.Create(span.Length, (IntPtr)ptr, s_replacePlusWithSpace);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void ReplacePlusWithSpaceCore(Span<char> buffer, IntPtr state)
|
||||
{
|
||||
fixed (char* ptr = &MemoryMarshal.GetReference(buffer))
|
||||
{
|
||||
var input = (ushort*)state.ToPointer();
|
||||
var output = (ushort*)ptr;
|
||||
|
||||
var i = (nint)0;
|
||||
var n = (nint)(uint)buffer.Length;
|
||||
|
||||
if (Sse41.IsSupported && n >= Vector128<ushort>.Count)
|
||||
{
|
||||
var vecPlus = Vector128.Create('+');
|
||||
var vecSpace = Vector128.Create(' ');
|
||||
|
||||
do
|
||||
{
|
||||
var vec = Sse2.LoadVector128(input + i);
|
||||
var mask = Sse2.CompareEqual(vec, vecPlus);
|
||||
var res = Sse41.BlendVariable(vec, vecSpace, mask);
|
||||
Sse2.Store(output + i, res);
|
||||
i += Vector128<ushort>.Count;
|
||||
} while (i <= n - Vector128<ushort>.Count);
|
||||
}
|
||||
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (input[i] != '+')
|
||||
{
|
||||
output[i] = input[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -2,7 +2,6 @@
|
||||
// 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;
|
||||
@ -13,7 +12,7 @@ namespace BootstrapBlazor.Components
|
||||
internal static class RouteTableFactory
|
||||
{
|
||||
[NotNull]
|
||||
private static Routing.IRouteTable? Routes { get; set; }
|
||||
private static Microsoft.AspNetCore.Components.Routing.IRouteTable? Routes { get; set; }
|
||||
private static readonly HashSet<Assembly> _assemblies = new();
|
||||
|
||||
/// <summary>
|
||||
@ -26,7 +25,7 @@ namespace BootstrapBlazor.Components
|
||||
{
|
||||
RefreshRouteTable(assemblies);
|
||||
if (url.IndexOf("?") > 0) url = url[..url.IndexOf("?")];
|
||||
var routeContext = new Routing.RouteContext(url);
|
||||
var routeContext = new Microsoft.AspNetCore.Components.Routing.RouteContext(url);
|
||||
Routes.Route(routeContext);
|
||||
return new RouteContext()
|
||||
{
|
||||
@ -41,7 +40,7 @@ namespace BootstrapBlazor.Components
|
||||
var assembliesSet = new HashSet<Assembly>(assemblies);
|
||||
if (!_assemblies.SetEquals(assembliesSet))
|
||||
{
|
||||
Routes = Routing.RouteTableFactory.Create(assemblies);
|
||||
Routes = Microsoft.AspNetCore.Components.Routing.RouteTableFactory.Create(assemblies);
|
||||
_assemblies.Clear();
|
||||
_assemblies.UnionWith(assembliesSet);
|
||||
}
|
||||
@ -51,7 +50,7 @@ namespace BootstrapBlazor.Components
|
||||
internal static class RouteTableFactory
|
||||
{
|
||||
[NotNull]
|
||||
private static Routing.RouteTable? Routes { get; set; }
|
||||
private static Microsoft.AspNetCore.Components.Routing.RouteTable? Routes { get; set; }
|
||||
private static readonly HashSet<Assembly> _assemblies = new();
|
||||
|
||||
/// <summary>
|
||||
@ -64,7 +63,7 @@ namespace BootstrapBlazor.Components
|
||||
{
|
||||
RefreshRouteTable(assemblies);
|
||||
if (url.IndexOf("?") > 0) url = url[..url.IndexOf("?")];
|
||||
var routeContext = new Routing.RouteContext(url);
|
||||
var routeContext = new Microsoft.AspNetCore.Components.Routing.RouteContext(url);
|
||||
Routes.Route(routeContext);
|
||||
return new RouteContext()
|
||||
{
|
||||
@ -79,7 +78,7 @@ namespace BootstrapBlazor.Components
|
||||
var assembliesSet = new HashSet<Assembly>(assemblies);
|
||||
if (!_assemblies.SetEquals(assembliesSet))
|
||||
{
|
||||
Routes = Routing.RouteTableFactory.Create(new RouteKey(null, assemblies));
|
||||
Routes = Microsoft.AspNetCore.Components.Routing.RouteTableFactory.Create(new Microsoft.AspNetCore.Components.Routing.RouteKey(null, assemblies));
|
||||
_assemblies.Clear();
|
||||
_assemblies.UnionWith(assembliesSet);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user