Skip to content

Commit bb57026

Browse files
Fix concurrency error with MappingExtensions.AsType()
1 parent 337bf18 commit bb57026

File tree

4 files changed

+77
-15
lines changed

4 files changed

+77
-15
lines changed

Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingConcurrencyTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using System;
1617
using System.Collections.Generic;
1718
using System.Linq;
19+
using System.Reflection;
20+
using System.Reflection.Emit;
21+
using System.Threading;
1822
using System.Threading.Tasks;
23+
using Neo4j.Driver.Internal;
1924
using Neo4j.Driver.Internal.Mapping;
2025
using Xunit;
2126
using Xunit.Abstractions;
@@ -66,4 +71,65 @@ public async void DefaultMapperShouldBeThreadSafe()
6671

6772
testOutputHelper.WriteLine("All threads finished.");
6873
}
74+
75+
[Fact]
76+
public async Task MapMethods_ShouldBeThreadSafe()
77+
{
78+
var testObjects = CreateMultiTypedArray(100);
79+
80+
const int numberOfThreads = 4;
81+
var tasks = new List<Task>(numberOfThreads);
82+
var resetEvent = new ManualResetEventSlim(false);
83+
84+
for (var i = 0; i < numberOfThreads; i++)
85+
{
86+
tasks.Add(
87+
Task.Run(() =>
88+
{
89+
resetEvent.Wait(); // Wait for the signal to start
90+
for (var j = 0; j < 100; j++)
91+
{
92+
foreach (var obj in testObjects)
93+
{
94+
obj.AsType(obj.GetType());
95+
}
96+
97+
MappingExtensions.ResetAsMethods();
98+
}
99+
}));
100+
}
101+
102+
resetEvent.Set(); // Signal all tasks to start
103+
await Task.WhenAll(tasks);
104+
}
105+
106+
private static object[] CreateMultiTypedArray(int numValues)
107+
{
108+
return Enumerable.Range(0, numValues)
109+
.Select(_ => GetUniquelyTypedValue())
110+
.ToArray();
111+
}
112+
113+
private static object GetUniquelyTypedValue()
114+
{
115+
var typeName = $"Type_{Guid.NewGuid()}";
116+
117+
var asmName = new AssemblyName($"Asm_{Guid.NewGuid()}");
118+
var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(
119+
asmName,
120+
AssemblyBuilderAccess.Run);
121+
122+
var moduleBuilder = asmBuilder.DefineDynamicModule($"Module{Guid.NewGuid()}");
123+
124+
// Create a completely empty public class
125+
var typeBuilder = moduleBuilder.DefineType(
126+
typeName,
127+
TypeAttributes.Public | TypeAttributes.Class);
128+
129+
Type emittedType = typeBuilder.CreateTypeInfo();
130+
131+
// Instantiate it
132+
var instance = Activator.CreateInstance(emittedType);
133+
return instance;
134+
}
69135
}

Neo4j.Driver/Neo4j.Driver.Tests/Neo4j.Driver.Tests.csproj

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@
1313
<GenerateAssemblyVersionAttribute>true</GenerateAssemblyVersionAttribute>
1414
<GenerateAssemblyFileVersionAttribute>true</GenerateAssemblyFileVersionAttribute>
1515
<LangVersion>default</LangVersion>
16-
<Platforms>AnyCPU</Platforms>
17-
<TargetFramework>net10.0</TargetFramework>
18-
</PropertyGroup>
19-
<PropertyGroup Condition=" '$(Configuration)' == 'Testkit' ">
20-
<Optimize Condition=" '$(Optimize)' == '' ">true</Optimize>
16+
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
2117
</PropertyGroup>
2218
<ItemGroup>
2319
<Content Include="App.config"/>
@@ -26,8 +22,7 @@
2622
<PackageReference Include="Microsoft.Reactive.Testing" Version="5.0.0"/>
2723
<PackageReference Include="Moq" Version="4.20.69"/>
2824
<PackageReference Include="Moq.AutoMock" Version="3.5.0"/>
29-
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
30-
<PackageReference Include="System.Net.Http" Version="4.3.4" />
25+
<PackageReference Include="System.Linq.Async" Version="7.0.0" />
3126
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0"/>
3227
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
3328
<PackageReference Include="TeamCity.VSTest.TestAdapter" Version="1.0.23"/>

Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// limitations under the License.
1515

1616
using System;
17-
using System.Collections.Generic;
17+
using System.Collections.Concurrent;
1818
using System.Reflection;
1919
using Neo4j.Driver.Mapping;
2020

@@ -25,15 +25,16 @@ internal static class MappingExtensions
2525
private static readonly MethodInfo AsGenericMethod =
2626
typeof(ValueExtensions).GetMethod(nameof(ValueExtensions.As), [typeof(object)]);
2727

28-
private static readonly Dictionary<Type, MethodInfo> AsMethods = new();
28+
private static readonly ConcurrentDictionary<Type, MethodInfo> AsMethods = new();
29+
30+
internal static void ResetAsMethods()
31+
{
32+
AsMethods.Clear();
33+
}
2934

3035
public static object AsType(this object obj, Type type)
3136
{
32-
if (!AsMethods.TryGetValue(type, out var asMethod))
33-
{
34-
asMethod = AsGenericMethod.MakeGenericMethod(type);
35-
AsMethods[type] = asMethod;
36-
}
37+
var asMethod = AsMethods.GetOrAdd(type, t => AsGenericMethod.MakeGenericMethod(t));
3738

3839
try
3940
{

Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public static void RegisterProvider(IMappingProvider provider)
269269

270270
public MethodInfo GetMapMethodForType(Type type)
271271
{
272-
return _mapMethods.AddOrUpdate(type, GetMapMethod, (_, m) => m);
272+
return _mapMethods.GetOrAdd(type, GetMapMethod);
273273

274274
MethodInfo GetMapMethod(Type t)
275275
{

0 commit comments

Comments
 (0)