diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9a6693de..6f627ece 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,7 +6,9 @@ updates:
target-branch: "develop"
schedule:
interval: "weekly"
- day: "monday"
+ day: "friday"
+ time: "17:00"
+ timezone: "America/New_York"
open-pull-requests-limit: 10
assignees:
- "philcarbone"
@@ -24,7 +26,9 @@ updates:
target-branch: "develop"
schedule:
interval: "weekly"
- day: "monday"
+ day: "friday"
+ time: "17:00"
+ timezone: "America/New_York"
open-pull-requests-limit: 5
assignees:
- "philcarbone"
diff --git a/src/Whizbang.Generators/JsonMessageTypeInfo.cs b/src/Whizbang.Generators/JsonMessageTypeInfo.cs
index ffcfca0b..a830171b 100644
--- a/src/Whizbang.Generators/JsonMessageTypeInfo.cs
+++ b/src/Whizbang.Generators/JsonMessageTypeInfo.cs
@@ -20,7 +20,16 @@ internal sealed record JsonMessageTypeInfo(
bool IsSerializable,
PropertyInfo[] Properties,
bool HasParameterizedConstructor
-);
+) {
+ ///
+ /// Unique identifier derived from fully qualified name, suitable for C# identifiers.
+ /// Strips "global::" prefix and replaces "." with "_".
+ /// E.g., "global::MyApp.Commands.StartCommand" becomes "MyApp_Commands_StartCommand".
+ /// This prevents duplicate field/method names when types have the same SimpleName.
+ ///
+ /// tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs:Generator_WithSameSimpleNameInDifferentNamespaces_GeneratesUniqueIdentifiersAsync
+ public string UniqueIdentifier => FullyQualifiedName.Replace("global::", "").Replace(".", "_");
+}
///
/// Information about a property for JSON serialization.
diff --git a/src/Whizbang.Generators/ListTypeInfo.cs b/src/Whizbang.Generators/ListTypeInfo.cs
index 5345f64c..f41c5ad1 100644
--- a/src/Whizbang.Generators/ListTypeInfo.cs
+++ b/src/Whizbang.Generators/ListTypeInfo.cs
@@ -15,4 +15,13 @@ public sealed record ListTypeInfo(
string ListTypeName,
string ElementTypeName,
string ElementSimpleName
-);
+) {
+ ///
+ /// Unique identifier derived from element type name, suitable for C# identifiers.
+ /// Strips "global::" prefix and replaces "." with "_".
+ /// E.g., "global::MyApp.Models.OrderLineItem" becomes "MyApp_Models_OrderLineItem".
+ /// This prevents duplicate field/method names when element types have the same SimpleName.
+ ///
+ /// tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs:Generator_WithSameSimpleNameInDifferentNamespaces_GeneratesUniqueIdentifiersAsync
+ public string ElementUniqueIdentifier => ElementTypeName.Replace("global::", "").Replace(".", "_");
+}
diff --git a/src/Whizbang.Generators/MessageJsonContextGenerator.cs b/src/Whizbang.Generators/MessageJsonContextGenerator.cs
index ac8729de..f23e91a2 100644
--- a/src/Whizbang.Generators/MessageJsonContextGenerator.cs
+++ b/src/Whizbang.Generators/MessageJsonContextGenerator.cs
@@ -50,7 +50,7 @@ public class MessageJsonContextGenerator : IIncrementalGenerator {
private const string PLACEHOLDER_MESSAGE_ID = "MessageId";
private const string PLACEHOLDER_FULLY_QUALIFIED_NAME = "__FULLY_QUALIFIED_NAME__";
private const string PLACEHOLDER_SIMPLE_NAME = "__SIMPLE_NAME__";
- private const string PLACEHOLDER_SAFE_NAME = "__SAFE_NAME__";
+ private const string PLACEHOLDER_UNIQUE_IDENTIFIER = "__UNIQUE_IDENTIFIER__";
private const string PLACEHOLDER_GLOBAL = "global::";
private const string PLACEHOLDER_INDEX = "__INDEX__";
private const string PLACEHOLDER_PROPERTY_TYPE = "__PROPERTY_TYPE__";
@@ -395,7 +395,7 @@ private static string _generateLazyFields(Assembly assembly, ImmutableArray t.IsCommand || t.IsEvent)) {
var field = envelopeFieldSnippet
.Replace(PLACEHOLDER_FULLY_QUALIFIED_NAME, type.FullyQualifiedName)
- .Replace(PLACEHOLDER_SAFE_NAME, _toSafeMethodName(type.FullyQualifiedName));
+ .Replace(PLACEHOLDER_UNIQUE_IDENTIFIER, type.UniqueIdentifier);
sb.AppendLine(field);
}
@@ -466,7 +466,7 @@ private static string _generateGetTypeInfo(Assembly assembly, ImmutableArray t.IsCommand || t.IsEvent)) {
var check = envelopeCheckSnippet
.Replace(PLACEHOLDER_FULLY_QUALIFIED_NAME, type.FullyQualifiedName)
- .Replace(PLACEHOLDER_SAFE_NAME, _toSafeMethodName(type.FullyQualifiedName));
+ .Replace(PLACEHOLDER_UNIQUE_IDENTIFIER, type.UniqueIdentifier);
sb.AppendLine(check);
sb.AppendLine();
}
@@ -487,7 +487,7 @@ private static string _generateGetTypeInfo(Assembly assembly, ImmutableArray Create_{safeName}(JsonSerializerOptions options) {{");
+ sb.AppendLine($"private JsonTypeInfo<{message.FullyQualifiedName}> Create_{message.UniqueIdentifier}(JsonSerializerOptions options) {{");
// Generate properties array
sb.AppendLine($" var properties = new JsonPropertyInfo[{message.Properties.Length}];");
@@ -724,8 +723,7 @@ private static string _generateMessageEnvelopeFactories(Assembly assembly, Immut
"PARAMETER_INFO_VALUES");
foreach (var message in messages) {
- var safeName = _toSafeMethodName(message.FullyQualifiedName);
- sb.AppendLine($"private JsonTypeInfo> CreateMessageEnvelope_{safeName}(JsonSerializerOptions options) {{");
+ sb.AppendLine($"private JsonTypeInfo> CreateMessageEnvelope_{message.UniqueIdentifier}(JsonSerializerOptions options) {{");
// Generate properties array for MessageEnvelope (MessageId, Payload, Hops)
sb.AppendLine(" var properties = new JsonPropertyInfo[3];");
@@ -1037,7 +1035,7 @@ private static string _generateListLazyFields(Assembly assembly, ImmutableArray<
foreach (var listType in listTypes) {
var field = snippet
.Replace("__ELEMENT_TYPE__", listType.ElementTypeName)
- .Replace("__ELEMENT_SAFE_NAME__", _toSafeMethodName(listType.ElementTypeName));
+ .Replace("__ELEMENT_UNIQUE_IDENTIFIER__", listType.ElementUniqueIdentifier);
sb.AppendLine(field);
}
@@ -1063,7 +1061,7 @@ private static string _generateListFactories(Assembly assembly, ImmutableArray? ___SAFE_NAME__;
+ private JsonTypeInfo<__FULLY_QUALIFIED_NAME__>? ___UNIQUE_IDENTIFIER__;
#endregion
#region LAZY_FIELD_MESSAGE_ENVELOPE
- private JsonTypeInfo>? _MessageEnvelope___SAFE_NAME__;
+ private JsonTypeInfo>? _MessageEnvelope___UNIQUE_IDENTIFIER__;
#endregion
#region GET_TYPE_INFO_VALUE_OBJECT
@@ -32,13 +32,13 @@ internal class JsonContextSnippets {
#region GET_TYPE_INFO_MESSAGE
if (type == typeof(__FULLY_QUALIFIED_NAME__)) {
- return Create___SAFE_NAME__(options);
+ return Create___UNIQUE_IDENTIFIER__(options);
}
#endregion
#region GET_TYPE_INFO_MESSAGE_ENVELOPE
if (type == typeof(MessageEnvelope<__FULLY_QUALIFIED_NAME__>)) {
- return CreateMessageEnvelope___SAFE_NAME__(options);
+ return CreateMessageEnvelope___UNIQUE_IDENTIFIER__(options);
}
#endregion
@@ -52,17 +52,17 @@ internal class JsonContextSnippets {
#endregion
#region LAZY_FIELD_LIST
-private JsonTypeInfo>? _List___ELEMENT_SAFE_NAME__;
+private JsonTypeInfo>? _List___ELEMENT_UNIQUE_IDENTIFIER__;
#endregion
#region GET_TYPE_INFO_LIST
if (type == typeof(global::System.Collections.Generic.List<__ELEMENT_TYPE__>)) {
- return CreateList___ELEMENT_SAFE_NAME__(options);
+ return CreateList___ELEMENT_UNIQUE_IDENTIFIER__(options);
}
#endregion
#region LIST_TYPE_FACTORY
-private JsonTypeInfo> CreateList___ELEMENT_SAFE_NAME__(JsonSerializerOptions options) {
+private JsonTypeInfo> CreateList___ELEMENT_UNIQUE_IDENTIFIER__(JsonSerializerOptions options) {
var elementInfo = GetOrCreateTypeInfo<__ELEMENT_TYPE__>(options);
var collectionInfo = new JsonCollectionInfoValues> {
ObjectCreator = static () => new global::System.Collections.Generic.List<__ELEMENT_TYPE__>(),
diff --git a/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs b/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs
index 472e32bb..117637de 100644
--- a/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs
+++ b/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs
@@ -155,7 +155,7 @@ public record CreateOrder(string OrderId) : ICommand;
await Assert.That(code).IsNotNull();
// Should generate specific factory method for MessageEnvelope
- // Safe names use fully qualified path to avoid collisions (e.g., MyApp_Commands_CreateOrder)
+ // Uses unique identifier from fully qualified name: MyApp.Commands.CreateOrder -> MyApp_Commands_CreateOrder
await Assert.That(code!).Contains("CreateMessageEnvelope_MyApp_Commands_CreateOrder");
await Assert.That(code).Contains("MessageEnvelope");
}
@@ -613,7 +613,51 @@ public record CreateOrder(string OrderId, List PublicItems) : ICom
await Assert.That(code).IsNotNull();
await Assert.That(code!).Contains("CreateOrder");
await Assert.That(code).Contains("PublicDetail");
- // PublicDetail should have factory method with safe name (namespace-qualified)
+ // PublicDetail should have factory method (uses unique identifier from FQN)
await Assert.That(code).Contains("Create_MyApp_PublicDetail");
}
+
+ [Test]
+ [RequiresAssemblyFiles()]
+ public async Task Generator_WithSameSimpleNameInDifferentNamespaces_GeneratesUniqueIdentifiersAsync() {
+ // Arrange - Two types with same SimpleName but different namespaces
+ // This would previously cause duplicate field names (_StartCommand) and factory methods
+ var source = """
+using Whizbang.Core;
+
+namespace MyApp.Commands {
+ public record StartCommand(string Data) : ICommand;
+}
+
+namespace MyApp.Events {
+ public record StartCommand(string Data) : IEvent;
+}
+""";
+
+ // Act
+ var result = GeneratorTestHelper.RunGenerator(source);
+
+ // Assert - Should not have compilation errors (duplicate identifiers would cause errors)
+ await Assert.That(result.Diagnostics).DoesNotContain(d => d.Severity == DiagnosticSeverity.Error);
+
+ var code = GeneratorTestHelper.GetGeneratedSource(result, "MessageJsonContext.g.cs");
+ await Assert.That(code).IsNotNull();
+
+ // Both types should be present with fully qualified names
+ await Assert.That(code!).Contains("global::MyApp.Commands.StartCommand");
+ await Assert.That(code).Contains("global::MyApp.Events.StartCommand");
+
+ // Should have unique field names (not duplicate _StartCommand)
+ // Uses namespace-qualified identifiers like _MyApp_Commands_StartCommand
+ await Assert.That(code).Contains("_MyApp_Commands_StartCommand");
+ await Assert.That(code).Contains("_MyApp_Events_StartCommand");
+
+ // Should have unique factory method names
+ await Assert.That(code).Contains("Create_MyApp_Commands_StartCommand");
+ await Assert.That(code).Contains("Create_MyApp_Events_StartCommand");
+
+ // Should have unique MessageEnvelope factory method names
+ await Assert.That(code).Contains("CreateMessageEnvelope_MyApp_Commands_StartCommand");
+ await Assert.That(code).Contains("CreateMessageEnvelope_MyApp_Events_StartCommand");
+ }
}