From 21a341d8ecd9ee582f5eb6bb14ca8036e2fc26ff Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 23 Mar 2021 15:42:07 -0700 Subject: [PATCH 1/5] Use System.Linq in fewer places * Replace with alternatives or iteration where possible * Rewrite Router component to avoid allocations --- .../src/AttributeAuthorizeDataCache.cs | 19 +- .../Components/src/ComponentFactory.cs | 26 +-- .../Lifetime/ComponentApplicationLifetime.cs | 2 - ...NetCore.Components.WarningSuppressions.xml | 14 +- .../src/Reflection/ComponentProperties.cs | 21 ++- .../src/Reflection/MemberAssignment.cs | 116 +++++++++++-- .../Components/src/Routing/RouteEntry.cs | 8 +- .../Components/src/Routing/RouteKey.cs | 64 +++++++ .../src/Routing/RouteTableFactory.cs | 162 ++++++++---------- .../Components/src/Routing/RouteTemplate.cs | 17 +- .../Components/src/Routing/Router.cs | 14 +- .../Components/src/Routing/TemplateSegment.cs | 7 +- .../test/CascadingParameterStateTest.cs | 7 +- .../test/ParameterViewTest.Assignment.cs | 4 +- .../Components/test/Routing/RouteKeyTest.cs | 106 ++++++++++++ .../test/Routing/RouteTableFactoryTests.cs | 22 +-- .../Components/test/Routing/RouterTest.cs | 3 +- .../EditContextDataAnnotationsExtensions.cs | 18 +- .../Forms/src/ValidationMessageStore.cs | 3 +- .../src/ComponentParametersTypeCache.cs | 13 +- .../Shared/src/RootComponentTypeCache.cs | 14 +- .../src/Hosting/WebAssemblyHost.cs | 7 +- .../src/Hosting/WebAssemblyHostBuilder.cs | 6 +- .../Hosting/WebAssemblyHostConfiguration.cs | 25 ++- .../src/Services/LazyAssemblyLoader.cs | 36 +++- .../src/Infrastructure/DotNetDispatcher.cs | 65 ++++--- ...icrosoft.JSInterop.WarningSuppressions.xml | 6 + 27 files changed, 572 insertions(+), 233 deletions(-) create mode 100644 src/Components/Components/src/Routing/RouteKey.cs create mode 100644 src/Components/Components/test/Routing/RouteKeyTest.cs diff --git a/src/Components/Authorization/src/AttributeAuthorizeDataCache.cs b/src/Components/Authorization/src/AttributeAuthorizeDataCache.cs index c1495fb303de..c04da2ab50c8 100644 --- a/src/Components/Authorization/src/AttributeAuthorizeDataCache.cs +++ b/src/Components/Authorization/src/AttributeAuthorizeDataCache.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; namespace Microsoft.AspNetCore.Components.Authorization @@ -29,13 +29,22 @@ private static IAuthorizeData[] ComputeAuthorizeDataForType(Type type) { // Allow Anonymous skips all authorization var allAttributes = type.GetCustomAttributes(inherit: true); - if (allAttributes.OfType().Any()) + List authorizeDatas = null; + for (var i = 0; i < allAttributes.Length; i++) { - return null; + if (allAttributes[i] is IAllowAnonymous) + { + return null; + } + + if (allAttributes[i] is IAuthorizeData authorizeData) + { + authorizeDatas ??= new(); + authorizeDatas.Add(authorizeData); + } } - var authorizeDataAttributes = allAttributes.OfType().ToArray(); - return authorizeDataAttributes.Length > 0 ? authorizeDataAttributes : null; + return authorizeDatas?.ToArray(); } } } diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs index f56094d47663..49b3a6a95b3a 100644 --- a/src/Components/Components/src/ComponentFactory.cs +++ b/src/Components/Components/src/ComponentFactory.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Components.Reflection; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -56,16 +56,22 @@ private void PerformPropertyInjection(IServiceProvider serviceProvider, ICompone private Action CreateInitializer([DynamicallyAccessedMembers(Component)] Type type) { // Do all the reflection up front - var injectableProperties = - MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags) - .Where(p => p.IsDefined(typeof(InjectAttribute))); + List<(string name, Type propertyType, PropertySetter setter)>? injectables = null; + foreach (var property in MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)) + { + if (!property.IsDefined(typeof(InjectAttribute))) + { + continue; + } - var injectables = injectableProperties.Select(property => - ( - propertyName: property.Name, - propertyType: property.PropertyType, - setter: new PropertySetter(type, property) - )).ToArray(); + injectables ??= new(); + injectables.Add((property.Name, property.PropertyType, new PropertySetter(type, property))); + } + + if (injectables is null) + { + return static (_, _) => { }; + } return Initialize; diff --git a/src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs b/src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs index cb44a9ff111c..0aa85ceefbda 100644 --- a/src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs +++ b/src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging; diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml index 81884f1bacb5..f0527f34ccac 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml @@ -17,7 +17,13 @@ ILLink IL2026 member - M:Microsoft.AspNetCore.Components.RouteTableFactory.GetRouteableComponents(System.Collections.Generic.IEnumerable{System.Reflection.Assembly}) + M:Microsoft.AspNetCore.Components.RouteTableFactory.<GetRouteableComponents>g__GetRouteableComponents|3_0(System.Collections.Generic.List{System.Type},System.Reflection.Assembly) + + + ILLink + IL2062 + member + M:Microsoft.AspNetCore.Components.RouteTableFactory.Create(System.Collections.Generic.Dictionary{System.Type,System.String[]}) ILLink @@ -49,12 +55,6 @@ member M:Microsoft.AspNetCore.Components.Reflection.ComponentProperties.SetProperties(Microsoft.AspNetCore.Components.ParameterView@,System.Object) - - ILLink - IL2072 - member - M:Microsoft.AspNetCore.Components.RouteTableFactory.Create(System.Collections.Generic.Dictionary{System.Type,System.String[]}) - ILLink IL2077 diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index def8a59b4850..57a9dc32912d 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -160,7 +159,7 @@ static void SetProperty(object target, PropertySetter writer, string parameterNa } } - internal static IEnumerable GetCandidateBindableProperties([DynamicallyAccessedMembers(Component)] Type targetType) + internal static MemberAssignment.PropertyEnumerable GetCandidateBindableProperties([DynamicallyAccessedMembers(Component)] Type targetType) => MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags); [DoesNotReturn] @@ -215,19 +214,23 @@ private static void ThrowForCaptureUnmatchedValuesConflict(Type targetType, stri 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.OrderBy(k => k))); + string.Join(Environment.NewLine, unmatched.Keys)); } [DoesNotReturn] private static void ThrowForMultipleCaptureUnmatchedValuesParameters([DynamicallyAccessedMembers(Component)] Type targetType) { // We don't care about perf here, we want to report an accurate and useful error. - var propertyNames = targetType - .GetProperties(_bindablePropertyFlags) - .Where(p => p.GetCustomAttribute()?.CaptureUnmatchedValues == true) - .Select(p => p.Name) - .OrderBy(p => p) - .ToArray(); + var propertyNames = new List(); + foreach (var property in targetType.GetProperties(_bindablePropertyFlags)) + { + if (property.GetCustomAttribute()?.CaptureUnmatchedValues == true) + { + propertyNames.Add(property.Name); + } + } + + propertyNames.Sort(StringComparer.Ordinal); throw new InvalidOperationException( $"Multiple properties were found on component type '{targetType.FullName}' with " + diff --git a/src/Components/Components/src/Reflection/MemberAssignment.cs b/src/Components/Components/src/Reflection/MemberAssignment.cs index dc5af2dc7f82..5a3f58a67b8a 100644 --- a/src/Components/Components/src/Reflection/MemberAssignment.cs +++ b/src/Components/Components/src/Reflection/MemberAssignment.cs @@ -3,48 +3,136 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.Reflection { internal class MemberAssignment { - public static IEnumerable GetPropertiesIncludingInherited( + public static PropertyEnumerable GetPropertiesIncludingInherited( [DynamicallyAccessedMembers(Component)] Type type, BindingFlags bindingFlags) { - var dictionary = new Dictionary>(); + var dictionary = new Dictionary(StringComparer.Ordinal); Type? currentType = type; while (currentType != null) { - var properties = currentType.GetProperties(bindingFlags | BindingFlags.DeclaredOnly); + var properties = currentType.GetProperties(bindingFlags | BindingFlags.DeclaredOnly); foreach (var property in properties) { if (!dictionary.TryGetValue(property.Name, out var others)) { - others = new List(); - dictionary.Add(property.Name, others); + dictionary.Add(property.Name, property); } - - if (others.Any(other => other.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition())) + else if (!IsInheritedProperty(property, others)) { - // This is an inheritance case. We can safely ignore the value of property since - // we have seen a more derived value. - continue; + List many; + if (others is PropertyInfo single) + { + many = new List { single }; + dictionary[property.Name] = many; + } + else + { + many = (List)others; + } + many.Add(property); } - - others.Add(property); } currentType = currentType.BaseType; } - return dictionary.Values.SelectMany(p => p); + return new PropertyEnumerable(dictionary); + } + + private static bool IsInheritedProperty(PropertyInfo property, object others) + { + if (others is PropertyInfo single) + { + return single.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition(); + } + + var many = (List)others; + foreach (var other in CollectionsMarshal.AsSpan(many)) + { + if (other.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition()) + { + return true; + } + } + + return false; + } + + public ref struct PropertyEnumerable + { + private readonly PropertyEnumerator _enumerator; + + public PropertyEnumerable(Dictionary dictionary) + { + _enumerator = new PropertyEnumerator(dictionary); + } + + public PropertyEnumerator GetEnumerator() => _enumerator; + } + + public ref struct PropertyEnumerator + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private Span.Enumerator _spanEnumerator; + + public PropertyEnumerator(Dictionary dictionary) + { + _dictionaryEnumerator = dictionary.GetEnumerator(); + _spanEnumerator = Span.Empty.GetEnumerator(); + } + + public PropertyInfo Current + { + get + { + if (_dictionaryEnumerator.Current.Value is PropertyInfo property) + { + return property; + } + + return _spanEnumerator.Current; + } + } + + public bool MoveNext() + { + if (_spanEnumerator.MoveNext()) + { + return true; + } + + if (!_dictionaryEnumerator.MoveNext()) + { + return false; + } + + var oneOrMoreProperties = _dictionaryEnumerator.Current.Value; + if (oneOrMoreProperties is PropertyInfo) + { + _spanEnumerator = Span.Empty.GetEnumerator(); + return true; + } + + var many = (List)oneOrMoreProperties; + _spanEnumerator = CollectionsMarshal.AsSpan(many).GetEnumerator(); + var moveNext = _spanEnumerator.MoveNext(); + Debug.Assert(moveNext, "We expect this to at least have one item."); + return moveNext; + } } } } diff --git a/src/Components/Components/src/Routing/RouteEntry.cs b/src/Components/Components/src/Routing/RouteEntry.cs index 94271a7721d2..14454fe47e8c 100644 --- a/src/Components/Components/src/Routing/RouteEntry.cs +++ b/src/Components/Components/src/Routing/RouteEntry.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Components.Routing [DebuggerDisplay("Handler = {Handler}, Template = {Template}")] internal class RouteEntry { - public RouteEntry(RouteTemplate template, [DynamicallyAccessedMembers(Component)] Type handler, string[] unusedRouteParameterNames) + public RouteEntry(RouteTemplate template, [DynamicallyAccessedMembers(Component)] Type handler, List? unusedRouteParameterNames) { Template = template; UnusedRouteParameterNames = unusedRouteParameterNames; @@ -23,7 +23,7 @@ public RouteEntry(RouteTemplate template, [DynamicallyAccessedMembers(Component) public RouteTemplate Template { get; } - public string[] UnusedRouteParameterNames { get; } + public List? UnusedRouteParameterNames { get; } [DynamicallyAccessedMembers(Component)] public Type Handler { get; } @@ -113,10 +113,10 @@ internal void Match(RouteContext context) parameters ??= new Dictionary(StringComparer.Ordinal); AddDefaultValues(parameters, templateIndex, Template.Segments); } - if (UnusedRouteParameterNames?.Length > 0) + if (UnusedRouteParameterNames?.Count > 0) { parameters ??= new Dictionary(StringComparer.Ordinal); - for (var i = 0; i < UnusedRouteParameterNames.Length; i++) + for (var i = 0; i < UnusedRouteParameterNames.Count; i++) { parameters[UnusedRouteParameterNames[i]] = null; } diff --git a/src/Components/Components/src/Routing/RouteKey.cs b/src/Components/Components/src/Routing/RouteKey.cs new file mode 100644 index 000000000000..522415a36d60 --- /dev/null +++ b/src/Components/Components/src/Routing/RouteKey.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNetCore.Components.Routing +{ + internal readonly struct RouteKey : IEquatable + { + public readonly Assembly? AppAssembly; + public readonly HashSet? AdditionalAssemblies; + + public RouteKey(Assembly appAssembly, IEnumerable additionalAssemblies) + { + AppAssembly = appAssembly; + AdditionalAssemblies = additionalAssemblies is null ? null : new HashSet(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); + } + } +} diff --git a/src/Components/Components/src/Routing/RouteTableFactory.cs b/src/Components/Components/src/Routing/RouteTableFactory.cs index e14e2df52dce..e2ecd0edb7b1 100644 --- a/src/Components/Components/src/Routing/RouteTableFactory.cs +++ b/src/Components/Components/src/Routing/RouteTableFactory.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Components.Routing; @@ -16,28 +14,41 @@ namespace Microsoft.AspNetCore.Components /// internal static class RouteTableFactory { - private static readonly ConcurrentDictionary Cache = - new ConcurrentDictionary(); + private static readonly ConcurrentDictionary Cache = new(); public static readonly IComparer RoutePrecedence = Comparer.Create(RouteComparison); - public static RouteTable Create(IEnumerable assemblies) + public static RouteTable Create(RouteKey routeKey) { - var key = new Key(assemblies.OrderBy(a => a.FullName).ToArray()); - if (Cache.TryGetValue(key, out var resolvedComponents)) + if (Cache.TryGetValue(routeKey, out var resolvedComponents)) { return resolvedComponents; } - var componentTypes = GetRouteableComponents(key.Assemblies); + var componentTypes = GetRouteableComponents(routeKey); var routeTable = Create(componentTypes); - Cache.TryAdd(key, routeTable); + Cache.TryAdd(routeKey, routeTable); return routeTable; } - private static List GetRouteableComponents(IEnumerable assemblies) + private static List GetRouteableComponents(RouteKey routeKey) { var routeableComponents = new List(); - foreach (var assembly in assemblies) + 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 routeableComponents, Assembly assembly) { foreach (var type in assembly.ExportedTypes) { @@ -47,8 +58,6 @@ private static List GetRouteableComponents(IEnumerable assemblie } } } - - return routeableComponents; } internal static RouteTable Create(List componentTypes) @@ -60,9 +69,14 @@ internal static RouteTable Create(List componentTypes) // // 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(inherit: false); + 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; + } - var templates = routeAttributes.Select(t => t.Template).ToArray(); templatesByHandler.Add(componentType, templates); } return Create(templatesByHandler); @@ -71,33 +85,61 @@ internal static RouteTable Create(List componentTypes) internal static RouteTable Create(Dictionary templatesByHandler) { var routes = new List(); - foreach (var keyValuePair in templatesByHandler) + foreach (var (type, templates) in templatesByHandler) { - var parsedTemplates = keyValuePair.Value.Select(v => TemplateParser.ParseTemplate(v)).ToArray(); - var allRouteParameterNames = parsedTemplates - .SelectMany(GetParameterNames) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + var allRouteParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var parsedTemplates = new (RouteTemplate, HashSet)[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 parsedTemplate in parsedTemplates) + foreach (var parameterName in parameterNames) + { + allRouteParameterNames.Add(parameterName); + } + } + + foreach (var (parsedTemplate, routeParameterNames) in parsedTemplates) { - var unusedRouteParameterNames = allRouteParameterNames - .Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase) - .ToArray(); - var entry = new RouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames); + var unusedRouteParameterNames = GetUnusedParameterNames(allRouteParameterNames, routeParameterNames); + var entry = new RouteEntry(parsedTemplate, type, unusedRouteParameterNames); routes.Add(entry); } } - return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray()); + routes.Sort(RoutePrecedence); + return new RouteTable(routes.ToArray()); + } + + private static HashSet GetParameterNames(RouteTemplate routeTemplate) + { + var parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var segment in routeTemplate.Segments) + { + if (segment.IsParameter) + { + parameterNames.Add(segment.Value); + } + } + + return parameterNames; } - private static string[] GetParameterNames(RouteTemplate routeTemplate) + private static List? GetUnusedParameterNames(HashSet allRouteParameterNames, HashSet routeParameterNames) { - return routeTemplate.Segments - .Where(s => s.IsParameter) - .Select(s => s.Value) - .ToArray(); + List? unusedParameters = null; + foreach (var item in allRouteParameterNames) + { + if (!routeParameterNames.Contains(item)) + { + unusedParameters ??= new(); + unusedParameters.Add(item); + } + } + + return unusedParameters; } /// @@ -196,61 +238,5 @@ private static int GetRank(TemplateSegment xSegment) _ => throw new InvalidOperationException($"Unknown segment definition '{xSegment}.") }; } - - private readonly struct Key : IEquatable - { - public readonly Assembly[] Assemblies; - - public Key(Assembly[] assemblies) - { - Assemblies = assemblies; - } - - public override bool Equals(object? obj) - { - return obj is Key other ? base.Equals(other) : false; - } - - public bool Equals(Key other) - { - if (Assemblies == null && other.Assemblies == null) - { - return true; - } - else if ((Assemblies == null) || (other.Assemblies == null)) - { - return false; - } - else if (Assemblies.Length != other.Assemblies.Length) - { - return false; - } - - for (var i = 0; i < Assemblies.Length; i++) - { - if (!Assemblies[i].Equals(other.Assemblies[i])) - { - return false; - } - } - - return true; - } - - public override int GetHashCode() - { - var hash = new HashCode(); - - if (Assemblies != null) - { - for (var i = 0; i < Assemblies.Length; i++) - { - hash.Add(Assemblies[i]); - } - } - - return hash.ToHashCode(); - } - } } } diff --git a/src/Components/Components/src/Routing/RouteTemplate.cs b/src/Components/Components/src/Routing/RouteTemplate.cs index eb37454f6f66..63ecd9dad3eb 100644 --- a/src/Components/Components/src/Routing/RouteTemplate.cs +++ b/src/Components/Components/src/Routing/RouteTemplate.cs @@ -1,9 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System.Diagnostics; -using System.Linq; namespace Microsoft.AspNetCore.Components.Routing { @@ -14,8 +12,19 @@ public RouteTemplate(string templateText, TemplateSegment[] segments) { TemplateText = templateText; Segments = segments; - OptionalSegmentsCount = segments.Count(template => template.IsOptional); - ContainsCatchAllSegment = segments.Any(template => template.IsCatchAll); + + for (var i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + if (segment.IsOptional) + { + OptionalSegmentsCount++; + } + if (segment.IsCatchAll) + { + ContainsCatchAllSegment = true; + } + } } public string TemplateText { get; } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index d9ec2fe20b5c..320403015858 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading; @@ -35,7 +33,7 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary private Task _previousOnNavigateTask = Task.CompletedTask; - private readonly HashSet _assemblies = new HashSet(); + private RouteKey _currentRouteKey; private bool _onNavigateCalled = false; @@ -145,14 +143,12 @@ private static string StringUntilAny(string str, char[] chars) private void RefreshRouteTable() { - var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies); - var assembliesSet = new HashSet(assemblies); + var routeKey = new RouteKey(AppAssembly, AdditionalAssemblies); - if (!_assemblies.SetEquals(assembliesSet)) + if (!routeKey.Equals(_currentRouteKey)) { - Routes = RouteTableFactory.Create(assemblies); - _assemblies.Clear(); - _assemblies.UnionWith(assembliesSet); + _currentRouteKey = routeKey; + Routes = RouteTableFactory.Create(routeKey); } } diff --git a/src/Components/Components/src/Routing/TemplateSegment.cs b/src/Components/Components/src/Routing/TemplateSegment.cs index 615ceae8bdb7..cbb03e04bcf0 100644 --- a/src/Components/Components/src/Routing/TemplateSegment.cs +++ b/src/Components/Components/src/Routing/TemplateSegment.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; namespace Microsoft.AspNetCore.Components.Routing { @@ -135,11 +134,11 @@ public bool Match(string pathSegment, out object? matchedParameterValue) 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(':', Constraints.Select(c => c.ToString()))}}}", + { 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(':', Constraints.Select(c => c.ToString()))}?}}", + { 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(':', Constraints.Select(c => c.ToString()))}?}}", + { IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => $"{{*{Value}:{string.Join(':', (object[])Constraints)}?}}", { IsParameter: false } => Value, _ => throw new InvalidOperationException("Invalid template segment.") }; diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index 3e78185899d4..bbef687c3157 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -1,16 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.AspNetCore.Components.Test.Helpers; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Test.Helpers; using Xunit; -namespace Microsoft.AspNetCore.Components.Test +namespace Microsoft.AspNetCore.Components { public class CascadingParameterStateTest { diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 8e2fb5d60024..aad5631b7baf 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -357,8 +357,8 @@ public void SettingCaptureUnmatchedValuesParameterExplicitlyAndImplicitly_Revers Assert.Equal( $"The property '{nameof(HasCaptureUnmatchedValuesProperty.CaptureUnmatchedValues)}' on component type '{typeof(HasCaptureUnmatchedValuesProperty).FullName}' cannot be set explicitly when " + $"also used to capture unmatched values. Unmatched values:" + Environment.NewLine + - $"test1" + Environment.NewLine + - $"test2", + $"test2" + Environment.NewLine + + $"test1", ex.Message); } diff --git a/src/Components/Components/test/Routing/RouteKeyTest.cs b/src/Components/Components/test/Routing/RouteKeyTest.cs new file mode 100644 index 000000000000..dce296826724 --- /dev/null +++ b/src/Components/Components/test/Routing/RouteKeyTest.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Components.Routing +{ + public class RouteKeyTest + { + [Fact] + public void RouteKey_Default_Equality() + { + // Arrange + var key1 = default(RouteKey); + var key2 = default(RouteKey); + + // Act & Assert + Assert.Equal(key1.GetHashCode(), key2.GetHashCode()); + Assert.True(key1.Equals(key2)); + } + + [Fact] + public void RouteKey_WithNoAdditionalAssemblies_Equality() + { + // Arrange + var key1 = new RouteKey(typeof(string).Assembly, null); + var key2 = new RouteKey(typeof(string).Assembly, null); + + // Act & Assert + Assert.Equal(key1.GetHashCode(), key2.GetHashCode()); + Assert.True(key1.Equals(key2)); + } + + [Fact] + public void RouteKey_WithNoAdditionalAssemblies_DifferentAssemblies() + { + // Arrange + var key1 = new RouteKey(typeof(string).Assembly, null); + var key2 = new RouteKey(typeof(ComponentBase).Assembly, null); + + // Act & Assert + Assert.NotEqual(key1.GetHashCode(), key2.GetHashCode()); + Assert.False(key1.Equals(key2)); + } + + [Fact] + public void RouteKey_DefaultAgainstNonDefault() + { + // Arrange + var key1 = default(RouteKey); + var key2 = new RouteKey(typeof(string).Assembly, null); + + // Act & Assert + Assert.NotEqual(key1.GetHashCode(), key2.GetHashCode()); + Assert.False(key1.Equals(key2)); + } + + [Fact] + public void RouteKey_WithAdditionalAssemblies() + { + // Arrange + var key1 = new RouteKey(typeof(string).Assembly, new[] { typeof(ComponentBase).Assembly, GetType().Assembly }); + var key2 = new RouteKey(typeof(string).Assembly, new[] { typeof(ComponentBase).Assembly, GetType().Assembly }); + + // Act & Assert + Assert.Equal(key1.GetHashCode(), key2.GetHashCode()); + Assert.True(key1.Equals(key2)); + } + + [Fact] + public void RouteKey_WithAdditionalAssemblies_DifferentOrder() + { + // Arrange + var key1 = new RouteKey(typeof(string).Assembly, new[] { typeof(ComponentBase).Assembly, GetType().Assembly }); + var key2 = new RouteKey(typeof(string).Assembly, new[] { GetType().Assembly, typeof(ComponentBase).Assembly }); + + // Act & Assert + Assert.Equal(key1.GetHashCode(), key2.GetHashCode()); + Assert.True(key1.Equals(key2)); + } + + [Fact] + public void RouteKey_WithAdditionalAssemblies_DifferentAppAssemblies() + { + // Arrange + var key1 = new RouteKey(typeof(string).Assembly, new[] { GetType().Assembly }); + var key2 = new RouteKey(typeof(ComponentBase).Assembly, new[] { GetType().Assembly, }); + + // Act & Assert + Assert.NotEqual(key1.GetHashCode(), key2.GetHashCode()); + Assert.False(key1.Equals(key2)); + } + + [Fact] + public void RouteKey_WithAdditionalAssemblies_DifferentAdditionalAssemblies() + { + // Arrange + var key1 = new RouteKey(typeof(ComponentBase).Assembly, new[] { typeof(object).Assembly }); + var key2 = new RouteKey(typeof(ComponentBase).Assembly, new[] { GetType().Assembly, }); + + // Act & Assert + Assert.Equal(key1.GetHashCode(), key2.GetHashCode()); + Assert.False(key1.Equals(key2)); + } + } +} diff --git a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs index 5e9636684ee9..29b1fd1ddd8f 100644 --- a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs +++ b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs @@ -5,10 +5,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using Microsoft.AspNetCore.Components.Routing; using Xunit; -namespace Microsoft.AspNetCore.Components.Test.Routing +namespace Microsoft.AspNetCore.Components.Routing { public class RouteTableFactoryTests { @@ -16,10 +15,10 @@ public class RouteTableFactoryTests public void CanCacheRouteTable() { // Arrange - var routes1 = RouteTableFactory.Create(new[] { GetType().Assembly, }); + var routes1 = RouteTableFactory.Create(new RouteKey(GetType().Assembly, null)); // Act - var routes2 = RouteTableFactory.Create(new[] { GetType().Assembly, }); + var routes2 = RouteTableFactory.Create(new RouteKey(GetType().Assembly, null)); // Assert Assert.Same(routes1, routes2); @@ -29,10 +28,10 @@ public void CanCacheRouteTable() public void CanCacheRouteTableWithDifferentAssembliesAndOrder() { // Arrange - var routes1 = RouteTableFactory.Create(new[] { typeof(object).Assembly, GetType().Assembly, }); + var routes1 = RouteTableFactory.Create(new RouteKey(typeof(object).Assembly, new[] { typeof(ComponentBase).Assembly, GetType().Assembly, })); // Act - var routes2 = RouteTableFactory.Create(new[] { GetType().Assembly, typeof(object).Assembly, }); + var routes2 = RouteTableFactory.Create(new RouteKey(typeof(object).Assembly, new[] { GetType().Assembly, typeof(ComponentBase).Assembly, })); // Assert Assert.Same(routes1, routes2); @@ -42,10 +41,10 @@ public void CanCacheRouteTableWithDifferentAssembliesAndOrder() public void DoesNotCacheRouteTableForDifferentAssemblies() { // Arrange - var routes1 = RouteTableFactory.Create(new[] { GetType().Assembly, }); + var routes1 = RouteTableFactory.Create(new RouteKey(GetType().Assembly, null)); // Act - var routes2 = RouteTableFactory.Create(new[] { GetType().Assembly, typeof(object).Assembly, }); + var routes2 = RouteTableFactory.Create(new RouteKey(GetType().Assembly, new[] { typeof(object).Assembly })); // Assert Assert.NotSame(routes1, routes2); @@ -977,7 +976,8 @@ public void SuppliesNullForUnusedHandlerParameters() routeTable.Route(context); // Assert - Assert.Collection(routeTable.Routes, + Assert.Collection( + routeTable.Routes, route => { Assert.Same(typeof(TestHandler1), route.Handler); @@ -994,13 +994,13 @@ public void SuppliesNullForUnusedHandlerParameters() { Assert.Same(typeof(TestHandler1), route.Handler); Assert.Equal("products/{param2}/{PaRam1}", route.Template.TemplateText); - Assert.Equal(Array.Empty(), route.UnusedRouteParameterNames.OrderBy(id => id).ToArray()); + Assert.Null(route.UnusedRouteParameterNames); }, route => { Assert.Same(typeof(TestHandler2), route.Handler); Assert.Equal("{unrelated}", route.Template.TemplateText); - Assert.Equal(Array.Empty(), route.UnusedRouteParameterNames.OrderBy(id => id).ToArray()); + Assert.Null(route.UnusedRouteParameterNames); }); Assert.Same(typeof(TestHandler1), context.Handler); diff --git a/src/Components/Components/test/Routing/RouterTest.cs b/src/Components/Components/test/Routing/RouterTest.cs index 3d5ee09f465a..e139d042bedc 100644 --- a/src/Components/Components/test/Routing/RouterTest.cs +++ b/src/Components/Components/test/Routing/RouterTest.cs @@ -8,14 +8,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Components.Test.Routing +namespace Microsoft.AspNetCore.Components.Routing { public class RouterTest { diff --git a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs index 258d4cb56ea3..a88060552c8c 100644 --- a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs +++ b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs @@ -6,8 +6,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Components.Forms { @@ -67,7 +67,10 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs) Validator.TryValidateProperty(propertyValue, validationContext, results); _messages.Clear(fieldIdentifier); - _messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage!)); + foreach (var result in CollectionsMarshal.AsSpan(results)) + { + _messages.Add(fieldIdentifier, result.ErrorMessage!); + } // We have to notify even if there were no messages before and are still no messages now, // because the "state" that changed might be the completion of some async validation task @@ -90,15 +93,16 @@ private void OnValidationRequested(object? sender, ValidationRequestedEventArgs continue; } - if (!validationResult.MemberNames.Any()) + var hasMemberNames = false; + foreach (var memberName in validationResult.MemberNames) { - _messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!); - continue; + hasMemberNames = true; + _messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!); } - foreach (var memberName in validationResult.MemberNames) + if (!hasMemberNames) { - _messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!); + _messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!); } } diff --git a/src/Components/Forms/src/ValidationMessageStore.cs b/src/Components/Forms/src/ValidationMessageStore.cs index 678765b3066f..d330c18b9e35 100644 --- a/src/Components/Forms/src/ValidationMessageStore.cs +++ b/src/Components/Forms/src/ValidationMessageStore.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; namespace Microsoft.AspNetCore.Components.Forms @@ -65,7 +64,7 @@ public void Add(Expression> accessor, IEnumerable messages) /// The identifier for the field. /// The validation messages for the specified field within this . public IEnumerable this[FieldIdentifier fieldIdentifier] - => _messages.TryGetValue(fieldIdentifier, out var messages) ? messages : Enumerable.Empty(); + => _messages.TryGetValue(fieldIdentifier, out var messages) ? messages : Array.Empty(); /// /// Gets the validation messages within this for the specified field. diff --git a/src/Components/Shared/src/ComponentParametersTypeCache.cs b/src/Components/Shared/src/ComponentParametersTypeCache.cs index 269ecbb45f30..98db6500d89c 100644 --- a/src/Components/Shared/src/ComponentParametersTypeCache.cs +++ b/src/Components/Shared/src/ComponentParametersTypeCache.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; namespace Microsoft.AspNetCore.Components @@ -29,8 +28,16 @@ internal class ComponentParametersTypeCache [RequiresUnreferencedCode("This type attempts to load component parameters that may be trimmed.")] private static Type? ResolveType(Key key, Assembly[] assemblies) { - var assembly = assemblies - .FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal)); + Assembly? assembly = null; + for (var i = 0; i < assemblies.Length; i++) + { + var current = assemblies[i]; + if (current.GetName().Name == key.Assembly) + { + assembly = current; + break; + } + } if (assembly == null) { diff --git a/src/Components/Shared/src/RootComponentTypeCache.cs b/src/Components/Shared/src/RootComponentTypeCache.cs index e4259f76974c..55f2ab7136ac 100644 --- a/src/Components/Shared/src/RootComponentTypeCache.cs +++ b/src/Components/Shared/src/RootComponentTypeCache.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; namespace Microsoft.AspNetCore.Components @@ -29,8 +27,16 @@ internal class RootComponentTypeCache private static Type? ResolveType(Key key, Assembly[] assemblies) { - var assembly = assemblies - .FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal)); + Assembly? assembly = null; + for (var i = 0; i < assemblies.Length; i++) + { + var current = assemblies[i]; + if (current.GetName().Name == key.Assembly) + { + assembly = current; + break; + } + } if (assembly == null) { diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index e53c30c0830a..a2c7f8c0777c 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -23,7 +23,7 @@ public sealed class WebAssemblyHost : IAsyncDisposable private readonly IServiceScope _scope; private readonly IServiceProvider _services; private readonly IConfiguration _configuration; - private readonly RootComponentMapping[] _rootComponents; + private readonly RootComponentMappingCollection _rootComponents; private readonly string? _persistedState; // NOTE: the host is disposable because it OWNs references to disposable things. @@ -43,7 +43,7 @@ internal WebAssemblyHost( IServiceProvider services, IServiceScope scope, IConfiguration configuration, - RootComponentMapping[] rootComponents, + RootComponentMappingCollection rootComponents, string? persistedState) { // To ensure JS-invoked methods don't get linked out, have a reference to their enclosing types @@ -165,9 +165,8 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl try { - for (var i = 0; i < rootComponents.Length; i++) + foreach (var rootComponent in rootComponents) { - var rootComponent = rootComponents[i]; await renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector, rootComponent.Parameters); } diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 8aade0bea771..c44ac52867e9 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Linq; using Microsoft.AspNetCore.Components.Lifetime; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Routing; @@ -36,7 +35,8 @@ public sealed class WebAssemblyHostBuilder /// /// The argument passed to the application's main method. /// A . - [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(JSInteropMethods))] + [DynamicDependency(nameof(JSInteropMethods.NotifyLocationChanged), typeof(JSInteropMethods))] + [DynamicDependency(nameof(JSInteropMethods.DispatchEvent), typeof(JSInteropMethods))] [DynamicDependency(JsonSerialized, typeof(WebEventDescriptor))] public static WebAssemblyHostBuilder CreateDefault(string[]? args = default) { @@ -236,7 +236,7 @@ public WebAssemblyHost Build() var services = _createServiceProvider(); var scope = services.GetRequiredService().CreateScope(); - return new WebAssemblyHost(services, scope, Configuration, RootComponents.ToArray(), _persistedState); + return new WebAssemblyHost(services, scope, Configuration, RootComponents, _persistedState); } internal void InitializeDefaultServices() diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs index 485a95502d6c..f28f94e16bdc 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs @@ -27,12 +27,12 @@ public class WebAssemblyHostConfiguration : IConfiguration, IConfigurationRoot, /// /// Gets the sources used to obtain configuration values. /// - IList IConfigurationBuilder.Sources => new ReadOnlyCollection(_sources.ToList()); + IList IConfigurationBuilder.Sources => new ReadOnlyCollection(_sources.ToArray()); /// /// Gets the providers used to obtain configuration values. /// - IEnumerable IConfigurationRoot.Providers => new ReadOnlyCollection(_providers.ToList()); + IEnumerable IConfigurationRoot.Providers => new ReadOnlyCollection(_providers.ToArray()); /// /// Gets a key/value collection that can be used to share data between the @@ -94,11 +94,22 @@ public string? this[string key] /// The configuration sub-sections. IEnumerable IConfiguration.GetChildren() { - return _providers - .SelectMany(s => s.GetChildKeys(Enumerable.Empty(), null)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(key => this.GetSection(key)) - .ToList(); + var hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + foreach (var provider in _providers) + { + foreach (var child in provider.GetChildKeys(Enumerable.Empty(), parentPath: null)) + { + if (!hashSet.Add(child)) + { + continue; + } + + result.Add(GetSection(child)); + } + } + + return result; } /// diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs b/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs index 2dcfa972fdc9..de60b977e39b 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; @@ -25,7 +24,7 @@ public sealed class LazyAssemblyLoader internal const string ReadLazyPDBs = "window.Blazor._internal.readLazyPdbs"; private readonly IJSRuntime _jsRuntime; - private readonly HashSet _loadedAssemblyCache; + private HashSet? _loadedAssemblyCache; /// /// Initializes a new instance of . @@ -34,7 +33,6 @@ public sealed class LazyAssemblyLoader public LazyAssemblyLoader(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; - _loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name + ".dll").ToHashSet(); } /// @@ -78,25 +76,49 @@ private Task> LoadAssembliesInServerAsync(IEnumerable> LoadAssembliesInClientAsync(IEnumerable assembliesToLoad) { + if (_loadedAssemblyCache is null) + { + var loadedAssemblyCache = new HashSet(StringComparer.Ordinal); + var appDomainAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + for (var i = 0; i < appDomainAssemblies.Length; i++) + { + var assembly = appDomainAssemblies[i]; + loadedAssemblyCache.Add(assembly.GetName().Name + ".dll"); + } + + _loadedAssemblyCache = loadedAssemblyCache; + } + // Check to see if the assembly has already been loaded and avoids reloading it if so. // Note: in the future, as an extra precuation, we can call `Assembly.Load` and check // to see if it throws FileNotFound to ensure that an assembly hasn't been loaded // between when the cache of loaded assemblies was instantiated in the constructor // and the invocation of this method. - var newAssembliesToLoad = assembliesToLoad.Where(assembly => !_loadedAssemblyCache.Contains(assembly)); - var loadedAssemblies = new List(); + var newAssembliesToLoad = new List(); + foreach (var assemblyToLoad in assembliesToLoad) + { + if (!_loadedAssemblyCache.Contains(assemblyToLoad)) + { + newAssembliesToLoad.Add(assemblyToLoad); + } + } - var jsRuntime = (IJSUnmarshalledRuntime)_jsRuntime; + if (newAssembliesToLoad.Count == 0) + { + return Array.Empty(); + } + var jsRuntime = (IJSUnmarshalledRuntime)_jsRuntime; var count = (int)await jsRuntime.InvokeUnmarshalled>( GetLazyAssemblies, newAssembliesToLoad.ToArray()); if (count == 0) { - return loadedAssemblies; + return Array.Empty(); } + var loadedAssemblies = new List(); var assemblies = jsRuntime.InvokeUnmarshalled(ReadLazyAssemblies); var pdbs = jsRuntime.InvokeUnmarshalled(ReadLazyPDBs); diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index abe9aa4e12ec..ad96e5e9c97f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Text; @@ -325,14 +324,16 @@ private static (MethodInfo methodInfo, Type[] parameterTypes) GetCachedMethodInf static Dictionary ScanTypeForCallableMethods(Type type) { var result = new Dictionary(StringComparer.Ordinal); - var invokableMethods = type - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(method => !method.ContainsGenericParameters && method.IsDefined(typeof(JSInvokableAttribute), inherit: false)); - foreach (var method in invokableMethods) + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public)) { + if (method.ContainsGenericParameters || !method.IsDefined(typeof(JSInvokableAttribute), inherit: false)) + { + continue; + } + var identifier = method.GetCustomAttribute(false)!.Identifier ?? method.Name!; - var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray(); + var parameterTypes = GetParameterTypes(method); if (result.ContainsKey(identifier)) { @@ -356,29 +357,51 @@ private static (MethodInfo methodInfo, Type[] parameterTypes) GetCachedMethodInf // TODO: Consider looking first for assembly-level attributes (i.e., if there are any, // only use those) to avoid scanning, especially for framework assemblies. var result = new Dictionary(StringComparer.Ordinal); - var invokableMethods = GetRequiredLoadedAssembly(assemblyKey) - .GetExportedTypes() - .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Static)) - .Where(method => !method.ContainsGenericParameters && method.IsDefined(typeof(JSInvokableAttribute), inherit: false)); - foreach (var method in invokableMethods) + var exportedTypes = GetRequiredLoadedAssembly(assemblyKey).GetExportedTypes(); + foreach (var type in exportedTypes) { - var identifier = method.GetCustomAttribute(false)!.Identifier ?? method.Name; - var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray(); - - if (result.ContainsKey(identifier)) + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) { - throw new InvalidOperationException($"The assembly '{assemblyKey.AssemblyName}' contains more than one " + - $"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " + - $"assembly must have different identifiers. You can pass a custom identifier as a parameter to " + - $"the [JSInvokable] attribute."); - } + if (method.ContainsGenericParameters || !method.IsDefined(typeof(JSInvokableAttribute), inherit: false)) + { + continue; + } - result.Add(identifier, (method, parameterTypes)); + var identifier = method.GetCustomAttribute(false)!.Identifier ?? method.Name; + var parameterTypes = GetParameterTypes(method); + + if (result.ContainsKey(identifier)) + { + throw new InvalidOperationException($"The assembly '{assemblyKey.AssemblyName}' contains more than one " + + $"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " + + $"assembly must have different identifiers. You can pass a custom identifier as a parameter to " + + $"the [JSInvokable] attribute."); + } + + result.Add(identifier, (method, parameterTypes)); + } } return result; } + private static Type[] GetParameterTypes(MethodInfo method) + { + var parameters = method.GetParameters(); + if (parameters.Length == 0) + { + return Type.EmptyTypes; + } + + var parameterTypes = new Type[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return parameterTypes; + } + private static Assembly GetRequiredLoadedAssembly(AssemblyKey assemblyKey) { // We don't want to load assemblies on demand here, because we don't necessarily trust diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml index 5ccb6bc9c3d4..cf97b36eddfb 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml @@ -7,6 +7,12 @@ member M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.ParseArguments(Microsoft.JSInterop.JSRuntime,System.String,System.String,System.Type[]) + + ILLink + IL2065 + member + M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.ScanAssemblyForCallableMethods(Microsoft.JSInterop.Infrastructure.DotNetDispatcher.AssemblyKey) + ILLink IL2091 From 40854f892ea2fbd0d927145074f99a0bc984a0ff Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 22 Apr 2021 16:01:17 -0700 Subject: [PATCH 2/5] Undo excessive optimization --- .../src/Reflection/MemberAssignment.cs | 82 ++++--------------- 1 file changed, 15 insertions(+), 67 deletions(-) diff --git a/src/Components/Components/src/Reflection/MemberAssignment.cs b/src/Components/Components/src/Reflection/MemberAssignment.cs index 5a3f58a67b8a..c9b2b8d5269f 100644 --- a/src/Components/Components/src/Reflection/MemberAssignment.cs +++ b/src/Components/Components/src/Reflection/MemberAssignment.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; @@ -13,7 +12,7 @@ namespace Microsoft.AspNetCore.Components.Reflection { internal class MemberAssignment { - public static PropertyEnumerable GetPropertiesIncludingInherited( + public static IEnumerable GetPropertiesIncludingInherited( [DynamicallyAccessedMembers(Component)] Type type, BindingFlags bindingFlags) { @@ -49,7 +48,20 @@ public static PropertyEnumerable GetPropertiesIncludingInherited( currentType = currentType.BaseType; } - return new PropertyEnumerable(dictionary); + foreach (var item in dictionary) + { + if (item.Value is PropertyInfo property) + { + yield return property; + } + var list = (List)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) @@ -70,69 +82,5 @@ private static bool IsInheritedProperty(PropertyInfo property, object others) return false; } - - public ref struct PropertyEnumerable - { - private readonly PropertyEnumerator _enumerator; - - public PropertyEnumerable(Dictionary dictionary) - { - _enumerator = new PropertyEnumerator(dictionary); - } - - public PropertyEnumerator GetEnumerator() => _enumerator; - } - - public ref struct PropertyEnumerator - { - // Do NOT make this readonly, or MoveNext will not work - private Dictionary.Enumerator _dictionaryEnumerator; - private Span.Enumerator _spanEnumerator; - - public PropertyEnumerator(Dictionary dictionary) - { - _dictionaryEnumerator = dictionary.GetEnumerator(); - _spanEnumerator = Span.Empty.GetEnumerator(); - } - - public PropertyInfo Current - { - get - { - if (_dictionaryEnumerator.Current.Value is PropertyInfo property) - { - return property; - } - - return _spanEnumerator.Current; - } - } - - public bool MoveNext() - { - if (_spanEnumerator.MoveNext()) - { - return true; - } - - if (!_dictionaryEnumerator.MoveNext()) - { - return false; - } - - var oneOrMoreProperties = _dictionaryEnumerator.Current.Value; - if (oneOrMoreProperties is PropertyInfo) - { - _spanEnumerator = Span.Empty.GetEnumerator(); - return true; - } - - var many = (List)oneOrMoreProperties; - _spanEnumerator = CollectionsMarshal.AsSpan(many).GetEnumerator(); - var moveNext = _spanEnumerator.MoveNext(); - Debug.Assert(moveNext, "We expect this to at least have one item."); - return moveNext; - } - } } } From 1a190dad332017e4e91765b22cd53936807e6e8a Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 23 Apr 2021 08:48:25 -0700 Subject: [PATCH 3/5] Update src/Components/Components/src/Reflection/ComponentProperties.cs --- src/Components/Components/src/Reflection/ComponentProperties.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index 57a9dc32912d..25dbac4041fc 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -220,7 +220,6 @@ private static void ThrowForCaptureUnmatchedValuesConflict(Type targetType, stri [DoesNotReturn] private static void ThrowForMultipleCaptureUnmatchedValuesParameters([DynamicallyAccessedMembers(Component)] Type targetType) { - // We don't care about perf here, we want to report an accurate and useful error. var propertyNames = new List(); foreach (var property in targetType.GetProperties(_bindablePropertyFlags)) { From ed24f1f3da760d90a4f4c3a947425db6db7a34be Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 23 Apr 2021 12:09:01 -0700 Subject: [PATCH 4/5] Fixup --- src/Components/Components/src/Reflection/ComponentProperties.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index 25dbac4041fc..bab5d9d62957 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -159,7 +159,7 @@ static void SetProperty(object target, PropertySetter writer, string parameterNa } } - internal static MemberAssignment.PropertyEnumerable GetCandidateBindableProperties([DynamicallyAccessedMembers(Component)] Type targetType) + internal static IEnumerable GetCandidateBindableProperties([DynamicallyAccessedMembers(Component)] Type targetType) => MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags); [DoesNotReturn] From 7f6d84c60dd44f68d7ce918cc38232331f5a6ffc Mon Sep 17 00:00:00 2001 From: Pranav K Date: Sat, 24 Apr 2021 07:57:05 -0700 Subject: [PATCH 5/5] fixup --- .../Microsoft.AspNetCore.Components.WarningSuppressions.xml | 6 ++++++ .../Components/src/Reflection/MemberAssignment.cs | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml index f0527f34ccac..7a1510c2300d 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml @@ -67,5 +67,11 @@ member M:Microsoft.AspNetCore.Components.LayoutView.<>c__DisplayClass13_0.<WrapInLayout>g__Render|0(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder) + + ILLink + IL2080 + member + M:Microsoft.AspNetCore.Components.Reflection.MemberAssignment.<GetPropertiesIncludingInherited>d__0.MoveNext + \ No newline at end of file diff --git a/src/Components/Components/src/Reflection/MemberAssignment.cs b/src/Components/Components/src/Reflection/MemberAssignment.cs index c9b2b8d5269f..03878aafd56c 100644 --- a/src/Components/Components/src/Reflection/MemberAssignment.cs +++ b/src/Components/Components/src/Reflection/MemberAssignment.cs @@ -53,9 +53,10 @@ public static IEnumerable GetPropertiesIncludingInherited( if (item.Value is PropertyInfo property) { yield return property; + continue; } - var list = (List)item.Value; + var list = (List)item.Value; var count = list.Count; for (var i = 0; i < count; i++) {