!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:
Argo 2021-11-15 11:25:05 +00:00
parent 51755c67d8
commit 5c781ff56b
24 changed files with 1030 additions and 27 deletions

View File

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

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<Version>6.0.3</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">

View File

@ -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>

View File

@ -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

View File

@ -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
{

View File

@ -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}")]

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -6,7 +6,7 @@
using System.Diagnostics;
using System.Linq;
namespace BootstrapBlazor.Components.Routing
namespace Microsoft.AspNetCore.Components.Routing
{
[DebuggerDisplay("{TemplateText}")]
internal class RouteTemplate

View File

@ -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

View File

@ -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.

View File

@ -5,7 +5,7 @@
using System;
using System.Linq;
namespace BootstrapBlazor.Components.Routing
namespace Microsoft.AspNetCore.Components.Routing
{
#if NET5_0
internal class TemplateSegment

View File

@ -5,7 +5,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace BootstrapBlazor.Components.Routing
namespace Microsoft.AspNetCore.Components.Routing
{
#if NET5_0
/// <summary>

View File

@ -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>

View File

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

View 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

View 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

View 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

View 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

View File

@ -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

View 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

View 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

View File

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