Visitar URL original
fix: circular ref guard by gavinbarron · Pull Request #3033 · microsoft/OpenAPI.NET · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 102 additions & 13 deletions src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace Microsoft.OpenApi;
/// <summary>
Expand All @@ -8,8 +9,13 @@
/// <typeparam name="T">The concrete class implementation type for the model.</typeparam>
/// <typeparam name="U">The interface type for the model.</typeparam>
/// <typeparam name="V">The type for the reference holding the additional fields and annotations</typeparam>
public abstract class BaseOpenApiReferenceHolder<T, U, V> : IOpenApiReferenceHolder<T, U, V> where T : class, IOpenApiReferenceable, U where U : IOpenApiReferenceable, IOpenApiSerializable where V : BaseOpenApiReference, new()

Check warning on line 12 in src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

View workflow job for this annotation

GitHub Actions / Build

Reduce the number of generic parameters in the 'BaseOpenApiReferenceHolder' class to no more than the 2 authorized.

Check warning on line 12 in src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

View workflow job for this annotation

GitHub Actions / Build

Reduce the number of generic parameters in the 'BaseOpenApiReferenceHolder' class to no more than the 2 authorized.
{
[ThreadStatic]
private static HashSet<BaseOpenApiReferenceHolder<T, U, V>>? t_activeReferenceAccesses;
[ThreadStatic]
private static HashSet<BaseOpenApiReferenceHolder<T, U, V>>? t_activeTargetActions;

/// <inheritdoc/>
public virtual U? Target
{
Expand All @@ -19,28 +25,114 @@
return Reference.HostDocument.ResolveReferenceTo<U>(Reference, this as IOpenApiSchema);
}
}

/// <summary>
/// Gets a value from the resolved target while detecting cycles in delegated member access.
/// </summary>
/// <typeparam name="TResult">The type of value to get from the target.</typeparam>
/// <param name="selector">Selects the value from the resolved target.</param>
/// <returns>The selected value, or the default value when the target cannot be resolved.</returns>
/// <remarks>
/// The guard remains active while <paramref name="selector"/> reads the target member. This covers
/// the complete delegated call chain without changing the immediate-resolution semantics of
/// <see cref="Target"/> or walking an acyclic chain more than once.
/// </remarks>
private protected TResult GetFromTarget<TResult>(Func<U, TResult> selector)
{
Utils.CheckArgumentNull(selector);
return ExecuteWithReferenceGuard(ref t_activeReferenceAccesses, () =>
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
return Target is { } target ? selector(target) : default!;
});
}

/// <summary>
/// Executes an action against the resolved target while detecting cycles in delegated access.
/// </summary>
/// <param name="action">The action to execute against the resolved target.</param>
private protected void ApplyToTarget(Action<U> action)
{
Utils.CheckArgumentNull(action);
ExecuteWithReferenceGuard<object?>(ref t_activeTargetActions, () =>
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
if (Target is { } target)
{
action(target);
}
return null;
});
}

private TResult ExecuteWithReferenceGuard<TResult>(
ref HashSet<BaseOpenApiReferenceHolder<T, U, V>>? activeReferences,
Func<TResult> action)
{
activeReferences ??= new HashSet<BaseOpenApiReferenceHolder<T, U, V>>(ReferenceHolderComparer.Instance);
if (!activeReferences.Add(this))
{
throw new InvalidOperationException($"Circular reference detected while resolving reference: {Reference.ReferenceV3}");
}

try
{
RuntimeHelpers.EnsureSufficientExecutionStack();
return action();
}
catch (InsufficientExecutionStackException ex)
{
throw new InvalidOperationException(
$"The chain of references starting at {Reference.ReferenceV3} is nested too deeply to resolve.",
ex);
}
finally
{
activeReferences.Remove(this);
if (activeReferences.Count == 0)
{
activeReferences = null;
}
}
}

/// <inheritdoc/>
public T? RecursiveTarget
{
get
{
return ResolveRecursiveTarget(new HashSet<BaseOpenApiReferenceHolder<T, U, V>>());
var visitedReferences = new HashSet<BaseOpenApiReferenceHolder<T, U, V>>(ReferenceHolderComparer.Instance);
BaseOpenApiReferenceHolder<T, U, V> current = this;

while (visitedReferences.Add(current))
{
switch (current.Target)
{
case BaseOpenApiReferenceHolder<T, U, V> 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<BaseOpenApiReferenceHolder<T, U, V>> visitedReferences)
private sealed class ReferenceHolderComparer : IEqualityComparer<BaseOpenApiReferenceHolder<T, U, V>>
{
if (!visitedReferences.Add(this))
internal static ReferenceHolderComparer Instance { get; } = new();

Check warning on line 125 in src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

View workflow job for this annotation

GitHub Actions / Build

A static field in a generic type is not shared among instances of different close constructed types.

Check warning on line 125 in src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

View workflow job for this annotation

GitHub Actions / Build

A static field in a generic type is not shared among instances of different close constructed types.

Check warning on line 125 in src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A static field in a generic type is not shared among instances of different close constructed types.

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AaAXuUsl6iLDfOXlIE59&open=AaAXuUsl6iLDfOXlIE59&pullRequest=3033

public bool Equals(BaseOpenApiReferenceHolder<T, U, V>? x, BaseOpenApiReferenceHolder<T, U, V>? y)
{
throw new InvalidOperationException($"Circular reference detected while resolving reference: {Reference.ReferenceV3}");
return ReferenceEquals(x, y);
}

return Target switch
public int GetHashCode(BaseOpenApiReferenceHolder<T, U, V> obj)
{
BaseOpenApiReferenceHolder<T, U, V> recursiveTarget => recursiveTarget.ResolveRecursiveTarget(visitedReferences),
T concrete => concrete,
_ => null
};
return RuntimeHelpers.GetHashCode(obj);
}
}
/// <summary>
/// Copy the reference as a target element with overrides.
Expand All @@ -56,7 +148,7 @@
protected BaseOpenApiReferenceHolder(BaseOpenApiReferenceHolder<T, U, V> source)
{
Utils.CheckArgumentNull(source);
Reference = CopyReference(source.Reference);

Check warning on line 151 in src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this call from a constructor to the overridable 'CopyReference' method.
//no need to copy summary and description as if they are not overridden, they will be fetched from the target
//if they are, the reference copy will handle it
}
Expand Down Expand Up @@ -160,9 +252,6 @@
Action<IOpenApiWriter, U> action)
{
Utils.CheckArgumentNull(writer);
if (Target is not null)
{
action(writer, Target);
}
ApplyToTarget(element => action(writer, element));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ private OpenApiCallbackReference(OpenApiCallbackReference callback) : base(callb
}

/// <inheritdoc/>
public Dictionary<RuntimeExpression, IOpenApiPathItem>? PathItems { get => Target?.PathItems; }
public Dictionary<RuntimeExpression, IOpenApiPathItem>? PathItems { get => GetFromTarget(static target => target.PathItems); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
public IDictionary<string, IOpenApiExtension>? Extensions { get => GetFromTarget(static target => target.Extensions); }

/// <inheritdoc/>
public override IOpenApiCallback CopyReferenceAsTargetElementWithOverrides(IOpenApiCallback source)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,31 @@ private OpenApiExampleReference(OpenApiExampleReference example) : base(example)
/// <inheritdoc/>
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;
}

/// <inheritdoc/>
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;
}

/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
public IDictionary<string, IOpenApiExtension>? Extensions { get => GetFromTarget(static target => target.Extensions); }

/// <inheritdoc/>
public string? ExternalValue { get => Target?.ExternalValue; }
public string? ExternalValue { get => GetFromTarget(static target => target.ExternalValue); }

/// <inheritdoc/>
public JsonNode? Value { get => Target?.Value; }
public JsonNode? Value { get => GetFromTarget(static target => target.Value); }

/// <inheritdoc/>
public JsonNode? DataValue { get => Target?.DataValue; }
public JsonNode? DataValue { get => GetFromTarget(static target => target.DataValue); }

/// <inheritdoc/>
public string? SerializedValue { get => Target?.SerializedValue; }
public string? SerializedValue { get => GetFromTarget(static target => target.SerializedValue); }

/// <inheritdoc/>
public override IOpenApiExample CopyReferenceAsTargetElementWithOverrides(IOpenApiExample source)
Expand Down
24 changes: 12 additions & 12 deletions src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,42 +36,42 @@ private OpenApiHeaderReference(OpenApiHeaderReference header) : base(header)
/// <inheritdoc/>
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;
}

/// <inheritdoc/>
public bool Required { get => Target?.Required ?? default; }
public bool Required { get => GetFromTarget(static target => target.Required); }

/// <inheritdoc/>
public bool Deprecated { get => Target?.Deprecated ?? default; }
public bool Deprecated { get => GetFromTarget(static target => target.Deprecated); }

/// <inheritdoc/>
public bool AllowEmptyValue { get => Target?.AllowEmptyValue ?? default; }
public bool AllowEmptyValue { get => GetFromTarget(static target => target.AllowEmptyValue); }

/// <inheritdoc/>
public IOpenApiSchema? Schema { get => Target?.Schema; }
public IOpenApiSchema? Schema { get => GetFromTarget(static target => target.Schema); }

/// <inheritdoc/>
public ParameterStyle? Style { get => Target?.Style; }
public ParameterStyle? Style { get => GetFromTarget(static target => target.Style); }

/// <inheritdoc/>
public bool Explode { get => Target?.Explode ?? default; }
public bool Explode { get => GetFromTarget(static target => target.Explode); }

/// <inheritdoc/>
public bool AllowReserved { get => Target?.AllowReserved ?? default; }
public bool AllowReserved { get => GetFromTarget(static target => target.AllowReserved); }

/// <inheritdoc/>
public JsonNode? Example { get => Target?.Example; }
public JsonNode? Example { get => GetFromTarget(static target => target.Example); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExample>? Examples { get => Target?.Examples; }
public IDictionary<string, IOpenApiExample>? Examples { get => GetFromTarget(static target => target.Examples); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiMediaType>? Content { get => Target?.Content; }
public IDictionary<string, IOpenApiMediaType>? Content { get => GetFromTarget(static target => target.Content); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
public IDictionary<string, IOpenApiExtension>? Extensions { get => GetFromTarget(static target => target.Extensions); }

/// <inheritdoc/>
public override IOpenApiHeader CopyReferenceAsTargetElementWithOverrides(IOpenApiHeader source)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,27 @@ private OpenApiLinkReference(OpenApiLinkReference reference) : base(reference)
/// <inheritdoc/>
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;
}

/// <inheritdoc/>
public string? OperationRef { get => Target?.OperationRef; }
public string? OperationRef { get => GetFromTarget(static target => target.OperationRef); }

/// <inheritdoc/>
public string? OperationId { get => Target?.OperationId; }
public string? OperationId { get => GetFromTarget(static target => target.OperationId); }

/// <inheritdoc/>
public OpenApiServer? Server { get => Target?.Server; }
public OpenApiServer? Server { get => GetFromTarget(static target => target.Server); }

/// <inheritdoc/>
public IDictionary<string, RuntimeExpressionAnyWrapper>? Parameters { get => Target?.Parameters; }
public IDictionary<string, RuntimeExpressionAnyWrapper>? Parameters { get => GetFromTarget(static target => target.Parameters); }

/// <inheritdoc/>
public RuntimeExpressionAnyWrapper? RequestBody { get => Target?.RequestBody; }
public RuntimeExpressionAnyWrapper? RequestBody { get => GetFromTarget(static target => target.RequestBody); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
public IDictionary<string, IOpenApiExtension>? Extensions { get => GetFromTarget(static target => target.Extensions); }

/// <inheritdoc/>
public override void SerializeAsV2(IOpenApiWriter writer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,28 @@ private OpenApiMediaTypeReference(OpenApiMediaTypeReference mediaTypeReference)
}

/// <inheritdoc/>
public IOpenApiSchema? Schema { get => Target?.Schema; }
public IOpenApiSchema? Schema { get => GetFromTarget(static target => target.Schema); }

/// <inheritdoc/>
public IOpenApiSchema? ItemSchema { get => Target?.ItemSchema; }
public IOpenApiSchema? ItemSchema { get => GetFromTarget(static target => target.ItemSchema); }

/// <inheritdoc/>
public JsonNode? Example { get => Target?.Example; }
public JsonNode? Example { get => GetFromTarget(static target => target.Example); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExample>? Examples { get => Target?.Examples; }
public IDictionary<string, IOpenApiExample>? Examples { get => GetFromTarget(static target => target.Examples); }

/// <inheritdoc/>
public IDictionary<string, OpenApiEncoding>? Encoding { get => Target?.Encoding; }
public IDictionary<string, OpenApiEncoding>? Encoding { get => GetFromTarget(static target => target.Encoding); }

/// <inheritdoc/>
public OpenApiEncoding? ItemEncoding { get => Target?.ItemEncoding; }
public OpenApiEncoding? ItemEncoding { get => GetFromTarget(static target => target.ItemEncoding); }

/// <inheritdoc/>
public IList<OpenApiEncoding>? PrefixEncoding { get => Target?.PrefixEncoding; }
public IList<OpenApiEncoding>? PrefixEncoding { get => GetFromTarget(static target => target.PrefixEncoding); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
public IDictionary<string, IOpenApiExtension>? Extensions { get => GetFromTarget(static target => target.Extensions); }

/// <inheritdoc/>
public override IOpenApiMediaType CopyReferenceAsTargetElementWithOverrides(IOpenApiMediaType source)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,51 +36,51 @@
}

/// <inheritdoc/>
public string? Name { get => Target?.Name; }
public string? Name { get => GetFromTarget(static target => target.Name); }

/// <inheritdoc/>
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;
}

/// <inheritdoc/>
public bool Required { get => Target?.Required ?? default; }
public bool Required { get => GetFromTarget(static target => target.Required); }

/// <inheritdoc/>
public bool Deprecated { get => Target?.Deprecated ?? default; }
public bool Deprecated { get => GetFromTarget(static target => target.Deprecated); }

/// <inheritdoc/>
[Obsolete("Use of AllowEmptyValue is not recommended and it is likely to be removed in a later revision.")]

Check warning on line 55 in src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs

View workflow job for this annotation

GitHub Actions / Build

Do not forget to remove this deprecated code someday.

Check warning on line 55 in src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs

View workflow job for this annotation

GitHub Actions / Build

Do not forget to remove this deprecated code someday.
public bool AllowEmptyValue { get => Target?.AllowEmptyValue ?? default; }
public bool AllowEmptyValue { get => GetFromTarget(static target => target.AllowEmptyValue); }

/// <inheritdoc/>
public bool AllowReserved { get => Target?.AllowReserved ?? default; }
public bool AllowReserved { get => GetFromTarget(static target => target.AllowReserved); }

/// <inheritdoc/>
public IOpenApiSchema? Schema { get => Target?.Schema; }
public IOpenApiSchema? Schema { get => GetFromTarget(static target => target.Schema); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExample>? Examples { get => Target?.Examples; }
public IDictionary<string, IOpenApiExample>? Examples { get => GetFromTarget(static target => target.Examples); }

/// <inheritdoc/>
public JsonNode? Example { get => Target?.Example; }
public JsonNode? Example { get => GetFromTarget(static target => target.Example); }

/// <inheritdoc/>
public ParameterLocation? In { get => Target?.In; }
public ParameterLocation? In { get => GetFromTarget(static target => target.In); }

/// <inheritdoc/>
public ParameterStyle? Style { get => Target?.Style; }
public ParameterStyle? Style { get => GetFromTarget(static target => target.Style); }

/// <inheritdoc/>
public bool Explode { get => Target?.Explode ?? default; }
public bool Explode { get => GetFromTarget(static target => target.Explode); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiMediaType>? Content { get => Target?.Content; }
public IDictionary<string, IOpenApiMediaType>? Content { get => GetFromTarget(static target => target.Content); }

/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get => Target?.Extensions; }
public IDictionary<string, IOpenApiExtension>? Extensions { get => GetFromTarget(static target => target.Extensions); }

/// <inheritdoc/>
public override IOpenApiParameter CopyReferenceAsTargetElementWithOverrides(IOpenApiParameter source)
Expand Down
Loading
Loading