Skip to content

feat: Add writer settings to enable collection sorting using a comparer #2363

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 15 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all 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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static class OpenApiExtensibleExtensions
/// <param name="extensions">A dictionary of <see cref="IOpenApiExtension"/>.</param>
/// <param name="extensionKey">The key corresponding to the <see cref="IOpenApiExtension"/>.</param>
/// <returns>A <see cref="string"/> value matching the provided extensionKey. Return null when extensionKey is not found. </returns>
internal static string GetExtension(this Dictionary<string, IOpenApiExtension> extensions, string extensionKey)
internal static string GetExtension(this IDictionary<string, IOpenApiExtension> extensions, string extensionKey)
{
if (extensions.TryGetValue(extensionKey, out var value) && value is JsonNodeExtension { Node: JsonValue castValue } && castValue.TryGetValue<string>(out var stringValue))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public override void Visit(OpenApiOperation operation)
// Order matters. Resolve operationId.
operationId = RemoveHashSuffix(operationId);
if (operationTypeExtension.IsEquals("action") || operationTypeExtension.IsEquals("function"))
operationId = RemoveKeyTypeSegment(operationId, operation.Parameters ?? []);
operationId = RemoveKeyTypeSegment(operationId, operation.Parameters ?? new List<IOpenApiParameter>());
operationId = SingularizeAndDeduplicateOperationId(operationId.SplitByChar('.'));
operationId = ResolveODataCastOperationId(operationId);
operationId = ResolveByRefOperationId(operationId);
Expand Down Expand Up @@ -142,7 +142,7 @@ private static string RemoveHashSuffix(string operationId)
return s_hashSuffixRegex.Match(operationId).Value;
}

private static string RemoveKeyTypeSegment(string operationId, List<IOpenApiParameter> parameters)
private static string RemoveKeyTypeSegment(string operationId, IList<IOpenApiParameter> parameters)
{
var segments = operationId.SplitByChar('.');
foreach (var parameter in parameters)
Expand All @@ -156,7 +156,7 @@ private static string RemoveKeyTypeSegment(string operationId, List<IOpenApiPara
return string.Join('.', segments);
}

private static void ResolveFunctionParameters(List<IOpenApiParameter> parameters)
private static void ResolveFunctionParameters(IList<IOpenApiParameter> parameters)
{
foreach (var parameter in parameters.OfType<OpenApiParameter>().Where(static p => p.Content?.Count > 0))
{
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi.Hidi/StatsVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public override void Visit(IOpenApiSchema schema)

public int HeaderCount { get; set; }

public override void Visit(Dictionary<string, IOpenApiHeader> headers)
public override void Visit(IDictionary<string, IOpenApiHeader> headers)
{
HeaderCount++;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi.Workbench/StatsVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public override void Visit(IOpenApiSchema schema)

public int HeaderCount { get; set; }

public override void Visit(Dictionary<string, IOpenApiHeader> headers)
public override void Visit(IDictionary<string, IOpenApiHeader> headers)
{
HeaderCount++;
}
Expand Down
50 changes: 50 additions & 0 deletions src/Microsoft.OpenApi/Extensions/CollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.OpenApi
{
/// <summary>
/// Dictionary extension methods
/// </summary>
internal static class CollectionExtensions
{
/// <summary>
/// Returns a new dictionary with entries sorted by key using a custom comparer.
/// </summary>
internal static IDictionary<TKey, TValue> Sort<TKey, TValue>(
this IDictionary<TKey, TValue> source,
IComparer<TKey> comparer)
where TKey : notnull
{
#if NET7_0_OR_GREATER
ArgumentNullException.ThrowIfNull(nameof(source));
ArgumentNullException.ThrowIfNull(nameof(comparer));
#else
if (source == null)
throw new ArgumentNullException(nameof(source));
if (comparer == null)
throw new ArgumentNullException(nameof(comparer));
#endif
return source.OrderBy(kvp => kvp.Key, comparer)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}

/// <summary>
/// Sorts any IEnumerable<T> using the specified comparer and returns a List</T>.
/// </summary>
internal static List<T> Sort<T>(this IEnumerable<T> source, IComparer<T> comparer)
{
#if NET7_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(comparer);
#else
if (source == null)
throw new ArgumentNullException(nameof(source));
if (comparer == null)
throw new ArgumentNullException(nameof(comparer));
#endif
return source.OrderBy(item => item, comparer).ToList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using System;
using System.Collections.Generic;

namespace Microsoft.OpenApi
{
Expand All @@ -28,7 +29,7 @@ public static void AddExtension<T>(this T element, string name, IOpenApiExtensio
throw new OpenApiException(string.Format(SRResource.ExtensionFieldNameMustBeginWithXDash, name));
}

element.Extensions ??= [];
element.Extensions ??= new Dictionary<string, IOpenApiExtension>();
element.Extensions[name] = Utils.CheckArgumentNull(any);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Interfaces/IMetadataContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public interface IMetadataContainer
/// A collection of properties associated with the current OpenAPI element to be used by the application.
/// Metadata are NOT (de)serialized with the schema and can be used for custom properties.
/// </summary>
Dictionary<string, object>? Metadata { get; set; }
IDictionary<string, object>? Metadata { get; set; }
}
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Interfaces/IOpenApiExtensible.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ public interface IOpenApiExtensible : IOpenApiElement
/// <summary>
/// Specification extensions.
/// </summary>
Dictionary<string, IOpenApiExtension>? Extensions { get; set; }
IDictionary<string, IOpenApiExtension>? Extensions { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public interface IOpenApiReadOnlyExtensible
/// <summary>
/// Specification extensions.
/// </summary>
Dictionary<string, IOpenApiExtension>? Extensions { get; }
IDictionary<string, IOpenApiExtension>? Extensions { get; }

}
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ public interface IOpenApiHeader : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// <summary>
/// Examples of the media type.
/// </summary>
public Dictionary<string, IOpenApiExample>? Examples { get; }
public IDictionary<string, IOpenApiExample>? Examples { get; }

/// <summary>
/// A map containing the representations for the header.
/// </summary>
public Dictionary<string, OpenApiMediaType>? Content { get; }
public IDictionary<string, OpenApiMediaType>? Content { get; }

}
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Models/Interfaces/IOpenApiLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public interface IOpenApiLink : IOpenApiDescribedElement, IOpenApiReadOnlyExtens
/// <summary>
/// A map representing parameters to pass to an operation as specified with operationId or identified via operationRef.
/// </summary>
public Dictionary<string, RuntimeExpressionAnyWrapper>? Parameters { get; }
public IDictionary<string, RuntimeExpressionAnyWrapper>? Parameters { get; }

/// <summary>
/// A literal value or {expression} to use as a request body when calling the target operation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public interface IOpenApiParameter : IOpenApiDescribedElement, IOpenApiReadOnlyE
/// Furthermore, if referencing a schema which contains an example,
/// the examples value SHALL override the example provided by the schema.
/// </summary>
public Dictionary<string, IOpenApiExample>? Examples { get; }
public IDictionary<string, IOpenApiExample>? Examples { get; }

/// <summary>
/// Example of the media type. The example SHOULD match the specified schema and encoding properties
Expand All @@ -101,5 +101,5 @@ public interface IOpenApiParameter : IOpenApiDescribedElement, IOpenApiReadOnlyE
/// When example or examples are provided in conjunction with the schema object,
/// the example MUST follow the prescribed serialization strategy for the parameter.
/// </summary>
public Dictionary<string, OpenApiMediaType>? Content { get; }
public IDictionary<string, OpenApiMediaType>? Content { get; }
}
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiPathItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ public interface IOpenApiPathItem : IOpenApiDescribedElement, IOpenApiSummarized
/// <summary>
/// An alternative server array to service all operations in this path.
/// </summary>
public List<OpenApiServer>? Servers { get; }
public IList<OpenApiServer>? Servers { get; }

/// <summary>
/// A list of parameters that are applicable for all the operations described under this path.
/// These parameters can be overridden at the operation level, but cannot be removed there.
/// </summary>
public List<IOpenApiParameter>? Parameters { get; }
public IList<IOpenApiParameter>? Parameters { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface IOpenApiRequestBody : IOpenApiDescribedElement, IOpenApiReadOnl
/// REQUIRED. The content of the request body. The key is a media type or media type range and the value describes it.
/// For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/*
/// </summary>
public Dictionary<string, OpenApiMediaType>? Content { get; }
public IDictionary<string, OpenApiMediaType>? Content { get; }
/// <summary>
/// Converts the request body to a body parameter in preparation for a v2 serialization.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ public interface IOpenApiResponse : IOpenApiDescribedElement, IOpenApiReadOnlyEx
/// <summary>
/// Maps a header name to its definition.
/// </summary>
public Dictionary<string, IOpenApiHeader>? Headers { get; }
public IDictionary<string, IOpenApiHeader>? Headers { get; }

/// <summary>
/// A map containing descriptions of potential response payloads.
/// The key is a media type or media type range and the value describes it.
/// </summary>
public Dictionary<string, OpenApiMediaType>? Content { get; }
public IDictionary<string, OpenApiMediaType>? Content { get; }

/// <summary>
/// A map of operations links that can be followed from the response.
/// The key of the map is a short name for the link,
/// following the naming constraints of the names for Component Objects.
/// </summary>
public Dictionary<string, IOpenApiLink>? Links { get; }
public IDictionary<string, IOpenApiLink>? Links { get; }
}
24 changes: 12 additions & 12 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// <summary>
/// $vocabulary- used in meta-schemas to identify the vocabularies available for use in schemas described by that meta-schema.
/// </summary>
public Dictionary<string, bool>? Vocabulary { get; }
public IDictionary<string, bool>? Vocabulary { get; }

/// <summary>
/// $dynamicRef - an applicator that allows for deferring the full resolution until runtime, at which point it is resolved each time it is encountered while evaluating an instance
Expand All @@ -51,7 +51,7 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// $defs - reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema.
/// The keyword does not directly affect the validation result
/// </summary>
public Dictionary<string, IOpenApiSchema>? Definitions { get; }
public IDictionary<string, IOpenApiSchema>? Definitions { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
Expand Down Expand Up @@ -144,19 +144,19 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
/// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema.
/// </summary>
public List<IOpenApiSchema>? AllOf { get; }
public IList<IOpenApiSchema>? AllOf { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
/// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema.
/// </summary>
public List<IOpenApiSchema>? OneOf { get; }
public IList<IOpenApiSchema>? OneOf { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
/// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema.
/// </summary>
public List<IOpenApiSchema>? AnyOf { get; }
public IList<IOpenApiSchema>? AnyOf { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
Expand All @@ -167,7 +167,7 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
/// </summary>
public HashSet<string>? Required { get; }
public ISet<string>? Required { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
Expand Down Expand Up @@ -195,7 +195,7 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
/// Property definitions MUST be a Schema Object and not a standard JSON Schema (inline or referenced).
/// </summary>
public Dictionary<string, IOpenApiSchema>? Properties { get; }
public IDictionary<string, IOpenApiSchema>? Properties { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
Expand All @@ -204,7 +204,7 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// egular expression dialect. Each property value of this object MUST be an object, and each object MUST
/// be a valid Schema Object not a standard JSON Schema.
/// </summary>
public Dictionary<string, IOpenApiSchema>? PatternProperties { get; }
public IDictionary<string, IOpenApiSchema>? PatternProperties { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
Expand Down Expand Up @@ -246,12 +246,12 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// To represent examples that cannot be naturally represented in JSON or YAML,
/// a list of values can be used to contain the examples with escaping where necessary.
/// </summary>
public List<JsonNode>? Examples { get; }
public IList<JsonNode>? Examples { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
/// </summary>
public List<JsonNode>? Enum { get; }
public IList<JsonNode>? Enum { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
Expand All @@ -278,10 +278,10 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// <summary>
/// This object stores any unrecognized keywords found in the schema.
/// </summary>
public Dictionary<string, JsonNode>? UnrecognizedKeywords { get; }
public IDictionary<string, JsonNode>? UnrecognizedKeywords { get; }

/// <summary>
/// Follow JSON Schema definition:https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.4
/// </summary>
public Dictionary<string, HashSet<string>>? DependentRequired { get; }
public IDictionary<string, HashSet<string>>? DependentRequired { get; }
}
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Models/OpenApiCallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class OpenApiCallback : IOpenApiExtensible, IOpenApiCallback
/// <summary>
/// This object MAY be extended with Specification Extensions.
/// </summary>
public Dictionary<string, IOpenApiExtension>? Extensions { get; set; }
public IDictionary<string, IOpenApiExtension>? Extensions { get; set; }

/// <summary>
/// Parameter-less constructor
Expand Down
Loading
Loading