From 65d7cc853cdbd4619c94749553f1f7cf808e9f29 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 18 Aug 2025 08:28:48 -0400 Subject: [PATCH 001/655] chore(main): release 2.0.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ Directory.Build.props | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 65f558e71..cf7239890 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.0.0" + ".": "2.0.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 31303c961..d93184f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.1](https://github.com/microsoft/openapi.net/compare/v2.0.0...v2.0.1) (2025-08-18) + + +### Bug Fixes + +* add missing disposable for stream ([9318c00](https://github.com/microsoft/openapi.net/commit/9318c00c31466bf67d2e9701659a4cda3d82d1dd)) +* add missing disposable for stream ([0c1ccbd](https://github.com/microsoft/openapi.net/commit/0c1ccbdc1ba4c53662ff6c7132366ae435d8fedb)) + ## [2.0.0](https://github.com/microsoft/OpenAPI.NET/compare/v2.0.0-preview.31...v2.0.0) (2025-07-10) diff --git a/Directory.Build.props b/Directory.Build.props index 720c2c059..7d3692ed2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ https://github.com/Microsoft/OpenAPI.NET © Microsoft Corporation. All rights reserved. OpenAPI .NET - 2.0.0 + 2.0.1 From f047dfe482d5b2fff2e7bf43edc8e7be04c457af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:28:29 +0000 Subject: [PATCH 002/655] chore(deps): bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release-please-gha.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please-gha.yml b/.github/workflows/release-please-gha.yml index c58ac27d1..28f1d59d1 100644 --- a/.github/workflows/release-please-gha.yml +++ b/.github/workflows/release-please-gha.yml @@ -19,7 +19,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Generate GitHub App token id: app-token From 90b3966e5e071e26570050733482660f41f944b0 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Tue, 19 Aug 2025 14:10:01 +0100 Subject: [PATCH 003/655] Merge pull request #2459 from martincostello/gh-2453-validate-schema-references feat: adds a default validation rule for unresolved references --- .../Properties/SRResource.Designer.cs | 9 + .../Properties/SRResource.resx | 3 + .../Validations/Rules/OpenApiDocumentRules.cs | 74 ++++++- .../PublicApi/PublicApi.approved.txt | 1 + .../OpenApiDocumentValidationTests.cs | 195 ++++++++++++++++++ .../Validations/ValidationRuleSetTests.cs | 4 +- 6 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 3de246750..bd42672a5 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -384,6 +384,15 @@ internal static string Validation_RuleAddTwice { } } + /// + /// Looks up a localized string similar to The schema reference '{0}' does not point to an existing schema.. + /// + internal static string Validation_SchemaReferenceDoesNotExist { + get { + return ResourceManager.GetString("Validation_SchemaReferenceDoesNotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to Schema {0} must contain property specified in the discriminator {1} in the required field list.. /// diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index 6cd1fc2ba..b758ff89b 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -231,6 +231,9 @@ Value '{0}' is not a valid value for variable '{1}'. If an enum is provided, it should not be empty and the value provided should exist in the enum + + The schema reference '{0}' does not point to an existing schema. + The argument '{0}' is null. diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index a0bff927e..d15b0a0a0 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -23,9 +23,81 @@ public static class OpenApiDocumentRules if (item.Info == null) { context.CreateError(nameof(OpenApiDocumentFieldIsMissing), - String.Format(SRResource.Validation_FieldIsRequired, "info", "document")); + string.Format(SRResource.Validation_FieldIsRequired, "info", "document")); } context.Exit(); }); + + /// + /// All references in the OpenAPI document must be valid. + /// + public static ValidationRule OpenApiDocumentReferencesAreValid => + new(nameof(OpenApiDocumentReferencesAreValid), + static (context, item) => + { + const string RuleName = nameof(OpenApiDocumentReferencesAreValid); + + var visitor = new OpenApiSchemaReferenceVisitor(RuleName, context); + var walker = new OpenApiWalker(visitor); + + walker.Walk(item); + }); + + private sealed class OpenApiSchemaReferenceVisitor( + string ruleName, + IValidationContext context) : OpenApiVisitorBase + { + public override void Visit(IOpenApiReferenceHolder referenceHolder) + { + if (referenceHolder is OpenApiSchemaReference reference) + { + ValidateSchemaReference(reference); + } + } + + public override void Visit(IOpenApiSchema schema) + { + if (schema is OpenApiSchemaReference reference) + { + ValidateSchemaReference(reference); + } + } + + private void ValidateSchemaReference(OpenApiSchemaReference reference) + { + if (!reference.Reference.IsLocal) + { + return; + } + + try + { + if (reference.RecursiveTarget is null) + { + // The reference was not followed to a valid schema somewhere in the document + context.Enter(GetSegment()); + context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, reference.Reference.ReferenceV3)); + context.Exit(); + } + } + catch (InvalidOperationException ex) + { + context.Enter(GetSegment()); + context.CreateWarning(ruleName, ex.Message); + context.Exit(); + } + + string GetSegment() + { + // Trim off the leading "#/" as the context is already at the root of the document + return +#if NET8_0_OR_GREATER + $"{PathString[2..]}/$ref"; +#else + PathString.Substring(2) + "/$ref"; +#endif + } + } + } } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 891d0d4f4..f8d83d291 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -627,6 +627,7 @@ namespace Microsoft.OpenApi public static class OpenApiDocumentRules { public static Microsoft.OpenApi.ValidationRule OpenApiDocumentFieldIsMissing { get; } + public static Microsoft.OpenApi.ValidationRule OpenApiDocumentReferencesAreValid { get; } } public static class OpenApiElementExtensions { diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs new file mode 100644 index 000000000..467d5f3a7 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests; + +public static class OpenApiDocumentValidationTests +{ + [Fact] + public static void ValidateSchemaReferencesAreValid() + { + // Arrange + var document = new OpenApiDocument + { + Components = new OpenApiComponents(), + Info = new OpenApiInfo + { + Title = "People Document", + Version = "1.0.0" + }, + Paths = [], + Workspace = new() + }; + + document.AddComponent("Person", new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" } + } + }); + + document.Paths.Add("/people", new OpenApiPathItem + { + Operations = new Dictionary() + { + [HttpMethod.Get] = new OpenApiOperation + { + Responses = new() + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("Person", document), + } + } + } + } + } + } + }); + + // Act + var errors = document.Validate(ValidationRuleSet.GetDefaultRuleSet()); + var result = !errors.Any(); + + // Assert + Assert.True(result); + Assert.NotNull(errors); + Assert.Empty(errors); + } + + [Fact] + public static void ValidateSchemaReferencesAreInvalid() + { + // Arrange + var document = new OpenApiDocument + { + Components = new OpenApiComponents(), + Info = new OpenApiInfo + { + Title = "Pets Document", + Version = "1.0.0" + }, + Paths = [], + Workspace = new() + }; + + document.AddComponent("Person", new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" } + } + }); + + document.Paths.Add("/pets", new OpenApiPathItem + { + Operations = new Dictionary() + { + [HttpMethod.Get] = new OpenApiOperation + { + Responses = new() + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("Pet", document), + } + } + } + } + } + } + }); + + // Act + var errors = document.Validate(ValidationRuleSet.GetDefaultRuleSet()); + var result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("The schema reference '#/components/schemas/Pet' does not point to an existing schema.", error.Message); + Assert.Equal("#/paths/~1pets/get/responses/200/content/application~1json/schema/$ref", error.Pointer); + } + + [Fact] + public static void ValidateCircularSchemaReferencesAreDetected() + { + // Arrange + var document = new OpenApiDocument + { + Components = new OpenApiComponents(), + Info = new OpenApiInfo + { + Title = "Infinite Document", + Version = "1.0.0" + }, + Paths = [], + Workspace = new() + }; + + document.AddComponent("Cycle", new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["self"] = new OpenApiSchemaReference("#/components/schemas/Cycle/properties/self", document) + } + }); + + document.Paths.Add("/cycle", new OpenApiPathItem + { + Operations = new Dictionary() + { + [HttpMethod.Get] = new OpenApiOperation + { + Responses = new() + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("Cycle", document) + } + } + } + } + } + } + }); + + // Act + var errors = document.Validate(ValidationRuleSet.GetDefaultRuleSet()); + var result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("Circular reference detected while resolving schema: #/components/schemas/Cycle/properties/self", error.Message); + Assert.Equal("#/components/schemas/Cycle/properties/self/$ref", error.Pointer); + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index 45a2dfcad..e1414232e 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -53,8 +53,8 @@ public void RuleSetConstructorsReturnsTheCorrectRules() Assert.Empty(ruleSet_4.Rules); // Update the number if you add new default rule(s). - Assert.Equal(19, ruleSet_1.Rules.Count); - Assert.Equal(19, ruleSet_2.Rules.Count); + Assert.Equal(20, ruleSet_1.Rules.Count); + Assert.Equal(20, ruleSet_2.Rules.Count); Assert.Equal(3, ruleSet_3.Rules.Count); } From 8a53bfa5ac265c09ce7aaa41c9441123fad60a02 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 10:03:58 +0100 Subject: [PATCH 004/655] Publish benchmark results Publish benchmark results to GitHub Actions workflow artifacts. --- .github/workflows/ci-cd.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 71f2c3cc1..06a1402cf 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -75,6 +75,13 @@ jobs: dotnet run -c Release working-directory: ./performance/benchmark + - name: Publish benchmark results + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: benchmark-results + path: "${{ github.workspace }}/performance/benchmark/BenchmarkDotNet.Artifacts/results" + - name: Run comparison tool for empty models run: dotnet run -c Release --project ./performance/resultsComparer/resultsComparer.csproj -- compare $OLD_REPORT $NEW_REPORT -p IdenticalMemoryUsage shell: bash From e6cb02f908b6b29727e6234b8818ed025037d9fa Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 10:04:34 +0100 Subject: [PATCH 005/655] Update .gitignore - Ignore BenchmarkDotNet profiler files. - Ignore Visual Studio profiler session files. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 4caae17f4..258fee87a 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,7 @@ ipch/ *.VC.VC.opendb # Visual Studio profiler +*.diagsession *.psess *.vsp *.vspx @@ -286,3 +287,7 @@ __pycache__/ *.btm.cs *.odx.cs *.xsd.cs + +# BenchmarkDotNet profiler files +*.nettrace +*.speedscope.json From 84f657e6a446fde0c18c728bcbb37523cf890f1d Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 10:07:31 +0100 Subject: [PATCH 006/655] Improve OpenApiWalker performance - Avoid allocations from lambda closures. - Avoid allocations from context strings. - Avoid allocations from copying arrays. - Remove redundant null checks. --- .../Services/OpenApiWalker.cs | 653 +++++++++++------- 1 file changed, 387 insertions(+), 266 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 09a7e5a09..984dcff4a 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -42,14 +42,43 @@ public void Walk(OpenApiDocument? doc) _visitor.Visit(doc); - Walk(OpenApiConstants.Info, () => Walk(doc.Info)); - Walk(OpenApiConstants.Servers, () => Walk(doc.Servers)); - Walk(OpenApiConstants.Paths, () => Walk(doc.Paths)); - Walk(OpenApiConstants.Webhooks, () => Walk(doc.Webhooks)); - Walk(OpenApiConstants.Components, () => Walk(doc.Components)); - Walk(OpenApiConstants.Security, () => Walk(doc.Security)); - Walk(OpenApiConstants.ExternalDocs, () => Walk(doc.ExternalDocs)); - Walk(OpenApiConstants.Tags, () => Walk(doc.Tags)); + if (doc.Info is { } info) + { + WalkItem(OpenApiConstants.Info, doc.Info, Walk); + } + + if (doc.Servers is { } servers) + { + WalkItem(OpenApiConstants.Servers, servers, Walk); + } + + if (doc.Paths is { } paths) + { + WalkItem(OpenApiConstants.Paths, doc.Paths, Walk); + } + + WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, Walk); + + if (doc.Components is { } components) + { + WalkItem(OpenApiConstants.Components, components, Walk); + } + + if (doc.Security is { } security) + { + WalkItem(OpenApiConstants.Security, security, Walk); + } + + if (doc.ExternalDocs is { } externalDocs) + { + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + } + + if (doc.Tags is { } tags) + { + WalkItem(OpenApiConstants.Tags, tags, Walk); + } + Walk(doc as IOpenApiExtensible); } @@ -66,12 +95,23 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); // Visit tags - if (tags != null) + if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - var tagsAsArray = tags.ToArray(); - for (var i = 0; i < tagsAsArray.Length; i++) + WalkItem("0", only, Walk); + } + else + { + int index = 0; + foreach (var tag in tags) { - Walk(i.ToString(), () => Walk(tagsAsArray[i])); + if (tag is null) + { + continue; + } + + var context = index.ToString(); + WalkItem(context, tag, Walk); + index++; } } } @@ -89,12 +129,23 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); // Visit tags - if (tags != null) + if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) + { + WalkItem("0", only, Walk); + } + else { - var referencesAsArray = tags.ToArray(); - for (var i = 0; i < referencesAsArray.Length; i++) + int index = 0; + foreach (var tag in tags) { - Walk(i.ToString(), () => Walk(referencesAsArray[i])); + if (tag is null) + { + continue; + } + + var context = index.ToString(); + WalkItem(context, tag, Walk); + index++; } } } @@ -136,115 +187,17 @@ internal void Walk(OpenApiComponents? components) _visitor.Visit(components); - Walk(OpenApiConstants.Schemas, () => - { - if (components.Schemas != null) - { - foreach (var item in components.Schemas) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.SecuritySchemes, () => - { - if (components.SecuritySchemes != null) - { - foreach (var item in components.SecuritySchemes) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Callbacks, () => - { - if (components.Callbacks != null) - { - foreach (var item in components.Callbacks) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.PathItems, () => - { - if (components.PathItems != null) - { - foreach (var path in components.PathItems) - { - Walk(path.Key, () => Walk(path.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Parameters, () => - { - if (components.Parameters != null) - { - foreach (var item in components.Parameters) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Examples, () => - { - if (components.Examples != null) - { - foreach (var item in components.Examples) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Headers, () => - { - if (components.Headers != null) - { - foreach (var item in components.Headers) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Links, () => - { - if (components.Links != null) - { - foreach (var item in components.Links) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.RequestBodies, () => - { - if (components.RequestBodies != null) - { - foreach (var item in components.RequestBodies) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Responses, () => - { - if (components.Responses != null) - { - foreach (var item in components.Responses) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); + var isComponent = true; + WalkDictionary(OpenApiConstants.Schemas, components.Schemas, Walk, isComponent); + WalkDictionary(OpenApiConstants.SecuritySchemes, components.SecuritySchemes, Walk, isComponent); + WalkDictionary(OpenApiConstants.Callbacks, components.Callbacks, Walk, isComponent); + WalkDictionary(OpenApiConstants.PathItems, components.PathItems, Walk, isComponent); + WalkDictionary(OpenApiConstants.Parameters, components.Parameters, Walk, isComponent); + WalkDictionary(OpenApiConstants.Examples, components.Examples, Walk, isComponent); + WalkDictionary(OpenApiConstants.Headers, components.Headers, Walk, isComponent); + WalkDictionary(OpenApiConstants.Links, components.Links, Walk, isComponent); + WalkDictionary(OpenApiConstants.RequestBodies, components.RequestBodies, Walk, isComponent); + WalkDictionary(OpenApiConstants.Responses, components.Responses, Walk, isComponent); Walk(components as IOpenApiExtensible); } @@ -262,16 +215,17 @@ internal void Walk(OpenApiPaths paths) _visitor.Visit(paths); // Visit Paths - if (paths != null) + foreach (var pathItem in paths) { - foreach (var pathItem in paths) + if (pathItem.Value is null) { - _visitor.CurrentKeys.Path = pathItem.Key; - Walk(pathItem.Key, () => Walk(pathItem.Value));// JSON Pointer uses ~1 as an escape character for / - _visitor.CurrentKeys.Path = null; + continue; } - } + _visitor.CurrentKeys.Path = pathItem.Key; + WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + _visitor.CurrentKeys.Path = null; + } } /// @@ -287,14 +241,16 @@ internal void Walk(IDictionary? webhooks) _visitor.Visit(webhooks); // Visit Webhooks - if (webhooks != null) + foreach (var pathItem in webhooks) { - foreach (var pathItem in webhooks) + if (pathItem.Value is null) { - _visitor.CurrentKeys.Path = pathItem.Key; - Walk(pathItem.Key, () => Walk(pathItem.Value));// JSON Pointer uses ~1 as an escape character for / - _visitor.CurrentKeys.Path = null; + continue; } + + _visitor.CurrentKeys.Path = pathItem.Key; + WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + _visitor.CurrentKeys.Path = null; } } @@ -311,12 +267,14 @@ internal void Walk(IList? servers) _visitor.Visit(servers); // Visit Servers - if (servers != null) + for (var i = 0; i < servers.Count; i++) { - for (var i = 0; i < servers.Count; i++) + if (servers[i] is not { } server) { - Walk(i.ToString(), () => Walk(servers[i])); + continue; } + + WalkItem(i.ToString(), server, Walk); } } @@ -331,11 +289,17 @@ internal void Walk(OpenApiInfo info) } _visitor.Visit(info); - if (info != null) + + if (info.Contact is { } contact) + { + WalkItem(OpenApiConstants.Contact, contact, Walk); + } + + if (info.License is { } license) { - Walk(OpenApiConstants.Contact, () => Walk(info.Contact)); - Walk(OpenApiConstants.License, () => Walk(info.License)); + WalkItem(OpenApiConstants.License, license, Walk); } + Walk(info as IOpenApiExtensible); } @@ -355,8 +319,13 @@ internal void Walk(IOpenApiExtensible? openApiExtensible) { foreach (var item in openApiExtensible.Extensions) { + if (item.Value == null) + { + continue; + } + _visitor.CurrentKeys.Extension = item.Key; - Walk(item.Key, () => Walk(item.Value)); + WalkItem(item.Key, item.Value, Walk); _visitor.CurrentKeys.Extension = null; } } @@ -423,9 +392,15 @@ internal void Walk(IOpenApiCallback callback, bool isComponent = false) { foreach (var item in callback.PathItems) { - _visitor.CurrentKeys.Callback = item.Key.ToString(); - var pathItem = item.Value; - Walk(item.Key.ToString(), () => Walk(pathItem)); + if (item.Value is null) + { + continue; + } + + var context = item.Key.ToString(); + + _visitor.CurrentKeys.Callback = context; + WalkItem(context, item.Value, Walk); _visitor.CurrentKeys.Callback = null; } } @@ -442,9 +417,9 @@ internal void Walk(OpenApiTag tag) } _visitor.Visit(tag); - if (tag.ExternalDocs != null) + if (tag.ExternalDocs is { } externalDocs) { - _visitor.Visit(tag.ExternalDocs); + _visitor.Visit(externalDocs); } _visitor.Visit(tag as IOpenApiExtensible); } @@ -454,11 +429,6 @@ internal void Walk(OpenApiTag tag) /// internal void Walk(OpenApiTagReference tag) { - if (tag == null) - { - return; - } - if (tag is IOpenApiReferenceHolder openApiReferenceHolder) { Walk(openApiReferenceHolder); @@ -476,7 +446,12 @@ internal void Walk(OpenApiServer? server) } _visitor.Visit(server); - Walk(OpenApiConstants.Variables, () => Walk(server.Variables)); + + if (server.Variables is { } variables) + { + WalkItem(OpenApiConstants.Variables, variables, Walk); + } + _visitor.Visit(server as IOpenApiExtensible); } @@ -492,14 +467,16 @@ internal void Walk(IDictionary? serverVariables) _visitor.Visit(serverVariables); - if (serverVariables != null) + foreach (var variable in serverVariables) { - foreach (var variable in serverVariables) + if (variable.Value == null) { - _visitor.CurrentKeys.ServerVariable = variable.Key; - Walk(variable.Key, () => Walk(variable.Value)); - _visitor.CurrentKeys.ServerVariable = null; + continue; } + + _visitor.CurrentKeys.ServerVariable = variable.Key; + WalkItem(variable.Key, variable.Value, Walk); + _visitor.CurrentKeys.ServerVariable = null; } } @@ -546,7 +523,11 @@ internal void Walk(IOpenApiPathItem pathItem, bool isComponent = false) if (pathItem != null) { - Walk(OpenApiConstants.Parameters, () => Walk(pathItem.Parameters)); + if (pathItem.Parameters is { } parameters) + { + WalkItem(OpenApiConstants.Parameters, parameters, Walk); + } + Walk(pathItem.Operations); } @@ -568,14 +549,17 @@ internal void Walk(IDictionary? operations) } _visitor.Visit(operations); - if (operations != null) + + foreach (var operation in operations) { - foreach (var operation in operations) + if (operation.Value is null) { - _visitor.CurrentKeys.Operation = operation.Key; - Walk(operation.Key.Method.ToLowerInvariant(), () => Walk(operation.Value)); - _visitor.CurrentKeys.Operation = null; + continue; } + + _visitor.CurrentKeys.Operation = operation.Key; + WalkItem(operation.Key.Method.ToLowerInvariant(), operation.Value, Walk); + _visitor.CurrentKeys.Operation = null; } } @@ -592,12 +576,30 @@ internal void Walk(OpenApiOperation operation) _visitor.Visit(operation); - Walk(OpenApiConstants.Parameters, () => Walk(operation.Parameters)); + if (operation.Parameters is { } parameters) + { + WalkItem(OpenApiConstants.Parameters, parameters, Walk); + } + Walk(OpenApiConstants.RequestBody, () => Walk(operation.RequestBody)); - Walk(OpenApiConstants.Responses, () => Walk(operation.Responses)); - Walk(OpenApiConstants.Callbacks, () => Walk(operation.Callbacks)); - Walk(OpenApiConstants.Tags, () => Walk(operation.Tags)); - Walk(OpenApiConstants.Security, () => Walk(operation.Security)); + + if (operation.Responses is { } responses) + { + WalkItem(OpenApiConstants.Responses, responses, Walk); + } + + WalkDictionary(OpenApiConstants.Callbacks, operation.Callbacks, Walk); + + if (operation.Tags is { } tags) + { + WalkItem(OpenApiConstants.Tags, tags, Walk); + } + + if (operation.Security is { } security) + { + WalkItem(OpenApiConstants.Security, security, Walk); + } + Walk(operation as IOpenApiExtensible); } @@ -613,12 +615,14 @@ internal void Walk(IList? securityRequirements) _visitor.Visit(securityRequirements); - if (securityRequirements != null) + for (var i = 0; i < securityRequirements.Count; i++) { - for (var i = 0; i < securityRequirements.Count; i++) + if (securityRequirements[i] is not { } requirement) { - Walk(i.ToString(), () => Walk(securityRequirements[i])); + continue; } + + WalkItem(i.ToString(), requirement, Walk); } } @@ -634,12 +638,14 @@ internal void Walk(IList? parameters) _visitor.Visit(parameters); - if (parameters != null) + for (var i = 0; i < parameters.Count; i++) { - for (var i = 0; i < parameters.Count; i++) + if (parameters[i] is not { } parameter) { - Walk(i.ToString(), () => Walk(parameters[i])); + continue; } + + WalkItem(i.ToString(), parameter, Walk); } } @@ -660,9 +666,18 @@ internal void Walk(IOpenApiParameter parameter, bool isComponent = false) } _visitor.Visit(parameter); - Walk(OpenApiConstants.Schema, () => Walk(parameter.Schema)); - Walk(OpenApiConstants.Content, () => Walk(parameter.Content)); - Walk(OpenApiConstants.Examples, () => Walk(parameter.Examples)); + + if (parameter.Schema is { } schema) + { + WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + } + + if (parameter.Content is { } content) + { + WalkItem(OpenApiConstants.Content, content, Walk); + } + + WalkDictionary(OpenApiConstants.Examples, parameter.Examples, Walk); Walk(parameter as IOpenApiExtensible); } @@ -679,15 +694,18 @@ internal void Walk(OpenApiResponses? responses) _visitor.Visit(responses); - if (responses != null) + foreach (var response in responses) { - foreach (var response in responses) + if (response.Value is null) { - _visitor.CurrentKeys.Response = response.Key; - Walk(response.Key, () => Walk(response.Value)); - _visitor.CurrentKeys.Response = null; + continue; } + + _visitor.CurrentKeys.Response = response.Key; + WalkItem(response.Key, response.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Response = null; } + Walk(responses as IOpenApiExtensible); } @@ -708,9 +726,14 @@ internal void Walk(IOpenApiResponse response, bool isComponent = false) } _visitor.Visit(response); - Walk(OpenApiConstants.Content, () => Walk(response.Content)); - Walk(OpenApiConstants.Links, () => Walk(response.Links)); - Walk(OpenApiConstants.Headers, () => Walk(response.Headers)); + + if (response.Content is { } content) + { + WalkItem(OpenApiConstants.Content, content, Walk); + } + + WalkDictionary(OpenApiConstants.Links, response.Links, Walk); + WalkDictionary(OpenApiConstants.Headers, response.Headers, Walk); Walk(response as IOpenApiExtensible); } @@ -732,10 +755,11 @@ internal void Walk(IOpenApiRequestBody? requestBody, bool isComponent = false) _visitor.Visit(requestBody); - if (requestBody is {Content: not null}) + if (requestBody.Content is { } content) { - Walk(OpenApiConstants.Content, () => Walk(requestBody.Content)); + WalkItem(OpenApiConstants.Content, content, Walk); } + Walk(requestBody as IOpenApiExtensible); } @@ -750,14 +774,17 @@ internal void Walk(IDictionary? headers) } _visitor.Visit(headers); - if (headers != null) + + foreach (var header in headers) { - foreach (var header in headers) + if (header.Value is null) { - _visitor.CurrentKeys.Header = header.Key; - Walk(header.Key, () => Walk(header.Value)); - _visitor.CurrentKeys.Header = null; + continue; } + + _visitor.CurrentKeys.Header = header.Key; + WalkItem(header.Key, header.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Header = null; } } @@ -772,14 +799,17 @@ internal void Walk(IDictionary? callbacks) } _visitor.Visit(callbacks); - if (callbacks != null) + + foreach (var callback in callbacks) { - foreach (var callback in callbacks) + if (callback.Value is null) { - _visitor.CurrentKeys.Callback = callback.Key; - Walk(callback.Key, () => Walk(callback.Value)); - _visitor.CurrentKeys.Callback = null; + continue; } + + _visitor.CurrentKeys.Callback = callback.Key; + WalkItem(callback.Key, callback.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Callback = null; } } @@ -794,14 +824,17 @@ internal void Walk(IDictionary? content) } _visitor.Visit(content); - if (content != null) + + foreach (var mediaType in content) { - foreach (var mediaType in content) + if (mediaType.Value is null) { - _visitor.CurrentKeys.Content = mediaType.Key; - Walk(mediaType.Key, () => Walk(mediaType.Value)); - _visitor.CurrentKeys.Content = null; + continue; } + + _visitor.CurrentKeys.Content = mediaType.Key; + WalkItem(mediaType.Key, mediaType.Value, Walk); + _visitor.CurrentKeys.Content = null; } } @@ -817,9 +850,18 @@ internal void Walk(OpenApiMediaType mediaType) _visitor.Visit(mediaType); - Walk(OpenApiConstants.Example, () => Walk(mediaType.Examples)); - Walk(OpenApiConstants.Schema, () => Walk(mediaType.Schema)); - Walk(OpenApiConstants.Encoding, () => Walk(mediaType.Encoding)); + WalkDictionary(OpenApiConstants.Example, mediaType.Examples, Walk); + + if (mediaType.Schema is { } schema) + { + WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + } + + if (mediaType.Encoding is { } encoding) + { + WalkItem(OpenApiConstants.Encoding, encoding, Walk); + } + Walk(mediaType as IOpenApiExtensible); } @@ -835,14 +877,16 @@ internal void Walk(IDictionary? encodings) _visitor.Visit(encodings); - if (encodings != null) + foreach (var item in encodings) { - foreach (var item in encodings) + if (item.Value is null) { - _visitor.CurrentKeys.Encoding = item.Key; - Walk(item.Key, () => Walk(item.Value)); - _visitor.CurrentKeys.Encoding = null; + continue; } + + _visitor.CurrentKeys.Encoding = item.Key; + WalkItem(item.Key, item.Value, Walk); + _visitor.CurrentKeys.Encoding = null; } } @@ -858,10 +902,11 @@ internal void Walk(OpenApiEncoding encoding) _visitor.Visit(encoding); - if (encoding.Headers != null) + if (encoding.Headers is { } headers) { - Walk(encoding.Headers); + Walk(headers); } + Walk(encoding as IOpenApiExtensible); } @@ -886,50 +931,50 @@ internal void Walk(IOpenApiSchema? schema, bool isComponent = false) _visitor.Visit(schema); - if (schema.Items != null) + if (schema.Items is { } items) { - Walk("items", () => Walk(schema.Items)); + WalkItem("items", items, Walk, isComponent: false); } - if (schema.Not != null) + if (schema.Not is { } not) { - Walk("not", () => Walk(schema.Not)); + WalkItem("not", not, Walk, isComponent: false); } - if (schema.AllOf != null) + if (schema.AllOf is { Count: > 0 } allOf) { - Walk("allOf", () => Walk(schema.AllOf)); + WalkItem("allOf", allOf, Walk); } - if (schema.AnyOf != null) + if (schema.AnyOf is { Count: > 0 } anyOf) { - Walk("anyOf", () => Walk(schema.AnyOf)); + WalkItem("anyOf", anyOf, Walk); } - if (schema.OneOf != null) + if (schema.OneOf is { Count: > 0 } oneOf) { - Walk("oneOf", () => Walk(schema.OneOf)); + WalkItem("oneOf", oneOf, Walk); } - if (schema.Properties != null) + if (schema.Properties is { } properties) { - Walk("properties", () => - { - foreach (var item in schema.Properties) - { - Walk(item.Key, () => Walk(item.Value)); - } - }); + WalkDictionary("properties", properties, Walk); } - if (schema.AdditionalProperties != null) + if (schema.AdditionalProperties is { } additionalProperties) { - Walk("additionalProperties", () => Walk(schema.AdditionalProperties)); + WalkItem("additionalProperties", additionalProperties, Walk, isComponent: false); } - Walk("discriminator", () => Walk(schema.Discriminator)); + if (schema.Discriminator is { } discriminator) + { + WalkItem("discriminator", discriminator, Walk); + } - Walk(OpenApiConstants.ExternalDocs, () => Walk(schema.ExternalDocs)); + if (schema.ExternalDocs is { } externalDocs) + { + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + } Walk(schema as IOpenApiExtensible); @@ -945,19 +990,12 @@ internal void Walk(OpenApiDiscriminator? openApiDiscriminator) _visitor.Visit(openApiDiscriminator); - if (openApiDiscriminator.Mapping != null) + if (openApiDiscriminator.Mapping is { Count: > 0 } mapping) { - Walk("mapping", () => - { - foreach (var item in openApiDiscriminator.Mapping) - { - Walk(item.Key, () => Walk((IOpenApiSchema)item.Value)); - } - }); + WalkDictionary("mapping", mapping, (item, _) => Walk((IOpenApiSchema)item)); } } - /// /// Visits dictionary of /// @@ -970,14 +1008,16 @@ internal void Walk(IDictionary? examples) _visitor.Visit(examples); - if (examples != null) + foreach (var example in examples) { - foreach (var example in examples) + if (example.Value is null) { - _visitor.CurrentKeys.Example = example.Key; - Walk(example.Key, () => Walk(example.Value)); - _visitor.CurrentKeys.Example = null; + continue; } + + _visitor.CurrentKeys.Example = example.Key; + WalkItem(example.Key, example.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Example = null; } } @@ -1027,12 +1067,14 @@ internal void Walk(List examples) _visitor.Visit(examples); // Visit Examples - if (examples != null) + for (var i = 0; i < examples.Count; i++) { - for (var i = 0; i < examples.Count; i++) + if (examples[i] is not { } example) { - Walk(i.ToString(), () => Walk(examples[i])); + continue; } + + WalkItem(i.ToString(), example, Walk, isComponent: false); } } @@ -1047,12 +1089,14 @@ internal void Walk(IList schemas) } // Visit Schemas - if (schemas != null) + for (var i = 0; i < schemas.Count; i++) { - for (var i = 0; i < schemas.Count; i++) + if (schemas[i] is not { } schema) { - Walk(i.ToString(), () => Walk(schemas[i])); + continue; } + + WalkItem(i.ToString(), schema, Walk, isComponent: false); } } @@ -1095,14 +1139,16 @@ internal void Walk(IDictionary? links) _visitor.Visit(links); - if (links != null) + foreach (var item in links) { - foreach (var item in links) + if (item.Value is null) { - _visitor.CurrentKeys.Link = item.Key; - Walk(item.Key, () => Walk(item.Value)); - _visitor.CurrentKeys.Link = null; + continue; } + + _visitor.CurrentKeys.Link = item.Key; + WalkItem(item.Key, item.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Link = null; } } @@ -1123,7 +1169,12 @@ internal void Walk(IOpenApiLink link, bool isComponent = false) } _visitor.Visit(link); - Walk(OpenApiConstants.Server, () => Walk(link.Server)); + + if (link.Server is { } server) + { + WalkItem(OpenApiConstants.Server, server, Walk); + } + Walk(link as IOpenApiExtensible); } @@ -1144,10 +1195,24 @@ internal void Walk(IOpenApiHeader header, bool isComponent = false) } _visitor.Visit(header); - Walk(OpenApiConstants.Content, () => Walk(header.Content)); - Walk(OpenApiConstants.Example, () => Walk(header.Example)); - Walk(OpenApiConstants.Examples, () => Walk(header.Examples)); - Walk(OpenApiConstants.Schema, () => Walk(header.Schema)); + + if (header.Content is { } content) + { + WalkItem(OpenApiConstants.Content, content, Walk); + } + + if (header.Example is { } example) + { + WalkItem(OpenApiConstants.Example, example, Walk); + } + + WalkDictionary(OpenApiConstants.Examples, header.Examples, Walk); + + if (header.Schema is { } schema) + { + WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + } + Walk(header as IOpenApiExtensible); } @@ -1257,6 +1322,62 @@ private void Walk(string context, Action walk) _visitor.Exit(); } + /// + /// Adds a segment to the context path to enable pointing to the current location in the document + /// + /// The type of the state. + /// An identifier for the context. + /// The state to pass to the walk action. + /// An action that walks objects within the context. + private void WalkItem(string context, T state, Action walk) + { + _visitor.Enter(context.Replace("/", "~1")); + walk(state); + _visitor.Exit(); + } + + /// + /// Adds a segment to the context path to enable pointing to the current location in the document + /// + /// The type of the state. + /// An identifier for the context. + /// The state to pass to the walk action. + /// Whether the state is a component. + /// An action that walks objects within the context. + private void WalkItem(string context, T state, Action walk, bool isComponent) + { + _visitor.Enter(context.Replace("/", "~1")); + walk(state, isComponent); + _visitor.Exit(); + } + + /// + /// Adds a segment to the context path to enable pointing to the current location in the document + /// + /// The type of the state. + /// An identifier for the context. + /// The state to pass to the walk action. + /// Whether the state is a component. + /// An action that walks objects within the context. + private void WalkDictionary( + string context, + IDictionary? state, + Action walk, + bool isComponent = false) + { + if (state != null && state.Count > 0) + { + _visitor.Enter(context.Replace("/", "~1")); + + foreach (var item in state) + { + WalkItem(item.Key, (item.Value, isComponent), (state) => walk(state.Value, state.isComponent)); + } + + _visitor.Exit(); + } + } + /// /// Identify if an element is just a reference to a component, or an actual component /// From 4b8ee8a4d9229f9342c0d6e9b3eb9f9a19c5eefa Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 12:59:58 +0100 Subject: [PATCH 007/655] Reduce allocations further Make all delegates static. --- .../Services/OpenApiWalker.cs | 165 +++++++++--------- 1 file changed, 84 insertions(+), 81 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 984dcff4a..e1603be8b 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -44,39 +44,39 @@ public void Walk(OpenApiDocument? doc) if (doc.Info is { } info) { - WalkItem(OpenApiConstants.Info, doc.Info, Walk); + WalkItem(OpenApiConstants.Info, doc.Info, static (self, item) => self.Walk(item)); } if (doc.Servers is { } servers) { - WalkItem(OpenApiConstants.Servers, servers, Walk); + WalkItem(OpenApiConstants.Servers, servers, static (self, item) => self.Walk(item)); } if (doc.Paths is { } paths) { - WalkItem(OpenApiConstants.Paths, doc.Paths, Walk); + WalkItem(OpenApiConstants.Paths, doc.Paths, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, Walk); + WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, static (self, item, isComponent) => self.Walk(item, isComponent)); if (doc.Components is { } components) { - WalkItem(OpenApiConstants.Components, components, Walk); + WalkItem(OpenApiConstants.Components, components, static (self, item) => self.Walk(item)); } if (doc.Security is { } security) { - WalkItem(OpenApiConstants.Security, security, Walk); + WalkItem(OpenApiConstants.Security, security, static (self, item) => self.Walk(item)); } if (doc.ExternalDocs is { } externalDocs) { - WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, static (self, item) => self.Walk(item)); } if (doc.Tags is { } tags) { - WalkItem(OpenApiConstants.Tags, tags, Walk); + WalkItem(OpenApiConstants.Tags, tags, static (self, item) => self.Walk(item)); } Walk(doc as IOpenApiExtensible); @@ -97,7 +97,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, Walk); + WalkItem("0", only, static (self, item) => self.Walk(item)); } else { @@ -110,7 +110,7 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, Walk); + WalkItem(context, tag, static (self, item) => self.Walk(item)); index++; } } @@ -131,7 +131,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, Walk); + WalkItem("0", only, static (self, item) => self.Walk(item)); } else { @@ -144,7 +144,7 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, Walk); + WalkItem(context, tag, static (self, item) => self.Walk(item)); index++; } } @@ -188,16 +188,16 @@ internal void Walk(OpenApiComponents? components) _visitor.Visit(components); var isComponent = true; - WalkDictionary(OpenApiConstants.Schemas, components.Schemas, Walk, isComponent); - WalkDictionary(OpenApiConstants.SecuritySchemes, components.SecuritySchemes, Walk, isComponent); - WalkDictionary(OpenApiConstants.Callbacks, components.Callbacks, Walk, isComponent); - WalkDictionary(OpenApiConstants.PathItems, components.PathItems, Walk, isComponent); - WalkDictionary(OpenApiConstants.Parameters, components.Parameters, Walk, isComponent); - WalkDictionary(OpenApiConstants.Examples, components.Examples, Walk, isComponent); - WalkDictionary(OpenApiConstants.Headers, components.Headers, Walk, isComponent); - WalkDictionary(OpenApiConstants.Links, components.Links, Walk, isComponent); - WalkDictionary(OpenApiConstants.RequestBodies, components.RequestBodies, Walk, isComponent); - WalkDictionary(OpenApiConstants.Responses, components.Responses, Walk, isComponent); + WalkDictionary(OpenApiConstants.Schemas, components.Schemas, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.SecuritySchemes, components.SecuritySchemes, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Callbacks, components.Callbacks, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.PathItems, components.PathItems, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Parameters, components.Parameters, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Examples, components.Examples, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Headers, components.Headers, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Links, components.Links, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.RequestBodies, components.RequestBodies, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Responses, components.Responses, static (self, item, isComponent) => self.Walk(item), isComponent); Walk(components as IOpenApiExtensible); } @@ -223,7 +223,7 @@ internal void Walk(OpenApiPaths paths) } _visitor.CurrentKeys.Path = pathItem.Key; - WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + WalkItem(pathItem.Key, pathItem.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false);// JSON Pointer uses ~1 as an escape character for / _visitor.CurrentKeys.Path = null; } } @@ -249,7 +249,7 @@ internal void Walk(IDictionary? webhooks) } _visitor.CurrentKeys.Path = pathItem.Key; - WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + WalkItem(pathItem.Key, pathItem.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false);// JSON Pointer uses ~1 as an escape character for / _visitor.CurrentKeys.Path = null; } } @@ -274,7 +274,7 @@ internal void Walk(IList? servers) continue; } - WalkItem(i.ToString(), server, Walk); + WalkItem(i.ToString(), server, static (self, item) => self.Walk(item)); } } @@ -292,12 +292,12 @@ internal void Walk(OpenApiInfo info) if (info.Contact is { } contact) { - WalkItem(OpenApiConstants.Contact, contact, Walk); + WalkItem(OpenApiConstants.Contact, contact, static (self, item) => self.Walk(item)); } if (info.License is { } license) { - WalkItem(OpenApiConstants.License, license, Walk); + WalkItem(OpenApiConstants.License, license, static (self, item) => self.Walk(item)); } Walk(info as IOpenApiExtensible); @@ -325,7 +325,7 @@ internal void Walk(IOpenApiExtensible? openApiExtensible) } _visitor.CurrentKeys.Extension = item.Key; - WalkItem(item.Key, item.Value, Walk); + WalkItem(item.Key, item.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Extension = null; } } @@ -400,7 +400,7 @@ internal void Walk(IOpenApiCallback callback, bool isComponent = false) var context = item.Key.ToString(); _visitor.CurrentKeys.Callback = context; - WalkItem(context, item.Value, Walk); + WalkItem(context, item.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Callback = null; } } @@ -449,7 +449,7 @@ internal void Walk(OpenApiServer? server) if (server.Variables is { } variables) { - WalkItem(OpenApiConstants.Variables, variables, Walk); + WalkItem(OpenApiConstants.Variables, variables, static (self, item) => self.Walk(item)); } _visitor.Visit(server as IOpenApiExtensible); @@ -475,7 +475,7 @@ internal void Walk(IDictionary? serverVariables) } _visitor.CurrentKeys.ServerVariable = variable.Key; - WalkItem(variable.Key, variable.Value, Walk); + WalkItem(variable.Key, variable.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.ServerVariable = null; } } @@ -525,7 +525,7 @@ internal void Walk(IOpenApiPathItem pathItem, bool isComponent = false) { if (pathItem.Parameters is { } parameters) { - WalkItem(OpenApiConstants.Parameters, parameters, Walk); + WalkItem(OpenApiConstants.Parameters, parameters, static (self, item) => self.Walk(item)); } Walk(pathItem.Operations); @@ -558,7 +558,7 @@ internal void Walk(IDictionary? operations) } _visitor.CurrentKeys.Operation = operation.Key; - WalkItem(operation.Key.Method.ToLowerInvariant(), operation.Value, Walk); + WalkItem(operation.Key.Method.ToLowerInvariant(), operation.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Operation = null; } } @@ -578,26 +578,29 @@ internal void Walk(OpenApiOperation operation) if (operation.Parameters is { } parameters) { - WalkItem(OpenApiConstants.Parameters, parameters, Walk); + WalkItem(OpenApiConstants.Parameters, parameters, static (self, item) => self.Walk(item)); } - Walk(OpenApiConstants.RequestBody, () => Walk(operation.RequestBody)); + if (operation.RequestBody is { } requestBody) + { + WalkItem(OpenApiConstants.RequestBody, requestBody , static (self, item) => self.Walk(item)); + } if (operation.Responses is { } responses) { - WalkItem(OpenApiConstants.Responses, responses, Walk); + WalkItem(OpenApiConstants.Responses, responses, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Callbacks, operation.Callbacks, Walk); + WalkDictionary(OpenApiConstants.Callbacks, operation.Callbacks, static (self, item, isComponent) => self.Walk(item, isComponent)); if (operation.Tags is { } tags) { - WalkItem(OpenApiConstants.Tags, tags, Walk); + WalkItem(OpenApiConstants.Tags, tags, static (self, item) => self.Walk(item)); } if (operation.Security is { } security) { - WalkItem(OpenApiConstants.Security, security, Walk); + WalkItem(OpenApiConstants.Security, security, static (self, item) => self.Walk(item)); } Walk(operation as IOpenApiExtensible); @@ -622,7 +625,7 @@ internal void Walk(IList? securityRequirements) continue; } - WalkItem(i.ToString(), requirement, Walk); + WalkItem(i.ToString(), requirement, static (self, item) => self.Walk(item)); } } @@ -645,7 +648,7 @@ internal void Walk(IList? parameters) continue; } - WalkItem(i.ToString(), parameter, Walk); + WalkItem(i.ToString(), parameter, static (self, item) => self.Walk(item)); } } @@ -669,15 +672,15 @@ internal void Walk(IOpenApiParameter parameter, bool isComponent = false) if (parameter.Schema is { } schema) { - WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + WalkItem(OpenApiConstants.Schema, schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (parameter.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Examples, parameter.Examples, Walk); + WalkDictionary(OpenApiConstants.Examples, parameter.Examples, static (self, item, isComponent) => self.Walk(item, isComponent)); Walk(parameter as IOpenApiExtensible); } @@ -702,7 +705,7 @@ internal void Walk(OpenApiResponses? responses) } _visitor.CurrentKeys.Response = response.Key; - WalkItem(response.Key, response.Value, Walk, isComponent: false); + WalkItem(response.Key, response.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Response = null; } @@ -729,11 +732,11 @@ internal void Walk(IOpenApiResponse response, bool isComponent = false) if (response.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Links, response.Links, Walk); - WalkDictionary(OpenApiConstants.Headers, response.Headers, Walk); + WalkDictionary(OpenApiConstants.Links, response.Links, static (self, item, isComponent) => self.Walk(item, isComponent)); + WalkDictionary(OpenApiConstants.Headers, response.Headers, static (self, item, isComponent) => self.Walk(item, isComponent)); Walk(response as IOpenApiExtensible); } @@ -757,7 +760,7 @@ internal void Walk(IOpenApiRequestBody? requestBody, bool isComponent = false) if (requestBody.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } Walk(requestBody as IOpenApiExtensible); @@ -783,7 +786,7 @@ internal void Walk(IDictionary? headers) } _visitor.CurrentKeys.Header = header.Key; - WalkItem(header.Key, header.Value, Walk, isComponent: false); + WalkItem(header.Key, header.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Header = null; } } @@ -808,7 +811,7 @@ internal void Walk(IDictionary? callbacks) } _visitor.CurrentKeys.Callback = callback.Key; - WalkItem(callback.Key, callback.Value, Walk, isComponent: false); + WalkItem(callback.Key, callback.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Callback = null; } } @@ -833,7 +836,7 @@ internal void Walk(IDictionary? content) } _visitor.CurrentKeys.Content = mediaType.Key; - WalkItem(mediaType.Key, mediaType.Value, Walk); + WalkItem(mediaType.Key, mediaType.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Content = null; } } @@ -850,16 +853,16 @@ internal void Walk(OpenApiMediaType mediaType) _visitor.Visit(mediaType); - WalkDictionary(OpenApiConstants.Example, mediaType.Examples, Walk); + WalkDictionary(OpenApiConstants.Example, mediaType.Examples, static (self, item, isComponent) => self.Walk(item, isComponent)); if (mediaType.Schema is { } schema) { - WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + WalkItem(OpenApiConstants.Schema, schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (mediaType.Encoding is { } encoding) { - WalkItem(OpenApiConstants.Encoding, encoding, Walk); + WalkItem(OpenApiConstants.Encoding, encoding, static (self, item) => self.Walk(item)); } Walk(mediaType as IOpenApiExtensible); @@ -885,7 +888,7 @@ internal void Walk(IDictionary? encodings) } _visitor.CurrentKeys.Encoding = item.Key; - WalkItem(item.Key, item.Value, Walk); + WalkItem(item.Key, item.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Encoding = null; } } @@ -933,47 +936,47 @@ internal void Walk(IOpenApiSchema? schema, bool isComponent = false) if (schema.Items is { } items) { - WalkItem("items", items, Walk, isComponent: false); + WalkItem("items", items, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (schema.Not is { } not) { - WalkItem("not", not, Walk, isComponent: false); + WalkItem("not", not, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (schema.AllOf is { Count: > 0 } allOf) { - WalkItem("allOf", allOf, Walk); + WalkItem("allOf", allOf, static (self, item) => self.Walk(item)); } if (schema.AnyOf is { Count: > 0 } anyOf) { - WalkItem("anyOf", anyOf, Walk); + WalkItem("anyOf", anyOf, static (self, item) => self.Walk(item)); } if (schema.OneOf is { Count: > 0 } oneOf) { - WalkItem("oneOf", oneOf, Walk); + WalkItem("oneOf", oneOf, static (self, item) => self.Walk(item)); } if (schema.Properties is { } properties) { - WalkDictionary("properties", properties, Walk); + WalkDictionary("properties", properties, static (self, item, isComponent) => self.Walk(item, isComponent)); } if (schema.AdditionalProperties is { } additionalProperties) { - WalkItem("additionalProperties", additionalProperties, Walk, isComponent: false); + WalkItem("additionalProperties", additionalProperties, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (schema.Discriminator is { } discriminator) { - WalkItem("discriminator", discriminator, Walk); + WalkItem("discriminator", discriminator, static (self, item) => self.Walk(item)); } if (schema.ExternalDocs is { } externalDocs) { - WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, static (self, item) => self.Walk(item)); } Walk(schema as IOpenApiExtensible); @@ -992,7 +995,7 @@ internal void Walk(OpenApiDiscriminator? openApiDiscriminator) if (openApiDiscriminator.Mapping is { Count: > 0 } mapping) { - WalkDictionary("mapping", mapping, (item, _) => Walk((IOpenApiSchema)item)); + WalkDictionary("mapping", mapping, static (self, item, _) => self.Walk((IOpenApiSchema)item)); } } @@ -1016,7 +1019,7 @@ internal void Walk(IDictionary? examples) } _visitor.CurrentKeys.Example = example.Key; - WalkItem(example.Key, example.Value, Walk, isComponent: false); + WalkItem(example.Key, example.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Example = null; } } @@ -1074,7 +1077,7 @@ internal void Walk(List examples) continue; } - WalkItem(i.ToString(), example, Walk, isComponent: false); + WalkItem(i.ToString(), example, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } } @@ -1096,7 +1099,7 @@ internal void Walk(IList schemas) continue; } - WalkItem(i.ToString(), schema, Walk, isComponent: false); + WalkItem(i.ToString(), schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } } @@ -1147,7 +1150,7 @@ internal void Walk(IDictionary? links) } _visitor.CurrentKeys.Link = item.Key; - WalkItem(item.Key, item.Value, Walk, isComponent: false); + WalkItem(item.Key, item.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Link = null; } } @@ -1172,7 +1175,7 @@ internal void Walk(IOpenApiLink link, bool isComponent = false) if (link.Server is { } server) { - WalkItem(OpenApiConstants.Server, server, Walk); + WalkItem(OpenApiConstants.Server, server, static (self, item) => self.Walk(item)); } Walk(link as IOpenApiExtensible); @@ -1198,19 +1201,19 @@ internal void Walk(IOpenApiHeader header, bool isComponent = false) if (header.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } if (header.Example is { } example) { - WalkItem(OpenApiConstants.Example, example, Walk); + WalkItem(OpenApiConstants.Example, example, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Examples, header.Examples, Walk); + WalkDictionary(OpenApiConstants.Examples, header.Examples, static (self, item, isComponent) => self.Walk(item, isComponent)); if (header.Schema is { } schema) { - WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + WalkItem(OpenApiConstants.Schema, schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } Walk(header as IOpenApiExtensible); @@ -1329,10 +1332,10 @@ private void Walk(string context, Action walk) /// An identifier for the context. /// The state to pass to the walk action. /// An action that walks objects within the context. - private void WalkItem(string context, T state, Action walk) + private void WalkItem(string context, T state, Action walk) { _visitor.Enter(context.Replace("/", "~1")); - walk(state); + walk(this, state); _visitor.Exit(); } @@ -1344,10 +1347,10 @@ private void WalkItem(string context, T state, Action walk) /// The state to pass to the walk action. /// Whether the state is a component. /// An action that walks objects within the context. - private void WalkItem(string context, T state, Action walk, bool isComponent) + private void WalkItem(string context, T state, Action walk, bool isComponent) { _visitor.Enter(context.Replace("/", "~1")); - walk(state, isComponent); + walk(this, state, isComponent); _visitor.Exit(); } @@ -1362,7 +1365,7 @@ private void WalkItem(string context, T state, Action walk, bool isC private void WalkDictionary( string context, IDictionary? state, - Action walk, + Action walk, bool isComponent = false) { if (state != null && state.Count > 0) @@ -1371,7 +1374,7 @@ private void WalkDictionary( foreach (var item in state) { - WalkItem(item.Key, (item.Value, isComponent), (state) => walk(state.Value, state.isComponent)); + WalkItem(item.Key, (this, item.Value, isComponent, walk), static (self, state) => state.walk(self, state.Value, state.isComponent)); } _visitor.Exit(); From fc91ab0b3aa9dcd4de64a3525a127a7b87c72d2b Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 13:37:20 +0100 Subject: [PATCH 008/655] Fix unused locals Resolve two CodeQL warnings. --- src/Microsoft.OpenApi/Services/OpenApiWalker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index e1603be8b..a00bf7b14 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -44,7 +44,7 @@ public void Walk(OpenApiDocument? doc) if (doc.Info is { } info) { - WalkItem(OpenApiConstants.Info, doc.Info, static (self, item) => self.Walk(item)); + WalkItem(OpenApiConstants.Info, info, static (self, item) => self.Walk(item)); } if (doc.Servers is { } servers) @@ -54,7 +54,7 @@ public void Walk(OpenApiDocument? doc) if (doc.Paths is { } paths) { - WalkItem(OpenApiConstants.Paths, doc.Paths, static (self, item) => self.Walk(item)); + WalkItem(OpenApiConstants.Paths, paths, static (self, item) => self.Walk(item)); } WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, static (self, item, isComponent) => self.Walk(item, isComponent)); From 3326022b016fd3fc795f45926a55895b02557a09 Mon Sep 17 00:00:00 2001 From: Ciaran Liedeman <3578740+cliedeman@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:47:04 +0200 Subject: [PATCH 009/655] Merge pull request #2468 from cliedeman/feat/validate-null-property fix: Validate schema property is not null --- .../Properties/SRResource.Designer.cs | 11 +++++- .../Properties/SRResource.resx | 3 ++ .../Validations/Rules/OpenApiSchemaRules.cs | 25 ++++++++++++ .../PublicApi/PublicApi.approved.txt | 1 + .../OpenApiSchemaValidationTests.cs | 38 +++++++++++++++++++ .../Validations/ValidationRuleSetTests.cs | 4 +- 6 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index bd42672a5..eb88eab6f 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -383,7 +383,16 @@ internal static string Validation_RuleAddTwice { return ResourceManager.GetString("Validation_RuleAddTwice", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Schema {0} property {1} is null.. + /// + internal static string Validation_SchemaPropertyObjectRequired { + get { + return ResourceManager.GetString("Validation_SchemaPropertyObjectRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to The schema reference '{0}' does not point to an existing schema.. /// diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index b758ff89b..c2e953ec4 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -219,6 +219,9 @@ Schema {0} must contain property specified in the discriminator {1} in the required field list. + + Schema {0} property {1} is null. + The string '{0}' MUST be in the format of an email address. diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs index 826eaa031..81a4ab88f 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs @@ -5,12 +5,37 @@ namespace Microsoft.OpenApi { + using System.Linq; + /// /// The validation rules for . /// [OpenApiRule] public static class OpenApiSchemaRules { + /// + /// Validates Schema Property has value + /// + public static ValidationRule ValidateSchemaPropertyHasValue => + new(nameof(ValidateSchemaPropertyHasValue), + (context, schema) => + { + if (schema.Properties is not null) + { + foreach (var property in schema.Properties + .Where(entry => entry.Value is null)) + { + context.Enter(property.Key); + context.CreateError(nameof(ValidateSchemaPropertyHasValue), + string.Format(SRResource.Validation_SchemaPropertyObjectRequired, + schema is OpenApiSchemaReference { Reference: not null } schemaReference + ? schemaReference.Reference.Id + : string.Empty, property.Key)); + context.Exit(); + } + } + }); + /// /// Validates Schema Discriminator /// diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index f8d83d291..5733eb735 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1248,6 +1248,7 @@ namespace Microsoft.OpenApi public static class OpenApiSchemaRules { public static Microsoft.OpenApi.ValidationRule ValidateSchemaDiscriminator { get; } + public static Microsoft.OpenApi.ValidationRule ValidateSchemaPropertyHasValue { get; } public static bool TraverseSchemaElements(string discriminatorName, System.Collections.Generic.IList? childSchema) { } public static bool ValidateChildSchemaAgainstDiscriminator(Microsoft.OpenApi.IOpenApiSchema schema, string? discriminatorName) { } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs index 028913e37..f22806825 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs @@ -174,6 +174,44 @@ public void ValidateDefaultShouldNotHaveDataTypeMismatchForComplexSchema() // Assert Assert.NotEmpty(validator.Warnings); } + + [Fact] + public void ValidateNullProperty() + { + // Arrange + var components = new OpenApiComponents + { + Schemas = new Dictionary + { + { + "schema1", + new OpenApiSchema + { + Properties = new Dictionary + { + ["property1"] = null, + } + } + } + } + }; + + // Act + var defaultRuleSet = ValidationRuleSet.GetDefaultRuleSet(); + defaultRuleSet.Add(typeof(IOpenApiSchema), OpenApiNonDefaultRules.SchemaMismatchedDataType); + var validator = new OpenApiValidator(defaultRuleSet); + var walker = new OpenApiWalker(validator); + walker.Walk(components); + + // Assert + Assert.NotEmpty(validator.Errors); + Assert.Equivalent(new List + { + new OpenApiValidatorError(nameof(OpenApiSchemaRules.ValidateSchemaPropertyHasValue),"#/schemas/schema1/property1", + string.Format(SRResource.Validation_SchemaPropertyObjectRequired, + string.Empty, "property1")) + }, validator.Errors); + } [Fact] public void ValidateSchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator() diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index e1414232e..50cf77a1f 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -53,8 +53,8 @@ public void RuleSetConstructorsReturnsTheCorrectRules() Assert.Empty(ruleSet_4.Rules); // Update the number if you add new default rule(s). - Assert.Equal(20, ruleSet_1.Rules.Count); - Assert.Equal(20, ruleSet_2.Rules.Count); + Assert.Equal(21, ruleSet_1.Rules.Count); + Assert.Equal(21, ruleSet_2.Rules.Count); Assert.Equal(3, ruleSet_3.Rules.Count); } From 41c53c95c6affdfd75051d99377538cb09a84f65 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 13:59:21 +0100 Subject: [PATCH 010/655] Address feedback - Create helpers to reduce code duplication. - Remove unused method. --- .../Services/OpenApiWalker.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index a00bf7b14..3f66d5515 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -97,7 +97,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, static (self, item) => self.Walk(item)); + WalkItem("0", only, Walk); } else { @@ -110,10 +110,12 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, static (self, item) => self.Walk(item)); + WalkItem(context, tag, Walk); index++; } } + + static void Walk(OpenApiWalker self, OpenApiTag tag) => self.Walk(tag); } /// @@ -131,7 +133,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, static (self, item) => self.Walk(item)); + WalkItem("0", only, Walk); } else { @@ -144,10 +146,12 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, static (self, item) => self.Walk(item)); + WalkItem(context, tag, Walk); index++; } } + + static void Walk(OpenApiWalker self, OpenApiTagReference tag) => self.Walk(tag); } /// @@ -1313,16 +1317,13 @@ internal void Walk(IOpenApiElement element) } } - /// - /// Adds a segment to the context path to enable pointing to the current location in the document - /// - /// An identifier for the context. - /// An action that walks objects within the context. - private void Walk(string context, Action walk) + private static string ReplaceSlashes(string value) { - _visitor.Enter(context.Replace("/", "~1")); - walk(); - _visitor.Exit(); +#if NET8_0_OR_GREATER + return value.Replace("/", "~1", StringComparison.Ordinal); +#else + return value.Replace("/", "~1"); +#endif } /// @@ -1334,7 +1335,7 @@ private void Walk(string context, Action walk) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk) { - _visitor.Enter(context.Replace("/", "~1")); + _visitor.Enter(ReplaceSlashes(context)); walk(this, state); _visitor.Exit(); } @@ -1349,7 +1350,7 @@ private void WalkItem(string context, T state, Action walk) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk, bool isComponent) { - _visitor.Enter(context.Replace("/", "~1")); + _visitor.Enter(ReplaceSlashes(context)); walk(this, state, isComponent); _visitor.Exit(); } @@ -1370,7 +1371,7 @@ private void WalkDictionary( { if (state != null && state.Count > 0) { - _visitor.Enter(context.Replace("/", "~1")); + _visitor.Enter(ReplaceSlashes(context)); foreach (var item in state) { From 70f4c87cb89e87c2bdcc5b5ee2721fb90ac315b1 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 15:19:17 +0100 Subject: [PATCH 011/655] Refactor tag methods Create common implementation to walk `OpenApiTag` and `OpenApiTagReference`. --- .../Services/OpenApiWalker.cs | 70 +++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 3f66d5515..c1e0774d2 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -94,28 +94,7 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); - // Visit tags - if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) - { - WalkItem("0", only, Walk); - } - else - { - int index = 0; - foreach (var tag in tags) - { - if (tag is null) - { - continue; - } - - var context = index.ToString(); - WalkItem(context, tag, Walk); - index++; - } - } - - static void Walk(OpenApiWalker self, OpenApiTag tag) => self.Walk(tag); + WalkTags(tags, static (self, tag) => self.Walk(tag)); } /// @@ -131,27 +110,7 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); // Visit tags - if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) - { - WalkItem("0", only, Walk); - } - else - { - int index = 0; - foreach (var tag in tags) - { - if (tag is null) - { - continue; - } - - var context = index.ToString(); - WalkItem(context, tag, Walk); - index++; - } - } - - static void Walk(OpenApiWalker self, OpenApiTagReference tag) => self.Walk(tag); + WalkTags(tags, static (self, tag) => self.Walk(tag)); } /// @@ -1394,6 +1353,31 @@ private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder, boo } return isReference; } + + private void WalkTags(ISet tags, Action walk) + where T : IOpenApiTag + { + // Visit tags + if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) + { + WalkItem("0", only, walk); + } + else + { + int index = 0; + foreach (var tag in tags) + { + if (tag is null) + { + continue; + } + + var context = index.ToString(); + WalkItem(context, tag, walk); + index++; + } + } + } } /// From 1c4985517ad4c236ea797566d43c5de5e076de82 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 20 Aug 2025 15:20:26 +0100 Subject: [PATCH 012/655] Apply suggestions from code review Remove redundant comment. --- src/Microsoft.OpenApi/Services/OpenApiWalker.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index c1e0774d2..8b5868216 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -1357,7 +1357,6 @@ private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder, boo private void WalkTags(ISet tags, Action walk) where T : IOpenApiTag { - // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { WalkItem("0", only, walk); From 09ba6822c7c7b6109b73b26166d2481d530acd37 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 15:52:28 +0100 Subject: [PATCH 013/655] Update performance baseline Add results from 1c4985517ad4c236ea797566d43c5de5e076de82. --- .../performance.Descriptions-report-github.md | 22 +- .../performance.Descriptions-report.csv | 8 +- .../performance.Descriptions-report.html | 22 +- .../performance.Descriptions-report.json | 1183 +---------------- .../performance.EmptyModels-report-github.md | 70 +- .../performance.EmptyModels-report.csv | 58 +- .../performance.EmptyModels-report.html | 70 +- .../performance.EmptyModels-report.json | 2 +- 8 files changed, 127 insertions(+), 1308 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 a507dd8e9..5c8d571e6 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -1,18 +1,18 @@ ``` -BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3981) -11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores -.NET SDK 8.0.409 - [Host] : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI - ShortRun : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI +BenchmarkDotNet v0.15.2, Linux Ubuntu 24.04.2 LTS (Noble Numbat) +AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores +.NET SDK 8.0.413 + [Host] : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX2 + ShortRun : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX2 Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------------- |-------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 470.3 μs | 138.05 μs | 7.57 μs | 58.5938 | 11.7188 | - | 380.53 KB | -| PetStoreJson | 166.0 μs | 43.84 μs | 2.40 μs | 39.0625 | 8.7891 | - | 242.67 KB | -| GHESYaml | 915,406.4 μs | 714,492.62 μs | 39,163.75 μs | 68000.0000 | 22000.0000 | 4000.0000 | 395800.98 KB | -| GHESJson | 470,609.4 μs | 264,698.88 μs | 14,509.04 μs | 42000.0000 | 15000.0000 | 3000.0000 | 257270.45 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------- |---------------:|--------------:|------------:|-----------:|-----------:|----------:|-------------:| +| PetStoreYaml | 529.5 μs | 62.50 μs | 3.43 μs | 23.4375 | 3.9063 | - | 387.26 KB | +| PetStoreJson | 240.8 μs | 15.69 μs | 0.86 μs | 13.6719 | 1.9531 | - | 249.1 KB | +| GHESYaml | 1,097,576.6 μs | 100,584.42 μs | 5,513.37 μs | 26000.0000 | 20000.0000 | 3000.0000 | 384492.38 KB | +| GHESJson | 516,328.2 μs | 87,964.22 μs | 4,821.62 μs | 16000.0000 | 9000.0000 | 2000.0000 | 245957.5 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index e6299ad40..1a21885c7 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv @@ -1,5 +1,5 @@ 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,11111111,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,470.3 μs,138.05 μs,7.57 μs,58.5938,11.7188,0.0000,380.53 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,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,166.0 μs,43.84 μs,2.40 μs,39.0625,8.7891,0.0000,242.67 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,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,"915,406.4 μs","714,492.62 μs","39,163.75 μs",68000.0000,22000.0000,4000.0000,395800.98 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,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,"470,609.4 μs","264,698.88 μs","14,509.04 μs",42000.0000,15000.0000,3000.0000,257270.45 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,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,529.5 μs,62.50 μs,3.43 μs,23.4375,3.9063,0.0000,387.26 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,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,240.8 μs,15.69 μs,0.86 μs,13.6719,1.9531,0.0000,249.1 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,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,097,576.6 μs","100,584.42 μs","5,513.37 μs",26000.0000,20000.0000,3000.0000,384492.38 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,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,"516,328.2 μs","87,964.22 μs","4,821.62 μs",16000.0000,9000.0000,2000.0000,245957.5 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index 4f578eca3..5661bcdf8 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-20250514-154213 +performance.Descriptions-20250820-142630 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.WPF.xaml b/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.WPF.xaml deleted file mode 100644 index 988e787b8..000000000 --- a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.WPF.xaml +++ /dev/null @@ -1,830 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Visible - - - - - - - - - - - Visible - - - - - - - - - - - - - - Visible - - - - - - - - - - - Visible - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Theme.Colors.xaml b/src/Microsoft.OpenApi.Workbench/Themes/Metro/Theme.Colors.xaml deleted file mode 100644 index 02babe07a..000000000 --- a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Theme.Colors.xaml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - #FF282828 - #FF3F3F3F - #FF565656 - #FF858585 - #FFB9B9B9 - #FFD7D7D7 - #FFE7E7E7 - #FFF4F4F4 - #FFF9F9F9 - #FFFFFFFF - - - #E5FFFFFF - #BFFFFFFF - #99FFFFFF - #72FFFFFF - #4CFFFFFF - #00FFFFFF - - - #72000000 - #4C000000 - #26000000 - #00000000 - #66E2E2E2 - - - #FF0086AF - #FF00AADE - #FF80D5EF - #FFB2E1EF - #2600AADE - - - #FFD0284C - #FFF55E7F - #FFFFCAD5 - - - #FF006481 - #FF8A9B0F - #FF3E4700 - #FFF14D0F - #FF8D2E00 - #FF81106B - #FF410135 - #FFFCA910 - #FF8D4902 - #FF037A54 - #FF003F2A - #FF154D85 - #FF02284D - #FF543511 - #FF211303 - #FF89806D - #FF393225 - #FF58458B - #FF211347 - #7FB9B9B9 - #33565656 - #7F3F3F3F - #FF686868 - #8000AADE - #CC3F3F3F - - - - #FF0092BE - #FF00AADE - #FF2BB9E5 - #FF55C8EB - #FF80D7F2 - - \ No newline at end of file From afd4967a9e6db390175e2df9e6f34ff77168d19d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:04:07 -0700 Subject: [PATCH 631/655] Omit null OpenAPI response descriptions (#3077) * Initial plan fix(openapi): omit null response descriptions Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> * test(models): update response serialization expectation Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Models/OpenApiResponse.cs | 8 +++---- .../OpenApiResponseSerializationTests.cs | 22 +++++++++++++++++++ .../Models/OpenApiOperationTests.cs | 4 ---- .../Models/OpenApiResponseTests.cs | 4 +--- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs index 4ee39336e..6bd02af25 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs @@ -73,7 +73,7 @@ public virtual void SerializeAsV3(IOpenApiWriter writer) SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer)); } - private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, + private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, Action callback) { Utils.CheckArgumentNull(writer); @@ -87,7 +87,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version } // description - writer.WriteRequiredProperty(OpenApiConstants.Description, Description); + writer.WriteProperty(OpenApiConstants.Description, Description); // headers writer.WriteOptionalMap(OpenApiConstants.Headers, Headers, callback); @@ -120,7 +120,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) writer.WriteStartObject(); // description - writer.WriteRequiredProperty(OpenApiConstants.Description, Description); + writer.WriteProperty(OpenApiConstants.Description, Description); var extensionsClone = Extensions is not null ? new Dictionary(Extensions) : null; @@ -177,7 +177,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) // so remove it from the cloned collection so we don't write it again. extensionsClone?.Remove(key); } - } + } } } diff --git a/test/Microsoft.OpenApi.Tests/Mocks/OpenApiResponseSerializationTests.cs b/test/Microsoft.OpenApi.Tests/Mocks/OpenApiResponseSerializationTests.cs index 27246fa7e..95a5a6980 100644 --- a/test/Microsoft.OpenApi.Tests/Mocks/OpenApiResponseSerializationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Mocks/OpenApiResponseSerializationTests.cs @@ -1,5 +1,7 @@ using System.IO; using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; using Moq; using Xunit; @@ -61,5 +63,25 @@ public void SerializeAsV3_DoesNotCallV31OrV2Serialization() _linkMock.Verify(l => l.SerializeAsV31(It.IsAny()), Times.Never); _linkMock.Verify(l => l.SerializeAsV2(It.IsAny()), Times.Never); } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public async Task SerializeResponseWithoutDescriptionAsJsonDoesNotWriteNullDescription(OpenApiSpecVersion specVersion) + { + // Arrange + var response = new OpenApiResponse(); + + // Act + var actual = await response.SerializeAsJsonAsync(specVersion, TestContext.Current.CancellationToken); + + // Assert + var node = JsonNode.Parse(actual); + Assert.NotNull(node); + var responseObject = Assert.IsType(node); + Assert.False(responseObject.ContainsKey(OpenApiConstants.Description)); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index 4dab19e76..d281f4437 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -343,7 +343,6 @@ public async Task SerializeOperationWithBodyAsV3JsonWorks() "$ref": "#/components/responses/response1" }, "400": { - "description": null, "content": { "application/json": { "schema": { @@ -416,7 +415,6 @@ public async Task SerializeAdvancedOperationWithTagAndSecurityAsV3JsonWorks() "$ref": "#/components/responses/response1" }, "400": { - "description": null, "content": { "application/json": { "schema": { @@ -648,7 +646,6 @@ public async Task SerializeOperationWithBodyAsV2JsonWorks() "$ref": "#/responses/response1" }, "400": { - "description": null, "schema": { "type": "number", "maximum": 10, @@ -718,7 +715,6 @@ public async Task SerializeAdvancedOperationWithTagAndSecurityAsV2JsonWorks() "$ref": "#/responses/response1" }, "400": { - "description": null, "schema": { "type": "number", "maximum": 10, diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs index 450058fe1..fdd291162 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs @@ -193,9 +193,7 @@ public async Task SerializeBasicResponseWorks( string format) { // Arrange - var expected = format == OpenApiConstants.Json ? @"{ - ""description"": null -}" : @"description: null"; + var expected = @"{ }"; // Act var actual = await BasicResponse.SerializeAsync(version, format, TestContext.Current.CancellationToken); From c35be9bbe33dbb038f5b3d45f12f49dba8859e66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 05:36:05 -0400 Subject: [PATCH 632/655] Bump Microsoft.SourceLink.GitHub from 10.0.400 to 10.0.401 (#3085) --- updated-dependencies: - dependency-name: Microsoft.SourceLink.GitHub dependency-version: 10.0.401 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index da0a45c5d..5c2dfa48a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,6 +19,6 @@ true - + \ No newline at end of file From 413f0aa52e1cede721b70a99cc8b92ec5d4984a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 05:36:47 -0400 Subject: [PATCH 633/655] Bump the microsoftextensions group with 6 updates (#3084) Bumps Microsoft.Extensions.DependencyInjection from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Logging from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Logging.Abstractions from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Logging.Console from 10.0.11 to 10.0.12 Bumps Microsoft.Extensions.Logging.Debug from 10.0.11 to 10.0.12 Bumps System.Text.Json from 10.0.11 to 10.0.12 --- updated-dependencies: - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: Microsoft.Extensions.Logging dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: Microsoft.Extensions.Logging dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: Microsoft.Extensions.Logging.Abstractions dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: Microsoft.Extensions.Logging.Console dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: System.Text.Json dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: Microsoft.Extensions.Logging.Console dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: Microsoft.Extensions.Logging.Debug dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions - dependency-name: Microsoft.Extensions.Logging.Debug dependency-version: 10.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: microsoftextensions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- performance/resultsComparer/resultsComparer.csproj | 10 +++++----- .../Microsoft.OpenApi.Hidi.csproj | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/performance/resultsComparer/resultsComparer.csproj b/performance/resultsComparer/resultsComparer.csproj index c6a0f2d33..9767a28ba 100644 --- a/performance/resultsComparer/resultsComparer.csproj +++ b/performance/resultsComparer/resultsComparer.csproj @@ -8,12 +8,12 @@ - - - - + + + + - + diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj index 5e2fd04ed..fab3173fd 100644 --- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj +++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj @@ -29,10 +29,10 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all From fb595ec43c7ef66d2adbf47d60cfa70a573b1394 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 05:37:11 -0400 Subject: [PATCH 634/655] chore(deps): bump dotnet-sdk from 10.0.400 to 10.0.401 (#3083) Bumps [dotnet-sdk](https://github.com/dotnet/sdk) from 10.0.400 to 10.0.401. - [Release notes](https://github.com/dotnet/sdk/releases) - [Commits](https://github.com/dotnet/sdk/compare/v10.0.400...v10.0.401) --- updated-dependencies: - dependency-name: dotnet-sdk dependency-version: 10.0.401 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 2f38dd557..7494875e1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.400" + "version": "10.0.401" }, "test": { "runner": "Microsoft.Testing.Platform" From 0afae131bf68a03b7133818b24584ac6d956fc73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:47:47 -0400 Subject: [PATCH 635/655] chore(deps): bump actions/setup-java from 6.0.0 to 6.0.1 (#3087) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/dd06d9cba3e5552c54d9f8ea23572deb30010f7c...de7274f081f381c8f8158605e0321c36c376e2e6) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index a75019cfd..a6954d70d 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -35,7 +35,7 @@ jobs: runs-on: windows-latest steps: - name: Set up JDK 17 - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: 'temurin' java-version: 17 From 3ced884842911823d597072d4e786872d2cc44ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:58:28 -0400 Subject: [PATCH 636/655] Bump the testing group with 1 update (#3088) Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.0 --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: testing - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: testing ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Microsoft.OpenApi.Hidi.Tests.csproj | 2 +- .../Microsoft.OpenApi.Readers.Tests.csproj | 2 +- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 2271cbb81..3e5f82aa4 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index e3601ba20..9598b058d 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -17,7 +17,7 @@ - + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 0a681a46c..1185dd65c 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -10,7 +10,7 @@ - + From 7adeb08e04bb1c4338399eb24742b728fd47dd5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:59:03 -0400 Subject: [PATCH 637/655] chore(deps): bump the codeql group with 2 updates (#3086) Bumps the codeql group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.37.9 to 4.38.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63) Updates `github/codeql-action/analyze` from 4.37.9 to 4.38.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: codeql - dependency-name: github/codeql-action/analyze dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: codeql ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cb35bda68..e3cb62f4a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: - name: Initialize CodeQL id: init_codeql - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: queries: security-and-quality @@ -54,6 +54,6 @@ jobs: - name: Perform CodeQL Analysis id: analyze_codeql - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file From fde81d9200c89a501094d47a75904f9896fb27f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:03:28 -0400 Subject: [PATCH 638/655] Bump the testing group with 1 update (#3090) Bumps xunit.v3 from 4.0.0 to 4.0.1 --- updated-dependencies: - dependency-name: xunit.v3 dependency-version: 4.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing - dependency-name: xunit.v3 dependency-version: 4.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Microsoft.OpenApi.Hidi.Tests.csproj | 2 +- .../Microsoft.OpenApi.Readers.Tests.csproj | 2 +- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 3e5f82aa4..64690e433 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -19,7 +19,7 @@ - + diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 9598b058d..eea3476a0 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -20,7 +20,7 @@ - + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 1185dd65c..5275f1ba8 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -15,7 +15,7 @@ - + From 4450d5fff83c29e4717214c4777d77e987585a3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:20:15 -0700 Subject: [PATCH 639/655] Bump Verify.XunitV3 from 32.0.0 to 33.0.2 (#3091) * Bump Verify.XunitV3 from 32.0.0 to 33.0.2 --- updated-dependencies: - dependency-name: Verify.XunitV3 dependency-version: 33.0.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * chore(dependencies): declare Verify open source exemption Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- Directory.Build.props | 2 ++ test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5c2dfa48a..1971fa2c6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,6 +8,8 @@ https://github.com/Microsoft/OpenAPI.NET https://github.com/microsoft/OpenAPI.NET/releases true + OpenSource + 2027-09 http://go.microsoft.com/fwlink/?LinkID=288890 https://github.com/Microsoft/OpenAPI.NET © Microsoft Corporation. All rights reserved. diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 5275f1ba8..07876a331 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -14,7 +14,7 @@ - + From f262b83ddbab37f3b04475876e058dae869dfc17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:35:33 -0400 Subject: [PATCH 640/655] Bump the testing group with 1 update (#3092) Bumps Microsoft.NET.Test.Sdk from 18.10.0 to 18.10.1 --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.10.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Microsoft.OpenApi.Hidi.Tests.csproj | 2 +- .../Microsoft.OpenApi.Readers.Tests.csproj | 2 +- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 64690e433..5e9e7ede9 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index eea3476a0..4ec972381 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -17,7 +17,7 @@ - + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 07876a331..4c7ecf73b 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -10,7 +10,7 @@ - + From 5c2db299ccb729226a243cd577361c4a32e4e069 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:46:12 -0400 Subject: [PATCH 641/655] Bump the testing group with 2 updates (#3093) Bumps Microsoft.Testing.Extensions.AzureDevOpsReport from 2.4.0 to 2.4.1 Bumps Microsoft.Testing.Extensions.GitHubActionsReport from 2.4.0 to 2.4.1 --- updated-dependencies: - dependency-name: Microsoft.Testing.Extensions.AzureDevOpsReport dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing - dependency-name: Microsoft.Testing.Extensions.AzureDevOpsReport dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing - dependency-name: Microsoft.Testing.Extensions.GitHubActionsReport dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing - dependency-name: Microsoft.Testing.Extensions.GitHubActionsReport dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Microsoft.OpenApi.Hidi.Tests.csproj | 4 ++-- .../Microsoft.OpenApi.Readers.Tests.csproj | 4 ++-- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 5e9e7ede9..a32deb9b6 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 4ec972381..e75ef4bcb 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 4c7ecf73b..32e6a5056 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -11,8 +11,8 @@ - - + + From efede53130f56ccf7737204fa3d8630507cddd0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:59:35 -0400 Subject: [PATCH 642/655] chore(deps): bump the codeql group with 2 updates (#3094) Bumps the codeql group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.38.0 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd) Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.38.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql - dependency-name: github/codeql-action/analyze dependency-version: 4.38.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e3cb62f4a..e3ac2cc16 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: - name: Initialize CodeQL id: init_codeql - uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 + uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: queries: security-and-quality @@ -54,6 +54,6 @@ jobs: - name: Perform CodeQL Analysis id: analyze_codeql - uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 + uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file From c719c42443db950eef44f8f6ce0c317f2ee98117 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 28 Sep 2026 14:36:28 -0400 Subject: [PATCH 643/655] ci: Change auto-merge strategy for Dependabot PRs (#3095) --- .github/workflows/auto-merge-dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index a3343774e..f6dafa170 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -26,7 +26,7 @@ jobs: - name: Enable auto-merge for Dependabot PRs # Only if version bump is not a major version change if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} - run: gh pr merge --auto --merge "$PR_URL" + run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} From 2fc1ed9aa9074fee02f2de1e7a49f56398f39d1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:33:28 -0400 Subject: [PATCH 644/655] Bump Verify.XunitV3 from 33.0.2 to 33.1.1 (#3096) --- updated-dependencies: - dependency-name: Verify.XunitV3 dependency-version: 33.1.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 32e6a5056..92745edea 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -14,7 +14,7 @@ - + From eebc63ccd29f89b850f5fd43f56f733b77aed985 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:55:36 -0400 Subject: [PATCH 645/655] chore(deps): bump the codeql group with 2 updates (#3097) Bumps the codeql group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.38.1 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1c5b675653bb5c22dbe9b12b556ec555138e09fd...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2) Updates `github/codeql-action/analyze` from 4.38.1 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1c5b675653bb5c22dbe9b12b556ec555138e09fd...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql - dependency-name: github/codeql-action/analyze dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e3ac2cc16..b2311adf8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: - name: Initialize CodeQL id: init_codeql - uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: queries: security-and-quality @@ -54,6 +54,6 @@ jobs: - name: Perform CodeQL Analysis id: analyze_codeql - uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 + uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file From 887bad8eb68c147a432eaabe1223d9293769474b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:57:05 -0400 Subject: [PATCH 646/655] Bump Moq from 4.20.72 to 4.21.0 (#3098) --- updated-dependencies: - dependency-name: Moq dependency-version: 4.21.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: Moq dependency-version: 4.21.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Microsoft.OpenApi.Hidi.Tests.csproj | 2 +- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index a32deb9b6..6daa7906a 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -18,7 +18,7 @@ - + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 92745edea..469d2cd08 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -13,7 +13,7 @@ - + From 1a7054e6e12edf45e5780cec9a15d2508cb9d9d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:04:10 -0400 Subject: [PATCH 647/655] Bump Verify.XunitV3 from 33.1.1 to 33.1.4 (#3099) --- updated-dependencies: - dependency-name: Verify.XunitV3 dependency-version: 33.1.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 469d2cd08..56730da5b 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -14,7 +14,7 @@ - + From 61203495a9721929d39d93ce7928d98067ce051b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:49:29 -0400 Subject: [PATCH 648/655] Bump the testing group with 1 update (#3103) Bumps coverlet.MTP from 10.0.1 to 10.1.0 --- updated-dependencies: - dependency-name: coverlet.MTP dependency-version: 10.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: testing - dependency-name: coverlet.MTP dependency-version: 10.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: testing ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Microsoft.OpenApi.Hidi.Tests.csproj | 2 +- .../Microsoft.OpenApi.Readers.Tests.csproj | 2 +- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 6daa7906a..c5adf7adc 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index e75ef4bcb..ccddceff0 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 56730da5b..35744dc95 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -9,7 +9,7 @@ - + From 3d0cd0b416f592c12f1dcfd89b38c9ca37714970 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:51:08 -0400 Subject: [PATCH 649/655] chore(deps): bump actions/upload-code-coverage from 1.4.2 to 1.4.3 (#3102) Bumps [actions/upload-code-coverage](https://github.com/actions/upload-code-coverage) from 1.4.2 to 1.4.3. - [Release notes](https://github.com/actions/upload-code-coverage/releases) - [Changelog](https://github.com/actions/upload-code-coverage/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/upload-code-coverage/compare/d8e329117199404bba6fc81efe8093dc7c015e34...bfa741d815a28cb064a8e3a0837e577457a017d5) --- updated-dependencies: - dependency-name: actions/upload-code-coverage dependency-version: 1.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 60207f6cc..f62a71594 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -64,7 +64,7 @@ jobs: - name: Upload coverage report if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') || (github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch) - uses: actions/upload-code-coverage@d8e329117199404bba6fc81efe8093dc7c015e34 # v1.4.2 + uses: actions/upload-code-coverage@bfa741d815a28cb064a8e3a0837e577457a017d5 # v1.4.3 with: file: ./reports/coverage/Cobertura.xml language: CSharp From cdc3c07ad55fd3b2f44d3a7aee2c518d76112f3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:51:36 -0400 Subject: [PATCH 650/655] Bump Verify.XunitV3 from 33.1.4 to 33.1.5 (#3104) --- updated-dependencies: - dependency-name: Verify.XunitV3 dependency-version: 33.1.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 35744dc95..299a888f5 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -14,7 +14,7 @@ - + From 8460814f250ce38282e02497f0bbc5e59ec04c31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:54:20 -0400 Subject: [PATCH 651/655] Bump Verify.XunitV3 from 33.1.5 to 33.2.0 (#3105) --- updated-dependencies: - dependency-name: Verify.XunitV3 dependency-version: 33.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 299a888f5..e79f44dba 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -14,7 +14,7 @@ - + From 15dca965af7d53ed2de8afc855de45b3455a0238 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:23:15 -0400 Subject: [PATCH 652/655] chore(deps): bump actions/upload-code-coverage from 1.4.3 to 1.4.4 (#3106) Bumps [actions/upload-code-coverage](https://github.com/actions/upload-code-coverage) from 1.4.3 to 1.4.4. - [Release notes](https://github.com/actions/upload-code-coverage/releases) - [Changelog](https://github.com/actions/upload-code-coverage/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/upload-code-coverage/compare/bfa741d815a28cb064a8e3a0837e577457a017d5...2b21a77928be8d5168c2b9581a67f2adbebacc52) --- updated-dependencies: - dependency-name: actions/upload-code-coverage dependency-version: 1.4.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f62a71594..4ae9e6ae0 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -64,7 +64,7 @@ jobs: - name: Upload coverage report if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') || (github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch) - uses: actions/upload-code-coverage@bfa741d815a28cb064a8e3a0837e577457a017d5 # v1.4.3 + uses: actions/upload-code-coverage@2b21a77928be8d5168c2b9581a67f2adbebacc52 # v1.4.4 with: file: ./reports/coverage/Cobertura.xml language: CSharp From e1a75437b76ebfc7c9eb446e9fd0b59a21afb14b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:18:48 -0600 Subject: [PATCH 653/655] ci(release): publish NuGet via ESRP and authenticate Hidi Docker restores (#3107) * ci(release): publish OpenAPI packages and symbols through ESRP Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> * ci(hidi): authenticate Docker restores using a BuildKit secret Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> * ci(release): share authenticated private-feed version checks Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .azure-pipelines/ci-build.yml | 198 ++++++++++++++++++---- Dockerfile | 4 +- scripts/check-nuget-package-published.ps1 | 84 +++++++++ 3 files changed, 256 insertions(+), 30 deletions(-) create mode 100644 scripts/check-nuget-package-published.ps1 diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 2c8cb8735..5e8a40e98 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -21,6 +21,8 @@ pr: variables: buildPlatform: 'Any CPU' buildConfiguration: 'Release' + NuGetOrganizationName: 'openapinet' + privateFeedBaseUrl: 'https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public' ProductBinPath: '$(Build.SourcesDirectory)\src\Microsoft.OpenApi\bin\$(BuildConfiguration)' REGISTRY: 'msgraphprodregistry.azurecr.io' IMAGE_NAME: 'public/openapi/hidi' @@ -88,7 +90,7 @@ extends: - + "@ | Set-Content -Path "$(Build.SourcesDirectory)/nuget.config" -Encoding UTF8 @@ -226,7 +228,16 @@ extends: inputs: targetFolder: $(Build.ArtifactStagingDirectory)/Nugets sourceFolder: $(Build.ArtifactStagingDirectory) - content: '*.nupkg' + Contents: | + *.nupkg + *.snupkg + + - task: CopyFiles@2 + displayName: 'Include version-check script in Nugets artifact' + inputs: + SourceFolder: '$(Build.SourcesDirectory)/scripts' + Contents: 'check-nuget-package-published.ps1' + TargetFolder: '$(Build.ArtifactStagingDirectory)/Nugets/scripts' # Copy repository files to be used in the deploy stage - task: CopyFiles@2 @@ -265,13 +276,46 @@ extends: pool: vmImage: ubuntu-latest steps: - - task: 1ES.PublishNuget@1 - displayName: 'NuGet push' + - task: PowerShell@2 + displayName: 'Check whether NuGet package version already published (idempotent)' + inputs: + targetType: filePath + filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1' + arguments: '-PackageId "Microsoft.OpenApi.Hidi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' + pwsh: true + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) + - task: CopyFiles@2 + displayName: 'Stage Hidi NuGet packages for ESRP release' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) + inputs: + SourceFolder: '$(Pipeline.Workspace)' + Contents: | + Microsoft.OpenApi.Hidi.*.nupkg + Microsoft.OpenApi.Hidi.*.snupkg + TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/hidi' + CleanTargetFolder: true + - task: EsrpRelease@14 + displayName: 'ESRP Release - Hidi NuGet' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) inputs: - packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.Hidi.*.nupkg' - packageParentPath: '$(Pipeline.Workspace)' - nuGetFeedType: external - publishFeedCredentials: 'OpenAPI Nuget Connection' + connectedservicename: 'Federated DevX ESRP Managed Identity Connection' + usemanagedidentity: false + keyvaultname: 'akv-prod-eastus' + authcertname: 'ReferenceLibraryPrivateCert' + signcertname: 'ReferencePackagePublisherCertificate' + clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8' + intent: 'packagedistribution' + contenttype: 'NuGet' + organizationname: '$(NuGetOrganizationName)' + contentsource: 'Folder' + folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/hidi' + waitforreleasecompletion: true + owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com' + approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com' + serviceendpointurl: 'https://api.esrp.microsoft.com/' + mainpublisher: 'ESRPRELPACMAN' + domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' - deployment: deploy_lib condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded()) @@ -290,21 +334,48 @@ extends: pool: vmImage: ubuntu-latest steps: - - pwsh: | - $fileNames = "$(Pipeline.Workspace)/Microsoft.OpenApi.Hidi.*.nupkg", "$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg" - foreach($fileName in $fileNames) { - if(Test-Path $fileName) { - Remove-Item $fileName -Verbose - } - } - displayName: remove other nupkgs to avoid duplication - - task: 1ES.PublishNuget@1 - displayName: 'NuGet push' + - task: PowerShell@2 + displayName: 'Check whether NuGet package version already published (idempotent)' + inputs: + targetType: filePath + filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1' + arguments: '-PackageId "Microsoft.OpenApi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' + pwsh: true + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) + - task: CopyFiles@2 + displayName: 'Stage OpenAPI NuGet packages for ESRP release' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) + inputs: + SourceFolder: '$(Pipeline.Workspace)' + Contents: | + Microsoft.OpenApi.*.nupkg + Microsoft.OpenApi.*.snupkg + !Microsoft.OpenApi.Hidi.* + !Microsoft.OpenApi.YamlReader.* + TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/openapi' + CleanTargetFolder: true + - task: EsrpRelease@14 + displayName: 'ESRP Release - OpenAPI NuGet' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) inputs: - packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.*.nupkg' - packageParentPath: '$(Pipeline.Workspace)' - nuGetFeedType: external - publishFeedCredentials: 'OpenAPI Nuget Connection' + connectedservicename: 'Federated DevX ESRP Managed Identity Connection' + usemanagedidentity: false + keyvaultname: 'akv-prod-eastus' + authcertname: 'ReferenceLibraryPrivateCert' + signcertname: 'ReferencePackagePublisherCertificate' + clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8' + intent: 'packagedistribution' + contenttype: 'NuGet' + organizationname: '$(NuGetOrganizationName)' + contentsource: 'Folder' + folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/openapi' + waitforreleasecompletion: true + owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com' + approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com' + serviceendpointurl: 'https://api.esrp.microsoft.com/' + mainpublisher: 'ESRPRELPACMAN' + domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' - deployment: deploy_yaml_reader condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded()) @@ -323,13 +394,46 @@ extends: pool: vmImage: ubuntu-latest steps: - - task: 1ES.PublishNuget@1 - displayName: 'NuGet push' + - task: PowerShell@2 + displayName: 'Check whether NuGet package version already published (idempotent)' inputs: - packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg' - packageParentPath: '$(Pipeline.Workspace)' - nuGetFeedType: external - publishFeedCredentials: 'OpenAPI Nuget Connection' + targetType: filePath + filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1' + arguments: '-PackageId "Microsoft.OpenApi.YamlReader" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' + pwsh: true + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) + - task: CopyFiles@2 + displayName: 'Stage YAML reader NuGet packages for ESRP release' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) + inputs: + SourceFolder: '$(Pipeline.Workspace)' + Contents: | + Microsoft.OpenApi.YamlReader.*.nupkg + Microsoft.OpenApi.YamlReader.*.snupkg + TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/yaml-reader' + CleanTargetFolder: true + - task: EsrpRelease@14 + displayName: 'ESRP Release - YAML reader NuGet' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) + inputs: + connectedservicename: 'Federated DevX ESRP Managed Identity Connection' + usemanagedidentity: false + keyvaultname: 'akv-prod-eastus' + authcertname: 'ReferenceLibraryPrivateCert' + signcertname: 'ReferencePackagePublisherCertificate' + clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8' + intent: 'packagedistribution' + contenttype: 'NuGet' + organizationname: '$(NuGetOrganizationName)' + contentsource: 'Folder' + folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/yaml-reader' + waitforreleasecompletion: true + owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com' + approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com' + serviceendpointurl: 'https://api.esrp.microsoft.com/' + mainpublisher: 'ESRPRELPACMAN' + domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' - deployment: create_github_release condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded()) @@ -459,7 +563,33 @@ extends: displayName: 'Get current date' name: setdate condition: eq(variables['Build.SourceBranch'], variables['PREVIEW_BRANCH']) - + + # Keep feed credentials out of the Docker build context and image layers. + - pwsh: | + if ([string]::IsNullOrWhiteSpace($env:FEED_ACCESS_TOKEN)) { + throw "No Azure Artifacts access token available for the Docker build." + } + $feedAccessToken = [System.Security.SecurityElement]::Escape($env:FEED_ACCESS_TOKEN) + @" + + + + + + + + + + + + + + + "@ | Set-Content -Path "$(Agent.TempDirectory)/hidi-docker.nuget.config" -Encoding UTF8 + displayName: 'Create Docker NuGet config (central feed)' + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) + - script: | docker run --privileged --rm msgraphprodregistry.azurecr.io/tonistiigi/binfmt --install all displayName: "Enable multi-platform builds" @@ -478,6 +608,7 @@ extends: # Using quotes around tags to prevent flag interpretation docker buildx build \ --platform linux/amd64,linux/arm64/v8 \ + --secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \ --push \ -t "$(REGISTRY)/$(IMAGE_NAME):nightly" \ -t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}.${BUILDDATE}${RUNNUMBER}" \ @@ -490,6 +621,7 @@ extends: echo "Building Docker image for release..." docker buildx build\ --platform linux/amd64,linux/arm64/v8 \ + --secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \ --push \ -t "$(REGISTRY)/$(IMAGE_NAME):latest" \ -t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}" \ @@ -497,6 +629,14 @@ extends: displayName: 'Build and Push Release Image' condition: contains(variables['Build.SourceBranch'], 'refs/tags/v') + - pwsh: | + $configPath = "$(Agent.TempDirectory)/hidi-docker.nuget.config" + if (Test-Path $configPath) { + Remove-Item $configPath -Force + } + displayName: 'Remove Docker NuGet config' + condition: always() + # once the nuget has been released, fill this form to get the public documentation updated. # https://dev.azure.com/msft-skilling/Content/_workitems/create/User%20Story?templateId=39fb91e3-64a2-4c8a-83db-b2bdf3603dd3&ownerId=c4a28f90-17ae-4384-b514-7273392b082b # https://learn.microsoft.com/en-us/dotnet/api/microsoft.openapi diff --git a/Dockerfile b/Dockerfile index 46cb00637..4e932b975 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,9 @@ COPY ./src ./hidi/src COPY ./Directory.Build.props ./hidi/Directory.Build.props COPY ./README.md ./hidi/README.md WORKDIR /app/hidi -RUN dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release +# CI supplies the private feed config as a secret; local builds use default NuGet sources. +RUN --mount=type=secret,id=nuget_config,target=/app/hidi/NuGet.Config \ + dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled AS runtime WORKDIR /app diff --git a/scripts/check-nuget-package-published.ps1 b/scripts/check-nuget-package-published.ps1 new file mode 100644 index 000000000..84f45419d --- /dev/null +++ b/scripts/check-nuget-package-published.ps1 @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS +Checks whether a NuGet artifact's version exists in an authenticated Azure Artifacts feed. +.DESCRIPTION +Resolves the package content endpoint from the private feed's NuGet v3 service index. +Sets nugetAlreadyPublished for the ESRP release steps; only a missing package or version +permits publishing. Feed authentication and other lookup failures fail the step. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageDirectory, + [Parameter(Mandatory = $true)] + [string]$PackageId, + [Parameter(Mandatory = $true)] + [string]$NuGetServiceIndexUrl, + [string]$FeedAccessToken = $env:FEED_ACCESS_TOKEN +) + +$ErrorActionPreference = 'Stop' + +function Assert-PrivateFeedUrl { + param([string]$Url) + + $uri = [uri]$Url + if (-not $uri.IsAbsoluteUri -or $uri.Scheme -ne 'https' -or + ($uri.Host -ne 'pkgs.dev.azure.com' -and -not $uri.Host.EndsWith('.pkgs.visualstudio.com'))) { + throw "NuGet lookups must use an HTTPS Azure Artifacts feed: $Url" + } +} + +Assert-PrivateFeedUrl -Url $NuGetServiceIndexUrl +if ([string]::IsNullOrWhiteSpace($FeedAccessToken)) { + throw 'FEED_ACCESS_TOKEN is required to query the private NuGet feed.' +} + +$packagePattern = '^' + [regex]::Escape($PackageId) + '\.(\d[\w\.\-]*)\.nupkg$' +$packages = @(Get-ChildItem -Path $PackageDirectory -File -Filter "$PackageId.*.nupkg" | + Where-Object { $_.Name -match $packagePattern }) +if ($packages.Count -ne 1) { + throw "Expected exactly one $PackageId nupkg to publish; found $($packages.Count)." +} +$version = [regex]::Match($packages[0].Name, $packagePattern, 'IgnoreCase').Groups[1].Value +$id = $PackageId.ToLowerInvariant() +$credentials = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("AzureDevOps:$FeedAccessToken")) +$headers = @{ + 'Authorization' = "Basic $credentials" + 'User-Agent' = 'openapi-azdo-pipeline' +} + +$index = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Headers $headers -MaximumRedirection 0 +$resource = $index.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | Select-Object -First 1 +if ([string]::IsNullOrWhiteSpace($resource.'@id')) { + throw "No PackageBaseAddress resource found in the NuGet service index at $NuGetServiceIndexUrl" +} +$uri = "$($resource.'@id'.TrimEnd('/'))/$id/index.json" +Assert-PrivateFeedUrl -Url $uri + +try { + $response = Invoke-RestMethod -Uri $uri -Headers $headers -MaximumRedirection 0 + if ($null -eq $response.versions) { + throw "No versions returned for NuGet $id by the private feed." + } + $alreadyPublished = $response.versions -contains $version +} +catch { + if ([int]$_.Exception.Response.StatusCode -eq 404) { + $alreadyPublished = $false + } + else { + throw + } +} + +if ($alreadyPublished) { + Write-Host "NuGet $id $version already present in the private feed; skipping ESRP release (idempotent re-run)." +} +else { + Write-Host "NuGet $id $version not found in the private feed; will publish via ESRP." +} +Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]$($alreadyPublished.ToString().ToLowerInvariant())" From 01d472995b3c466feb5bbf61e303bcd7752da16a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:28:57 -0700 Subject: [PATCH 654/655] fix: components-key validation timeouts under load (#3108) * Initial plan * fix(library): avoid components key validation timeouts under load Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> * fix(library): use conditional regex fallback for older frameworks Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- agents.md | 7 +++ .../Expressions/CompositeExpression.cs | 19 +++++- .../Rules/OpenApiComponentsRules.cs | 23 +++++-- .../Expressions/RuntimeExpressionTests.cs | 22 +++++++ .../OpenApiComponentsValidationTests.cs | 63 ++++++++++++++++++- 5 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 agents.md diff --git a/agents.md b/agents.md new file mode 100644 index 000000000..abeed736e --- /dev/null +++ b/agents.md @@ -0,0 +1,7 @@ +# Regex handling + +- Analyze regex patterns for matching complexity and excessive backtracking, including on long and near-matching inputs. Prefer equivalent patterns with less backtracking when available, while preserving matching semantics and target-framework compatibility. +- For fixed patterns on modern targets, use source-generated regexes with explicit match timeouts (`GeneratedRegex` under `NET8_0_OR_GREATER`). +- Use conditional compilation to provide a regular `Regex` with the same pattern and an explicit match timeout for older targets. Do not duplicate regex validation with a manually maintained character scanner. +- Older-runtime regex matching may still time out under load because timeouts use wall-clock time. If consumers encounter this limitation, recommend upgrading to a modern runtime that uses the source-generated implementation. +- Keep shared patterns in constants and reference those constants in validation diagnostics and tests. diff --git a/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs b/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs index cac554318..db2a97caf 100644 --- a/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs +++ b/src/Microsoft.OpenApi/Expressions/CompositeExpression.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.Linq; using System.Text.RegularExpressions; @@ -10,10 +11,17 @@ namespace Microsoft.OpenApi /// /// String literal with embedded expressions /// - public class CompositeExpression : RuntimeExpression + public partial class CompositeExpression : RuntimeExpression { private readonly string template; - private readonly Regex expressionPattern = new(@"{(?\$[^}]*)"); + private const string ExpressionPattern = @"{(?\$[^}]*)"; + +#if NET8_0_OR_GREATER + [GeneratedRegex(ExpressionPattern, RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex ExpressionRegex(); +#else + private static readonly Regex ExpressionRegex = new(ExpressionPattern, RegexOptions.None, TimeSpan.FromMilliseconds(100)); +#endif /// /// Expressions embedded into string literal @@ -24,12 +32,17 @@ public class CompositeExpression : RuntimeExpression /// Create a composite expression from a string literal with an embedded expression /// /// + /// Extracting embedded expressions exceeds the regex match timeout. public CompositeExpression(string expression) { template = expression; // Extract subexpressions and convert to RuntimeExpressions - var matches = expressionPattern.Matches(expression); +#if NET8_0_OR_GREATER + var matches = ExpressionRegex().Matches(expression); +#else + var matches = ExpressionRegex.Matches(expression); +#endif foreach (var item in matches.Cast()) { diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs index e3c6e1b4f..7473bce91 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs @@ -11,12 +11,19 @@ namespace Microsoft.OpenApi /// The validation rules for . /// [OpenApiRule] - public static class OpenApiComponentsRules + public static partial class OpenApiComponentsRules { /// - /// The key regex. + /// The key regex pattern. /// - internal static readonly Regex KeyRegex = new(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, TimeSpan.FromMilliseconds(100)); + internal const string KeyPattern = @"^[a-zA-Z0-9\.\-_]+$"; + +#if NET8_0_OR_GREATER + [GeneratedRegex(KeyPattern, RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex KeyRegex(); +#else + private static readonly Regex KeyRegex = new(KeyPattern, RegexOptions.None, TimeSpan.FromMilliseconds(100)); +#endif /// /// All the fixed fields declared above are objects @@ -54,12 +61,18 @@ private static void ValidateKeys(IValidationContext context, IEnumerable foreach (var key in keys) { - if (!KeyRegex.IsMatch(key)) +#if NET8_0_OR_GREATER + var isValidKey = KeyRegex().IsMatch(key); +#else + var isValidKey = KeyRegex.IsMatch(key); +#endif + if (!isValidKey) { context.CreateError(nameof(KeyMustBeRegularExpression), - string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyRegex.ToString())); + string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyPattern)); } } } + } } diff --git a/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs b/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs index 08a1debc0..91509184b 100644 --- a/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs +++ b/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs @@ -145,6 +145,28 @@ public void BuildRuntimeExpressionTwiceCreatesNewEquivalentInstances(string expr Assert.Equal(runtimeExpression1, runtimeExpression2); } + [Fact] + public void CompositeRuntimeExpressionPreservesMultilineCaptures() + { + const string expression = "prefix {$request.header.foo\nbar} {$url} suffix"; + + var composite = Assert.IsType(RuntimeExpression.Build(expression)); + + Assert.Equal(expression, composite.Expression); + Assert.Equal(new[] { "$request.header.foo\nbar", "$url" }, + composite.ContainedExpressions.Select(static item => item.Expression)); + } + + [Fact] + public void CompositeRuntimeExpressionPreservesUnterminatedCapture() + { + const string expression = "prefix {$url"; + + var composite = Assert.IsType(RuntimeExpression.Build(expression)); + + Assert.IsType(Assert.Single(composite.ContainedExpressions)); + } + [Fact] public void CompositeRuntimeExpressionContainsExpression() { diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs index 68f89a2a2..8993c17a5 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs @@ -2,7 +2,11 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; using Xunit; namespace Microsoft.OpenApi.Validations.Tests @@ -32,8 +36,65 @@ public void ValidateKeyMustMatchRegularExpressionInComponents() Assert.False(result); Assert.NotNull(errors); var error = Assert.Single(errors); - Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, "responses", OpenApiComponentsRules.KeyRegex.ToString()), + Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, "responses", OpenApiComponentsRules.KeyPattern), error.Message); } + + [Theory] + [InlineData("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_", true)] + [InlineData("", false)] + [InlineData("a b", false)] + [InlineData("a/b", false)] + [InlineData("é", false)] + [InlineData("12", false)] + [InlineData("a\n", true)] + [InlineData("\n", false)] + [InlineData("a\n\n", false)] + [InlineData("a\r\n", false)] + [InlineData("a\nb", false)] + [InlineData("a\0", false)] + public void ValidateComponentKeyPreservesRegexBehavior(string key, bool isValid) + { + var components = new OpenApiComponents + { + Schemas = new Dictionary + { + { key, new OpenApiSchema() } + } + }; + + var rules = new ValidationRuleSet(); + rules.Add(typeof(OpenApiComponents), OpenApiComponentsRules.KeyMustBeRegularExpression); + var errors = components.Validate(rules); + + Assert.Equal(isValid, !errors.Any()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task LoadAsyncValidatesLongComponentKeys(bool isValid) + { + var key = new string('a', 1_000_000) + (isValid ? string.Empty : "!"); + var json = """ + {"openapi":"3.1.0","info":{"title":"Test","version":"1.0"},"paths":{},"components":{"schemas":{ + """ + JsonSerializer.Serialize(key) + ":{\"type\":\"string\"}}}}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + var result = await OpenApiDocument.LoadAsync(stream, cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(result.Document); + Assert.NotNull(result.Diagnostic); + if (isValid) + { + Assert.Empty(result.Diagnostic.Errors); + } + else + { + var error = Assert.Single(result.Diagnostic.Errors); + Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, + key, "schemas", OpenApiComponentsRules.KeyPattern), error.Message); + } + } } } From 1ff69b41d7e4bf4ccead9743b4ddb6a2b30041d9 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 9 Oct 2026 13:44:42 -0400 Subject: [PATCH 655/655] ci: ports additional validation back from yoko (#3113) --- scripts/check-nuget-package-published.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/check-nuget-package-published.ps1 b/scripts/check-nuget-package-published.ps1 index 84f45419d..0f31961ec 100644 --- a/scripts/check-nuget-package-published.ps1 +++ b/scripts/check-nuget-package-published.ps1 @@ -64,6 +64,11 @@ try { if ($null -eq $response.versions) { throw "No versions returned for NuGet $id by the private feed." } + if ($response.versions -isnot [array] -or @($response.versions | Where-Object { + $_ -isnot [string] -or $_ -notmatch '^\d[\w\.\-]*$' + }).Count -gt 0) { + throw "Invalid version list returned for NuGet $id by the private feed." + } $alreadyPublished = $response.versions -contains $version } catch { @@ -81,4 +86,4 @@ if ($alreadyPublished) { else { Write-Host "NuGet $id $version not found in the private feed; will publish via ESRP." } -Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]$($alreadyPublished.ToString().ToLowerInvariant())" +Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]$($alreadyPublished.ToString().ToLowerInvariant())" \ No newline at end of file