diff --git a/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs b/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs index 2b95051bf4..0e58553feb 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs @@ -125,7 +125,7 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo // A node carries a single state property in practice; use FirstOrDefault rather than // SingleOrDefault so a malformed producer can't throw out of the consumer pump. - TestNodeStateProperty? state = update.TestNode.Properties.OfType().FirstOrDefault(); + TestNodeStateProperty? state = update.TestNode.Properties.FirstOrDefault(); if (state is null) { return Task.CompletedTask; @@ -477,7 +477,7 @@ private static bool OverlapsAnyFailedWindow(VideoSegment segment, double[] faile private (DateTimeOffset Start, DateTimeOffset End) ResolveTiming(TestNodeUpdateMessage update, string testUid) { - TimingProperty? timing = update.TestNode.Properties.OfType().FirstOrDefault(); + TimingProperty? timing = update.TestNode.Properties.FirstOrDefault(); if (timing is not null) { return (timing.GlobalTiming.StartTime, timing.GlobalTiming.EndTime); diff --git a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs index 2027f78d76..6f5caa7ba0 100644 --- a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs +++ b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs @@ -213,6 +213,44 @@ public bool Any() return found; } + /// + /// Returns the first property of the type, or if none is found. + /// Unlike , this method does not throw when multiple properties of the + /// same type are present — it simply returns the first one encountered. + /// + /// The type of the property. + /// The first property of the given type, or if none is found. + public TProperty? FirstOrDefault() + where TProperty : IProperty + { + if (_testNodeStateProperty is TProperty testNodeStateProperty) + { + return testNodeStateProperty; + } + + // We don't want to walk the linked list if we know that we're looking for a TestNodeStateProperty. + if (typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty))) + { + return default; + } + + // Direct linked-list walk: avoids the array allocation from OfType() and the subsequent + // LINQ FirstOrDefault() call. Early-returns on the first match so no duplicate tracking + // is needed (unlike SingleOrDefault()). + Property? current = _property; + while (current is not null) + { + if (current.Current is TProperty match) + { + return match; + } + + current = current.Next; + } + + return default; + } + /// /// Returns the only property of the type, and throws an exception if there is not exactly one element. /// diff --git a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt index 709927ceb1..6385a4e1fd 100644 --- a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable [TPEXP]Microsoft.Testing.Platform.Extensions.IBlockingDataConsumer +Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault() -> TProperty? diff --git a/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110..4017897f58 100644 --- a/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault() -> TProperty?