diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingConcurrencyTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingConcurrencyTests.cs new file mode 100644 index 000000000..17b96ee76 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingConcurrencyTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Neo4j.Driver.Internal.Mapping; +using Xunit; +using Xunit.Abstractions; + +namespace Neo4j.Driver.Tests.Mapping; + +public class MappingConcurrencyTests(ITestOutputHelper testOutputHelper) +{ + private interface ITestTask + { + Task Start(); + } + + private class TestTask : ITestTask + { + public Task Start() + { + return Task.Run( + () => + { + for (var i = 0; i < 50; i++) + { + DefaultMapper.Get(); + DefaultMapper.Reset(); + } + }); + } + } + + private record DummyType1(string Name, int Age); + private record DummyType2(string Name, int Age); + private record DummyType3(string Name, int Age); + private record DummyType4(string Name, int Age); + + [Fact] + public async void DefaultMapperShouldBeThreadSafe() + { + List threads = + [ + new TestTask(), + new TestTask(), + new TestTask(), + new TestTask() + ]; + + // wait for all threads to finish + await Task.WhenAll(threads.Select(t => t.Start())); + + testOutputHelper.WriteLine("All threads finished."); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DefaultMapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DefaultMapper.cs index 5ac8b7233..775d70bc4 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DefaultMapper.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DefaultMapper.cs @@ -14,6 +14,7 @@ // limitations under the License. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; using Neo4j.Driver.Mapping; @@ -22,7 +23,7 @@ namespace Neo4j.Driver.Internal.Mapping; internal static class DefaultMapper { - private static readonly Dictionary Mappers = new(); + private static readonly ConcurrentDictionary Mappers = new(); public static void Reset() { @@ -75,7 +76,7 @@ public static IRecordMapper Get(HashSet mappedSetters = null) mapper = mappingBuilder.Build(); // cache the mapper for future use - Mappers[type] = mapper; + Mappers.TryAdd(type, mapper); return (IRecordMapper)mapper; }