From 4a4b97b016f0f63a172e90930e76ca83b739fbae Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 28 May 2026 21:06:03 +0100 Subject: [PATCH] perf: single-pass property data-source attribute scans Replace GetCustomAttributes() + OfType/Any/FirstOrDefault LINQ chains with a single-pass foreach that returns the first IDataSourceAttribute. - ReflectionInstanceFactory: drops the OfType iterator + array allocation per property; common case (no data source) now allocates nothing extra. - StaticPropertyReflectionInitializer: collapses the double attribute-array materialisation (filter + fetch) into one scan per property. Skips ConstructorHelper.cs (#6056, separate open PR). --- .../StaticPropertyReflectionInitializer.cs | 40 ++++++++++++------- .../Discovery/ReflectionInstanceFactory.cs | 26 ++++++++---- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/TUnit.Core/StaticPropertyReflectionInitializer.cs b/TUnit.Core/StaticPropertyReflectionInitializer.cs index 8ba0f9e1de..07c3ba3978 100644 --- a/TUnit.Core/StaticPropertyReflectionInitializer.cs +++ b/TUnit.Core/StaticPropertyReflectionInitializer.cs @@ -60,15 +60,26 @@ public static async Task InitializeStaticPropertiesForType(Type type) return; } - // Get all static properties with data source attributes - var staticProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Static) - .Where(p => p.CanWrite && HasDataSourceAttribute(p)); + var staticProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Static); foreach (var property in staticProperties) { + if (!property.CanWrite) + { + continue; + } + + // Single-pass lookup avoids materialising the attribute array twice + // (once to filter, once to fetch) and skips properties with no data source. + var dataSourceAttr = GetFirstDataSourceAttribute(property); + if (dataSourceAttr is null) + { + continue; + } + try { - await InitializeStaticProperty(type, property); + await InitializeStaticProperty(property, dataSourceAttr); } catch (Exception ex) { @@ -78,23 +89,24 @@ public static async Task InitializeStaticPropertiesForType(Type type) } } - private static bool HasDataSourceAttribute(PropertyInfo property) + private static IDataSourceAttribute? GetFirstDataSourceAttribute(PropertyInfo property) { - return property.GetCustomAttributes() - .Any(attr => attr is IDataSourceAttribute); + foreach (var attribute in property.GetCustomAttributes()) + { + if (attribute is IDataSourceAttribute dataSource) + { + return dataSource; + } + } + + return null; } #if NET8_0_OR_GREATER [RequiresUnreferencedCode("Data source initialization may require dynamic code generation")] #endif - private static async Task InitializeStaticProperty(Type type, PropertyInfo property) + private static async Task InitializeStaticProperty(PropertyInfo property, IDataSourceAttribute dataSourceAttr) { - if (property.GetCustomAttributes() - .FirstOrDefault(attr => attr is IDataSourceAttribute) is not IDataSourceAttribute dataSourceAttr) - { - return; - } - // Create metadata for the data source var metadata = new DataGeneratorMetadata { diff --git a/TUnit.Engine/Discovery/ReflectionInstanceFactory.cs b/TUnit.Engine/Discovery/ReflectionInstanceFactory.cs index 41ac6a77f8..d4725a73a8 100644 --- a/TUnit.Engine/Discovery/ReflectionInstanceFactory.cs +++ b/TUnit.Engine/Discovery/ReflectionInstanceFactory.cs @@ -62,19 +62,16 @@ private static async Task InjectPropertiesAsync(object instance, Type type) continue; } - // Look for data source attributes on the property - var dataSourceAttrs = property.GetCustomAttributes() - .OfType() - .ToArray(); + // Look for the first data source attribute on the property. + // Single-pass avoids allocating the OfType iterator + array for the + // common case of properties without a data source attribute. + var dataSource = GetFirstDataSourceAttribute(property); - if (dataSourceAttrs.Length == 0) + if (dataSource is null) { continue; } - // Try to get data from the first data source - var dataSource = dataSourceAttrs[0]; - try { var metadata = CreatePropertyMetadata(property, type, dataSource); @@ -116,6 +113,19 @@ private static async Task InjectPropertiesAsync(object instance, Type type) } } + private static IDataSourceAttribute? GetFirstDataSourceAttribute(PropertyInfo property) + { + foreach (var attribute in property.GetCustomAttributes()) + { + if (attribute is IDataSourceAttribute dataSource) + { + return dataSource; + } + } + + return null; + } + private static DataGeneratorMetadata CreatePropertyMetadata(PropertyInfo property, Type containingType, IDataSourceAttribute dataSource) { var propertyMetadata = new PropertyMetadata