Skip to content

Commit 8f8b5f5

Browse files
rynowakwtgodbe
authored andcommitted
Make constraint cache thread safe (#17146)
* Make constraint cache thread safe Fixes: #17101 This changes the constraint cache to use ConcurrentDictionary. This code is invoked in a multithreaded way in Blazor server resulting in internal failures in dictionary. Since this is a threading issue there's no good way to unit test it, but I noticed we're missing tests in general for this class, so I added a few for the caching behavior. * PR feedback
1 parent 7ceadd0 commit 8f8b5f5

File tree

2 files changed

+48
-5
lines changed

2 files changed

+48
-5
lines changed

src/Components/Components/src/Routing/RouteConstraint.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
5+
using System.Collections.Concurrent;
66
using System.Globalization;
77

88
namespace Microsoft.AspNetCore.Components.Routing
99
{
1010
internal abstract class RouteConstraint
1111
{
12-
private static readonly IDictionary<string, RouteConstraint> _cachedConstraints
13-
= new Dictionary<string, RouteConstraint>();
12+
// note: the things that prevent this cache from growing unbounded is that
13+
// we're the only caller to this code path, and the fact that there are only
14+
// 8 possible instances that we create.
15+
//
16+
// The values passed in here for parsing are always static text defined in route attributes.
17+
private static readonly ConcurrentDictionary<string, RouteConstraint> _cachedConstraints
18+
= new ConcurrentDictionary<string, RouteConstraint>();
1419

1520
public abstract bool Match(string pathSegment, out object convertedValue);
1621

@@ -30,8 +35,10 @@ public static RouteConstraint Parse(string template, string segment, string cons
3035
var newInstance = CreateRouteConstraint(constraint);
3136
if (newInstance != null)
3237
{
33-
_cachedConstraints[constraint] = newInstance;
34-
return newInstance;
38+
// We've done to the work to create the constraint now, but it's possible
39+
// we're competing with another thread. GetOrAdd can ensure only a single
40+
// instance is returned so that any extra ones can be GC'ed.
41+
return _cachedConstraints.GetOrAdd(constraint, newInstance);
3542
}
3643
else
3744
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Xunit;
5+
6+
namespace Microsoft.AspNetCore.Components.Routing
7+
{
8+
public class RouteConstraintTest
9+
{
10+
[Fact]
11+
public void Parse_CreatesDifferentConstraints_ForDifferentKinds()
12+
{
13+
// Arrange
14+
var original = RouteConstraint.Parse("ignore", "ignore", "int");
15+
16+
// Act
17+
var another = RouteConstraint.Parse("ignore", "ignore", "guid");
18+
19+
// Assert
20+
Assert.NotSame(original, another);
21+
}
22+
23+
[Fact]
24+
public void Parse_CachesCreatedConstraint_ForSameKind()
25+
{
26+
// Arrange
27+
var original = RouteConstraint.Parse("ignore", "ignore", "int");
28+
29+
// Act
30+
var another = RouteConstraint.Parse("ignore", "ignore", "int");
31+
32+
// Assert
33+
Assert.Same(original, another);
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)