From 3665f587e7fb9be8cd3143cf8ba501a0a809f4d7 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:47:12 +0900 Subject: [PATCH 1/5] chore: add net11 target framework --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 20149bb8cb4..cfc63e069c1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ true - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 net8.0;net9.0;net10.0;net11.0 Preview enable From 0b1ecc8b838981323acdd34a0c3a271ddb492dd1 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:48:08 +0900 Subject: [PATCH 2/5] chore: use union types instead of oneof for net11 tfm --- src/Docfx.Build/ApiPage/ApiPage.cs | 60 ++++++++++------ .../ApiPage/ApiPageHtmlTemplate.cs | 5 +- .../ApiPage/ApiPageMarkdownTemplate.cs | 4 +- src/Docfx.Build/ApiPage/CustomClassifiers.cs | 72 +++++++++++++++++++ src/Docfx.Build/Docfx.Build.csproj | 9 +++ src/Docfx.Build/OneOfJsonConverterFactory.cs | 3 +- src/Docfx.Dotnet/Docfx.Dotnet.csproj | 13 +++- src/Docfx.Dotnet/DotnetApiCatalog.ApiPage.cs | 9 ++- 8 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 src/Docfx.Build/ApiPage/CustomClassifiers.cs diff --git a/src/Docfx.Build/ApiPage/ApiPage.cs b/src/Docfx.Build/ApiPage/ApiPage.cs index f6dcf16a216..aa676aa7f06 100644 --- a/src/Docfx.Build/ApiPage/ApiPage.cs +++ b/src/Docfx.Build/ApiPage/ApiPage.cs @@ -3,24 +3,50 @@ using System.Text.Json; using System.Text.Json.Serialization; -using OneOf; #nullable enable namespace Docfx.Build.ApiPage; -struct LinkSpan -{ - public required string text { get; init; } - public string? url { get; init; } -} +#if NET11_0_OR_GREATER +readonly union BoolOrString(bool, string); +readonly union StringOrArrayOfString(string, string[]); +readonly union Span(string, LinkSpan); +[JsonUnion(TypeClassifier = typeof(InlineClassifier))] +readonly union Inline(Span, Span[]); + +[JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))] +readonly union Heading(H1, H2, H3, H4, H5, H6); + +[JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))] +readonly union Api(Api1, Api2, Api3, Api4); + +[JsonUnion(TypeClassifier = typeof(BlockClassifier))] +readonly union Block(Heading, Api, Markdown, Facts, Parameters, List, Inheritance, Code); +#else [GenerateOneOf] partial class Span : OneOfBase { } [GenerateOneOf] partial class Inline : OneOfBase { } +[GenerateOneOf] +partial class Heading : OneOfBase { } + +[GenerateOneOf] +partial class Api : OneOfBase { } + +[GenerateOneOf] +partial class Block : OneOfBase { } +#endif + +struct LinkSpan +{ + public required string text { get; init; } + public string? url { get; init; } +} + struct Markdown { public required string markdown { get; init; } @@ -62,14 +88,11 @@ struct H6 public string? id { get; init; } } -[GenerateOneOf] -partial class Heading : OneOfBase { } - abstract class ApiBase { public string? id { get; init; } - public OneOf? deprecated { get; init; } - public OneOf? preview { get; init; } + public BoolOrString? deprecated { get; init; } + public BoolOrString? preview { get; init; } public string? src { get; init; } public Dictionary? metadata { get; init; } } @@ -94,10 +117,6 @@ class Api4 : ApiBase public required string api4 { get; init; } } - -[GenerateOneOf] -partial class Api : OneOfBase { } - record struct Fact(string name, Inline value); struct Facts @@ -127,8 +146,8 @@ class Parameter public Inline? type { get; init; } public string? @default { get; init; } public string? description { get; init; } - public OneOf? deprecated { get; init; } - public OneOf? preview { get; init; } + public BoolOrString? deprecated { get; init; } + public BoolOrString? preview { get; init; } public bool? optional { get; init; } } @@ -137,9 +156,6 @@ struct Parameters public required Parameter[] parameters { get; init; } } -[GenerateOneOf] -partial class Block : OneOfBase { } - record ApiPage { public static JsonSerializerOptions JsonSerializerOptions { get; } = new() @@ -147,14 +163,16 @@ record ApiPage DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | JsonIgnoreCondition.WhenWritingDefault, }; +#if !NET11_0_OR_GREATER static ApiPage() { JsonSerializerOptions.Converters.Add(new OneOfJsonConverterFactory()); } +#endif public required string title { get; init; } public required Block[] body { get; init; } public string? languageId { get; init; } - public Dictionary>? metadata { get; init; } + public Dictionary? metadata { get; init; } } diff --git a/src/Docfx.Build/ApiPage/ApiPageHtmlTemplate.cs b/src/Docfx.Build/ApiPage/ApiPageHtmlTemplate.cs index ea1ab38d6d7..73ea533f26b 100644 --- a/src/Docfx.Build/ApiPage/ApiPageHtmlTemplate.cs +++ b/src/Docfx.Build/ApiPage/ApiPageHtmlTemplate.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using OneOf; using static Docfx.Build.HtmlTemplate; #nullable enable @@ -69,7 +68,7 @@ HtmlTemplate Api(Api api) """); } - HtmlTemplate? Badge(OneOf? value, string text, string cssClass, string fontSize) + HtmlTemplate? Badge(BoolOrString? value, string text, string cssClass, string fontSize) { var isDeprecated = value?.Value switch { @@ -81,7 +80,7 @@ HtmlTemplate Api(Api api) return isDeprecated ? Html($" {text}") : null; } - HtmlTemplate Alert(OneOf? value, string cssClass) + HtmlTemplate Alert(BoolOrString? value, string cssClass) { return value?.Value is string ds && !string.IsNullOrEmpty(ds) ? Html($"\n") diff --git a/src/Docfx.Build/ApiPage/ApiPageMarkdownTemplate.cs b/src/Docfx.Build/ApiPage/ApiPageMarkdownTemplate.cs index 6972f574a94..527a4cc20cc 100644 --- a/src/Docfx.Build/ApiPage/ApiPageMarkdownTemplate.cs +++ b/src/Docfx.Build/ApiPage/ApiPageMarkdownTemplate.cs @@ -52,10 +52,10 @@ FormattableString Facts(Facts facts) => $"{string.Concat(facts.facts.Select(fact => $"{Escape(fact.name)}: {Inline(fact.value)} \n"))}\n"; FormattableString List(List list) => - $"{string.Join(", \n", list.list.Select(Inline))}\n\n"; + $"{string.Join(", \n", list.list.Select(x => Inline(x)))}\n\n"; FormattableString Inheritance(Inheritance inheritance) => - $"{string.Join(" \u2190 \n", inheritance.inheritance.Select(Inline))}\n\n"; + $"{string.Join(" \u2190 \n", inheritance.inheritance.Select(x => Inline(x)))}\n\n"; FormattableString Code(Code code) => $""" diff --git a/src/Docfx.Build/ApiPage/CustomClassifiers.cs b/src/Docfx.Build/ApiPage/CustomClassifiers.cs new file mode 100644 index 00000000000..19166b9a81f --- /dev/null +++ b/src/Docfx.Build/ApiPage/CustomClassifiers.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET11_0_OR_GREATER +using System.Text.Json; +using System.Text.Json.Serialization; + +#nullable enable + +namespace Docfx.Build.ApiPage; + + +/// +/// Custom classifier for inline types. +/// It's required because built-in JsonUnionTypeStructuralClassifier don't support nested union type. +/// +internal sealed class InlineClassifier : JsonTypeClassifierFactory +{ + public override JsonTypeClassifier CreateJsonClassifier(JsonTypeClassifierContext context, JsonSerializerOptions options) + { + return static (ref Utf8JsonReader reader) => reader.TokenType switch + { + JsonTokenType.String => typeof(Span), + JsonTokenType.StartObject => typeof(Span), + JsonTokenType.StartArray => typeof(Span[]), + _ => null, + }; + } +} + +/// +/// Custom classifier for block types. +/// It's required because built-in JsonUnionTypeStructuralClassifier don't support nested union type. +/// +internal sealed class BlockClassifier : JsonTypeClassifierFactory +{ + public override JsonTypeClassifier CreateJsonClassifier(JsonTypeClassifierContext context, JsonSerializerOptions options) + { + return static (ref Utf8JsonReader reader) => + { + if (reader.TokenType is not JsonTokenType.StartObject) + return null; + + while (reader.Read() && reader.TokenType is not JsonTokenType.EndObject) + { + if (reader.TokenType is not JsonTokenType.PropertyName) + continue; + + Type? type = reader.GetString() switch + { + "h1" or "h2" or "h3" or "h4" or "h5" or "h6" => typeof(Heading), + "api1" or "api2" or "api3" or "api4" => typeof(Api), + "markdown" => typeof(Markdown), + "facts" => typeof(Facts), + "parameters" => typeof(Parameters), + "list" => typeof(List), + "inheritance" => typeof(Inheritance), + "code" => typeof(Code), + _ => null, + }; + + if (type is not null) + return type; + + reader.Read(); + reader.Skip(); + } + + return null; + }; + } +} +#endif diff --git a/src/Docfx.Build/Docfx.Build.csproj b/src/Docfx.Build/Docfx.Build.csproj index 7ea4fddf800..28a3f72be2c 100644 --- a/src/Docfx.Build/Docfx.Build.csproj +++ b/src/Docfx.Build/Docfx.Build.csproj @@ -18,8 +18,17 @@ + + + + + + + + diff --git a/src/Docfx.Build/OneOfJsonConverterFactory.cs b/src/Docfx.Build/OneOfJsonConverterFactory.cs index 497af6dc308..4d8aa4dea9e 100644 --- a/src/Docfx.Build/OneOfJsonConverterFactory.cs +++ b/src/Docfx.Build/OneOfJsonConverterFactory.cs @@ -1,5 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if !NET11_0_OR_GREATER using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -9,7 +10,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Docfx.Build.ApiPage; -using OneOf; #nullable enable @@ -258,3 +258,4 @@ private static void Assert( ]; } } +#endif diff --git a/src/Docfx.Dotnet/Docfx.Dotnet.csproj b/src/Docfx.Dotnet/Docfx.Dotnet.csproj index 7ff17fbca8d..ba184d8c2fe 100644 --- a/src/Docfx.Dotnet/Docfx.Dotnet.csproj +++ b/src/Docfx.Dotnet/Docfx.Dotnet.csproj @@ -6,6 +6,7 @@ + @@ -27,8 +28,6 @@ - - @@ -41,4 +40,14 @@ + + + + + + + + + diff --git a/src/Docfx.Dotnet/DotnetApiCatalog.ApiPage.cs b/src/Docfx.Dotnet/DotnetApiCatalog.ApiPage.cs index 5b3fba2fd0a..1aa912c22b2 100644 --- a/src/Docfx.Dotnet/DotnetApiCatalog.ApiPage.cs +++ b/src/Docfx.Dotnet/DotnetApiCatalog.ApiPage.cs @@ -10,7 +10,6 @@ using Docfx.Plugins; using HtmlAgilityPack; using Microsoft.CodeAnalysis; -using OneOf; #nullable enable @@ -98,7 +97,7 @@ _ when SymbolHelper.IsMember(method) => "Method", throw new NotSupportedException($"Unknown symbol type kind {symbols[0].symbol}"); } - var metadata = new Dictionary>(); + var metadata = new Dictionary(); if (!string.IsNullOrEmpty(comment?.Summary)) metadata["description"] = HtmlInnerText(comment.Summary); @@ -144,14 +143,14 @@ void Api(int level, string title, ISymbol symbol, Compilation compilation) }); } - OneOf? Deprecated(ISymbol symbol) + BoolOrString? Deprecated(ISymbol symbol) { if (symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == "ObsoleteAttribute") is { } obsoleteAttribute) - return obsoleteAttribute.ConstructorArguments.FirstOrDefault().Value is string reason && !string.IsNullOrEmpty(reason) ? (OneOf?)reason : true; + return obsoleteAttribute.ConstructorArguments.FirstOrDefault().Value is string reason && !string.IsNullOrEmpty(reason) ? (BoolOrString?)reason : true; return null; } - OneOf? Preview(ISymbol symbol, ISymbol? originalSymbol = null) + BoolOrString? Preview(ISymbol symbol, ISymbol? originalSymbol = null) { if (symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == "ExperimentalAttribute") is { } experimentalAttribute) { From 4a3e14a7294b542f1200e0d35387e02e53a888fc Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:01:17 +0900 Subject: [PATCH 3/5] chore: add .NET 11 SDK setup code and update setup-dotnet version --- .github/actions/build/action.yml | 8 ++------ .github/workflows/ci.yml | 2 ++ .github/workflows/lint.yml | 2 +- .github/workflows/nightly.yml | 18 +++++++++--------- .github/workflows/release.yml | 4 ++-- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index ba01ecfe561..ff6a22853ff 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -19,18 +19,14 @@ runs: cache: ${{ inputs.cache == 'true' && 'npm' || '' }} cache-dependency-path: templates/package-lock.json - - uses: actions/setup-dotnet@v5 + - uses: actions/setup-dotnet@v6 with: dotnet-version: | + 11.x 10.x 9.x 8.x - - uses: actions/setup-dotnet@v5 - if: env.DOCFX_PREVIEW_BUILD == 'true' - with: - dotnet-version: 11.0.100-rc.1.26425.128 - - uses: actions/cache@v6 if: inputs.cache == 'true' with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec09b311f97..08dd228d9c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] framework: [net10.0] include: + - os: ubuntu-latest + framework: net11.0 - os: ubuntu-latest framework: net9.0 - os: ubuntu-latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e29bf64eab6..c2aa15a4232 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.x + dotnet-version: 11.x - name: Run `dotnet format` command run: | diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 827e87c6166..a4811cdd552 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: inputs: validation_only: - description: 'Compare latest stable and this ref without package publication or Pages deployment' + description: 'Compare latest stable and this ref without package publication or Pages deployment' type: boolean default: true schedule: @@ -75,9 +75,9 @@ jobs: contents: read packages: write steps: - - uses: actions/setup-dotnet@v5 + - uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.x + dotnet-version: 11.x - uses: actions/download-artifact@v8 with: name: nightly-publication-packages @@ -95,9 +95,9 @@ jobs: actions: read steps: - uses: actions/checkout@v7 - - uses: actions/setup-dotnet@v5 + - uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.x + dotnet-version: 11.x - uses: actions/download-artifact@v8 with: name: nightly-tool-package @@ -140,24 +140,24 @@ jobs: - name: Resolve exact SDK candidates from official metadata id: matrix run: node test/compatibility/report.mjs resolve drop/compatibility-matrix.json - - uses: actions/setup-dotnet@v5 + - uses: actions/setup-dotnet@v6 continue-on-error: true with: dotnet-version: ${{ steps.matrix.outputs.sdks }} - name: Test compatibility reporting run: node --test test/compatibility/report.test.mjs - - name: Measure and check latest stable and exact nightly packages + - name: Measure and check latest stable and exact nightly packages shell: pwsh env: GH_TOKEN: ${{ github.token }} TOOL_VERSION: ${{ needs.build-nightly-package.outputs.version }} run: | - ./test/compatibility/Measure-Compatibility.ps1 -MatrixPath drop/compatibility-matrix.json -OutputDirectory drop/compatibility-report -NightlyPackage "drop/nightly/docfx.$env:TOOL_VERSION.nupkg" -NightlyFrameworks net8.0,net9.0,net10.0,net11.0 + ./test/compatibility/Measure-Compatibility.ps1 -MatrixPath drop/compatibility-matrix.json -OutputDirectory drop/compatibility-report -NightlyPackage "drop/nightly/docfx.$env:TOOL_VERSION.nupkg" -NightlyFrameworks net8.0,net9.0,net10.0,net11.0 - name: Upload compatibility evidence, including failed cases if: always() uses: actions/upload-artifact@v7 with: name: ${{ inputs.validation_only && 'sdk-compatibility-validation-v1' || 'sdk-compatibility-v1' }} path: drop/compatibility-report - if-no-files-found: error + if-no-files-found: error retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df9bb7a3098..0428b38a378 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: steps: - uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.x + dotnet-version: 11.x - uses: actions/download-artifact@v8 with: @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.x + dotnet-version: 11.x - uses: actions/download-artifact@v8 with: From 325c5ebea9becc1794820a34c499b8e5fb403430 Mon Sep 17 00:00:00 2001 From: "Liangying.Wei" Date: Fri, 25 Sep 2026 10:14:35 +1000 Subject: [PATCH 4/5] style: remove unused import from Percy tests --- test/docfx.Snapshot.Tests/PercyTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/docfx.Snapshot.Tests/PercyTest.cs b/test/docfx.Snapshot.Tests/PercyTest.cs index 76685bb7074..d7e4c3eb8c1 100644 --- a/test/docfx.Snapshot.Tests/PercyTest.cs +++ b/test/docfx.Snapshot.Tests/PercyTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http.Json; using System.Net.NetworkInformation; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; From 71a2d85b741127a535208b72aa645916fedbaff3 Mon Sep 17 00:00:00 2001 From: "Liangying.Wei" Date: Fri, 25 Sep 2026 10:25:08 +1000 Subject: [PATCH 5/5] fix: retain HTTP JSON import for older target frameworks --- test/docfx.Snapshot.Tests/PercyTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/docfx.Snapshot.Tests/PercyTest.cs b/test/docfx.Snapshot.Tests/PercyTest.cs index d7e4c3eb8c1..a83a5aeaf40 100644 --- a/test/docfx.Snapshot.Tests/PercyTest.cs +++ b/test/docfx.Snapshot.Tests/PercyTest.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if !NET11_0_OR_GREATER +using System.Net.Http.Json; +#endif using System.Net.NetworkInformation; using System.Runtime.CompilerServices; using System.Text.RegularExpressions;