Skip to content

Commit 5dd141d

Browse files
bieluclaude
andcommitted
fix: make reference equality consistent and fail clearly on unresolved references
Reference classes overrode Equals and == without GetHashCode, so equal references hashed differently. LINQ Distinct kept duplicates, which made V2 serialization with inlined references throw "An item with the same key has already been added" when two references to the same recursive schema were found in a loop. - Add GetHashCode, using the resolved target's hash (consistent with references comparing equal to their target) or the $ref string when unresolved. - Unresolved references now compare their $ref strings instead of all being equal (null == null), and Equals(null element) returns false. - Setters, inlined serialization and bindings collection operations on an unresolved reference throw InvalidOperationException naming the $ref instead of NullReferenceException. Boolean schema getters return false when unresolved, like the other getters return null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 70e73db commit 5dd141d

20 files changed

Lines changed: 543 additions & 224 deletions

‎src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,19 @@ public override bool Equals(object obj)
151151
return this.Equals(reference);
152152
}
153153

154+
public override int GetHashCode()
155+
{
156+
return this.Reference?.GetHashCode() ?? 0;
157+
}
158+
159+
/// <summary>
160+
/// Creates the exception thrown when the referenced element is needed but the reference cannot be resolved.
161+
/// </summary>
162+
internal InvalidOperationException CreateUnresolvedException()
163+
{
164+
return new InvalidOperationException($"The reference '{this.Reference}' is not resolved, so the element it points to is not available.");
165+
}
166+
154167
/// <summary>
155168
/// Serialize <see cref="AsyncApiReference"/> to Async Api.
156169
/// </summary>

‎src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class AsyncApiAvroSchemaReference : AsyncApiAvroSchema, IAsyncApiReferenc
1111
{
1212
private AsyncApiAvroSchema target;
1313

14+
private AsyncApiAvroSchema Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException();
15+
1416
private AsyncApiAvroSchema Target
1517
{
1618
get
@@ -27,7 +29,7 @@ public AsyncApiAvroSchemaReference(string reference)
2729

2830
public override string Type => this.Target?.Type;
2931

30-
public override IDictionary<string, AsyncApiAny> Metadata { get => this.Target?.Metadata; set => this.Target.Metadata = value; }
32+
public override IDictionary<string, AsyncApiAny> Metadata { get => this.Target?.Metadata; set => this.Resolved.Metadata = value; }
3133

3234
public bool UnresolvedReference { get { return this.Target == null; } }
3335

@@ -40,7 +42,7 @@ public override T As<T>()
4042
return null;
4143
}
4244

43-
return this.Target.As<T>();
45+
return this.Resolved.As<T>();
4446
}
4547

4648
public override bool Is<T>()
@@ -50,7 +52,7 @@ public override bool Is<T>()
5052
return false;
5153
}
5254

53-
return this.Target.Is<T>();
55+
return this.Resolved.Is<T>();
5456
}
5557

5658
public override bool TryGetAs<T>(out T result)
@@ -61,7 +63,7 @@ public override bool TryGetAs<T>(out T result)
6163
return false;
6264
}
6365

64-
return this.Target.TryGetAs(out result);
66+
return this.Resolved.TryGetAs(out result);
6567
}
6668

6769
public override void SerializeV2(IAsyncApiWriter writer)
@@ -77,7 +79,7 @@ public override void SerializeV2(IAsyncApiWriter writer)
7779
return;
7880
}
7981

80-
this.Target.SerializeV2(writer);
82+
this.Resolved.SerializeV2(writer);
8183
}
8284

8385
public override void SerializeV3(IAsyncApiWriter writer)
@@ -93,7 +95,7 @@ public override void SerializeV3(IAsyncApiWriter writer)
9395
return;
9496
}
9597

96-
this.Target.SerializeV3(writer);
98+
this.Resolved.SerializeV3(writer);
9799
}
98100
}
99101
}

‎src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public class AsyncApiBindingsReference<TBinding> : AsyncApiBindings<TBinding>, I
1616

1717
private AsyncApiBindings<TBinding> target;
1818

19+
private AsyncApiBindings<TBinding> Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException();
20+
1921
private AsyncApiBindings<TBinding> Target
2022
{
2123
get
@@ -27,18 +29,18 @@ private AsyncApiBindings<TBinding> Target
2729

2830
public override void Add(TBinding binding)
2931
{
30-
this.Target.Add(binding);
32+
this.Resolved.Add(binding);
3133
}
3234

33-
public override ICollection<string> Keys => this.Target.Keys;
35+
public override ICollection<string> Keys => this.Resolved.Keys;
3436

35-
public override ICollection<TBinding> Values => this.Target.Values;
37+
public override ICollection<TBinding> Values => this.Resolved.Values;
3638

3739
public override IDictionary<string, IAsyncApiExtension> Extensions => this.target.Extensions;
3840

39-
public override int Count => this.Target.Count;
41+
public override int Count => this.Resolved.Count;
4042

41-
public override bool IsReadOnly => this.Target.IsReadOnly;
43+
public override bool IsReadOnly => this.Resolved.IsReadOnly;
4244

4345
public AsyncApiBindingsReference(string reference)
4446
{
@@ -84,7 +86,7 @@ public override void SerializeV2(IAsyncApiWriter writer)
8486
return;
8587
}
8688

87-
this.Target.SerializeV2(writer);
89+
this.Resolved.SerializeV2(writer);
8890
}
8991

9092
public override void SerializeV3(IAsyncApiWriter writer)
@@ -100,57 +102,57 @@ public override void SerializeV3(IAsyncApiWriter writer)
100102
return;
101103
}
102104

103-
this.Target.SerializeV3(writer);
105+
this.Resolved.SerializeV3(writer);
104106
}
105107

106108
public override void Add(string key, TBinding value)
107109
{
108-
this.Target.Add(key, value);
110+
this.Resolved.Add(key, value);
109111
}
110112

111113
public override bool ContainsKey(string key)
112114
{
113-
return this.Target.ContainsKey(key);
115+
return this.Resolved.ContainsKey(key);
114116
}
115117

116118
public override bool Remove(string key)
117119
{
118-
return this.Target.Remove(key);
120+
return this.Resolved.Remove(key);
119121
}
120122

121123
public override bool TryGetValue(string key, out TBinding value)
122124
{
123-
return this.Target.TryGetValue(key, out value);
125+
return this.Resolved.TryGetValue(key, out value);
124126
}
125127

126128
public override void Add(KeyValuePair<string, TBinding> item)
127129
{
128-
this.Target.Add(item);
130+
this.Resolved.Add(item);
129131
}
130132

131133
public override void Clear()
132134
{
133-
this.Target.Clear();
135+
this.Resolved.Clear();
134136
}
135137

136138
public override bool Contains(KeyValuePair<string, TBinding> item)
137139
{
138-
return this.Target.Contains(item);
140+
return this.Resolved.Contains(item);
139141
}
140142

141143
public override void CopyTo(KeyValuePair<string, TBinding>[] array, int arrayIndex)
142144
{
143-
this.Target.CopyTo(array, arrayIndex);
145+
this.Resolved.CopyTo(array, arrayIndex);
144146
}
145147

146148
public override bool Remove(KeyValuePair<string, TBinding> item)
147149
{
148-
return this.Target.Remove(item);
150+
return this.Resolved.Remove(item);
149151
}
150152

151153
public override IEnumerator<KeyValuePair<string, TBinding>> GetEnumerator()
152154
{
153-
return this.Target.GetEnumerator();
155+
return this.Resolved.GetEnumerator();
154156
}
155157
}
156158
}

‎src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class AsyncApiChannelReference : AsyncApiChannel, IAsyncApiReferenceable,
1111
{
1212
private AsyncApiChannel target;
1313

14+
private AsyncApiChannel Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException();
15+
1416
private AsyncApiChannel Target
1517
{
1618
get
@@ -25,27 +27,27 @@ public AsyncApiChannelReference(string reference)
2527
this.Reference = new AsyncApiReference(reference, ReferenceType.Channel);
2628
}
2729

28-
public override string Address { get => this.Target?.Address; set => this.Target.Address = value; }
30+
public override string Address { get => this.Target?.Address; set => this.Resolved.Address = value; }
2931

30-
public override IDictionary<string, AsyncApiMessage> Messages { get => this.Target?.Messages; set => this.Target.Messages = value; }
32+
public override IDictionary<string, AsyncApiMessage> Messages { get => this.Target?.Messages; set => this.Resolved.Messages = value; }
3133

32-
public override string Title { get => this.Target?.Title; set => this.Target.Title = value; }
34+
public override string Title { get => this.Target?.Title; set => this.Resolved.Title = value; }
3335

34-
public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; }
36+
public override string Summary { get => this.Target?.Summary; set => this.Resolved.Summary = value; }
3537

36-
public override string Description { get => this.Target?.Description; set => this.Target.Description = value; }
38+
public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; }
3739

38-
public override IList<AsyncApiServerReference> Servers { get => this.Target?.Servers; set => this.Target.Servers = value; }
40+
public override IList<AsyncApiServerReference> Servers { get => this.Target?.Servers; set => this.Resolved.Servers = value; }
3941

40-
public override IDictionary<string, AsyncApiParameter> Parameters { get => this.Target?.Parameters; set => this.Target.Parameters = value; }
42+
public override IDictionary<string, AsyncApiParameter> Parameters { get => this.Target?.Parameters; set => this.Resolved.Parameters = value; }
4143

42-
public override IList<AsyncApiTag> Tags { get => this.Target?.Tags; set => this.Target.Tags = value; }
44+
public override IList<AsyncApiTag> Tags { get => this.Target?.Tags; set => this.Resolved.Tags = value; }
4345

44-
public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; }
46+
public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Resolved.ExternalDocs = value; }
4547

46-
public override AsyncApiBindings<IChannelBinding> Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; }
48+
public override AsyncApiBindings<IChannelBinding> Bindings { get => this.Target?.Bindings; set => this.Resolved.Bindings = value; }
4749

48-
public override IDictionary<string, IAsyncApiExtension> Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; }
50+
public override IDictionary<string, IAsyncApiExtension> Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; }
4951

5052
public AsyncApiReference Reference { get; set; }
5153

@@ -61,7 +63,7 @@ public override void SerializeV2(IAsyncApiWriter writer)
6163
else
6264
{
6365
this.Reference.Workspace = writer.Workspace;
64-
this.Target.SerializeV2(writer);
66+
this.Resolved.SerializeV2(writer);
6567
}
6668
}
6769

@@ -75,7 +77,7 @@ public override void SerializeV3(IAsyncApiWriter writer)
7577
else
7678
{
7779
this.Reference.Workspace = writer.Workspace;
78-
this.Target.SerializeV3(writer);
80+
this.Resolved.SerializeV3(writer);
7981
}
8082
}
8183

@@ -98,6 +100,11 @@ public bool Equals(AsyncApiChannelReference other)
98100
return this.Equals(reference);
99101
}
100102

103+
if (this.Target is null || other.Target is null)
104+
{
105+
return this.Target is null && other.Target is null && this.Reference == other.Reference;
106+
}
107+
101108
return this.Target == other.Target;
102109
}
103110

@@ -118,7 +125,12 @@ public override bool Equals(object obj)
118125

119126
public bool Equals(AsyncApiChannel other)
120127
{
121-
return this.Target == other;
128+
return other is not null && this.Target == other;
129+
}
130+
131+
public override int GetHashCode()
132+
{
133+
return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0;
122134
}
123135
}
124136
}

‎src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class AsyncApiCorrelationIdReference : AsyncApiCorrelationId, IAsyncApiRe
1515
{
1616
private AsyncApiCorrelationId target;
1717

18+
private AsyncApiCorrelationId Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException();
19+
1820
private AsyncApiCorrelationId Target
1921
{
2022
get
@@ -29,11 +31,11 @@ public AsyncApiCorrelationIdReference(string reference)
2931
this.Reference = new AsyncApiReference(reference, ReferenceType.CorrelationId);
3032
}
3133

32-
public override string Description { get => this.Target?.Description; set => this.Target.Description = value; }
34+
public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; }
3335

34-
public override string Location { get => this.Target?.Location; set => this.Target.Location = value; }
36+
public override string Location { get => this.Target?.Location; set => this.Resolved.Location = value; }
3537

36-
public override IDictionary<string, IAsyncApiExtension> Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; }
38+
public override IDictionary<string, IAsyncApiExtension> Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; }
3739

3840
public AsyncApiReference Reference { get; set; }
3941

@@ -58,6 +60,11 @@ public bool Equals(AsyncApiCorrelationIdReference other)
5860
return this.Equals(reference);
5961
}
6062

63+
if (this.Target is null || other.Target is null)
64+
{
65+
return this.Target is null && other.Target is null && this.Reference == other.Reference;
66+
}
67+
6168
return this.Target == other.Target;
6269
}
6370

@@ -78,7 +85,12 @@ public override bool Equals(object obj)
7885

7986
public bool Equals(AsyncApiCorrelationId other)
8087
{
81-
return this.Target == other;
88+
return other is not null && this.Target == other;
89+
}
90+
91+
public override int GetHashCode()
92+
{
93+
return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0;
8294
}
8395

8496
public override void SerializeV2(IAsyncApiWriter writer)
@@ -91,7 +103,7 @@ public override void SerializeV2(IAsyncApiWriter writer)
91103
else
92104
{
93105
this.Reference.Workspace = writer.Workspace;
94-
this.Target.SerializeV2(writer);
106+
this.Resolved.SerializeV2(writer);
95107
}
96108
}
97109

@@ -105,7 +117,7 @@ public override void SerializeV3(IAsyncApiWriter writer)
105117
else
106118
{
107119
this.Reference.Workspace = writer.Workspace;
108-
this.Target.SerializeV3(writer);
120+
this.Resolved.SerializeV3(writer);
109121
}
110122
}
111123
}

0 commit comments

Comments
 (0)