From 5dd141d50299c0aa7c9d522c2fdd014ca2b6ca27 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:31:04 +0100 Subject: [PATCH 1/4] 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 --- .../Models/AsyncApiReference.cs | 13 ++ .../References/AsyncApiAvroSchemaReference.cs | 14 +- .../AsyncApiBindingsReference{TBinding}.cs | 36 ++--- .../References/AsyncApiChannelReference.cs | 40 ++++-- .../AsyncApiCorrelationIdReference.cs | 24 +++- .../AsyncApiExternalDocumentationReference.cs | 24 +++- .../References/AsyncApiJsonSchemaReference.cs | 112 ++++++++------- .../References/AsyncApiMessageReference.cs | 46 ++++--- .../AsyncApiMessageTraitReference.cs | 42 ++++-- .../AsyncApiMultiFormatSchemaReference.cs | 10 +- .../References/AsyncApiOperationReference.cs | 44 +++--- .../AsyncApiOperationReplyAddressReference.cs | 22 ++- .../AsyncApiOperationReplyReference.cs | 24 +++- .../AsyncApiOperationTraitReference.cs | 34 +++-- .../References/AsyncApiParameterReference.cs | 30 ++-- .../AsyncApiSecuritySchemeReference.cs | 26 ++-- .../References/AsyncApiServerReference.cs | 42 ++++-- .../AsyncApiServerVariableReference.cs | 28 ++-- .../Models/References/AsyncApiTagReference.cs | 26 +++- .../AsyncApiReferenceEquality_Should.cs | 130 ++++++++++++++++++ 20 files changed, 543 insertions(+), 224 deletions(-) create mode 100644 test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs index 8bb7916..4619fde 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs @@ -151,6 +151,19 @@ public override bool Equals(object obj) return this.Equals(reference); } + public override int GetHashCode() + { + return this.Reference?.GetHashCode() ?? 0; + } + + /// + /// Creates the exception thrown when the referenced element is needed but the reference cannot be resolved. + /// + internal InvalidOperationException CreateUnresolvedException() + { + return new InvalidOperationException($"The reference '{this.Reference}' is not resolved, so the element it points to is not available."); + } + /// /// Serialize to Async Api. /// diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs index 2ed4b33..c1f864d 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs @@ -11,6 +11,8 @@ public class AsyncApiAvroSchemaReference : AsyncApiAvroSchema, IAsyncApiReferenc { private AsyncApiAvroSchema target; + private AsyncApiAvroSchema Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiAvroSchema Target { get @@ -27,7 +29,7 @@ public AsyncApiAvroSchemaReference(string reference) public override string Type => this.Target?.Type; - public override IDictionary Metadata { get => this.Target?.Metadata; set => this.Target.Metadata = value; } + public override IDictionary Metadata { get => this.Target?.Metadata; set => this.Resolved.Metadata = value; } public bool UnresolvedReference { get { return this.Target == null; } } @@ -40,7 +42,7 @@ public override T As() return null; } - return this.Target.As(); + return this.Resolved.As(); } public override bool Is() @@ -50,7 +52,7 @@ public override bool Is() return false; } - return this.Target.Is(); + return this.Resolved.Is(); } public override bool TryGetAs(out T result) @@ -61,7 +63,7 @@ public override bool TryGetAs(out T result) return false; } - return this.Target.TryGetAs(out result); + return this.Resolved.TryGetAs(out result); } public override void SerializeV2(IAsyncApiWriter writer) @@ -77,7 +79,7 @@ public override void SerializeV2(IAsyncApiWriter writer) return; } - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } public override void SerializeV3(IAsyncApiWriter writer) @@ -93,7 +95,7 @@ public override void SerializeV3(IAsyncApiWriter writer) return; } - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs index d16fd24..73d75a5 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs @@ -16,6 +16,8 @@ public class AsyncApiBindingsReference : AsyncApiBindings, I private AsyncApiBindings target; + private AsyncApiBindings Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiBindings Target { get @@ -27,18 +29,18 @@ private AsyncApiBindings Target public override void Add(TBinding binding) { - this.Target.Add(binding); + this.Resolved.Add(binding); } - public override ICollection Keys => this.Target.Keys; + public override ICollection Keys => this.Resolved.Keys; - public override ICollection Values => this.Target.Values; + public override ICollection Values => this.Resolved.Values; public override IDictionary Extensions => this.target.Extensions; - public override int Count => this.Target.Count; + public override int Count => this.Resolved.Count; - public override bool IsReadOnly => this.Target.IsReadOnly; + public override bool IsReadOnly => this.Resolved.IsReadOnly; public AsyncApiBindingsReference(string reference) { @@ -84,7 +86,7 @@ public override void SerializeV2(IAsyncApiWriter writer) return; } - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } public override void SerializeV3(IAsyncApiWriter writer) @@ -100,57 +102,57 @@ public override void SerializeV3(IAsyncApiWriter writer) return; } - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } public override void Add(string key, TBinding value) { - this.Target.Add(key, value); + this.Resolved.Add(key, value); } public override bool ContainsKey(string key) { - return this.Target.ContainsKey(key); + return this.Resolved.ContainsKey(key); } public override bool Remove(string key) { - return this.Target.Remove(key); + return this.Resolved.Remove(key); } public override bool TryGetValue(string key, out TBinding value) { - return this.Target.TryGetValue(key, out value); + return this.Resolved.TryGetValue(key, out value); } public override void Add(KeyValuePair item) { - this.Target.Add(item); + this.Resolved.Add(item); } public override void Clear() { - this.Target.Clear(); + this.Resolved.Clear(); } public override bool Contains(KeyValuePair item) { - return this.Target.Contains(item); + return this.Resolved.Contains(item); } public override void CopyTo(KeyValuePair[] array, int arrayIndex) { - this.Target.CopyTo(array, arrayIndex); + this.Resolved.CopyTo(array, arrayIndex); } public override bool Remove(KeyValuePair item) { - return this.Target.Remove(item); + return this.Resolved.Remove(item); } public override IEnumerator> GetEnumerator() { - return this.Target.GetEnumerator(); + return this.Resolved.GetEnumerator(); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs index 0447d04..177ad6b 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs @@ -11,6 +11,8 @@ public class AsyncApiChannelReference : AsyncApiChannel, IAsyncApiReferenceable, { private AsyncApiChannel target; + private AsyncApiChannel Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiChannel Target { get @@ -25,27 +27,27 @@ public AsyncApiChannelReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Channel); } - public override string Address { get => this.Target?.Address; set => this.Target.Address = value; } + public override string Address { get => this.Target?.Address; set => this.Resolved.Address = value; } - public override IDictionary Messages { get => this.Target?.Messages; set => this.Target.Messages = value; } + public override IDictionary Messages { get => this.Target?.Messages; set => this.Resolved.Messages = value; } - public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + public override string Title { get => this.Target?.Title; set => this.Resolved.Title = value; } - public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + public override string Summary { get => this.Target?.Summary; set => this.Resolved.Summary = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override IList Servers { get => this.Target?.Servers; set => this.Target.Servers = value; } + public override IList Servers { get => this.Target?.Servers; set => this.Resolved.Servers = value; } - public override IDictionary Parameters { get => this.Target?.Parameters; set => this.Target.Parameters = value; } + public override IDictionary Parameters { get => this.Target?.Parameters; set => this.Resolved.Parameters = value; } - public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Resolved.Tags = value; } - public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Resolved.ExternalDocs = value; } - public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Resolved.Bindings = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -61,7 +63,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -75,7 +77,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } @@ -98,6 +100,11 @@ public bool Equals(AsyncApiChannelReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -118,7 +125,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiChannel other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs index 822dfbc..20ea800 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs @@ -15,6 +15,8 @@ public class AsyncApiCorrelationIdReference : AsyncApiCorrelationId, IAsyncApiRe { private AsyncApiCorrelationId target; + private AsyncApiCorrelationId Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiCorrelationId Target { get @@ -29,11 +31,11 @@ public AsyncApiCorrelationIdReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.CorrelationId); } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override string Location { get => this.Target?.Location; set => this.Target.Location = value; } + public override string Location { get => this.Target?.Location; set => this.Resolved.Location = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -58,6 +60,11 @@ public bool Equals(AsyncApiCorrelationIdReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -78,7 +85,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiCorrelationId other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -91,7 +103,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -105,7 +117,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs index 0fc7680..9cf9daf 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs @@ -11,6 +11,8 @@ public class AsyncApiExternalDocumentationReference : AsyncApiExternalDocumentat { private AsyncApiExternalDocumentation target; + private AsyncApiExternalDocumentation Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiExternalDocumentation Target { get @@ -25,11 +27,11 @@ public AsyncApiExternalDocumentationReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.ExternalDocs); } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override Uri Url { get => this.Target?.Url; set => this.Target.Url = value; } + public override Uri Url { get => this.Target?.Url; set => this.Resolved.Url = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -54,6 +56,11 @@ public bool Equals(AsyncApiExternalDocumentationReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -74,7 +81,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiExternalDocumentation other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } /// @@ -94,7 +106,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -108,7 +120,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs index 1545479..e5bbb27 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs @@ -11,6 +11,8 @@ public class AsyncApiJsonSchemaReference : AsyncApiJsonSchema, IAsyncApiReferenc { private AsyncApiJsonSchema target; + private AsyncApiJsonSchema Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiJsonSchema Target { get @@ -35,259 +37,259 @@ public bool UnresolvedReference public override string Title { get => this.Target?.Title; - set => this.Target.Title = value; + set => this.Resolved.Title = value; } public override SchemaType? Type { get => this.Target?.Type; - set => this.Target.Type = value; + set => this.Resolved.Type = value; } public override string Format { get => this.Target?.Format; - set => this.Target.Format = value; + set => this.Resolved.Format = value; } public override string Description { get => this.Target?.Description; - set => this.Target.Description = value; + set => this.Resolved.Description = value; } public override double? Maximum { get => this.Target?.Maximum; - set => this.Target.Maximum = value; + set => this.Resolved.Maximum = value; } public override double? ExclusiveMaximum { get => this.Target?.ExclusiveMaximum; - set => this.Target.ExclusiveMaximum = value; + set => this.Resolved.ExclusiveMaximum = value; } public override double? Minimum { get => this.Target?.Minimum; - set => this.Target.Minimum = value; + set => this.Resolved.Minimum = value; } public override double? ExclusiveMinimum { get => this.Target?.ExclusiveMinimum; - set => this.Target.ExclusiveMinimum = value; + set => this.Resolved.ExclusiveMinimum = value; } public override int? MaxLength { get => this.Target?.MaxLength; - set => this.Target.MaxLength = value; + set => this.Resolved.MaxLength = value; } public override int? MinLength { get => this.Target?.MinLength; - set => this.Target.MinLength = value; + set => this.Resolved.MinLength = value; } public override string Pattern { get => this.Target?.Pattern; - set => this.Target.Pattern = value; + set => this.Resolved.Pattern = value; } public override double? MultipleOf { get => this.Target?.MultipleOf; - set => this.Target.MultipleOf = value; + set => this.Resolved.MultipleOf = value; } public override AsyncApiAny Default { get => this.Target?.Default; - set => this.Target.Default = value; + set => this.Resolved.Default = value; } public override bool ReadOnly { - get => this.Target.ReadOnly; - set => this.Target.ReadOnly = value; + get => this.Target?.ReadOnly ?? false; + set => this.Resolved.ReadOnly = value; } public override bool WriteOnly { - get => this.Target.WriteOnly; - set => this.Target.WriteOnly = value; + get => this.Target?.WriteOnly ?? false; + set => this.Resolved.WriteOnly = value; } public override IList AllOf { get => this.Target?.AllOf; - set => this.Target.AllOf = value; + set => this.Resolved.AllOf = value; } public override IList OneOf { get => this.Target?.OneOf; - set => this.Target.OneOf = value; + set => this.Resolved.OneOf = value; } public override IList AnyOf { get => this.Target?.AnyOf; - set => this.Target.AnyOf = value; + set => this.Resolved.AnyOf = value; } public override AsyncApiJsonSchema Not { get => this.Target?.Not; - set => this.Target.Not = value; + set => this.Resolved.Not = value; } public override AsyncApiJsonSchema Contains { get => this.Target?.Contains; - set => this.Target.Contains = value; + set => this.Resolved.Contains = value; } public override AsyncApiJsonSchema If { get => this.Target?.If; - set => this.Target.If = value; + set => this.Resolved.If = value; } public override AsyncApiJsonSchema Then { get => this.Target?.Then; - set => this.Target.Then = value; + set => this.Resolved.Then = value; } public override AsyncApiJsonSchema Else { get => this.Target?.Else; - set => this.Target.Else = value; + set => this.Resolved.Else = value; } public override ISet Required { get => this.Target?.Required; - set => this.Target.Required = value; + set => this.Resolved.Required = value; } public override AsyncApiJsonSchema Items { get => this.Target?.Items; - set => this.Target.Items = value; + set => this.Resolved.Items = value; } public override AsyncApiJsonSchema AdditionalItems { get => this.Target?.AdditionalItems; - set => this.Target.AdditionalItems = value; + set => this.Resolved.AdditionalItems = value; } public override int? MaxItems { get => this.Target?.MaxItems; - set => this.Target.MaxItems = value; + set => this.Resolved.MaxItems = value; } public override int? MinItems { get => this.Target?.MinItems; - set => this.Target.MinItems = value; + set => this.Resolved.MinItems = value; } public override bool? UniqueItems { get => this.Target?.UniqueItems; - set => this.Target.UniqueItems = value; + set => this.Resolved.UniqueItems = value; } public override IDictionary Properties { get => this.Target?.Properties; - set => this.Target.Properties = value; + set => this.Resolved.Properties = value; } public override int? MaxProperties { get => this.Target?.MaxProperties; - set => this.Target.MaxProperties = value; + set => this.Resolved.MaxProperties = value; } public override int? MinProperties { get => this.Target?.MinProperties; - set => this.Target.MinProperties = value; + set => this.Resolved.MinProperties = value; } public override AsyncApiJsonSchema AdditionalProperties { get => this.Target?.AdditionalProperties; - set => this.Target.AdditionalProperties = value; + set => this.Resolved.AdditionalProperties = value; } public override IDictionary PatternProperties { get => this.Target?.PatternProperties; - set => this.Target.PatternProperties = value; + set => this.Resolved.PatternProperties = value; } public override AsyncApiJsonSchema PropertyNames { get => this.Target?.PropertyNames; - set => this.Target.PropertyNames = value; + set => this.Resolved.PropertyNames = value; } public override string Discriminator { get => this.Target?.Discriminator; - set => this.Target.Discriminator = value; + set => this.Resolved.Discriminator = value; } public override IList Enum { get => this.Target?.Enum; - set => this.Target.Enum = value; + set => this.Resolved.Enum = value; } public override IList Examples { get => this.Target?.Examples; - set => this.Target.Examples = value; + set => this.Resolved.Examples = value; } public override AsyncApiAny Const { get => this.Target?.Const; - set => this.Target.Const = value; + set => this.Resolved.Const = value; } public override bool Nullable { - get => this.Target.Nullable; - set => this.Target.Nullable = value; + get => this.Target?.Nullable ?? false; + set => this.Resolved.Nullable = value; } public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; - set => this.Target.ExternalDocs = value; + set => this.Resolved.ExternalDocs = value; } public override bool Deprecated { - get => this.Target.Deprecated; - set => this.Target.Deprecated = value; + get => this.Target?.Deprecated ?? false; + set => this.Resolved.Deprecated = value; } public override IDictionary Extensions { get => this.Target?.Extensions; - set => this.Target.Extensions = value; + set => this.Resolved.Extensions = value; } public static bool operator !=(AsyncApiJsonSchemaReference left, AsyncApiJsonSchemaReference right) => !(left == right); @@ -309,6 +311,11 @@ public bool Equals(AsyncApiJsonSchemaReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -329,7 +336,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiJsonSchema other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -355,7 +367,7 @@ public override void SerializeV2(IAsyncApiWriter writer) return; } - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } public override void SerializeV3(IAsyncApiWriter writer) @@ -381,7 +393,7 @@ public override void SerializeV3(IAsyncApiWriter writer) return; } - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs index fd2099a..7eb46a4 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs @@ -14,6 +14,8 @@ public class AsyncApiMessageReference : AsyncApiMessage, IAsyncApiReferenceable, { private AsyncApiMessage target; + private AsyncApiMessage Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiMessage Target { get @@ -28,33 +30,33 @@ public AsyncApiMessageReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Message); } - public override AsyncApiMultiFormatSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } + public override AsyncApiMultiFormatSchema Headers { get => this.Target?.Headers; set => this.Resolved.Headers = value; } - public override AsyncApiMultiFormatSchema Payload { get => this.Target?.Payload; set => this.Target.Payload = value; } + public override AsyncApiMultiFormatSchema Payload { get => this.Target?.Payload; set => this.Resolved.Payload = value; } - public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Target.CorrelationId = value; } + public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Resolved.CorrelationId = value; } - public override string ContentType { get => this.Target?.ContentType; set => this.Target.ContentType = value; } + public override string ContentType { get => this.Target?.ContentType; set => this.Resolved.ContentType = value; } - public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + public override string Name { get => this.Target?.Name; set => this.Resolved.Name = value; } - public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + public override string Title { get => this.Target?.Title; set => this.Resolved.Title = value; } - public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + public override string Summary { get => this.Target?.Summary; set => this.Resolved.Summary = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Resolved.Tags = value; } - public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Resolved.ExternalDocs = value; } - public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Resolved.Bindings = value; } - public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } + public override IList Examples { get => this.Target?.Examples; set => this.Resolved.Examples = value; } - public override IList Traits { get => this.Target?.Traits; set => this.Target.Traits = value; } + public override IList Traits { get => this.Target?.Traits; set => this.Resolved.Traits = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -79,6 +81,11 @@ public bool Equals(AsyncApiMessageReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -99,7 +106,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiMessage other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -127,7 +139,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -141,7 +153,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs index ba950df..2691d11 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs @@ -15,6 +15,8 @@ public class AsyncApiMessageTraitReference : AsyncApiMessageTrait, IAsyncApiRefe { private AsyncApiMessageTrait target; + private AsyncApiMessageTrait Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiMessageTrait Target { get @@ -29,29 +31,29 @@ public AsyncApiMessageTraitReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.MessageTrait); } - public override AsyncApiMultiFormatSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } + public override AsyncApiMultiFormatSchema Headers { get => this.Target?.Headers; set => this.Resolved.Headers = value; } - public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Target.CorrelationId = value; } + public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Resolved.CorrelationId = value; } - public override string ContentType { get => this.Target?.ContentType; set => this.Target.ContentType = value; } + public override string ContentType { get => this.Target?.ContentType; set => this.Resolved.ContentType = value; } - public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + public override string Name { get => this.Target?.Name; set => this.Resolved.Name = value; } - public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + public override string Title { get => this.Target?.Title; set => this.Resolved.Title = value; } - public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + public override string Summary { get => this.Target?.Summary; set => this.Resolved.Summary = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Resolved.Tags = value; } - public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Resolved.ExternalDocs = value; } - public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Resolved.Bindings = value; } - public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } + public override IList Examples { get => this.Target?.Examples; set => this.Resolved.Examples = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -76,6 +78,11 @@ public bool Equals(AsyncApiMessageTraitReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -96,7 +103,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiMessageTrait other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -109,7 +121,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -123,7 +135,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs index 9e165a4..a61239b 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs @@ -13,6 +13,8 @@ public class AsyncApiMultiFormatSchemaReference : AsyncApiMultiFormatSchema, IAs { private AsyncApiMultiFormatSchema target; + private AsyncApiMultiFormatSchema Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiMultiFormatSchema Target { get @@ -27,9 +29,9 @@ public AsyncApiMultiFormatSchemaReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Schema); } - public override string SchemaFormat { get => this.Target?.SchemaFormat; set => this.Target.SchemaFormat = value; } + public override string SchemaFormat { get => this.Target?.SchemaFormat; set => this.Resolved.SchemaFormat = value; } - public override IAsyncApiSchema Schema { get => this.Target?.Schema; set => this.Target.Schema = value; } + public override IAsyncApiSchema Schema { get => this.Target?.Schema; set => this.Resolved.Schema = value; } public AsyncApiReference Reference { get; set; } @@ -45,7 +47,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -59,7 +61,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs index 57c7a0c..a7dd252 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs @@ -15,6 +15,8 @@ public class AsyncApiOperationReference : AsyncApiOperation, IAsyncApiReferencea { private AsyncApiOperation target; + private AsyncApiOperation Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiOperation Target { get @@ -29,31 +31,31 @@ public AsyncApiOperationReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Operation); } - public override AsyncApiAction Action { get => this.Target.Action; set => this.Target.Action = value; } + public override AsyncApiAction Action { get => this.Resolved.Action; set => this.Resolved.Action = value; } - public override AsyncApiChannelReference Channel { get => this.Target?.Channel; set => this.Target.Channel = value; } + public override AsyncApiChannelReference Channel { get => this.Target?.Channel; set => this.Resolved.Channel = value; } - public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + public override string Title { get => this.Target?.Title; set => this.Resolved.Title = value; } - public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + public override string Summary { get => this.Target?.Summary; set => this.Resolved.Summary = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } + public override IList Security { get => this.Target?.Security; set => this.Resolved.Security = value; } - public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Resolved.Tags = value; } - public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Resolved.ExternalDocs = value; } - public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Resolved.Bindings = value; } - public override IList Traits { get => this.Target?.Traits; set => this.Target.Traits = value; } + public override IList Traits { get => this.Target?.Traits; set => this.Resolved.Traits = value; } - public override IList Messages { get => this.Target?.Messages; set => this.Target.Messages = value; } + public override IList Messages { get => this.Target?.Messages; set => this.Resolved.Messages = value; } - public override AsyncApiOperationReply Reply { get => this.Target?.Reply; set => this.Target.Reply = value; } + public override AsyncApiOperationReply Reply { get => this.Target?.Reply; set => this.Resolved.Reply = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -78,6 +80,11 @@ public bool Equals(AsyncApiOperationReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -98,13 +105,18 @@ public override bool Equals(object obj) public bool Equals(AsyncApiOperation other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } public override void SerializeV3(IAsyncApiWriter writer) @@ -117,7 +129,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs index 7c6460c..1c0e852 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs @@ -11,6 +11,8 @@ public class AsyncApiOperationReplyAddressReference : AsyncApiOperationReplyAddr { private AsyncApiOperationReplyAddress target; + private AsyncApiOperationReplyAddress Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiOperationReplyAddress Target { get @@ -25,11 +27,11 @@ public AsyncApiOperationReplyAddressReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.OperationReplyAddress); } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override string Location { get => this.Target?.Location; set => this.Target.Location = value; } + public override string Location { get => this.Target?.Location; set => this.Resolved.Location = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -54,6 +56,11 @@ public bool Equals(AsyncApiOperationReplyAddressReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -74,7 +81,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiOperationReplyAddress other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV3(IAsyncApiWriter writer) @@ -87,7 +99,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs index 7570277..ca224c0 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs @@ -11,6 +11,8 @@ public class AsyncApiOperationReplyReference : AsyncApiOperationReply, IAsyncApi { private AsyncApiOperationReply target; + private AsyncApiOperationReply Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiOperationReply Target { get @@ -25,13 +27,13 @@ public AsyncApiOperationReplyReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.OperationReply); } - public override AsyncApiOperationReplyAddress Address { get => this.Target?.Address; set => this.Target.Address = value; } + public override AsyncApiOperationReplyAddress Address { get => this.Target?.Address; set => this.Resolved.Address = value; } - public override AsyncApiChannelReference Channel { get => this.Target?.Channel; set => this.Target.Channel = value; } + public override AsyncApiChannelReference Channel { get => this.Target?.Channel; set => this.Resolved.Channel = value; } - public override IList Messages { get => this.Target?.Messages; set => this.Target.Messages = value; } + public override IList Messages { get => this.Target?.Messages; set => this.Resolved.Messages = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -56,6 +58,11 @@ public bool Equals(AsyncApiOperationReplyReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -76,7 +83,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiOperationReply other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV3(IAsyncApiWriter writer) @@ -89,7 +101,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs index aa6c42f..93919b7 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs @@ -15,6 +15,8 @@ public class AsyncApiOperationTraitReference : AsyncApiOperationTrait, IAsyncApi { private AsyncApiOperationTrait target; + private AsyncApiOperationTrait Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiOperationTrait Target { get @@ -29,21 +31,21 @@ public AsyncApiOperationTraitReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.OperationTrait); } - public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + public override string Title { get => this.Target?.Title; set => this.Resolved.Title = value; } - public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + public override string Summary { get => this.Target?.Summary; set => this.Resolved.Summary = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } + public override IList Security { get => this.Target?.Security; set => this.Resolved.Security = value; } - public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Resolved.Tags = value; } - public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Resolved.ExternalDocs = value; } - public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Resolved.Bindings = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -68,6 +70,11 @@ public bool Equals(AsyncApiOperationTraitReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -88,7 +95,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiOperationTrait other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -101,7 +113,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -115,7 +127,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs index 9c5562f..ebcae8b 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs @@ -14,6 +14,8 @@ public class AsyncApiParameterReference : AsyncApiParameter, IAsyncApiReferencea { private AsyncApiParameter target; + private AsyncApiParameter Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiParameter Target { get @@ -28,17 +30,17 @@ public AsyncApiParameterReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Parameter); } - public override IList Enum { get => this.Target?.Enum; set => this.Target.Enum = value; } + public override IList Enum { get => this.Target?.Enum; set => this.Resolved.Enum = value; } - public override string Default { get => this.Target?.Default; set => this.Target.Default = value; } + public override string Default { get => this.Target?.Default; set => this.Resolved.Default = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } + public override IList Examples { get => this.Target?.Examples; set => this.Resolved.Examples = value; } - public override string Location { get => this.Target?.Location; set => this.Target.Location = value; } + public override string Location { get => this.Target?.Location; set => this.Resolved.Location = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -63,6 +65,11 @@ public bool Equals(AsyncApiParameterReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -83,7 +90,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiParameter other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -96,7 +108,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -110,7 +122,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs index 55d8fd8..4a8230f 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs @@ -12,6 +12,8 @@ public class AsyncApiSecuritySchemeReference : AsyncApiSecurityScheme, IAsyncApi { private AsyncApiSecurityScheme target; + private AsyncApiSecurityScheme Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiSecurityScheme Target { get @@ -26,25 +28,25 @@ public AsyncApiSecuritySchemeReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.SecurityScheme); } - public override SecuritySchemeType Type { get => this.Target.Type; set => this.Target.Type = value; } + public override SecuritySchemeType Type { get => this.Resolved.Type; set => this.Resolved.Type = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + public override string Name { get => this.Target?.Name; set => this.Resolved.Name = value; } - public override ParameterLocation? In { get => this.Target?.In; set => this.Target.In = value; } + public override ParameterLocation? In { get => this.Target?.In; set => this.Resolved.In = value; } - public override string Scheme { get => this.Target?.Scheme; set => this.Target.Scheme = value; } + public override string Scheme { get => this.Target?.Scheme; set => this.Resolved.Scheme = value; } - public override string BearerFormat { get => this.Target?.BearerFormat; set => this.Target.BearerFormat = value; } + public override string BearerFormat { get => this.Target?.BearerFormat; set => this.Resolved.BearerFormat = value; } - public override AsyncApiOAuthFlows Flows { get => this.Target?.Flows; set => this.Target.Flows = value; } + public override AsyncApiOAuthFlows Flows { get => this.Target?.Flows; set => this.Resolved.Flows = value; } - public override Uri OpenIdConnectUrl { get => this.Target?.OpenIdConnectUrl; set => this.Target.OpenIdConnectUrl = value; } + public override Uri OpenIdConnectUrl { get => this.Target?.OpenIdConnectUrl; set => this.Resolved.OpenIdConnectUrl = value; } - public override ISet Scopes { get => this.Target?.Scopes; set => this.Target.Scopes = value; } + public override ISet Scopes { get => this.Target?.Scopes; set => this.Resolved.Scopes = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -60,7 +62,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -74,7 +76,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs index 50d8b8b..8932580 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs @@ -14,6 +14,8 @@ public class AsyncApiServerReference : AsyncApiServer, IAsyncApiReferenceable, I { private AsyncApiServer target; + private AsyncApiServer Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiServer Target { get @@ -28,29 +30,29 @@ public AsyncApiServerReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Server); } - public override string Host { get => this.Target?.Host; set => this.Target.Host = value; } + public override string Host { get => this.Target?.Host; set => this.Resolved.Host = value; } - public override string PathName { get => this.Target?.PathName; set => this.Target.PathName = value; } + public override string PathName { get => this.Target?.PathName; set => this.Resolved.PathName = value; } - public override string Protocol { get => this.Target?.Protocol; set => this.Target.Protocol = value; } + public override string Protocol { get => this.Target?.Protocol; set => this.Resolved.Protocol = value; } - public override string ProtocolVersion { get => this.Target?.ProtocolVersion; set => this.Target.ProtocolVersion = value; } + public override string ProtocolVersion { get => this.Target?.ProtocolVersion; set => this.Resolved.ProtocolVersion = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + public override string Title { get => this.Target?.Title; set => this.Resolved.Title = value; } - public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + public override string Summary { get => this.Target?.Summary; set => this.Resolved.Summary = value; } - public override IDictionary Variables { get => this.Target?.Variables; set => this.Target.Variables = value; } + public override IDictionary Variables { get => this.Target?.Variables; set => this.Resolved.Variables = value; } - public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } + public override IList Security { get => this.Target?.Security; set => this.Resolved.Security = value; } - public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Resolved.Tags = value; } - public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Resolved.Bindings = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -75,6 +77,11 @@ public bool Equals(AsyncApiServerReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -95,7 +102,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiServer other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -108,7 +120,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -122,7 +134,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs index c625ff5..e6abebf 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs @@ -14,6 +14,8 @@ public class AsyncApiServerVariableReference : AsyncApiServerVariable, IAsyncApi { private AsyncApiServerVariable target; + private AsyncApiServerVariable Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiServerVariable Target { get @@ -28,15 +30,15 @@ public AsyncApiServerVariableReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.ServerVariable); } - public override IList Enum { get => this.Target?.Enum; set => this.Target.Enum = value; } + public override IList Enum { get => this.Target?.Enum; set => this.Resolved.Enum = value; } - public override string Default { get => this.Target?.Default; set => this.Target.Default = value; } + public override string Default { get => this.Target?.Default; set => this.Resolved.Default = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } + public override IList Examples { get => this.Target?.Examples; set => this.Resolved.Examples = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -61,6 +63,11 @@ public bool Equals(AsyncApiServerVariableReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -81,7 +88,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiServerVariable other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } public override void SerializeV2(IAsyncApiWriter writer) @@ -94,7 +106,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -108,7 +120,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs index 1b35c04..8750911 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs @@ -11,6 +11,8 @@ public class AsyncApiTagReference : AsyncApiTag, IAsyncApiReferenceable, IEquata { private AsyncApiTag target; + private AsyncApiTag Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); + private AsyncApiTag Target { get @@ -25,13 +27,13 @@ public AsyncApiTagReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Tag); } - public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + public override string Name { get => this.Target?.Name; set => this.Resolved.Name = value; } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Description { get => this.Target?.Description; set => this.Resolved.Description = value; } - public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Resolved.ExternalDocs = value; } - public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Resolved.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -56,6 +58,11 @@ public bool Equals(AsyncApiTagReference other) return this.Equals(reference); } + if (this.Target is null || other.Target is null) + { + return this.Target is null && other.Target is null && this.Reference == other.Reference; + } + return this.Target == other.Target; } @@ -76,7 +83,12 @@ public override bool Equals(object obj) public bool Equals(AsyncApiTag other) { - return this.Target == other; + return other is not null && this.Target == other; + } + + public override int GetHashCode() + { + return this.Target?.GetHashCode() ?? this.Reference?.GetHashCode() ?? 0; } /// @@ -96,7 +108,7 @@ public override void SerializeV2(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV2(writer); + this.Resolved.SerializeV2(writer); } } @@ -110,7 +122,7 @@ public override void SerializeV3(IAsyncApiWriter writer) else { this.Reference.Workspace = writer.Workspace; - this.Target.SerializeV3(writer); + this.Resolved.SerializeV3(writer); } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs new file mode 100644 index 0000000..d3df707 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs @@ -0,0 +1,130 @@ +namespace ByteBard.AsyncAPI.Tests +{ + using System; + using System.IO; + using System.Linq; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Writers; + using FluentAssertions; + using NUnit.Framework; + + public class AsyncApiReferenceEquality_Should : TestBase + { + private const string RecursiveSchemaDocument = + """ + asyncapi: 3.0.0 + info: + title: test + version: 1.0.0 + channels: + first: + messages: + a: + payload: + $ref: '#/components/schemas/Node' + second: + messages: + b: + payload: + $ref: '#/components/schemas/Node' + components: + schemas: + Node: + type: object + properties: + next: + $ref: '#/components/schemas/Node' + prev: + $ref: '#/components/schemas/Node' + """; + + [Test] + public void UnresolvedReferences_AreEqualOnlyWhenTheyPointAtTheSameComponent() + { + var first = new AsyncApiChannelReference("#/components/channels/first"); + var firstAgain = new AsyncApiChannelReference("#/components/channels/first"); + var second = new AsyncApiChannelReference("#/components/channels/second"); + + first.Should().Be(firstAgain); + (first == firstAgain).Should().BeTrue(); + first.GetHashCode().Should().Be(firstAgain.GetHashCode()); + + first.Should().NotBe(second); + (first == second).Should().BeFalse(); + first.Equals((AsyncApiChannel)null).Should().BeFalse(); + } + + [Test] + public void ResolvedReferences_EqualTheirTargetAndShareItsHashCode() + { + var document = new AsyncApiStringReader().Read(RecursiveSchemaDocument, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + + var target = (AsyncApiJsonSchema)document.Components.Schemas["Node"].Schema; + var first = (AsyncApiJsonSchemaReference)target.Properties["next"]; + var second = (AsyncApiJsonSchemaReference)target.Properties["prev"]; + + first.Should().NotBeSameAs(second); + first.Should().Be(second); + first.Equals(target).Should().BeTrue(); + first.GetHashCode().Should().Be(second.GetHashCode()).And.Be(target.GetHashCode()); + new[] { first, second }.Distinct().Should().ContainSingle(); + } + + [Test] + public void SerializingWithInlinedReferences_DoesNotFailWhenTheSameLoopIsReachedTwice() + { + var yaml = + """ + asyncapi: 2.6.0 + info: + title: test + version: 1.0.0 + channels: + first: + publish: + message: + payload: + $ref: '#/components/schemas/Node' + components: + schemas: + Node: + type: object + properties: + next: + $ref: '#/components/schemas/Node' + prev: + $ref: '#/components/schemas/Node' + """; + var document = new AsyncApiStringReader().Read(yaml, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + + var output = new StringWriter(); + var writer = new AsyncApiYamlWriter(output, new AsyncApiWriterSettings { ReferenceInline = ReferenceInlineSetting.InlineReferences }); + + var act = () => document.SerializeV2(writer); + + act.Should().NotThrow(); + } + + [Test] + public void SettingAPropertyOnAnUnresolvedReference_ThrowsWithTheReference() + { + var reference = new AsyncApiChannelReference("#/components/channels/missing"); + + var act = () => reference.Address = "users"; + + act.Should().Throw().WithMessage("*#/components/channels/missing*"); + } + + [Test] + public void ReadingAFlagOnAnUnresolvedSchemaReference_ReturnsFalse() + { + var reference = new AsyncApiJsonSchemaReference("#/components/schemas/missing"); + + reference.ReadOnly.Should().BeFalse(); + reference.Deprecated.Should().BeFalse(); + } + } +} From 2bb38df81cfe01d9f5fc81863a3349f0c4119534 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:31:04 +0100 Subject: [PATCH 2/4] build: treat Equals without GetHashCode as an error Co-Authored-By: Claude Opus 5.5 --- Directory.Build.props | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..a852604 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,6 @@ + + + + $(WarningsAsErrors);CS0659;CS0661 + + From 42f01a72426907864c469401bd150ae53773ed3f Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:31:24 +0100 Subject: [PATCH 3/4] chore: drop nullable annotations outside a nullable context (CS8632) Co-Authored-By: Claude Opus 5.5 --- src/ByteBard.AsyncAPI.Bindings/Sns/Statement.cs | 2 +- src/ByteBard.AsyncAPI.Bindings/Sqs/Statement.cs | 2 +- src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs | 2 +- src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/Sns/Statement.cs b/src/ByteBard.AsyncAPI.Bindings/Sns/Statement.cs index 3cfa073..474bb05 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Sns/Statement.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Sns/Statement.cs @@ -26,7 +26,7 @@ public class Statement : IAsyncApiExtensible /// /// The resource(s) that this policy applies to. /// - public StringOrStringList? Resource { get; set; } + public StringOrStringList Resource { get; set; } /// /// Specific circumstances under which the policy grants permission. diff --git a/src/ByteBard.AsyncAPI.Bindings/Sqs/Statement.cs b/src/ByteBard.AsyncAPI.Bindings/Sqs/Statement.cs index 0602fb8..43e7827 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Sqs/Statement.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Sqs/Statement.cs @@ -26,7 +26,7 @@ public class Statement : IAsyncApiExtensible /// /// The resource(s) that this policy applies to. /// - public StringOrStringList? Resource { get; set; } + public StringOrStringList Resource { get; set; } /// /// Specific circumstances under which the policy grants permission. diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 9881dd0..6f8a635 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -73,7 +73,7 @@ public ICollection> /// /// Uri where relative references should be resolved from when using the External Reference Loader. /// - public Uri? BaseUri { get; set; } + public Uri BaseUri { get; set; } /// /// External reference reader implementation provided by users for reading external resources. diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs index 87b0a25..42425aa 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs @@ -15,7 +15,7 @@ public class AsyncApiChannel : IAsyncApiSerializable, IAsyncApiExtensible /// /// An optional string representation of this channel's address. The address is typically the "topic name", "routing key", "event type", or "path". When null or absent, it MUST be interpreted as unknown. This is useful when the address is generated dynamically at runtime or can't be known upfront. It MAY contain Channel Address Expressions. Query parameters and fragments SHALL NOT be used, instead use bindings to define them. /// - public virtual string? Address { get; set; } + public virtual string Address { get; set; } /// /// A map of the messages that will be sent to this channel by any application at any time. Every message sent to this channel MUST be valid against one, and only one, of the message objects defined in this map. From eecc609e88ddede07a1acbf670f9287fcf016ac0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:22:31 +0100 Subject: [PATCH 4/4] refactor: resolve bindings reference through Target only Nothing in AsyncApiBindingsReference reads Target in a way that allows null, so the throw moves into Target and the extra Resolved property goes. Extensions now goes through Target too instead of the unresolved field, so it resolves the reference and fails clearly when it can't. Co-Authored-By: Claude Opus 5.5 --- .../AsyncApiBindingsReference{TBinding}.cs | 40 +++++++++---------- .../AsyncApiReferenceEquality_Should.cs | 11 +++++ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs index 73d75a5..b8fcf9f 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs @@ -16,31 +16,29 @@ public class AsyncApiBindingsReference : AsyncApiBindings, I private AsyncApiBindings target; - private AsyncApiBindings Resolved => this.Target ?? throw this.Reference.CreateUnresolvedException(); - private AsyncApiBindings Target { get { this.target ??= this.Reference.Workspace?.ResolveReference>(this.Reference); - return this.target; + return this.target ?? throw this.Reference.CreateUnresolvedException(); } } public override void Add(TBinding binding) { - this.Resolved.Add(binding); + this.Target.Add(binding); } - public override ICollection Keys => this.Resolved.Keys; + public override ICollection Keys => this.Target.Keys; - public override ICollection Values => this.Resolved.Values; + public override ICollection Values => this.Target.Values; - public override IDictionary Extensions => this.target.Extensions; + public override IDictionary Extensions => this.Target.Extensions; - public override int Count => this.Resolved.Count; + public override int Count => this.Target.Count; - public override bool IsReadOnly => this.Resolved.IsReadOnly; + public override bool IsReadOnly => this.Target.IsReadOnly; public AsyncApiBindingsReference(string reference) { @@ -86,7 +84,7 @@ public override void SerializeV2(IAsyncApiWriter writer) return; } - this.Resolved.SerializeV2(writer); + this.Target.SerializeV2(writer); } public override void SerializeV3(IAsyncApiWriter writer) @@ -102,57 +100,57 @@ public override void SerializeV3(IAsyncApiWriter writer) return; } - this.Resolved.SerializeV3(writer); + this.Target.SerializeV3(writer); } public override void Add(string key, TBinding value) { - this.Resolved.Add(key, value); + this.Target.Add(key, value); } public override bool ContainsKey(string key) { - return this.Resolved.ContainsKey(key); + return this.Target.ContainsKey(key); } public override bool Remove(string key) { - return this.Resolved.Remove(key); + return this.Target.Remove(key); } public override bool TryGetValue(string key, out TBinding value) { - return this.Resolved.TryGetValue(key, out value); + return this.Target.TryGetValue(key, out value); } public override void Add(KeyValuePair item) { - this.Resolved.Add(item); + this.Target.Add(item); } public override void Clear() { - this.Resolved.Clear(); + this.Target.Clear(); } public override bool Contains(KeyValuePair item) { - return this.Resolved.Contains(item); + return this.Target.Contains(item); } public override void CopyTo(KeyValuePair[] array, int arrayIndex) { - this.Resolved.CopyTo(array, arrayIndex); + this.Target.CopyTo(array, arrayIndex); } public override bool Remove(KeyValuePair item) { - return this.Resolved.Remove(item); + return this.Target.Remove(item); } public override IEnumerator> GetEnumerator() { - return this.Resolved.GetEnumerator(); + return this.Target.GetEnumerator(); } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs index d3df707..8456a3c 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReferenceEquality_Should.cs @@ -4,6 +4,7 @@ namespace ByteBard.AsyncAPI.Tests using System.IO; using System.Linq; using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers; using ByteBard.AsyncAPI.Writers; using FluentAssertions; @@ -118,6 +119,16 @@ public void SettingAPropertyOnAnUnresolvedReference_ThrowsWithTheReference() act.Should().Throw().WithMessage("*#/components/channels/missing*"); } + [Test] + public void ReadingExtensionsOnAnUnresolvedBindingsReference_ThrowsWithTheReference() + { + var reference = new AsyncApiBindingsReference("#/components/channelBindings/missing"); + + var act = () => reference.Extensions; + + act.Should().Throw().WithMessage("*#/components/channelBindings/missing*"); + } + [Test] public void ReadingAFlagOnAnUnresolvedSchemaReference_ReturnsFalse() {