Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions src/Aspire.Hosting.Azure/Provisioning/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,22 @@ public static JsonNode Prop(this JsonNode obj, string key)
{
var jsonObj = obj.AsObject();

// Try to get the existing node
var node = jsonObj[key];
if (node is not null)
// Lock on the JsonObject to ensure thread-safe access when multiple
// bicep resources are being provisioned in parallel
lock (jsonObj)
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locking on the JsonObject instance itself can be problematic as it's a public object that external code might also lock on, potentially causing deadlocks. Consider using a private static readonly object or ConcurrentDictionary for thread-safe operations instead.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@JamesNK JamesNK Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this. I think thread safety would be better added to prevent multiple threads from using the JSON at once, rather than making Prop method itself safe.

Making Prop safe against itself just kicks the problem down to the next thing that breaks when multiple threads are reading/writing JSON at the same time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Address the feedback here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this. I think thread safety would be better added to prevent multiple threads from using the JSON at once, rather than making Prop method itself safe.
Making Prop safe against itself just kicks the problem down to the next thing that breaks when multiple threads are reading/writing JSON at the same time.

@copilot consider this feedback. Look at how the json objects are being used and from where. And how we can make it thread safe for writes, and reads.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this. I think thread safety would be better added to prevent multiple threads from using the JSON at once, rather than making Prop method itself safe.
Making Prop safe against itself just kicks the problem down to the next thing that breaks when multiple threads are reading/writing JSON at the same time.

@copilot This feedback has not been correctly addressed. Look at how the json objects are being used and from where. And how we can make it thread safe for writes, and reads. And then fix that. Add relevant tests too.

{
return node;
}

// Create a new node and try to add it
node = new JsonObject();

if (!jsonObj.TryAdd(key, node))
{
node = jsonObj[key];
if (node is null)
// Try to get the existing node
var node = jsonObj[key];
if (node is not null)
{
throw new InvalidOperationException($"Failed to get or create property '{key}'");
return node;
}
}

return node;
// Create a new node and add it
node = new JsonObject();
jsonObj.Add(key, node);

return node;
}
}
}
106 changes: 106 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/JsonExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Nodes;

namespace Aspire.Hosting.Azure.Tests;

public class JsonExtensionsTests
{
[Fact]
public async Task Prop_ConcurrentAccess_DoesNotThrow()
{
// Arrange
var rootJson = new JsonObject();
const int threadCount = 10;
const int iterationsPerThread = 100;
var tasks = new Task[threadCount];

// Act - Multiple threads accessing the same property concurrently
for (int i = 0; i < threadCount; i++)
{
int threadId = i;
tasks[i] = Task.Run(() =>
{
for (int j = 0; j < iterationsPerThread; j++)
{
// All threads try to get or create the same "Azure" property
var azureNode = rootJson.Prop("Azure");

// Each thread also creates a unique property
var threadNode = azureNode.Prop($"Thread{threadId}");

// And a shared property under Azure
var deploymentsNode = azureNode.Prop("Deployments");

// Access a deeper nested property
var resourceNode = deploymentsNode.Prop($"Resource{j % 5}");
}
});
}

// Assert - Should complete without exceptions
await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(10));

// Verify the structure was created correctly
Assert.NotNull(rootJson["Azure"]);
var azureObj = rootJson["Azure"]!.AsObject();
Assert.NotNull(azureObj["Deployments"]);

// Check that all thread-specific nodes were created
for (int i = 0; i < threadCount; i++)
{
Assert.NotNull(azureObj[$"Thread{i}"]);
}
}

[Fact]
public void Prop_ReturnsExistingNode_WhenNodeAlreadyExists()
{
// Arrange
var rootJson = new JsonObject();
var azureNode = rootJson.Prop("Azure");
azureNode.AsObject()["TestProperty"] = "TestValue";

// Act
var retrievedNode = rootJson.Prop("Azure");

// Assert
Assert.Same(azureNode, retrievedNode);
Assert.Equal("TestValue", retrievedNode["TestProperty"]!.GetValue<string>());
}

[Fact]
public void Prop_CreatesNewNode_WhenNodeDoesNotExist()
{
// Arrange
var rootJson = new JsonObject();

// Act
var newNode = rootJson.Prop("NewProperty");

// Assert
Assert.NotNull(newNode);
Assert.Same(rootJson["NewProperty"], newNode);
}

[Fact]
public void Prop_NestedAccess_CreatesHierarchy()
{
// Arrange
var rootJson = new JsonObject();

// Act
var deeply = rootJson.Prop("Level1")
.Prop("Level2")
.Prop("Level3")
.Prop("Level4");

// Assert
Assert.NotNull(rootJson["Level1"]);
Assert.NotNull(rootJson["Level1"]!["Level2"]);
Assert.NotNull(rootJson["Level1"]!["Level2"]!["Level3"]);
Assert.NotNull(rootJson["Level1"]!["Level2"]!["Level3"]!["Level4"]);
Assert.Same(deeply, rootJson["Level1"]!["Level2"]!["Level3"]!["Level4"]);
}
}