diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index 6e5c3223ca..d887c797b4 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -30,6 +30,7 @@ Code imported from https://github.com/elaberge/Metsys.Bson without any changes using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -695,7 +696,7 @@ namespace Metsys.Bson [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = "Bson uses reflection")] internal class TypeHelper { - private static readonly IDictionary _cachedTypeLookup = new Dictionary(); + private static readonly ConcurrentDictionary _cachedTypeLookup = new ConcurrentDictionary(); private static readonly BsonConfiguration _configuration = BsonConfiguration.Instance; private readonly IDictionary _properties; @@ -725,13 +726,7 @@ namespace Metsys.Bson public static TypeHelper GetHelperForType(Type type) { - TypeHelper helper; - if (!_cachedTypeLookup.TryGetValue(type, out helper)) - { - helper = new TypeHelper(type); - _cachedTypeLookup[type] = helper; - } - return helper; + return _cachedTypeLookup.GetOrAdd(type, t => new TypeHelper(t)); } public static string FindProperty(LambdaExpression lambdaExpression) diff --git a/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs index 06011b13a6..d5f8fa9030 100644 --- a/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs +++ b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs @@ -162,6 +162,48 @@ namespace Avalonia.DesignerSupport.Tests } + [Fact] + [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Sync context is explicitly disabled")] + void BsonSerializationIsThreadSafe() + { + Init(); + // This test verifies that concurrent serialization doesn't cause infinite loops + // or corruption in the TypeHelper cache + var messages = Enumerable.Range(0, 100).Select(i => new MeasureViewportMessage + { + Width = i, + Height = i * 2 + }).ToArray(); + + var tasks = new List(); + var exceptions = new ConcurrentBag(); + + // Spawn multiple threads that all try to serialize messages concurrently + for (int i = 0; i < 10; i++) + { + var task = Task.Run(() => + { + try + { + foreach (var message in messages) + { + _client.Send(message).Wait(TimeoutInMs); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + tasks.Add(task); + } + + Task.WaitAll(tasks.ToArray(), TimeoutInMs * messages.Length * 10); + + // Verify no exceptions occurred + Assert.Empty(exceptions); + } + public void Dispose() { _disposables.ForEach(d => d.Dispose());