From 00db9a5ec3099f9dc369f74dfc683a8fe08729bc Mon Sep 17 00:00:00 2001 From: Gavin Barron Date: Wed, 19 Aug 2026 05:36:21 -0700 Subject: [PATCH 1/3] fix: circular ref guard (#3033) * fix: harden yaml parsing * fix tests and nesting behavior on streams with SharpYaml update * CodeQL fixes * update benchmarks * fix: circular ref guard * update benchmarks * fix: harden yaml parsing * fix tests and nesting behavior on streams with SharpYaml update * CodeQL fixes * update benchmarks * fix: circular ref guard * update benchmarks * codeql updates Signed-off-by: Vincent Biret --- .../References/BaseOpenApiReferenceHolder.cs | 130 +++++++- .../References/OpenApiCallbackReference.cs | 4 +- .../References/OpenApiExampleReference.cs | 10 +- .../References/OpenApiHeaderReference.cs | 24 +- .../Models/References/OpenApiLinkReference.cs | 14 +- .../References/OpenApiParameterReference.cs | 28 +- .../References/OpenApiPathItemReference.cs | 12 +- .../References/OpenApiRequestBodyReference.cs | 12 +- .../References/OpenApiResponseReference.cs | 10 +- .../References/OpenApiSchemaReference.cs | 124 ++++---- .../OpenApiSecuritySchemeReference.cs | 18 +- .../Models/References/OpenApiTagReference.cs | 8 +- .../BaseOpenApiReferenceHolderTests.cs | 116 +++++++ .../References/OpenApiSchemaReferenceTests.cs | 299 ++++++++++++++++++ 14 files changed, 664 insertions(+), 145 deletions(-) create mode 100644 test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs diff --git a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs index d28e9dd8f..058a8caa1 100644 --- a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs +++ b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Microsoft.OpenApi; /// @@ -10,6 +11,11 @@ namespace Microsoft.OpenApi; /// The type for the reference holding the additional fields and annotations public abstract class BaseOpenApiReferenceHolder : IOpenApiReferenceHolder where T : class, IOpenApiReferenceable, U where U : IOpenApiReferenceable, IOpenApiSerializable where V : BaseOpenApiReference, new() { + [ThreadStatic] + private static HashSet>? t_activeReferenceAccesses; + [ThreadStatic] + private static HashSet>? t_activeTargetActions; + /// public virtual U? Target { @@ -19,28 +25,129 @@ public virtual U? Target return Reference.HostDocument.ResolveReferenceTo(Reference, this as IOpenApiSchema); } } + + /// + /// Gets a value from the resolved target while detecting cycles in delegated member access. + /// + /// The type of value to get from the target. + /// Selects the value from the resolved target. + /// The selected value, or the default value when the target cannot be resolved. + /// + /// The guard remains active while reads the target member. This covers + /// the complete delegated call chain without changing the immediate-resolution semantics of + /// or walking an acyclic chain more than once. + /// + private protected TResult GetFromTarget(Func selector) + { + Utils.CheckArgumentNull(selector); + return ExecuteWithReferenceAccessGuard(this, () => + { + return Target is { } target ? selector(target) : default!; + }); + } + + /// + /// Executes an action against the resolved target while detecting cycles in delegated access. + /// + /// The action to execute against the resolved target. + private protected void ApplyToTarget(Action action) + { + Utils.CheckArgumentNull(action); + ExecuteWithTargetActionGuard(this, () => + { + if (Target is { } target) + { + action(target); + } + return null; + }); + } + + private static TResult ExecuteWithReferenceAccessGuard( + BaseOpenApiReferenceHolder holder, + Func action) + { + return ExecuteWithReferenceGuard(ref t_activeReferenceAccesses, holder, action); + } + + private static TResult ExecuteWithTargetActionGuard( + BaseOpenApiReferenceHolder holder, + Func action) + { + return ExecuteWithReferenceGuard(ref t_activeTargetActions, holder, action); + } + + private static TResult ExecuteWithReferenceGuard( + ref HashSet>? activeReferences, + BaseOpenApiReferenceHolder holder, + Func action) + { + activeReferences ??= new HashSet>(ReferenceHolderComparer.Instance); + if (!activeReferences.Add(holder)) + { + throw new InvalidOperationException($"Circular reference detected while resolving reference: {holder.Reference.ReferenceV3}"); + } + + try + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + return action(); + } + catch (InsufficientExecutionStackException ex) + { + throw new InvalidOperationException( + $"The chain of references starting at {holder.Reference.ReferenceV3} is nested too deeply to resolve.", + ex); + } + finally + { + activeReferences.Remove(holder); + if (activeReferences.Count == 0) + { + activeReferences = null; + } + } + } + /// public T? RecursiveTarget { get { - return ResolveRecursiveTarget(new HashSet>()); + var visitedReferences = new HashSet>(ReferenceHolderComparer.Instance); + BaseOpenApiReferenceHolder current = this; + + while (visitedReferences.Add(current)) + { + switch (current.Target) + { + case BaseOpenApiReferenceHolder recursiveTarget: + current = recursiveTarget; + break; + case T concrete: + return concrete; + default: + return null; + } + } + + throw new InvalidOperationException($"Circular reference detected while resolving reference: {current.Reference.ReferenceV3}"); } } - private T? ResolveRecursiveTarget(ISet> visitedReferences) + private sealed class ReferenceHolderComparer : IEqualityComparer> { - if (!visitedReferences.Add(this)) + internal static ReferenceHolderComparer Instance { get; } = new(); + + public bool Equals(BaseOpenApiReferenceHolder? x, BaseOpenApiReferenceHolder? y) { - throw new InvalidOperationException($"Circular reference detected while resolving reference: {Reference.ReferenceV3}"); + return ReferenceEquals(x, y); } - return Target switch + public int GetHashCode(BaseOpenApiReferenceHolder obj) { - BaseOpenApiReferenceHolder recursiveTarget => recursiveTarget.ResolveRecursiveTarget(visitedReferences), - T concrete => concrete, - _ => null - }; + return RuntimeHelpers.GetHashCode(obj); + } } /// /// Copy the reference as a target element with overrides. @@ -147,9 +254,6 @@ private protected void SerializeInternal(IOpenApiWriter writer, Action action) { Utils.CheckArgumentNull(writer); - if (Target is not null) - { - action(writer, Target); - } + ApplyToTarget(element => action(writer, element)); } } diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiCallbackReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiCallbackReference.cs index bdf22eb03..cd6cfdeff 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiCallbackReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiCallbackReference.cs @@ -33,10 +33,10 @@ private OpenApiCallbackReference(OpenApiCallbackReference callback) : base(callb } /// - public Dictionary? PathItems { get => Target?.PathItems; } + public Dictionary? PathItems { get => GetFromTarget(static target => target.PathItems); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// public override IOpenApiCallback CopyReferenceAsTargetElementWithOverrides(IOpenApiCallback source) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiExampleReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiExampleReference.cs index a616975bf..b0743588e 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiExampleReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiExampleReference.cs @@ -35,25 +35,25 @@ private OpenApiExampleReference(OpenApiExampleReference example) : base(example) /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// public string? Summary { - get => string.IsNullOrEmpty(Reference.Summary) ? Target?.Summary : Reference.Summary; + get => string.IsNullOrEmpty(Reference.Summary) ? GetFromTarget(static target => target.Summary) : Reference.Summary; set => Reference.Summary = value; } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// - public string? ExternalValue { get => Target?.ExternalValue; } + public string? ExternalValue { get => GetFromTarget(static target => target.ExternalValue); } /// - public JsonNode? Value { get => Target?.Value; } + public JsonNode? Value { get => GetFromTarget(static target => target.Value); } /// public override IOpenApiExample CopyReferenceAsTargetElementWithOverrides(IOpenApiExample source) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs index 838b029d4..e7f8eacaa 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs @@ -36,42 +36,42 @@ private OpenApiHeaderReference(OpenApiHeaderReference header) : base(header) /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// - public bool Required { get => Target?.Required ?? default; } + public bool Required { get => GetFromTarget(static target => target.Required); } /// - public bool Deprecated { get => Target?.Deprecated ?? default; } + public bool Deprecated { get => GetFromTarget(static target => target.Deprecated); } /// - public bool AllowEmptyValue { get => Target?.AllowEmptyValue ?? default; } + public bool AllowEmptyValue { get => GetFromTarget(static target => target.AllowEmptyValue); } /// - public IOpenApiSchema? Schema { get => Target?.Schema; } + public IOpenApiSchema? Schema { get => GetFromTarget(static target => target.Schema); } /// - public ParameterStyle? Style { get => Target?.Style; } + public ParameterStyle? Style { get => GetFromTarget(static target => target.Style); } /// - public bool Explode { get => Target?.Explode ?? default; } + public bool Explode { get => GetFromTarget(static target => target.Explode); } /// - public bool AllowReserved { get => Target?.AllowReserved ?? default; } + public bool AllowReserved { get => GetFromTarget(static target => target.AllowReserved); } /// - public JsonNode? Example { get => Target?.Example; } + public JsonNode? Example { get => GetFromTarget(static target => target.Example); } /// - public IDictionary? Examples { get => Target?.Examples; } + public IDictionary? Examples { get => GetFromTarget(static target => target.Examples); } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => GetFromTarget(static target => target.Content); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// public override IOpenApiHeader CopyReferenceAsTargetElementWithOverrides(IOpenApiHeader source) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiLinkReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiLinkReference.cs index a11decf63..2e338493c 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiLinkReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiLinkReference.cs @@ -34,27 +34,27 @@ private OpenApiLinkReference(OpenApiLinkReference reference) : base(reference) /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// - public string? OperationRef { get => Target?.OperationRef; } + public string? OperationRef { get => GetFromTarget(static target => target.OperationRef); } /// - public string? OperationId { get => Target?.OperationId; } + public string? OperationId { get => GetFromTarget(static target => target.OperationId); } /// - public OpenApiServer? Server { get => Target?.Server; } + public OpenApiServer? Server { get => GetFromTarget(static target => target.Server); } /// - public IDictionary? Parameters { get => Target?.Parameters; } + public IDictionary? Parameters { get => GetFromTarget(static target => target.Parameters); } /// - public RuntimeExpressionAnyWrapper? RequestBody { get => Target?.RequestBody; } + public RuntimeExpressionAnyWrapper? RequestBody { get => GetFromTarget(static target => target.RequestBody); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// public override void SerializeAsV2(IOpenApiWriter writer) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs index a2ebdb1b6..ccc947088 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs @@ -36,51 +36,51 @@ private OpenApiParameterReference(OpenApiParameterReference parameter):base(para } /// - public string? Name { get => Target?.Name; } + public string? Name { get => GetFromTarget(static target => target.Name); } /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// - public bool Required { get => Target?.Required ?? default; } + public bool Required { get => GetFromTarget(static target => target.Required); } /// - public bool Deprecated { get => Target?.Deprecated ?? default; } + public bool Deprecated { get => GetFromTarget(static target => target.Deprecated); } /// [Obsolete("Use of AllowEmptyValue is not recommended and it is likely to be removed in a later revision.")] - public bool AllowEmptyValue { get => Target?.AllowEmptyValue ?? default; } + public bool AllowEmptyValue { get => GetFromTarget(static target => target.AllowEmptyValue); } /// - public bool AllowReserved { get => Target?.AllowReserved ?? default; } + public bool AllowReserved { get => GetFromTarget(static target => target.AllowReserved); } /// - public IOpenApiSchema? Schema { get => Target?.Schema; } + public IOpenApiSchema? Schema { get => GetFromTarget(static target => target.Schema); } /// - public IDictionary? Examples { get => Target?.Examples; } + public IDictionary? Examples { get => GetFromTarget(static target => target.Examples); } /// - public JsonNode? Example { get => Target?.Example; } + public JsonNode? Example { get => GetFromTarget(static target => target.Example); } /// - public ParameterLocation? In { get => Target?.In; } + public ParameterLocation? In { get => GetFromTarget(static target => target.In); } /// - public ParameterStyle? Style { get => Target?.Style; } + public ParameterStyle? Style { get => GetFromTarget(static target => target.Style); } /// - public bool Explode { get => Target?.Explode ?? default; } + public bool Explode { get => GetFromTarget(static target => target.Explode); } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => GetFromTarget(static target => target.Content); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// public override IOpenApiParameter CopyReferenceAsTargetElementWithOverrides(IOpenApiParameter source) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiPathItemReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiPathItemReference.cs index 291c75308..9df1dd719 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiPathItemReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiPathItemReference.cs @@ -38,28 +38,28 @@ private OpenApiPathItemReference(OpenApiPathItemReference pathItem) : base(pathI /// public string? Summary { - get => string.IsNullOrEmpty(Reference.Summary) ? Target?.Summary : Reference.Summary; + get => string.IsNullOrEmpty(Reference.Summary) ? GetFromTarget(static target => target.Summary) : Reference.Summary; set => Reference.Summary = value; } /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// - public Dictionary? Operations { get => Target?.Operations; } + public Dictionary? Operations { get => GetFromTarget(static target => target.Operations); } /// - public IList? Servers { get => Target?.Servers; } + public IList? Servers { get => GetFromTarget(static target => target.Servers); } /// - public IList? Parameters { get => Target?.Parameters; } + public IList? Parameters { get => GetFromTarget(static target => target.Parameters); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// public override IOpenApiPathItem CopyReferenceAsTargetElementWithOverrides(IOpenApiPathItem source) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs index a23d325d7..5dea45316 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs @@ -36,18 +36,18 @@ private OpenApiRequestBodyReference(OpenApiRequestBodyReference openApiRequestBo /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => GetFromTarget(static target => target.Content); } /// - public bool Required { get => Target?.Required ?? false; } + public bool Required { get => GetFromTarget(static target => target.Required); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// public override IOpenApiRequestBody CopyReferenceAsTargetElementWithOverrides(IOpenApiRequestBody source) @@ -64,7 +64,7 @@ public override void SerializeAsV2(IOpenApiWriter writer) { if (writer.GetSettings().ShouldInlineReference(Reference)) { - return Target?.ConvertToBodyParameter(writer); + return GetFromTarget(target => target.ConvertToBodyParameter(writer)); } return Reference.Id is not null ? new OpenApiParameterReference(Reference.Id, Reference.HostDocument) : null; @@ -74,7 +74,7 @@ public override void SerializeAsV2(IOpenApiWriter writer) { if (writer.GetSettings().ShouldInlineReference(Reference)) { - return Target?.ConvertToFormDataParameters(writer); + return GetFromTarget(target => target.ConvertToFormDataParameters(writer)); } if (Content == null || !Content.Any()) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs index f76ddea15..50fb0c1a8 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs @@ -35,21 +35,21 @@ private OpenApiResponseReference(OpenApiResponseReference openApiResponseReferen /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => GetFromTarget(static target => target.Content); } /// - public IDictionary? Headers { get => Target?.Headers; } + public IDictionary? Headers { get => GetFromTarget(static target => target.Headers); } /// - public IDictionary? Links { get => Target?.Links; } + public IDictionary? Links { get => GetFromTarget(static target => target.Links); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// public override IOpenApiResponse CopyReferenceAsTargetElementWithOverrides(IOpenApiResponse source) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs index 427541a78..6453e7475 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs @@ -62,163 +62,163 @@ public override IOpenApiSchema? Target /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// public string? Title { - get => string.IsNullOrEmpty(Reference.Title) ? Target?.Title : Reference.Title; + get => string.IsNullOrEmpty(Reference.Title) ? GetFromTarget(static target => target.Title) : Reference.Title; set => Reference.Title = value; } /// - public Uri? Schema { get => Reference.Schema ?? Target?.Schema; set => Reference.Schema = value; } + public Uri? Schema { get => Reference.Schema ?? GetFromTarget(static target => target.Schema); set => Reference.Schema = value; } /// - public string? Id { get => string.IsNullOrEmpty(Reference.SchemaId) ? Target?.Id : Reference.SchemaId; set => Reference.SchemaId = value; } + public string? Id { get => string.IsNullOrEmpty(Reference.SchemaId) ? GetFromTarget(static target => target.Id) : Reference.SchemaId; set => Reference.SchemaId = value; } /// - public string? Comment { get => string.IsNullOrEmpty(Reference.Comment) ? Target?.Comment : Reference.Comment; set => Reference.Comment = value; } + public string? Comment { get => string.IsNullOrEmpty(Reference.Comment) ? GetFromTarget(static target => target.Comment) : Reference.Comment; set => Reference.Comment = value; } /// - public IDictionary? Vocabulary { get => Reference.Vocabulary ?? Target?.Vocabulary; set => Reference.Vocabulary = value; } + public IDictionary? Vocabulary { get => Reference.Vocabulary ?? GetFromTarget(static target => target.Vocabulary); set => Reference.Vocabulary = value; } /// - public string? DynamicRef { get => string.IsNullOrEmpty(Reference.DynamicRef) ? Target?.DynamicRef : Reference.DynamicRef; set => Reference.DynamicRef = value; } + public string? DynamicRef { get => string.IsNullOrEmpty(Reference.DynamicRef) ? GetFromTarget(static target => target.DynamicRef) : Reference.DynamicRef; set => Reference.DynamicRef = value; } /// - public string? DynamicAnchor { get => string.IsNullOrEmpty(Reference.DynamicAnchor) ? Target?.DynamicAnchor : Reference.DynamicAnchor; set => Reference.DynamicAnchor = value; } + public string? DynamicAnchor { get => string.IsNullOrEmpty(Reference.DynamicAnchor) ? GetFromTarget(static target => target.DynamicAnchor) : Reference.DynamicAnchor; set => Reference.DynamicAnchor = value; } /// - public IDictionary? Definitions { get => Reference.Definitions ?? Target?.Definitions; set => Reference.Definitions = value; } + public IDictionary? Definitions { get => Reference.Definitions ?? GetFromTarget(static target => target.Definitions); set => Reference.Definitions = value; } /// - public string? Anchor { get => string.IsNullOrEmpty(Reference.Anchor) ? (Target as IOpenApiSchemaMissingProperties)?.Anchor : Reference.Anchor; set => Reference.Anchor = value; } + public string? Anchor { get => string.IsNullOrEmpty(Reference.Anchor) ? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.Anchor) : Reference.Anchor; set => Reference.Anchor = value; } /// - public string? ExclusiveMaximum { get => string.IsNullOrEmpty(Reference.ExclusiveMaximum) ? Target?.ExclusiveMaximum : Reference.ExclusiveMaximum; set => Reference.ExclusiveMaximum = value; } + public string? ExclusiveMaximum { get => string.IsNullOrEmpty(Reference.ExclusiveMaximum) ? GetFromTarget(static target => target.ExclusiveMaximum) : Reference.ExclusiveMaximum; set => Reference.ExclusiveMaximum = value; } /// - public string? ExclusiveMinimum { get => string.IsNullOrEmpty(Reference.ExclusiveMinimum) ? Target?.ExclusiveMinimum : Reference.ExclusiveMinimum; set => Reference.ExclusiveMinimum = value; } + public string? ExclusiveMinimum { get => string.IsNullOrEmpty(Reference.ExclusiveMinimum) ? GetFromTarget(static target => target.ExclusiveMinimum) : Reference.ExclusiveMinimum; set => Reference.ExclusiveMinimum = value; } /// - public JsonSchemaType? Type { get => Reference.SchemaType ?? Target?.Type; set => Reference.SchemaType = value; } + public JsonSchemaType? Type { get => Reference.SchemaType ?? GetFromTarget(static target => target.Type); set => Reference.SchemaType = value; } /// - public string? Const { get => Reference.WasConstExplicitlySet ? Reference.Const : Target?.Const; set => Reference.Const = value; } + public string? Const { get => Reference.WasConstExplicitlySet ? Reference.Const : GetFromTarget(static target => target.Const); set => Reference.Const = value; } /// - public string? Format { get => string.IsNullOrEmpty(Reference.Format) ? Target?.Format : Reference.Format; set => Reference.Format = value; } + public string? Format { get => string.IsNullOrEmpty(Reference.Format) ? GetFromTarget(static target => target.Format) : Reference.Format; set => Reference.Format = value; } /// - public string? Maximum { get => string.IsNullOrEmpty(Reference.Maximum) ? Target?.Maximum : Reference.Maximum; set => Reference.Maximum = value; } + public string? Maximum { get => string.IsNullOrEmpty(Reference.Maximum) ? GetFromTarget(static target => target.Maximum) : Reference.Maximum; set => Reference.Maximum = value; } /// - public string? Minimum { get => string.IsNullOrEmpty(Reference.Minimum) ? Target?.Minimum : Reference.Minimum; set => Reference.Minimum = value; } + public string? Minimum { get => string.IsNullOrEmpty(Reference.Minimum) ? GetFromTarget(static target => target.Minimum) : Reference.Minimum; set => Reference.Minimum = value; } /// - public int? MaxLength { get => Reference.MaxLength ?? Target?.MaxLength; set => Reference.MaxLength = value; } + public int? MaxLength { get => Reference.MaxLength ?? GetFromTarget(static target => target.MaxLength); set => Reference.MaxLength = value; } /// - public int? MinLength { get => Reference.MinLength ?? Target?.MinLength; set => Reference.MinLength = value; } + public int? MinLength { get => Reference.MinLength ?? GetFromTarget(static target => target.MinLength); set => Reference.MinLength = value; } /// - public string? Pattern { get => string.IsNullOrEmpty(Reference.Pattern) ? Target?.Pattern : Reference.Pattern; set => Reference.Pattern = value; } + public string? Pattern { get => string.IsNullOrEmpty(Reference.Pattern) ? GetFromTarget(static target => target.Pattern) : Reference.Pattern; set => Reference.Pattern = value; } /// - public decimal? MultipleOf { get => Reference.MultipleOf ?? Target?.MultipleOf; set => Reference.MultipleOf = value; } + public decimal? MultipleOf { get => Reference.MultipleOf ?? GetFromTarget(static target => target.MultipleOf); set => Reference.MultipleOf = value; } /// public JsonNode? Default { - get => Reference.Default ?? Target?.Default; + get => Reference.Default ?? GetFromTarget(static target => target.Default); set => Reference.Default = value; } /// public bool ReadOnly { - get => Reference.ReadOnly ?? Target?.ReadOnly ?? false; + get => Reference.ReadOnly ?? GetFromTarget(static target => target.ReadOnly); set => Reference.ReadOnly = value; } /// public bool WriteOnly { - get => Reference.WriteOnly ?? Target?.WriteOnly ?? false; + get => Reference.WriteOnly ?? GetFromTarget(static target => target.WriteOnly); set => Reference.WriteOnly = value; } /// - public IList? AllOf { get => Reference.AllOf ?? Target?.AllOf; set => Reference.AllOf = value; } + public IList? AllOf { get => Reference.AllOf ?? GetFromTarget(static target => target.AllOf); set => Reference.AllOf = value; } /// - public IList? OneOf { get => Reference.OneOf ?? Target?.OneOf; set => Reference.OneOf = value; } + public IList? OneOf { get => Reference.OneOf ?? GetFromTarget(static target => target.OneOf); set => Reference.OneOf = value; } /// - public IList? AnyOf { get => Reference.AnyOf ?? Target?.AnyOf; set => Reference.AnyOf = value; } + public IList? AnyOf { get => Reference.AnyOf ?? GetFromTarget(static target => target.AnyOf); set => Reference.AnyOf = value; } /// - public IOpenApiSchema? Not { get => Reference.Not ?? Target?.Not; set => Reference.Not = value; } + public IOpenApiSchema? Not { get => Reference.Not ?? GetFromTarget(static target => target.Not); set => Reference.Not = value; } /// - public ISet? Required { get => Reference.Required ?? Target?.Required; set => Reference.Required = value; } + public ISet? Required { get => Reference.Required ?? GetFromTarget(static target => target.Required); set => Reference.Required = value; } /// - public IOpenApiSchema? Items { get => Reference.Items ?? Target?.Items; set => Reference.Items = value; } + public IOpenApiSchema? Items { get => Reference.Items ?? GetFromTarget(static target => target.Items); set => Reference.Items = value; } /// - public int? MaxItems { get => Reference.MaxItems ?? Target?.MaxItems; set => Reference.MaxItems = value; } + public int? MaxItems { get => Reference.MaxItems ?? GetFromTarget(static target => target.MaxItems); set => Reference.MaxItems = value; } /// - public int? MinItems { get => Reference.MinItems ?? Target?.MinItems; set => Reference.MinItems = value; } + public int? MinItems { get => Reference.MinItems ?? GetFromTarget(static target => target.MinItems); set => Reference.MinItems = value; } /// - public bool? UniqueItems { get => Reference.UniqueItems ?? Target?.UniqueItems; set => Reference.UniqueItems = value; } + public bool? UniqueItems { get => Reference.UniqueItems ?? GetFromTarget(static target => target.UniqueItems); set => Reference.UniqueItems = value; } /// - public IOpenApiSchema? Contains { get => Reference.Contains ?? (Target as IOpenApiSchemaMissingProperties)?.Contains; set => Reference.Contains = value; } + public IOpenApiSchema? Contains { get => Reference.Contains ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.Contains); set => Reference.Contains = value; } /// - public uint? MaxContains { get => Reference.MaxContains ?? (Target as IOpenApiSchemaMissingProperties)?.MaxContains; set => Reference.MaxContains = value; } + public uint? MaxContains { get => Reference.MaxContains ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.MaxContains); set => Reference.MaxContains = value; } /// - public uint? MinContains { get => Reference.MinContains ?? (Target as IOpenApiSchemaMissingProperties)?.MinContains; set => Reference.MinContains = value; } + public uint? MinContains { get => Reference.MinContains ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.MinContains); set => Reference.MinContains = value; } /// - public IDictionary? Properties { get => Reference.Properties ?? Target?.Properties; set => Reference.Properties = value; } + public IDictionary? Properties { get => Reference.Properties ?? GetFromTarget(static target => target.Properties); set => Reference.Properties = value; } /// - public IDictionary? PatternProperties { get => Reference.PatternProperties ?? Target?.PatternProperties; set => Reference.PatternProperties = value; } + public IDictionary? PatternProperties { get => Reference.PatternProperties ?? GetFromTarget(static target => target.PatternProperties); set => Reference.PatternProperties = value; } /// - public int? MaxProperties { get => Reference.MaxProperties ?? Target?.MaxProperties; set => Reference.MaxProperties = value; } + public int? MaxProperties { get => Reference.MaxProperties ?? GetFromTarget(static target => target.MaxProperties); set => Reference.MaxProperties = value; } /// - public int? MinProperties { get => Reference.MinProperties ?? Target?.MinProperties; set => Reference.MinProperties = value; } + public int? MinProperties { get => Reference.MinProperties ?? GetFromTarget(static target => target.MinProperties); set => Reference.MinProperties = value; } /// - public bool AdditionalPropertiesAllowed { get => Reference.AdditionalPropertiesAllowed ?? Target?.AdditionalPropertiesAllowed ?? true; set => Reference.AdditionalPropertiesAllowed = value; } + public bool AdditionalPropertiesAllowed { get => Reference.AdditionalPropertiesAllowed ?? GetFromTarget(static target => (bool?)target.AdditionalPropertiesAllowed) ?? true; set => Reference.AdditionalPropertiesAllowed = value; } /// - public IOpenApiSchema? AdditionalProperties { get => Reference.AdditionalProperties ?? Target?.AdditionalProperties; set => Reference.AdditionalProperties = value; } + public IOpenApiSchema? AdditionalProperties { get => Reference.AdditionalProperties ?? GetFromTarget(static target => target.AdditionalProperties); set => Reference.AdditionalProperties = value; } /// - public OpenApiDiscriminator? Discriminator { get => Reference.Discriminator ?? Target?.Discriminator; set => Reference.Discriminator = value; } + public OpenApiDiscriminator? Discriminator { get => Reference.Discriminator ?? GetFromTarget(static target => target.Discriminator); set => Reference.Discriminator = value; } /// [Obsolete("Use Examples instead.")] - public JsonNode? Example { get => Reference.Example ?? Target?.Example; set => Reference.Example = value; } + public JsonNode? Example { get => Reference.Example ?? GetFromTarget(static target => target.Example); set => Reference.Example = value; } /// public IList? Examples { - get => Reference.Examples ?? Target?.Examples; + get => Reference.Examples ?? GetFromTarget(static target => target.Examples); set => Reference.Examples = value; } /// - public IList? Enum { get => Reference.Enum ?? Target?.Enum; set => Reference.Enum = value; } + public IList? Enum { get => Reference.Enum ?? GetFromTarget(static target => target.Enum); set => Reference.Enum = value; } /// - public bool UnevaluatedProperties { get => Reference.UnevaluatedProperties ?? Target?.UnevaluatedProperties ?? true; set => Reference.UnevaluatedProperties = value; } + public bool UnevaluatedProperties { get => Reference.UnevaluatedProperties ?? GetFromTarget(static target => (bool?)target.UnevaluatedProperties) ?? true; set => Reference.UnevaluatedProperties = value; } /// - public IOpenApiSchema? UnevaluatedPropertiesSchema { get => Reference.UnevaluatedPropertiesSchema ?? (Target as IOpenApiSchemaMissingProperties)?.UnevaluatedPropertiesSchema; set => Reference.UnevaluatedPropertiesSchema = value; } + public IOpenApiSchema? UnevaluatedPropertiesSchema { get => Reference.UnevaluatedPropertiesSchema ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.UnevaluatedPropertiesSchema); set => Reference.UnevaluatedPropertiesSchema = value; } /// - public string? ContentEncoding { get => string.IsNullOrEmpty(Reference.ContentEncoding) ? (Target as IOpenApiSchemaMissingProperties)?.ContentEncoding : Reference.ContentEncoding; set => Reference.ContentEncoding = value; } + public string? ContentEncoding { get => string.IsNullOrEmpty(Reference.ContentEncoding) ? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.ContentEncoding) : Reference.ContentEncoding; set => Reference.ContentEncoding = value; } /// - public string? ContentMediaType { get => string.IsNullOrEmpty(Reference.ContentMediaType) ? (Target as IOpenApiSchemaMissingProperties)?.ContentMediaType : Reference.ContentMediaType; set => Reference.ContentMediaType = value; } + public string? ContentMediaType { get => string.IsNullOrEmpty(Reference.ContentMediaType) ? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.ContentMediaType) : Reference.ContentMediaType; set => Reference.ContentMediaType = value; } /// - public IOpenApiSchema? ContentSchema { get => Reference.ContentSchema ?? (Target as IOpenApiSchemaMissingProperties)?.ContentSchema; set => Reference.ContentSchema = value; } + public IOpenApiSchema? ContentSchema { get => Reference.ContentSchema ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.ContentSchema); set => Reference.ContentSchema = value; } /// - public IOpenApiSchema? PropertyNames { get => Reference.PropertyNames ?? (Target as IOpenApiSchemaMissingProperties)?.PropertyNames; set => Reference.PropertyNames = value; } + public IOpenApiSchema? PropertyNames { get => Reference.PropertyNames ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.PropertyNames); set => Reference.PropertyNames = value; } /// - public IDictionary? DependentSchemas { get => Reference.DependentSchemas ?? (Target as IOpenApiSchemaMissingProperties)?.DependentSchemas; set => Reference.DependentSchemas = value; } + public IDictionary? DependentSchemas { get => Reference.DependentSchemas ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.DependentSchemas); set => Reference.DependentSchemas = value; } /// - public IOpenApiSchema? If { get => Reference.If ?? (Target as IOpenApiSchemaMissingProperties)?.If; set => Reference.If = value; } + public IOpenApiSchema? If { get => Reference.If ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.If); set => Reference.If = value; } /// - public IOpenApiSchema? Then { get => Reference.Then ?? (Target as IOpenApiSchemaMissingProperties)?.Then; set => Reference.Then = value; } + public IOpenApiSchema? Then { get => Reference.Then ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.Then); set => Reference.Then = value; } /// - public IOpenApiSchema? Else { get => Reference.Else ?? (Target as IOpenApiSchemaMissingProperties)?.Else; set => Reference.Else = value; } + public IOpenApiSchema? Else { get => Reference.Else ?? GetFromTarget(static target => (target as IOpenApiSchemaMissingProperties)?.Else); set => Reference.Else = value; } /// - public OpenApiExternalDocs? ExternalDocs { get => Reference.ExternalDocs ?? Target?.ExternalDocs; set => Reference.ExternalDocs = value; } + public OpenApiExternalDocs? ExternalDocs { get => Reference.ExternalDocs ?? GetFromTarget(static target => target.ExternalDocs); set => Reference.ExternalDocs = value; } /// public bool Deprecated { - get => Reference.Deprecated ?? Target?.Deprecated ?? false; + get => Reference.Deprecated ?? GetFromTarget(static target => target.Deprecated); set => Reference.Deprecated = value; } /// - public OpenApiXml? Xml { get => Reference.Xml ?? Target?.Xml; set => Reference.Xml = value; } + public OpenApiXml? Xml { get => Reference.Xml ?? GetFromTarget(static target => target.Xml); set => Reference.Xml = value; } /// public IDictionary? Extensions { - get => Reference.Extensions ?? Target?.Extensions; + get => Reference.Extensions ?? GetFromTarget(static target => target.Extensions); set => Reference.Extensions = value; } /// - public IDictionary? UnrecognizedKeywords { get => Reference.UnrecognizedKeywords ?? Target?.UnrecognizedKeywords; set => Reference.UnrecognizedKeywords = value; } + public IDictionary? UnrecognizedKeywords { get => Reference.UnrecognizedKeywords ?? GetFromTarget(static target => target.UnrecognizedKeywords); set => Reference.UnrecognizedKeywords = value; } /// - public IDictionary>? DependentRequired { get => Reference.DependentRequired ?? Target?.DependentRequired; set => Reference.DependentRequired = value; } + public IDictionary>? DependentRequired { get => Reference.DependentRequired ?? GetFromTarget(static target => target.DependentRequired); set => Reference.DependentRequired = value; } /// public override void SerializeAsV31(IOpenApiWriter writer) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs index aa83105fa..4405d6546 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiSecuritySchemeReference.cs @@ -32,33 +32,33 @@ private OpenApiSecuritySchemeReference(OpenApiSecuritySchemeReference openApiSec /// public string? Description { - get => string.IsNullOrEmpty(Reference.Description) ? Target?.Description : Reference.Description; + get => string.IsNullOrEmpty(Reference.Description) ? GetFromTarget(static target => target.Description) : Reference.Description; set => Reference.Description = value; } /// - public string? Name { get => Target?.Name; } + public string? Name { get => GetFromTarget(static target => target.Name); } /// - public ParameterLocation? In { get => Target?.In; } + public ParameterLocation? In { get => GetFromTarget(static target => target.In); } /// - public string? Scheme { get => Target?.Scheme; } + public string? Scheme { get => GetFromTarget(static target => target.Scheme); } /// - public string? BearerFormat { get => Target?.BearerFormat; } + public string? BearerFormat { get => GetFromTarget(static target => target.BearerFormat); } /// - public OpenApiOAuthFlows? Flows { get => Target?.Flows; } + public OpenApiOAuthFlows? Flows { get => GetFromTarget(static target => target.Flows); } /// - public Uri? OpenIdConnectUrl { get => Target?.OpenIdConnectUrl; } + public Uri? OpenIdConnectUrl { get => GetFromTarget(static target => target.OpenIdConnectUrl); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// - public SecuritySchemeType? Type { get => Target?.Type; } + public SecuritySchemeType? Type { get => GetFromTarget(static target => target.Type); } /// public override IOpenApiSecurityScheme CopyReferenceAsTargetElementWithOverrides(IOpenApiSecurityScheme source) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs index 92d1d1308..afd8b25de 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs @@ -47,17 +47,17 @@ private OpenApiTagReference(OpenApiTagReference openApiTagReference) : base(open /// public string? Description { - get => Target?.Description; + get => GetFromTarget(static target => target.Description); } /// - public OpenApiExternalDocs? ExternalDocs { get => Target?.ExternalDocs; } + public OpenApiExternalDocs? ExternalDocs { get => GetFromTarget(static target => target.ExternalDocs); } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions { get => GetFromTarget(static target => target.Extensions); } /// - public string? Name { get => Target?.Name ?? Reference?.Id; } + public string? Name { get => GetFromTarget(static target => target.Name) ?? Reference?.Id; } /// public override IOpenApiTag CopyReferenceAsTargetElementWithOverrides(IOpenApiTag source) { diff --git a/test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs new file mode 100644 index 000000000..24d2b038e --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Models.References +{ + public class BaseOpenApiReferenceHolderTests + { + [Fact] + public void DelegatedAccessDetectsCyclesForEveryComponentReferenceType() + { + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiCallbackReference("B", document)); + document.AddComponent("B", new OpenApiCallbackReference("A", document)); + return document.Components!.Callbacks!["A"].PathItems; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiExampleReference("B", document)); + document.AddComponent("B", new OpenApiExampleReference("A", document)); + return document.Components!.Examples!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiHeaderReference("B", document)); + document.AddComponent("B", new OpenApiHeaderReference("A", document)); + return document.Components!.Headers!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiLinkReference("B", document)); + document.AddComponent("B", new OpenApiLinkReference("A", document)); + return document.Components!.Links!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.Components = new OpenApiComponents + { + MediaTypes = new Dictionary + { + ["A"] = new OpenApiMediaTypeReference("B", document), + ["B"] = new OpenApiMediaTypeReference("A", document) + } + }; + document.RegisterComponents(); + return document.Components.MediaTypes["A"].Schema; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiParameterReference("B", document)); + document.AddComponent("B", new OpenApiParameterReference("A", document)); + return document.Components!.Parameters!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiPathItemReference("B", document)); + document.AddComponent("B", new OpenApiPathItemReference("A", document)); + return document.Components!.PathItems!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiRequestBodyReference("B", document)); + document.AddComponent("B", new OpenApiRequestBodyReference("A", document)); + return document.Components!.RequestBodies!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiResponseReference("B", document)); + document.AddComponent("B", new OpenApiResponseReference("A", document)); + return document.Components!.Responses!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiSchemaReference("B", document)); + document.AddComponent("B", new OpenApiSchemaReference("A", document)); + return document.Components!.Schemas!["A"].Description; + }); + AssertCircularAccessThrows(document => + { + document.AddComponent("A", new OpenApiSecuritySchemeReference("B", document)); + document.AddComponent("B", new OpenApiSecuritySchemeReference("A", document)); + return document.Components!.SecuritySchemes!["A"].Description; + }); + } + + [Fact] + public void InliningACircularNonSchemaReferenceThrows() + { + // Arrange + var document = new OpenApiDocument(); + document.AddComponent("A", new OpenApiResponseReference("B", document)); + document.AddComponent("B", new OpenApiResponseReference("A", document)); + var response = Assert.IsType(document.Components!.Responses!["A"]); + var writer = new OpenApiJsonWriter( + new StringWriter(), + new OpenApiWriterSettings { InlineLocalReferences = true }); + + // Act + var exception = Assert.Throws(() => response.SerializeAsV31(writer)); + + // Assert + Assert.Contains("Circular reference detected while resolving reference:", exception.Message, StringComparison.Ordinal); + } + + private static void AssertCircularAccessThrows(Func access) + { + var exception = Assert.Throws(() => access(new OpenApiDocument())); + Assert.Contains("Circular reference detected while resolving reference:", exception.Message, StringComparison.Ordinal); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs index 1954857ed..c8dbb4536 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -580,5 +581,303 @@ public async Task SchemaReferenceExtensionsNotWrittenInV2() // Assert: In v2, ONLY $ref should appear - no description, no extensions Assert.Equal(@"{""$ref"":""#/definitions/Pet""}", output); } + + private const string CircularSchemaReferencePairJson = + """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "0.0.1" }, + "paths": {}, + "components": { + "schemas": { + "A": { "$ref": "#/components/schemas/B" }, + "B": { "$ref": "#/components/schemas/A" } + } + } + } + """; + + [Fact] + public void CircularSchemaReferencePairThrowsWhenReadingAllOf() + { + // Arrange + var readResult = OpenApiDocument.Parse(CircularSchemaReferencePairJson, "json"); + Assert.NotNull(readResult.Document); + var schemaA = readResult.Document.Components!.Schemas!["A"]; + + // Act + var exception = Assert.Throws(() => _ = schemaA.AllOf); + + // Assert + Assert.Contains("Circular reference detected while resolving reference:", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void CircularSchemaReferencePairTargetReturnsTheImmediateReference() + { + // Arrange + var readResult = OpenApiDocument.Parse(CircularSchemaReferencePairJson, "json"); + Assert.NotNull(readResult.Document); + var schemaA = Assert.IsType(readResult.Document.Components!.Schemas!["A"]); + var schemaB = readResult.Document.Components.Schemas["B"]; + + // Act + var target = schemaA.Target; + + // Assert + Assert.Same(schemaB, target); + } + + [Fact] + public void CircularSchemaReferencePairRecursiveTargetThrows() + { + // Arrange + var readResult = OpenApiDocument.Parse(CircularSchemaReferencePairJson, "json"); + Assert.NotNull(readResult.Document); + var schemaA = Assert.IsType(readResult.Document.Components!.Schemas!["A"]); + + // Act + var exception = Assert.Throws(() => _ = schemaA.RecursiveTarget); + + // Assert + Assert.Contains("Circular reference detected while resolving reference:", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void CircularSchemaReferencePairThrowsWhenReadingDescription() + { + // Arrange + var readResult = OpenApiDocument.Parse(CircularSchemaReferencePairJson, "json"); + Assert.NotNull(readResult.Document); + var schemaA = readResult.Document.Components!.Schemas!["A"]; + + // Act + var exception = Assert.Throws(() => _ = schemaA.Description); + + // Assert + Assert.Contains("Circular reference detected while resolving reference:", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void CircularSchemaReferenceChainOfThreeThrowsWhenReadingType() + { + // Arrange + var readResult = OpenApiDocument.Parse( + """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "0.0.1" }, + "paths": {}, + "components": { + "schemas": { + "A": { "$ref": "#/components/schemas/B" }, + "B": { "$ref": "#/components/schemas/C" }, + "C": { "$ref": "#/components/schemas/A" } + } + } + } + """, "json"); + Assert.NotNull(readResult.Document); + var schemaA = readResult.Document.Components!.Schemas!["A"]; + + // Act + var exception = Assert.Throws(() => _ = schemaA.Type); + + // Assert + Assert.Contains("Circular reference detected while resolving reference:", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task CircularSchemaReferencePairStillSerializesAsAReference() + { + // Arrange + var readResult = OpenApiDocument.Parse(CircularSchemaReferencePairJson, "json"); + Assert.NotNull(readResult.Document); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true }); + + // Act + readResult.Document.SerializeAsV3(writer); + await writer.FlushAsync(TestContext.Current.CancellationToken); + var output = outputStringWriter.ToString(); + + // Assert + Assert.Contains(@"""A"":{""$ref"":""#/components/schemas/B""}", output, StringComparison.Ordinal); + Assert.Contains(@"""B"":{""$ref"":""#/components/schemas/A""}", output, StringComparison.Ordinal); + } + + [Fact] + public void SchemaReferenceChainThatTerminatesStillResolvesTheTarget() + { + // Arrange + var readResult = OpenApiDocument.Parse( + """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "0.0.1" }, + "paths": {}, + "components": { + "schemas": { + "A": { "$ref": "#/components/schemas/B" }, + "B": { "$ref": "#/components/schemas/C" }, + "C": { "type": "string", "description": "The concrete schema" } + } + } + } + """, "json"); + Assert.NotNull(readResult.Document); + var schemaA = readResult.Document.Components!.Schemas!["A"]; + + // Act + var description = schemaA.Description; + var repeatedDescription = schemaA.Description; + + // Assert + Assert.Equal("The concrete schema", description); + Assert.Equal("The concrete schema", repeatedDescription); + Assert.Equal(JsonSchemaType.String, schemaA.Type); + } + + [Fact] + public void DelegatedAccessResolvesEachReferenceOnce() + { + // Arrange + const int referenceCount = 100; + var references = Enumerable.Range(0, referenceCount) + .Select(index => new CountingSchemaReference($"S{index}")) + .ToArray(); + for (var index = 0; index < references.Length - 1; index++) + { + references[index].ImmediateTarget = references[index + 1]; + } + references[^1].ImmediateTarget = new OpenApiSchema { Description = "end" }; + + // Act + var description = references[0].Description; + + // Assert + Assert.Equal("end", description); + Assert.Equal(referenceCount, references.Sum(reference => reference.TargetAccessCount)); + } + + [Fact] + public void RecursiveTargetResolvesADeepChainIteratively() + { + // Arrange + const int referenceCount = 10_000; + var references = Enumerable.Range(0, referenceCount) + .Select(index => new CountingSchemaReference($"S{index}")) + .ToArray(); + var concreteTarget = new OpenApiSchema { Description = "end" }; + for (var index = 0; index < references.Length - 1; index++) + { + references[index].ImmediateTarget = references[index + 1]; + } + references[^1].ImmediateTarget = concreteTarget; + + // Act + var target = references[0].RecursiveTarget; + + // Assert + Assert.Same(concreteTarget, target); + Assert.Equal(referenceCount, references.Sum(reference => reference.TargetAccessCount)); + } + + [Fact] + public void FilteringADocumentWithCircularSchemaReferencesPreservesTheReferences() + { + // Arrange + var readResult = OpenApiDocument.Parse( + """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "0.0.1" }, + "paths": { + "/test": { + "get": { + "operationId": "getTest", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/A" } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "A": { "$ref": "#/components/schemas/B" }, + "B": { "$ref": "#/components/schemas/A" } + } + } + } + """, "json"); + Assert.NotNull(readResult.Document); + var predicate = OpenApiFilterService.CreatePredicate(operationIds: "getTest"); + + // Act + var filtered = OpenApiFilterService.CreateFilteredDocument(readResult.Document, predicate); + + // Assert + Assert.NotNull(filtered.Components); + Assert.NotNull(filtered.Components.Schemas); + Assert.Contains("A", filtered.Components.Schemas.Keys); + Assert.Contains("B", filtered.Components.Schemas.Keys); + } + + [Fact] + public void CircularSchemaReferencePairIsStillReportedAsResolved() + { + // Arrange + var readResult = OpenApiDocument.Parse(CircularSchemaReferencePairJson, "json"); + Assert.NotNull(readResult.Document); + var schemaA = Assert.IsType(readResult.Document.Components!.Schemas!["A"]); + + // Act + var unresolved = schemaA.UnresolvedReference; + + // Assert: the reference does resolve to an element, only the chain never terminates + Assert.False(unresolved); + } + + [Fact] + public void ValidatingADocumentWithACircularSchemaReferencePairDoesNotThrow() + { + // Arrange + var readResult = OpenApiDocument.Parse(CircularSchemaReferencePairJson, "json"); + Assert.NotNull(readResult.Document); + + // Act + var errors = readResult.Document.Validate(ValidationRuleSet.GetDefaultRuleSet()); + + // Assert + Assert.NotNull(errors); + } + + private sealed class CountingSchemaReference : OpenApiSchemaReference + { + public CountingSchemaReference(string referenceId) + : base(referenceId) + { + } + + public IOpenApiSchema ImmediateTarget { get; set; } + + public int TargetAccessCount { get; private set; } + + public override IOpenApiSchema Target + { + get + { + TargetAccessCount++; + return ImmediateTarget; + } + } + } } } From d20027c3868d5143b42320d3270b6885b0c91143 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 19 Aug 2026 09:56:14 -0400 Subject: [PATCH 2/3] chore: removes invalid property test due to bad cherry-pick Signed-off-by: Vincent Biret --- .../References/BaseOpenApiReferenceHolderTests.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs index 24d2b038e..de54a09d4 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/BaseOpenApiReferenceHolderTests.cs @@ -38,19 +38,6 @@ public void DelegatedAccessDetectsCyclesForEveryComponentReferenceType() return document.Components!.Links!["A"].Description; }); AssertCircularAccessThrows(document => - { - document.Components = new OpenApiComponents - { - MediaTypes = new Dictionary - { - ["A"] = new OpenApiMediaTypeReference("B", document), - ["B"] = new OpenApiMediaTypeReference("A", document) - } - }; - document.RegisterComponents(); - return document.Components.MediaTypes["A"].Schema; - }); - AssertCircularAccessThrows(document => { document.AddComponent("A", new OpenApiParameterReference("B", document)); document.AddComponent("B", new OpenApiParameterReference("A", document)); From b0b00847430a91132cd18ea13e4380bc9055347e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 19 Aug 2026 10:42:30 -0400 Subject: [PATCH 3/3] chore: refreshes performance reports --- .../performance.Descriptions-report-github.md | 24 +++---- .../performance.Descriptions-report.csv | 12 ++-- .../performance.Descriptions-report.html | 24 +++---- .../performance.Descriptions-report.json | 2 +- .../performance.EmptyModels-report-github.md | 68 +++++++++---------- .../performance.EmptyModels-report.csv | 56 +++++++-------- .../performance.EmptyModels-report.html | 66 +++++++++--------- .../performance.EmptyModels-report.json | 2 +- 8 files changed, 127 insertions(+), 127 deletions(-) diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md index 0ba80cfca..1d6f5c560 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -1,20 +1,20 @@ ``` -BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.9106/25H2/2025Update/HudsonValley2) (Hyper-V) -AMD EPYC 7763 2.44GHz, 1 CPU, 16 logical and 8 physical cores +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.9168/25H2/2025Update/HudsonValley2) +Snapdragon X 12-core X1E80100 3.40 GHz (Max: 3.42GHz), 1 CPU, 12 logical and 12 physical cores .NET SDK 10.0.303 - [Host] : .NET 8.0.30 (8.0.30, 8.0.3026.36720), X64 RyuJIT x86-64-v3 - ShortRun : .NET 8.0.30 (8.0.30, 8.0.3026.36720), X64 RyuJIT x86-64-v3 + [Host] : .NET 8.0.30 (8.0.30, 8.0.3026.36720), Arm64 RyuJIT armv8.0-a + ShortRun : .NET 8.0.30 (8.0.30, 8.0.3026.36720), Arm64 RyuJIT armv8.0-a Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------------- |---------------:|---------------:|--------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 955.1 μs | 2,072.5 μs | 113.60 μs | 15.6250 | - | - | 327.36 KB | -| PetStoreJson | 299.0 μs | 145.4 μs | 7.97 μs | 11.7188 | 1.9531 | - | 209.52 KB | -| GHESYaml | 1,198,752.5 μs | 3,307,785.9 μs | 181,310.91 μs | 18000.0000 | 10000.0000 | 2000.0000 | 267040.88 KB | -| GHESJson | 615,836.5 μs | 496,026.6 μs | 27,188.89 μs | 9000.0000 | 8000.0000 | 2000.0000 | 140387.58 KB | -| GHESNextYaml | 1,379,670.6 μs | 1,464,340.6 μs | 80,265.45 μs | 30000.0000 | 11000.0000 | 2000.0000 | 468968.06 KB | -| GHESNextJson | 950,071.7 μs | 425,730.9 μs | 23,335.75 μs | 22000.0000 | 10000.0000 | 2000.0000 | 344709.98 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------- |-------------:|-------------:|-------------:|-----------:|-----------:|----------:|-------------:| +| PetStoreYaml | 335.7 μs | 239.3 μs | 13.12 μs | 78.1250 | 7.8125 | - | 327.37 KB | +| PetStoreJson | 131.8 μs | 106.7 μs | 5.85 μs | 50.7813 | 11.7188 | - | 209.53 KB | +| GHESYaml | 497,859.7 μs | 200,217.0 μs | 10,974.57 μs | 47000.0000 | 14000.0000 | 3000.0000 | 267512.44 KB | +| GHESJson | 278,562.0 μs | 234,097.6 μs | 12,831.68 μs | 23000.0000 | 11000.0000 | 2000.0000 | 140859.51 KB | +| GHESNextYaml | 710,430.7 μs | 664,647.3 μs | 36,431.56 μs | 84000.0000 | 15000.0000 | 3000.0000 | 469450.01 KB | +| GHESNextJson | 491,725.9 μs | 233,616.5 μs | 12,805.31 μs | 59000.0000 | 13000.0000 | 3000.0000 | 345193.66 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index d1391675a..9c5d277d5 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv @@ -1,7 +1,7 @@ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Gen1,Gen2,Allocated -PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,955.1 μs,"2,072.5 μs",113.60 μs,15.6250,0.0000,0.0000,327.36 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,299.0 μs,145.4 μs,7.97 μs,11.7188,1.9531,0.0000,209.52 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,198,752.5 μs","3,307,785.9 μs","181,310.91 μs",18000.0000,10000.0000,2000.0000,267040.88 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"615,836.5 μs","496,026.6 μs","27,188.89 μs",9000.0000,8000.0000,2000.0000,140387.58 KB -GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,379,670.6 μs","1,464,340.6 μs","80,265.45 μs",30000.0000,11000.0000,2000.0000,468968.06 KB -GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"950,071.7 μs","425,730.9 μs","23,335.75 μs",22000.0000,10000.0000,2000.0000,344709.98 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,335.7 μs,239.3 μs,13.12 μs,78.1250,7.8125,0.0000,327.37 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,131.8 μs,106.7 μs,5.85 μs,50.7813,11.7188,0.0000,209.53 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"497,859.7 μs","200,217.0 μs","10,974.57 μs",47000.0000,14000.0000,3000.0000,267512.44 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"278,562.0 μs","234,097.6 μs","12,831.68 μs",23000.0000,11000.0000,2000.0000,140859.51 KB +GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"710,430.7 μs","664,647.3 μs","36,431.56 μs",84000.0000,15000.0000,3000.0000,469450.01 KB +GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"491,725.9 μs","233,616.5 μs","12,805.31 μs",59000.0000,13000.0000,3000.0000,345193.66 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index adbd34f57..e3a7a9c62 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html @@ -2,7 +2,7 @@ -performance.Descriptions-20260818-185713 +performance.Descriptions-20260819-103535