From f1fcf50cb83f4e8372462c245a88962ae1ada437 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 11 Feb 2025 13:36:11 -0800 Subject: [PATCH 01/13] Populate XML doc comments into OpenAPI document --- AspNetCore.sln | 178 ++--- eng/Dependencies.props | 1 + eng/Versions.props | 1 + src/OpenApi/OpenApi.slnf | 2 + .../Microsoft.AspNetCore.OpenApi.targets | 21 + .../gen/Helpers/AddOpenApiInvocation.cs | 18 + .../Helpers/AddOpenApiInvocationComparer.cs | 27 + .../gen/Helpers/AddOpenApiOverloadVariant.cs | 12 + .../gen/Helpers/AssemblyTypeSymbolsVisitor.cs | 82 +++ .../Helpers/DocumentationCommentXmlNames.cs | 61 ++ src/OpenApi/gen/Helpers/ISymbolExtensions.cs | 192 ++++++ src/OpenApi/gen/Helpers/StringExtensions.cs | 90 +++ ...AspNetCore.OpenApi.SourceGenerators.csproj | 31 + .../gen/XmlCommentGenerator.Emitter.cs | 392 +++++++++++ src/OpenApi/gen/XmlCommentGenerator.Parser.cs | 172 +++++ src/OpenApi/gen/XmlCommentGenerator.cs | 57 ++ src/OpenApi/gen/XmlComments/XmlComment.cs | 550 +++++++++++++++ .../gen/XmlComments/XmlParameterComment.cs | 39 ++ .../gen/XmlComments/XmlResponseComment.cs | 39 ++ .../sample/Controllers/TestController.cs | 8 - .../sample/Controllers/XmlController.cs | 39 ++ .../sample/Endpoints/MapFormEndpoints.cs | 33 + .../sample/Endpoints/MapResponsesEndpoints.cs | 22 + .../sample/Endpoints/MapSchemasEndoints.cs | 36 + .../sample/Endpoints/MapVersionedEndpoints.cs | 34 + .../sample/Endpoints/MapXmlEndpoints.cs | 112 ++++ src/OpenApi/sample/Program.cs | 82 +-- src/OpenApi/sample/Sample.csproj | 8 + .../src/Microsoft.AspNetCore.OpenApi.csproj | 4 + ...nerateAdditionalXmlFilesForOpenApiTests.cs | 120 ++++ ...soft.AspNetCore.OpenApi.Build.Tests.csproj | 20 + .../AddOpenApiTests.cs | 46 ++ .../Helpers/HostFactoryResolver.cs | 363 ++++++++++ .../Helpers/SnapshotTestHelper.cs | 168 +++++ ...Core.OpenApi.SourceGenerators.Tests.csproj | 33 + .../ModuleInitializer.cs | 13 + .../OperationTests.Controllers.cs | 89 +++ .../OperationTests.MinimalApis.cs | 164 +++++ .../SchemaTests.cs | 223 +++++++ ...ApiXmlCommentSupport.generated.verified.cs | 260 ++++++++ ...ApiXmlCommentSupport.generated.verified.cs | 233 +++++++ ...ApiXmlCommentSupport.generated.verified.cs | 237 +++++++ ...ApiXmlCommentSupport.generated.verified.cs | 259 ++++++++ .../OpenApiDocumentIntegrationTests.cs | 43 +- ...ment_documentName=controllers.verified.txt | 96 +++ ...piDocument_documentName=forms.verified.txt | 227 +++++++ ...cument_documentName=responses.verified.txt | 208 ++++++ ...t_documentName=schemas-by-ref.verified.txt | 624 ++++++++++++++++++ ...enApiDocument_documentName=v1.verified.txt | 208 ++++++ ...enApiDocument_documentName=v2.verified.txt | 69 ++ ...nApiDocument_documentName=xml.verified.txt | 397 +++++++++++ .../Shared/SharedTypes.cs | 107 +++ 52 files changed, 6333 insertions(+), 217 deletions(-) create mode 100644 src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets create mode 100644 src/OpenApi/gen/Helpers/AddOpenApiInvocation.cs create mode 100644 src/OpenApi/gen/Helpers/AddOpenApiInvocationComparer.cs create mode 100644 src/OpenApi/gen/Helpers/AddOpenApiOverloadVariant.cs create mode 100644 src/OpenApi/gen/Helpers/AssemblyTypeSymbolsVisitor.cs create mode 100644 src/OpenApi/gen/Helpers/DocumentationCommentXmlNames.cs create mode 100644 src/OpenApi/gen/Helpers/ISymbolExtensions.cs create mode 100644 src/OpenApi/gen/Helpers/StringExtensions.cs create mode 100644 src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj create mode 100644 src/OpenApi/gen/XmlCommentGenerator.Emitter.cs create mode 100644 src/OpenApi/gen/XmlCommentGenerator.Parser.cs create mode 100644 src/OpenApi/gen/XmlCommentGenerator.cs create mode 100644 src/OpenApi/gen/XmlComments/XmlComment.cs create mode 100644 src/OpenApi/gen/XmlComments/XmlParameterComment.cs create mode 100644 src/OpenApi/gen/XmlComments/XmlResponseComment.cs create mode 100644 src/OpenApi/sample/Controllers/XmlController.cs create mode 100644 src/OpenApi/sample/Endpoints/MapFormEndpoints.cs create mode 100644 src/OpenApi/sample/Endpoints/MapResponsesEndpoints.cs create mode 100644 src/OpenApi/sample/Endpoints/MapSchemasEndoints.cs create mode 100644 src/OpenApi/sample/Endpoints/MapVersionedEndpoints.cs create mode 100644 src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/Microsoft.AspNetCore.OpenApi.Build.Tests.csproj create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/AddOpenApiTests.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/ModuleInitializer.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.Controllers.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=forms.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=responses.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v1.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v2.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt diff --git a/AspNetCore.sln b/AspNetCore.sln index 1805c5617512..52fe9c9b652c 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1584,7 +1584,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "aspnetcoreCA", "src\Install EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "iisca", "src\Installers\Windows\AspNetCoreModule-Setup\IIS-Setup\iisca\lib\iisca.vcxproj", "{7324770C-0871-4D73-BE3D-5E2F3E9E1B1E}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CommonLib", "src\Installers\Windows\AspNetCoreModule-Setup\IIS-Setup\IIS-Common\lib\IISSetup.CommonLib.vcxproj", "{B54A8F61-60DE-4AD9-87CA-D102F230678E}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IISSetup.CommonLib", "src\Installers\Windows\AspNetCoreModule-Setup\IIS-Setup\IIS-Common\lib\IISSetup.CommonLib.vcxproj", "{B54A8F61-60DE-4AD9-87CA-D102F230678E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCoreModule-Setup", "AspNetCoreModule-Setup", "{D30A658D-61F6-444B-9AC7-F66A1A1B86B6}" EndProject @@ -1812,6 +1812,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assets", "Assets", "{2B858B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.App.Internal.Assets", "src\Assets\Microsoft.AspNetCore.App.Internal.Assets.csproj", "{2AAE7819-BC3E-48F4-9CFA-5DD4CD5FFD62}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{96EC4DD3-028E-6E27-5B14-08C21B07CE89}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{1BBD75D2-429D-D565-A98E-36437448E8C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.SourceGenerators", "src\OpenApi\gen\Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj", "{C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4C4590E4-54F5-3693-9633-60A8DCA89385}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests\Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj", "{51BE4B37-D772-4BFA-8DAE-CF18730BEB89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Build.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Build.Tests\Microsoft.AspNetCore.OpenApi.Build.Tests.csproj", "{10F96346-4215-4642-B837-0B5B08AC27AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10959,6 +10971,54 @@ Global {2AAE7819-BC3E-48F4-9CFA-5DD4CD5FFD62}.Release|x64.Build.0 = Release|Any CPU {2AAE7819-BC3E-48F4-9CFA-5DD4CD5FFD62}.Release|x86.ActiveCfg = Release|Any CPU {2AAE7819-BC3E-48F4-9CFA-5DD4CD5FFD62}.Release|x86.Build.0 = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|arm64.ActiveCfg = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|arm64.Build.0 = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|x64.ActiveCfg = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|x64.Build.0 = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|x86.ActiveCfg = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Debug|x86.Build.0 = Debug|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|Any CPU.Build.0 = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|arm64.ActiveCfg = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|arm64.Build.0 = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|x64.ActiveCfg = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|x64.Build.0 = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|x86.ActiveCfg = Release|Any CPU + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2}.Release|x86.Build.0 = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|arm64.ActiveCfg = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|arm64.Build.0 = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|x64.ActiveCfg = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|x64.Build.0 = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|x86.ActiveCfg = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Debug|x86.Build.0 = Debug|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|Any CPU.Build.0 = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|arm64.ActiveCfg = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|arm64.Build.0 = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|x64.ActiveCfg = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|x64.Build.0 = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|x86.ActiveCfg = Release|Any CPU + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89}.Release|x86.Build.0 = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|arm64.ActiveCfg = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|arm64.Build.0 = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|x64.ActiveCfg = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|x64.Build.0 = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Debug|x86.Build.0 = Debug|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|Any CPU.Build.0 = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|arm64.ActiveCfg = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|arm64.Build.0 = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|x64.ActiveCfg = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|x64.Build.0 = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|x86.ActiveCfg = Release|Any CPU + {10F96346-4215-4642-B837-0B5B08AC27AE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11745,116 +11805,12 @@ Global {7324770C-0871-4D73-BE3D-5E2F3E9E1B1E} = {D30A658D-61F6-444B-9AC7-F66A1A1B86B6} {B54A8F61-60DE-4AD9-87CA-D102F230678E} = {D30A658D-61F6-444B-9AC7-F66A1A1B86B6} {D30A658D-61F6-444B-9AC7-F66A1A1B86B6} = {5E46DC83-C39C-4E3A-B242-C064607F4367} - {5244BC49-2568-4701-80A6-EAB8950AB5FA} = {31854B06-DC6B-4416-97C3-559CC390B8B4} - {6F1B115C-1903-40CB-837D-7961AB610F4E} = {5E46DC83-C39C-4E3A-B242-C064607F4367} - {56DFE643-7F0F-40C8-9F7F-8EA5357781EF} = {3CBC4802-E9B8-48B7-BC8C-B0AFB9EEC643} - {E0BE6B86-F8DB-405D-AC05-78C8C9D3857D} = {C3722C5D-E159-4AB3-AF60-769185B31B47} - {8EB0B983-8851-4565-B92F-366F1B126E61} = {C3722C5D-E159-4AB3-AF60-769185B31B47} - {93D3CC76-9FA9-4198-B49D-54BA918105EE} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C} - {6C06163A-80E9-49C1-817C-B391852BA563} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} - {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} = {6C06163A-80E9-49C1-817C-B391852BA563} - {825BCF97-67A9-4834-B3A8-C3DC97A90E41} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} - {DC349A25-0DBF-4468-99E1-B95C22D3A7EF} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} - {AA5ABFBC-177C-421E-B743-005E0FD1248B} = {E5963C9F-20A6-4385-B364-814D2581FADF} - {5D5A3B60-A014-447C-9126-B1FA6C821C8D} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} - {B5AC1D8B-9D43-4261-AE0F-6B7574656F2C} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} - {C3FFA4E4-0E7E-4866-A15F-034245BFD800} = {B5AC1D8B-9D43-4261-AE0F-6B7574656F2C} - {5465F96F-33D5-454E-9C40-494E58AEEE5D} = {E5963C9F-20A6-4385-B364-814D2581FADF} - {97996D39-7722-4AFC-A41A-AD61CA7A413D} = {5465F96F-33D5-454E-9C40-494E58AEEE5D} - {37144E52-611B-40E8-807C-2821F5A814CB} = {5465F96F-33D5-454E-9C40-494E58AEEE5D} - {559FE354-7E08-4310-B4F3-AE30F34DEED5} = {5465F96F-33D5-454E-9C40-494E58AEEE5D} - {94F95276-7CDF-44A8-B159-D09702EF6794} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} - {EA7D844B-C180-41C7-9D55-273AD88BF71F} = {94F95276-7CDF-44A8-B159-D09702EF6794} - {7A331A1C-E2C4-4E37-B0A0-B5AA10661229} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB} - {DD076DDA-7956-4361-A7D4-2B8025AB3DFD} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB} - {5CDB8ABC-9DD0-4A9F-8948-EED5FFE89F67} = {DD076DDA-7956-4361-A7D4-2B8025AB3DFD} - {151E6F9E-107B-4DDC-A2B1-95115801FD14} = {DD076DDA-7956-4361-A7D4-2B8025AB3DFD} - {B43BE3EB-9846-4484-88D8-05165202A0FC} = {DD076DDA-7956-4361-A7D4-2B8025AB3DFD} - {9A8AE587-A3DB-4211-8354-430C4CCBEB9B} = {B43BE3EB-9846-4484-88D8-05165202A0FC} - {2E28881D-A188-47AF-800A-B5877AD8C288} = {9A8AE587-A3DB-4211-8354-430C4CCBEB9B} - {A53696E8-6065-41BA-84FB-E89E0DACFF6C} = {9A8AE587-A3DB-4211-8354-430C4CCBEB9B} - {109C702D-DACE-4F82-A490-15E5AFA94005} = {151E6F9E-107B-4DDC-A2B1-95115801FD14} - {E3C5FAD2-8AB7-47C8-AAFD-8262551A5D11} = {151E6F9E-107B-4DDC-A2B1-95115801FD14} - {90CF4DC6-AC53-459F-9EAB-623A11EADAA3} = {B43BE3EB-9846-4484-88D8-05165202A0FC} - {F18E97AE-3A3F-424D-8DC2-4D001A167F98} = {B43BE3EB-9846-4484-88D8-05165202A0FC} - {8C3E422A-F281-4B93-A567-88C7A1ED0412} = {B43BE3EB-9846-4484-88D8-05165202A0FC} - {EB14F068-AD55-4970-B9B4-1FBE33704243} = {5CDB8ABC-9DD0-4A9F-8948-EED5FFE89F67} - {B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C} - {489020F2-80D9-4468-A5D3-07E785837A5D} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} - {8FED7E65-A7DD-4F13-8980-BF03E77B6C85} = {489020F2-80D9-4468-A5D3-07E785837A5D} - {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} - {9716D0D0-2251-44DD-8596-67D253EEF41C} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9} - {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} - {EFC8EA45-572D-4D8D-A597-9045A2D8EC40} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} - {8EE73488-2B92-42BD-96C9-0DD65405C828} = {1D865E78-7A66-4CA9-92EE-2B350E45281F} - {41FF4F96-98D2-4482-A2A7-4B179E80D285} = {1D865E78-7A66-4CA9-92EE-2B350E45281F} - {51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F} - {487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088} - {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} - {3309FA1E-4E95-49E9-BE2A-827D01FD63C0} = {5465F96F-33D5-454E-9C40-494E58AEEE5D} - {B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730} - {AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} - {7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67} - {89896261-C5DD-4901-BCA7-7A5F718BC008} = {AB4B9E75-719C-4589-B852-20FBFD727730} - {F0FBA346-D8BC-4FAE-A4B2-85B33FA23055} = {08D53E58-4AAE-40C4-8497-63EC8664F304} - {AA7445F5-BD28-400C-8507-E2E0D3CF7D7E} = {08D53E58-4AAE-40C4-8497-63EC8664F304} - {A5946454-4788-4871-8F23-A9471D55F115} = {4FDDC525-4E60-4CAF-83A3-261C5B43721F} - {76C3E22D-092B-4E8A-81F0-DCF071BFF4CD} = {0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F} - {0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF} - {7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9} = {08D53E58-4AAE-40C4-8497-63EC8664F304} - {91C3C03E-EA56-4ABA-9E73-A3DA4C2833D9} = {1D865E78-7A66-4CA9-92EE-2B350E45281F} - {F4A9CC79-5FE3-4F2B-9CC3-7F42DEDB4853} = {7B976D8F-EA31-4C0B-97BD-DFD9B3CC86FB} - {07FDBE0D-B7A1-43DE-B120-F699C30E7CEF} = {E19E55A2-1562-47A7-8EA6-B51F2CA0CC4C} - {BA649043-EF2B-42DC-B422-A46127BE8296} = {7B976D8F-EA31-4C0B-97BD-DFD9B3CC86FB} - {3A6FD623-F7F3-404B-8A39-CAFB40CA6A08} = {08D53E58-4AAE-40C4-8497-63EC8664F304} - {890B5210-48EF-488F-93A2-F13BCB07C780} = {4DA84F2B-1948-439B-85AB-E99E31331A9C} - {A8E2AB77-8F57-47C2-A961-2F316793CAFF} = {890B5210-48EF-488F-93A2-F13BCB07C780} - {F43CC5EA-6032-4A11-A9B2-6D48CB5EB082} = {4DA84F2B-1948-439B-85AB-E99E31331A9C} - {74377D3E-E0C6-41A4-89ED-11A9C00142A9} = {166E48ED-9738-4E13-8618-0D805F6F0F65} - {3C7C65BF-0C13-418E-90BD-EC9C3CD282CB} = {74377D3E-E0C6-41A4-89ED-11A9C00142A9} - {F057512B-55BF-4A8B-A027-A0505F8BA10C} = {4FDDC525-4E60-4CAF-83A3-261C5B43721F} - {10173568-A65E-44E5-8C6F-4AA49D0577A1} = {F057512B-55BF-4A8B-A027-A0505F8BA10C} - {97C7D2A4-87E5-4A4A-A170-D736427D5C21} = {F057512B-55BF-4A8B-A027-A0505F8BA10C} - {4730F56D-24EF-4BB2-AA75-862E31205F3A} = {225AEDCF-7162-4A86-AC74-06B84660B379} - {C406D9E0-1585-43F9-AA8F-D468AF84A996} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF} - {7757E360-40F5-4C90-9D7F-E6B0E62BA9E2} = {C406D9E0-1585-43F9-AA8F-D468AF84A996} - {F0BF2260-5AE2-4248-81DE-AC5B9FC6A931} = {C406D9E0-1585-43F9-AA8F-D468AF84A996} - {A05652B3-953E-4915-9D7F-0E361D988815} = {0CE1CC26-98CE-4022-A81C-E32AAFC9B819} - {7BAEB9BF-28F4-4DFD-9A04-E5193683C261} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF} - {AE4D272D-6F13-42C8-9404-C149188AFA33} = {7BAEB9BF-28F4-4DFD-9A04-E5193683C261} - {5D438258-CB19-4282-814F-974ABBC71411} = {7BAEB9BF-28F4-4DFD-9A04-E5193683C261} - {F5AE525F-F435-40F9-A567-4D5EC3B50D6E} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1} - {56291265-B7BF-4756-92AB-FC30F09381D1} = {822D1519-77F0-484A-B9AB-F694C2CC25F1} - {66FA1041-5556-43A0-9CA3-F9937F085F6E} = {56291265-B7BF-4756-92AB-FC30F09381D1} - {37FC77EA-AC44-4D08-B002-8EFF415C424A} = {64B2A28F-6D82-4F2B-B0BB-88DE5216DD2C} - {87D58D50-20D1-4091-88C5-8D88DCCC2DE3} = {6126DCE4-9692-4EE2-B240-C65743572995} - {F7BCD3AD-31E2-4223-B215-851C3D0AB78A} = {B55A5DE1-5AF3-4B18-AF04-C1735B071DA6} - {055F86AA-FB37-40CC-B39E-C29CE7547BB7} = {B8825E86-B8EA-4666-B681-C443D027C95D} - {137AD17B-066F-4ED4-80FA-8D21C7B76CA6} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} - {CAEB7F57-28A8-451C-95D0-45FCAA3C726C} = {C445B129-0A4D-41F5-8347-6534B6B12303} - {A939893A-B3CD-48F6-80D3-340C8A6E275B} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} - {F232B503-D412-45EE-8B31-EFD46B9FA302} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} - {B0A8E5D4-BC5A-448E-B222-431B6B2EB58E} = {05A169C7-4F20-4516-B10A-B13C5649D346} - {15D08EA7-8C63-45FB-8B4D-C5F8E43B433E} = {05A169C7-4F20-4516-B10A-B13C5649D346} - {433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995} - {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995} - {6DEC24A8-A166-432F-8E3B-58FFCDA92F52} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} - {6469F11E-8CEE-4292-820B-324DFFC88EBC} = {0F39820F-F4A5-41C6-9809-D79B68F032EF} - {8D2CC6ED-5105-4F52-8757-C21F4DE78589} = {6469F11E-8CEE-4292-820B-324DFFC88EBC} - {9DC6B242-457B-4767-A84B-C3D23B76C642} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} - {D53F0EF7-0CDC-49B4-AA2D-229901B0A734} = {9DC6B242-457B-4767-A84B-C3D23B76C642} - {5B5F86CC-3598-463C-9F9B-F78FBB6642F4} = {8275510E-0E6C-45A8-99DF-4F106BC7F075} - {274100A5-5B2D-4EA2-AC42-A62257FC6BDC} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} - {4D8DE54A-4F32-4881-B07B-DDC79619E573} = {274100A5-5B2D-4EA2-AC42-A62257FC6BDC} - {9536C284-65B4-4884-BB50-06D629095C3E} = {274100A5-5B2D-4EA2-AC42-A62257FC6BDC} - {6A19D94D-2BC6-4198-BE2E-342688FDBA4B} = {A1B75FC7-A777-4412-A635-D0C9ED8FE7A0} - {D8F7091E-A2D1-4E81-BA7C-97EAE392D683} = {A1B75FC7-A777-4412-A635-D0C9ED8FE7A0} - {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1} - {B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} - {B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7} = {B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC} - {C3928C15-1836-46DB-A09D-9EFBCCA33E08} = {B5D98AEB-9409-4280-8225-9C1EC6A791B2} - {2B858B82-5F0B-4A24-B3C0-5E99149F70D6} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} - {2AAE7819-BC3E-48F4-9CFA-5DD4CD5FFD62} = {2B858B82-5F0B-4A24-B3C0-5E99149F70D6} + {96EC4DD3-028E-6E27-5B14-08C21B07CE89} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} + {1BBD75D2-429D-D565-A98E-36437448E8C0} = {96EC4DD3-028E-6E27-5B14-08C21B07CE89} + {C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2} = {1BBD75D2-429D-D565-A98E-36437448E8C0} + {4C4590E4-54F5-3693-9633-60A8DCA89385} = {96EC4DD3-028E-6E27-5B14-08C21B07CE89} + {51BE4B37-D772-4BFA-8DAE-CF18730BEB89} = {4C4590E4-54F5-3693-9633-60A8DCA89385} + {10F96346-4215-4642-B837-0B5B08AC27AE} = {4C4590E4-54F5-3693-9633-60A8DCA89385} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 26dc86e0e31c..69b6ce66a6c3 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -223,6 +223,7 @@ and are generated based on the last package release. + diff --git a/eng/Versions.props b/eng/Versions.props index ed5451e43aa8..dfa71c0b14ec 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -326,6 +326,7 @@ 2.7.27 5.0.0 6.6.2 + 2.2.0 19.14.0 2.0.3 1.15.0 diff --git a/src/OpenApi/OpenApi.slnf b/src/OpenApi/OpenApi.slnf index 3d616bfa4daf..87b2bee86a31 100644 --- a/src/OpenApi/OpenApi.slnf +++ b/src/OpenApi/OpenApi.slnf @@ -10,10 +10,12 @@ "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", + "src\\OpenApi\\gen\\Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj", "src\\OpenApi\\perf\\Microbenchmarks\\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj", "src\\OpenApi\\sample\\Sample.csproj", "src\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj", "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", + "src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests\\Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj", "src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests\\Microsoft.AspNetCore.OpenApi.Tests.csproj", "src\\OpenApi\\sample\\Sample.csproj", "src\\OpenApi\\perf\\Microbenchmarks\\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj" diff --git a/src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets b/src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets new file mode 100644 index 000000000000..c046f1ca157e --- /dev/null +++ b/src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets @@ -0,0 +1,21 @@ + + + + $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated + + $(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.OpenApi.Generated + + + + + + + + diff --git a/src/OpenApi/gen/Helpers/AddOpenApiInvocation.cs b/src/OpenApi/gen/Helpers/AddOpenApiInvocation.cs new file mode 100644 index 000000000000..464d4bd17a96 --- /dev/null +++ b/src/OpenApi/gen/Helpers/AddOpenApiInvocation.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.AspNetCore.OpenApi.SourceGenerators; + +/// +/// Represents an invocation of the AddOpenApi method. +/// +/// The variant of the AddOpenApi method. +/// The invocation expression. +/// The location of the invocation that can be intercepted. +internal sealed record AddOpenApiInvocation( + AddOpenApiOverloadVariant Variant, + InvocationExpressionSyntax InvocationExpression, + InterceptableLocation? Location); diff --git a/src/OpenApi/gen/Helpers/AddOpenApiInvocationComparer.cs b/src/OpenApi/gen/Helpers/AddOpenApiInvocationComparer.cs new file mode 100644 index 000000000000..98589347a5be --- /dev/null +++ b/src/OpenApi/gen/Helpers/AddOpenApiInvocationComparer.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.OpenApi.SourceGenerators; + +internal class AddOpenApiInvocationComparer : IEqualityComparer +{ + public static AddOpenApiInvocationComparer Instance { get; } = new(); + public bool Equals(AddOpenApiInvocation? x, AddOpenApiInvocation? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + if (x is null || y is null) + { + return false; + } + + return x.Variant.Equals(y.Variant); + } + + public int GetHashCode(AddOpenApiInvocation obj) => + HashCode.Combine(obj.Variant); +} diff --git a/src/OpenApi/gen/Helpers/AddOpenApiOverloadVariant.cs b/src/OpenApi/gen/Helpers/AddOpenApiOverloadVariant.cs new file mode 100644 index 000000000000..b0f7d154c47d --- /dev/null +++ b/src/OpenApi/gen/Helpers/AddOpenApiOverloadVariant.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OpenApi.SourceGenerators; + +internal enum AddOpenApiOverloadVariant +{ + AddOpenApi, + AddOpenApiDocumentName, + AddOpenApiDocumentNameConfigureOptions, + AddOpenApiConfigureOptions, +} diff --git a/src/OpenApi/gen/Helpers/AssemblyTypeSymbolsVisitor.cs b/src/OpenApi/gen/Helpers/AssemblyTypeSymbolsVisitor.cs new file mode 100644 index 000000000000..5f962a0c48fd --- /dev/null +++ b/src/OpenApi/gen/Helpers/AssemblyTypeSymbolsVisitor.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.OpenApi.SourceGenerators; + +/// +/// Visits the assembly symbols to collect public types, properties, and methods that might +/// contain XML documentation comments. +/// +internal sealed class AssemblyTypeSymbolsVisitor(IAssemblySymbol assemblySymbol, CancellationToken cancellation) : SymbolVisitor +{ + private readonly CancellationToken _cancellationToken = cancellation; + private readonly HashSet _exportedTypes = new(SymbolEqualityComparer.Default); + private readonly HashSet _exportedProperties = new(SymbolEqualityComparer.Default); + private readonly HashSet _exportedMethods = new(SymbolEqualityComparer.Default); + + public ImmutableArray GetPublicTypes() => [.. _exportedTypes]; + public ImmutableArray GetPublicProperties() => [.. _exportedProperties]; + public ImmutableArray GetPublicMethods() => [.. _exportedMethods]; + + public void VisitAssembly() => VisitAssembly(assemblySymbol); + + public override void VisitAssembly(IAssemblySymbol symbol) + { + _cancellationToken.ThrowIfCancellationRequested(); + symbol.GlobalNamespace.Accept(this); + } + + public override void VisitNamespace(INamespaceSymbol symbol) + { + foreach (var namespaceOrType in symbol.GetMembers()) + { + _cancellationToken.ThrowIfCancellationRequested(); + namespaceOrType.Accept(this); + } + } + + public override void VisitNamedType(INamedTypeSymbol type) + { + _cancellationToken.ThrowIfCancellationRequested(); + + if (!IsDeclaredInAssembly(type) || !_exportedTypes.Add(type)) + { + return; + } + + var nestedTypes = type.GetTypeMembers(); + + foreach (var nestedType in nestedTypes) + { + _cancellationToken.ThrowIfCancellationRequested(); + nestedType.Accept(this); + } + + var properties = type.GetMembers().OfType(); + foreach (var property in properties) + { + _cancellationToken.ThrowIfCancellationRequested(); + if (IsDeclaredInAssembly(property) && _exportedProperties.Add(property)) + { + property.Type.Accept(this); + } + } + var methods = type.GetMembers().OfType(); + foreach (var method in methods) + { + _cancellationToken.ThrowIfCancellationRequested(); + if (IsDeclaredInAssembly(method) && _exportedMethods.Add(method)) + { + method.Accept(this); + } + } + } + + private bool IsDeclaredInAssembly(ISymbol symbol) => + SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, assemblySymbol); +} diff --git a/src/OpenApi/gen/Helpers/DocumentationCommentXmlNames.cs b/src/OpenApi/gen/Helpers/DocumentationCommentXmlNames.cs new file mode 100644 index 000000000000..d9c2fb98d2e3 --- /dev/null +++ b/src/OpenApi/gen/Helpers/DocumentationCommentXmlNames.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml; + +internal static class DocumentationCommentXmlNames +{ + public const string CElementName = "c"; + public const string CodeElementName = "code"; + public const string CompletionListElementName = "completionlist"; + public const string DescriptionElementName = "description"; + public const string ExampleElementName = "example"; + public const string ExceptionElementName = "exception"; + public const string IncludeElementName = "include"; + public const string InheritdocElementName = "inheritdoc"; + public const string ItemElementName = "item"; + public const string ListElementName = "list"; + public const string ListHeaderElementName = "listheader"; + public const string ParaElementName = "para"; + public const string ParameterElementName = "param"; + public const string ParameterReferenceElementName = "paramref"; + public const string PermissionElementName = "permission"; + public const string PlaceholderElementName = "placeholder"; + public const string PreliminaryElementName = "preliminary"; + public const string RemarksElementName = "remarks"; + public const string ReturnsElementName = "returns"; + public const string SeeElementName = "see"; + public const string SeeAlsoElementName = "seealso"; + public const string SummaryElementName = "summary"; + public const string TermElementName = "term"; + public const string ThreadSafetyElementName = "threadsafety"; + public const string TypeParameterElementName = "typeparam"; + public const string TypeParameterReferenceElementName = "typeparamref"; + public const string ValueElementName = "value"; + public const string CrefAttributeName = "cref"; + public const string HrefAttributeName = "href"; + public const string FileAttributeName = "file"; + public const string InstanceAttributeName = "instance"; + public const string LangwordAttributeName = "langword"; + public const string NameAttributeName = "name"; + public const string PathAttributeName = "path"; + public const string StaticAttributeName = "static"; + public const string TypeAttributeName = "type"; + + public static bool ElementEquals(string name1, string name2, bool fromVb = false) + { + return string.Equals(name1, name2, fromVb ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + } + + public static bool AttributeEquals(string name1, string name2) + { + return string.Equals(name1, name2, StringComparison.Ordinal); + } + + public static new bool Equals(object left, object right) + { + return object.Equals(left, right); + } +} diff --git a/src/OpenApi/gen/Helpers/ISymbolExtensions.cs b/src/OpenApi/gen/Helpers/ISymbolExtensions.cs new file mode 100644 index 000000000000..176bfad73faf --- /dev/null +++ b/src/OpenApi/gen/Helpers/ISymbolExtensions.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml; + +internal static class ISymbolExtensions +{ + public static ImmutableArray ExplicitOrImplicitInterfaceImplementations(this ISymbol symbol) + { + if (symbol.Kind is not SymbolKind.Method and not SymbolKind.Property and not SymbolKind.Event) + { + return []; + } + + var result = ImmutableArray.CreateBuilder(); + + foreach (var iface in symbol.ContainingType.AllInterfaces) + { + foreach (var interfaceMember in iface.GetMembers()) + { + var impl = symbol.ContainingType.FindImplementationForInterfaceMember(interfaceMember); + if (SymbolEqualityComparer.Default.Equals(symbol, impl)) + { + result.Add(interfaceMember); + } + } + } + + // There are explicit methods that FindImplementationForInterfaceMember. For example `abstract explicit impls` + // like `abstract void I.M()`. So add these back in directly using symbol.ExplicitInterfaceImplementations. + result.AddRange(symbol.ExplicitInterfaceImplementations()); + + return result.ToImmutable(); + } + + public static ImmutableArray ExplicitInterfaceImplementations(this ISymbol symbol) + => symbol switch + { + IEventSymbol @event => ImmutableArray.CastUp(@event.ExplicitInterfaceImplementations), + IMethodSymbol method => ImmutableArray.CastUp(method.ExplicitInterfaceImplementations), + IPropertySymbol property => ImmutableArray.CastUp(property.ExplicitInterfaceImplementations), + _ => [], + }; + + public static ImmutableArray GetAllTypeParameters(this ISymbol? symbol) + { + var results = ImmutableArray.CreateBuilder(); + + while (symbol != null) + { + results.AddRange(symbol.GetTypeParameters()); + symbol = symbol.ContainingType; + } + + return results.ToImmutable(); + } + + public static ImmutableArray GetTypeParameters(this ISymbol? symbol) + => symbol switch + { + IMethodSymbol m => m.TypeParameters, + INamedTypeSymbol nt => nt.TypeParameters, + _ => [], + }; + + public static ImmutableArray GetAllTypeArguments(this ISymbol symbol) + { + var results = ImmutableArray.CreateBuilder(); + results.AddRange(symbol.GetTypeArguments()); + + var containingType = symbol.ContainingType; + while (containingType != null) + { + results.AddRange(containingType.GetTypeArguments()); + containingType = containingType.ContainingType; + } + + return results.ToImmutable(); + } + + public static ImmutableArray GetTypeArguments(this ISymbol? symbol) + => symbol switch + { + IMethodSymbol m => m.TypeArguments, + INamedTypeSymbol nt => nt.TypeArguments, + _ => [], + }; + + public static ISymbol? GetOverriddenMember(this ISymbol? symbol, bool allowLooseMatch = false) + { + if (symbol is null) + { + return null; + } + + ISymbol? exactMatch = symbol switch + { + IMethodSymbol method => method.OverriddenMethod, + IPropertySymbol property => property.OverriddenProperty, + IEventSymbol @event => @event.OverriddenEvent, + _ => null, + }; + + if (exactMatch != null) + { + return exactMatch; + } + + if (allowLooseMatch && + (symbol.IsVirtual || symbol.IsAbstract || symbol.IsOverride)) + { + foreach (var baseType in symbol.ContainingType.GetBaseTypes()) + { + if (TryFindLooseMatch(symbol, baseType, out var looseMatch)) + { + return looseMatch; + } + } + } + + return null; + + static bool TryFindLooseMatch(ISymbol symbol, INamedTypeSymbol baseType, [NotNullWhen(true)] out ISymbol? looseMatch) + { + IMethodSymbol? bestMethod = null; + var parameterCount = symbol.GetParameters().Length; + + foreach (var member in baseType.GetMembers(symbol.Name)) + { + if (member.Kind != symbol.Kind) + { + continue; + } + + if (!member.IsOverridable()) + { + continue; + } + + if (symbol.Kind is SymbolKind.Event or SymbolKind.Property) + { + // We've found a matching event/property in the base type (perhaps differing by return type). This + // is a good enough match to return as a loose match for the starting symbol. + looseMatch = member; + return true; + } + else if (member is IMethodSymbol method) + { + // Prefer methods that are closed in parameter count to the original method we started with. + if (bestMethod is null || Math.Abs(method.Parameters.Length - parameterCount) < Math.Abs(bestMethod.Parameters.Length - parameterCount)) + { + bestMethod = method; + } + } + } + + looseMatch = bestMethod; + return looseMatch != null; + } + } + + public static bool IsOverridable([NotNullWhen(true)] this ISymbol? symbol) + { + // Members can only have overrides if they are virtual, abstract or override and is not sealed. + return symbol is { ContainingType.TypeKind: TypeKind.Class, IsSealed: false } && + (symbol.IsVirtual || symbol.IsAbstract || symbol.IsOverride); + } + + public static ImmutableArray GetParameters(this ISymbol? symbol) + => symbol switch + { + IMethodSymbol m => m.Parameters, + IPropertySymbol nt => nt.Parameters, + _ => [], + }; + + public static IEnumerable GetBaseTypes(this ITypeSymbol? type) + { + var current = type?.BaseType; + while (current != null) + { + yield return current; + current = current.BaseType; + } + } +} diff --git a/src/OpenApi/gen/Helpers/StringExtensions.cs b/src/OpenApi/gen/Helpers/StringExtensions.cs new file mode 100644 index 000000000000..ee4a81284295 --- /dev/null +++ b/src/OpenApi/gen/Helpers/StringExtensions.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml; + +/// +/// Extension methods for string manipulation. +/// +public static class StringExtensions +{ + /// + /// Trims whitespace from each line of text while preserving relative indentation. + /// + /// The text to trim. + /// Optional indentation to apply. + /// The trimmed text with preserved indentation structure. + public static string TrimEachLine(this string text, string indent = "") + { + var minLeadingWhitespace = int.MaxValue; + var lines = text.ReadLines().ToList(); + foreach (var line in lines) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var leadingWhitespace = 0; + while (leadingWhitespace < line.Length && char.IsWhiteSpace(line[leadingWhitespace])) + { + leadingWhitespace++; + } + + minLeadingWhitespace = Math.Min(minLeadingWhitespace, leadingWhitespace); + } + + var builder = new StringBuilder(); + + // Trim leading empty lines + var trimStart = true; + + // Apply indentation to all lines except the first, + // since the first new line in
 is significant
+        var firstLine = true;
+
+        foreach (var line in lines)
+        {
+            if (trimStart && string.IsNullOrWhiteSpace(line))
+            {
+                continue;
+            }
+
+            if (firstLine)
+            {
+                firstLine = false;
+            }
+            else
+            {
+                builder.Append(indent);
+            }
+
+            if (string.IsNullOrWhiteSpace(line))
+            {
+                builder.AppendLine();
+                continue;
+            }
+
+            trimStart = false;
+            builder.AppendLine(line.Substring(minLeadingWhitespace));
+        }
+
+        return builder.ToString().TrimEnd();
+    }
+
+    public static IEnumerable ReadLines(this string text)
+    {
+        string line;
+        using var sr = new StringReader(text);
+        while ((line = sr.ReadLine()) != null)
+        {
+            yield return line;
+        }
+    }
+}
diff --git a/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj b/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj
new file mode 100644
index 000000000000..77f31b901e50
--- /dev/null
+++ b/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj
@@ -0,0 +1,31 @@
+
+
+  
+    netstandard2.0
+    true
+    true
+    true
+    false
+    enable
+    true
+    false
+  
+
+  
+    
+    
+  
+
+  
+    
+    
+    
+    
+    
+  
+
+  
+    
+  
+
+
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
new file mode 100644
index 000000000000..b297f5809f60
--- /dev/null
+++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
@@ -0,0 +1,392 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
+
+public sealed partial class XmlCommentGenerator : IIncrementalGenerator
+{
+    public static string GeneratedCodeConstructor => $@"System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(XmlCommentGenerator).Assembly.FullName}"", ""{typeof(XmlCommentGenerator).Assembly.GetName().Version}"")";
+    public static string GeneratedCodeAttribute => $"[{GeneratedCodeConstructor}]";
+
+    internal static string GenerateXmlCommentSupportSource(string commentsFromXmlFile, string? commentsFromCompilation, ImmutableArray<(AddOpenApiInvocation Source, int Index, ImmutableArray Elements)> groupedAddOpenApiInvocations) => $$"""
+//------------------------------------------------------------------------------
+// 
+//     This code was generated by a tool.
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// 
+//------------------------------------------------------------------------------
+#nullable enable
+
+namespace System.Runtime.CompilerServices
+{
+    {{GeneratedCodeAttribute}}
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+    file sealed class InterceptsLocationAttribute : System.Attribute
+    {
+        public InterceptsLocationAttribute(int version, string data)
+        {
+        }
+    }
+}
+
+namespace Microsoft.AspNetCore.OpenApi.Generated
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text.Json;
+    using System.Text.Json.Nodes;
+    using System.Threading;
+    using System.Threading.Tasks;
+    using Microsoft.AspNetCore.OpenApi;
+    using Microsoft.AspNetCore.Mvc.Controllers;
+    using Microsoft.Extensions.DependencyInjection;
+    using Microsoft.OpenApi.Models;
+    using Microsoft.OpenApi.Any;
+
+    {{GeneratedCodeAttribute}}
+    file record XmlComment(
+        string? Summary,
+        string? Description,
+        string? Remarks,
+        string? Returns,
+        string? Value,
+        bool Deprecated,
+        List? Examples,
+        List? Parameters,
+        List? Responses);
+
+    {{GeneratedCodeAttribute}}
+    file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+    {{GeneratedCodeAttribute}}
+    file record XmlResponseComment(string Code, string? Description, string? Example);
+
+    {{GeneratedCodeAttribute}}
+    file static class XmlCommentCache
+    {
+        private static Dictionary<(Type?, string?), XmlComment>? _cache;
+        public static Dictionary<(Type?, string?), XmlComment> Cache
+        {
+            get
+            {
+                if (_cache is null)
+                {
+                    _cache = GenerateCacheEntries();
+                }
+                return _cache;
+            }
+        }
+
+        private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
+        {
+            var _cache = new Dictionary<(Type?, string?), XmlComment>();
+{{commentsFromXmlFile}}
+{{commentsFromCompilation}}
+            return _cache;
+        }
+    }
+
+    {{GeneratedCodeAttribute}}
+    file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+    {
+        public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+        {
+            var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+                ? controllerActionDescriptor.MethodInfo
+                : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+            if (methodInfo is null)
+            {
+                return Task.CompletedTask;
+            }
+            if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
+            {
+                if (methodComment.Summary is not null)
+                {
+                    operation.Summary = methodComment.Summary;
+                }
+                if (methodComment.Description is not null)
+                {
+                    operation.Description = methodComment.Description;
+                }
+                if (methodComment.Remarks is not null)
+                {
+                    operation.Description = methodComment.Remarks;
+                }
+                if (methodComment.Parameters is { Count: > 0})
+                {
+                    foreach (var parameterComment in methodComment.Parameters)
+                    {
+                        var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+                        var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+                        if (operationParameter is not null)
+                        {
+                            operationParameter.Description = parameterComment.Description;
+                            if (parameterComment.Example is { } jsonString)
+                            {
+                                operationParameter.Example = jsonString.Parse();
+                            }
+                            operationParameter.Deprecated = parameterComment.Deprecated;
+                        }
+                        else
+                        {
+                            var requestBody = operation.RequestBody;
+                            if (requestBody is not null)
+                            {
+                                requestBody.Description = parameterComment.Description;
+                                if (parameterComment.Example is { } jsonString)
+                                {
+                                    foreach (var mediaType in requestBody.Content.Values)
+                                    {
+                                        mediaType.Example = jsonString.Parse();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+                {
+                    foreach (var response in operation.Responses)
+                    {
+                        var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+                        if (responseComment is not null)
+                        {
+                            response.Value.Description = responseComment.Description;
+                        }
+
+                    }
+                }
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+
+    {{GeneratedCodeAttribute}}
+    file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+    {
+        public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+        {
+            if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+            {
+                if (XmlCommentCache.Cache.TryGetValue((propertyInfo.DeclaringType, propertyInfo.Name), out var propertyComment))
+                {
+                    schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+                    if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+                    {
+                        schema.Example = jsonString.Parse();
+                    }
+                }
+            }
+            if (XmlCommentCache.Cache.TryGetValue((context.JsonTypeInfo.Type, null), out var typeComment))
+            {
+                schema.Description = typeComment.Summary;
+                if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+                {
+                    schema.Example = jsonString.Parse();
+                }
+            }
+            return Task.CompletedTask;
+        }
+    }
+
+    file static class JsonNodeExtensions
+    {
+        public static JsonNode? Parse(this string? json)
+        {
+            if (json is null)
+            {
+                return null;
+            }
+
+            try
+            {
+                return JsonNode.Parse(json);
+            }
+            catch (JsonException)
+            {
+                try
+                {
+                    // If parsing fails, try wrapping in quotes to make it a valid JSON string
+                    return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+                }
+                catch (JsonException)
+                {
+                    return null;
+                }
+            }
+        }
+    }
+
+    {{GeneratedCodeAttribute}}
+    file static class GeneratedServiceCollectionExtensions
+    {
+{{GenerateAddOpenApiInterceptions(groupedAddOpenApiInvocations)}}
+    }
+}
+""";
+
+    internal static string GetAddOpenApiInterceptor(AddOpenApiOverloadVariant overloadVariant) => overloadVariant switch
+    {
+        AddOpenApiOverloadVariant.AddOpenApi => """
+        public static IServiceCollection AddOpenApi(this IServiceCollection services)
+                {
+                    return services.AddOpenApi("v1", options =>
+                    {
+                        options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                        options.AddOperationTransformer(new XmlCommentOperationTransformer());
+                    });
+                }
+        """,
+        AddOpenApiOverloadVariant.AddOpenApiDocumentName => """
+        public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName)
+                {
+                    return services.AddOpenApi(documentName, options =>
+                    {
+                        options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                        options.AddOperationTransformer(new XmlCommentOperationTransformer());
+                    });
+                }
+        """,
+        AddOpenApiOverloadVariant.AddOpenApiConfigureOptions => """
+        public static IServiceCollection AddOpenApi(this IServiceCollection services, Action configureOptions)
+                {
+                    return services.AddOpenApi("v1", options =>
+                    {
+                        configureOptions(options);
+                        options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                        options.AddOperationTransformer(new XmlCommentOperationTransformer());
+                    });
+                }
+        """,
+        AddOpenApiOverloadVariant.AddOpenApiDocumentNameConfigureOptions => """
+        public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName, Action configureOptions)
+                {
+                    // This overload is not intercepted.
+                    return OpenApiServiceCollectionExtensions.AddOpenApi(services, documentName, options =>
+                    {
+                        configureOptions(options);
+                        options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                        options.AddOperationTransformer(new XmlCommentOperationTransformer());
+                    });
+                }
+        """,
+        _ => throw new InvalidOperationException("Invalid overload variant for `AddOpenApi`.")
+    };
+
+    internal static string GenerateAddOpenApiInterceptions(ImmutableArray<(AddOpenApiInvocation Source, int Index, ImmutableArray Elements)> groupedAddOpenApiInvocations)
+    {
+        var writer = new StringWriter();
+        var codeWriter = new CodeWriter(writer, baseIndent: 2);
+        foreach (var (source, _, locations) in groupedAddOpenApiInvocations)
+        {
+            foreach (var location in locations)
+            {
+                if (location is not null)
+                {
+                    codeWriter.WriteLine(location.GetInterceptsLocationAttributeSyntax());
+                }
+            }
+            codeWriter.WriteLine(GetAddOpenApiInterceptor(source.Variant));
+        }
+        return writer.ToString();
+    }
+
+    internal static string EmitCommentsCache(IEnumerable<(string, string?, XmlComment?)> comments, CancellationToken cancellationToken)
+    {
+        var writer = new StringWriter();
+        var codeWriter = new CodeWriter(writer, baseIndent: 3);
+        foreach (var (type, member, comment) in comments)
+        {
+            if (comment is not null)
+            {
+                var typeKey = $"(typeof({type})";
+                var memberKey = member is not null ? $"{SymbolDisplay.FormatLiteral(member, true)}" : "null";
+                codeWriter.WriteLine($"_cache.Add({typeKey}, {memberKey}), {EmitSourceGeneratedXmlComment(comment)});");
+            }
+        }
+        return writer.ToString();
+    }
+
+    internal static string EmitSourceGeneratedXmlComment(XmlComment comment)
+    {
+        var writer = new StringWriter();
+        var codeWriter = new CodeWriter(writer, baseIndent: 0);
+        codeWriter.Write($"new XmlComment(");
+        codeWriter.Write(comment.Summary is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Summary, true)}\"\", ");
+        codeWriter.Write(comment.Description is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Description, true)}\"\", ");
+        codeWriter.Write(comment.Remarks is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Remarks, true)}\"\", ");
+        codeWriter.Write(comment.Returns is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Returns, true)}\"\", ");
+        codeWriter.Write(comment.Value is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Value, true)}\"\", ");
+        codeWriter.Write(comment.Deprecated is null ? "false," : $"{SymbolDisplay.FormatPrimitive(comment.Deprecated == true ? "true" : "false", false, false)}, ");
+        if (comment.Examples is null)
+        {
+            codeWriter.Write("null, ");
+        }
+        else
+        {
+            codeWriter.Write("new List");
+            codeWriter.Write("{");
+            foreach (var example in comment.Examples)
+            {
+                codeWriter.Write($"@\"{example}\", ");
+            }
+            codeWriter.Write("}, ");
+        }
+        if (comment.Parameters is null)
+        {
+            codeWriter.Write("null, ");
+        }
+        else
+        {
+            codeWriter.Write("new List");
+            codeWriter.Write("{");
+            foreach (var parameter in comment.Parameters)
+            {
+                var exampleLiteral = string.IsNullOrEmpty(parameter.Example)
+                    ? "null"
+                    : $"\"\"\"{parameter.Example!}\"\"\"";
+                codeWriter.Write($"new XmlParameterComment(@\"{parameter.Name}\", @\"{parameter.Description}\", {exampleLiteral}, {(parameter.Deprecated == true ? "true" : "false")}), ");
+            }
+            codeWriter.Write("} ,");
+        }
+        if (comment.Responses is null)
+        {
+            codeWriter.Write("null");
+        }
+        else
+        {
+            codeWriter.Write("new List");
+            codeWriter.Write("{");
+            foreach (var response in comment.Responses)
+            {
+                codeWriter.Write($"new XmlResponseComment(@\"{response.Code}\", @\"{response.Description}\", @\"{response.Example}\"), ");
+            }
+            codeWriter.Write("}");
+        }
+        codeWriter.Write(")");
+        return writer.ToString();
+    }
+
+    internal static void Emit(SourceProductionContext context,
+        string commentsFromXmlFile,
+        string commentsFromCompilation,
+        ImmutableArray<(AddOpenApiInvocation Source, int Index, ImmutableArray Elements)> groupedAddOpenApiInvocations)
+    {
+        context.AddSource("OpenApiXmlCommentSupport.generated.cs", GenerateXmlCommentSupportSource(commentsFromXmlFile, commentsFromCompilation, groupedAddOpenApiInvocations));
+    }
+}
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
new file mode 100644
index 000000000000..ff674eb26698
--- /dev/null
+++ b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
@@ -0,0 +1,172 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Text;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
+
+public sealed partial class XmlCommentGenerator
+{
+    private static readonly SymbolDisplayFormat _typeKeyFormat = new(
+        globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
+        typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+        genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters);
+
+    internal static List<(string, string)> ParseXmlFile(AdditionalText additionalText, CancellationToken cancellationToken)
+    {
+        var text = additionalText.GetText(cancellationToken);
+        if (text is null)
+        {
+            return [];
+        }
+        var xml = XDocument.Parse(text.ToString());
+        var members = xml.Descendants("member");
+        var comments = new List<(string, string)>();
+        foreach (var member in members)
+        {
+            var name = member.Attribute("name")?.Value;
+            if (name is not null)
+            {
+                comments.Add((name, member.ToString()));
+            }
+        }
+        return comments;
+    }
+
+    internal static List<(string, string)> ParseCompilation(Compilation compilation, CancellationToken cancellationToken)
+    {
+        var visitor = new AssemblyTypeSymbolsVisitor(compilation.Assembly, cancellationToken);
+        visitor.VisitAssembly();
+        var types = visitor.GetPublicTypes();
+        var comments = new List<(string, string)>();
+        foreach (var type in types)
+        {
+            if (DocumentationCommentId.CreateDeclarationId(type) is string name &&
+                type.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
+            {
+                comments.Add((name, xml));
+            }
+        }
+        var properties = visitor.GetPublicProperties();
+        foreach (var property in properties)
+        {
+            if (DocumentationCommentId.CreateDeclarationId(property) is string name &&
+                property.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
+            {
+                comments.Add((name, xml));
+            }
+        }
+        var methods = visitor.GetPublicMethods();
+        foreach (var method in methods)
+        {
+            // If the method is a constructor for a record, skip it because we will have already processed the type.
+            if (method.MethodKind == MethodKind.Constructor)
+            {
+                continue;
+            }
+            if (DocumentationCommentId.CreateDeclarationId(method) is string name &&
+                method.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
+            {
+                comments.Add((name, xml));
+            }
+        }
+        return comments;
+    }
+
+    // Type names are used in a `typeof()` expression, so we need to replace generic arguments
+    // with empty strings to avoid compiler errors.
+    private static string ReplaceGenericArguments(string typeName)
+    {
+        var stack = new Stack();
+        var result = new StringBuilder(typeName);
+
+        for (var i = 0; i < result.Length; i++)
+        {
+            if (result[i] == '<')
+            {
+                stack.Push(i);
+            }
+            else if (result[i] == '>' && stack.Count > 0)
+            {
+                var start = stack.Pop();
+                // Replace everything between < and > with empty strings separated by commas
+                var segment = result.ToString(start + 1, i - start - 1);
+                var commaCount = segment.Count(c => c == ',');
+                var replacement = string.Join(",", Enumerable.Repeat("", commaCount + 1));
+                result.Remove(start + 1, i - start - 1);
+                result.Insert(start + 1, replacement);
+                i = start + replacement.Length + 1;
+            }
+        }
+
+        return result.ToString();
+    }
+
+    internal static IEnumerable<(string, string?, XmlComment?)> ParseComments(
+        (List<(string, string)> RawComments, Compilation Compilation) input,
+        CancellationToken cancellationToken)
+    {
+        var compilation = input.Compilation;
+        var comments = new List<(string, string?, XmlComment?)>();
+        foreach (var (name, value) in input.RawComments)
+        {
+            if (DocumentationCommentId.GetFirstSymbolForDeclarationId(name, compilation) is ISymbol symbol)
+            {
+                var parsedComment = XmlComment.Parse(symbol, compilation, value, cancellationToken);
+                if (parsedComment is not null)
+                {
+                    var typeInfo = symbol is IPropertySymbol or IMethodSymbol
+                    ? ReplaceGenericArguments(symbol.ContainingType.OriginalDefinition.ToDisplayString(_typeKeyFormat))
+                    : ReplaceGenericArguments(symbol.OriginalDefinition.ToDisplayString(_typeKeyFormat));
+                    var propertyInfo = symbol is IPropertySymbol or IMethodSymbol
+                        ? symbol.Name
+                        : null;
+                    comments.Add((typeInfo, propertyInfo, parsedComment));
+                }
+            }
+        }
+        return comments;
+    }
+
+    internal static bool FilterInvocations(SyntaxNode node, CancellationToken _)
+        => node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: "AddOpenApi" } };
+
+    internal static AddOpenApiInvocation GetAddOpenApiOverloadVariant(GeneratorSyntaxContext context, CancellationToken cancellationToken)
+    {
+        var invocationExpression = (InvocationExpressionSyntax)context.Node;
+        var interceptableLocation = context.SemanticModel.GetInterceptableLocation(invocationExpression, cancellationToken);
+        var argumentsCount = invocationExpression.ArgumentList.Arguments.Count;
+        if (argumentsCount == 0)
+        {
+            return new(AddOpenApiOverloadVariant.AddOpenApi, invocationExpression, interceptableLocation);
+        }
+        else if (argumentsCount == 2)
+        {
+            return new(AddOpenApiOverloadVariant.AddOpenApiDocumentNameConfigureOptions, invocationExpression, interceptableLocation);
+        }
+        else
+        {
+            // We need to disambiguate between the two overloads that take a string and a delegate
+            // AddOpenApi("v1") vs. AddOpenApi(options => { }). The implementation here is pretty naive and
+            // won't handle cases where the document name is provided by a variable or a method call.
+            var argument = invocationExpression.ArgumentList.Arguments[0];
+            if (argument.Expression is LiteralExpressionSyntax)
+            {
+                return new(AddOpenApiOverloadVariant.AddOpenApiDocumentName, invocationExpression, interceptableLocation);
+            }
+            else
+            {
+                return new(AddOpenApiOverloadVariant.AddOpenApiConfigureOptions, invocationExpression, interceptableLocation);
+            }
+        }
+    }
+}
diff --git a/src/OpenApi/gen/XmlCommentGenerator.cs b/src/OpenApi/gen/XmlCommentGenerator.cs
new file mode 100644
index 000000000000..25f716493b83
--- /dev/null
+++ b/src/OpenApi/gen/XmlCommentGenerator.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
+
+[Generator]
+public sealed partial class XmlCommentGenerator : IIncrementalGenerator
+{
+    public void Initialize(IncrementalGeneratorInitializationContext context)
+    {
+        // Pull out XML comments from referenced assemblies passed in as AdditionalFiles.
+        var commentsFromXmlFile = context.AdditionalTextsProvider
+            .Where(file => file.Path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
+            .Select(ParseXmlFile);
+        // Pull out XML comments from the target assembly using the information produced
+        // by Roslyn into the compilation.
+        var commentsFromTargetAssembly = context.CompilationProvider
+            .Select(ParseCompilation);
+        // Map string XML comments to structured data from both the AdditionalFiles
+        // and the target assembly.
+        var parsedCommentsFromXmlFile = commentsFromXmlFile
+            .Combine(context.CompilationProvider)
+            .Select(ParseComments);
+        var parsedCommentsFromCompilation = commentsFromTargetAssembly
+            .Combine(context.CompilationProvider)
+            .Select(ParseComments);
+        // Discover AddOpenApi invocations so that we can intercept them with an implicit
+        // registration of the transformers for mapping XML doc comments to the OpenAPI file.
+        var groupedAddOpenApiInvocations = context.SyntaxProvider
+            .CreateSyntaxProvider(FilterInvocations, GetAddOpenApiOverloadVariant)
+            .GroupWith((variantDetails) => variantDetails.Location, AddOpenApiInvocationComparer.Instance)
+            .Collect();
+
+        var generatedCommentsFromXmlFile = parsedCommentsFromXmlFile
+            .Select(EmitCommentsCache);
+        var generatedCommentsFromCompilation = parsedCommentsFromCompilation
+            .Select(EmitCommentsCache);
+
+        var result = generatedCommentsFromXmlFile.Collect()
+            .Combine(generatedCommentsFromCompilation)
+            .Combine(groupedAddOpenApiInvocations);
+
+        context.RegisterSourceOutput(result, (context, output) =>
+        {
+            var groupedAddOpenApiInvocations = output.Right;
+            var (generatedCommentsFromXmlFile, generatedCommentsFromCompilation) = output.Left;
+            var compiledXmlFileComments = !generatedCommentsFromXmlFile.IsDefaultOrEmpty
+                ? string.Join("\n", generatedCommentsFromXmlFile)
+                : string.Empty;
+            Emit(context, compiledXmlFileComments, generatedCommentsFromCompilation, groupedAddOpenApiInvocations);
+        });
+    }
+}
diff --git a/src/OpenApi/gen/XmlComments/XmlComment.cs b/src/OpenApi/gen/XmlComments/XmlComment.cs
new file mode 100644
index 000000000000..321a23bf032b
--- /dev/null
+++ b/src/OpenApi/gen/XmlComments/XmlComment.cs
@@ -0,0 +1,550 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Microsoft.AspNetCore.Analyzers.Infrastructure;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+
+internal sealed class XmlComment
+{
+    private const string IdSelector = @"((?![0-9])[\w_])+[\w\(\)\.\{\}\[\]\|\*\^~#@!`,_<>:]*";
+    private static readonly Regex CommentIdRegex = new(@"^(?N|T|M|P|F|E|Overload):(?" + IdSelector + ")$", RegexOptions.Compiled);
+    public string? Summary { get; internal set; }
+    public string? Description { get; internal set; }
+    public string? Value { get; internal set; }
+    public string? Remarks { get; internal set; }
+    public string? Returns { get; internal set; }
+    public bool? Deprecated { get; internal set; }
+    public List? Examples { get; internal set; }
+    public List Parameters { get; internal set; } = [];
+    public List Responses { get; internal set; } = [];
+
+    private XmlComment(string xml)
+    {
+        // Treat  as 
+        if (xml.StartsWith("", StringComparison.InvariantCulture) && xml.EndsWith("", StringComparison.InvariantCulture))
+        {
+            xml = xml.Substring(5, xml.Length - 11);
+            xml = xml.Trim();
+        }
+
+        // Transform triple slash comment
+        var doc = XDocument.Parse(xml, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+        ResolveCrefLink(doc, "//seealso[@cref]");
+        ResolveCrefLink(doc, "//see[@cref]");
+
+        var nav = doc.CreateNavigator();
+        Summary = GetSingleNodeValue(nav, "/member/summary");
+        Description = GetSingleNodeValue(nav, "/member/description");
+        Remarks = GetSingleNodeValue(nav, "/member/remarks");
+        Returns = GetSingleNodeValue(nav, "/member/returns");
+        Value = GetSingleNodeValue(nav, "/member/value");
+        Deprecated = GetSingleNodeValue(nav, "/member/deprecated") == "true";
+
+        Examples = [.. GetMultipleExampleNodes(nav, "/member/example")];
+        Parameters = XmlParameterComment.GetXmlParameterListComment(nav, "/member/param");
+        Responses = XmlResponseComment.GetXmlResponseCommentList(nav, "/member/response");
+    }
+
+    public static XmlComment? Parse(ISymbol symbol, Compilation compilation, string xmlText, CancellationToken cancellationToken)
+    {
+        // Avoid processing empty or malformed XML comments.
+        if (string.IsNullOrEmpty(xmlText) ||
+            xmlText.StartsWith("
     true
     true
+    true
+    $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated
+    true
   
 
   
@@ -31,4 +34,9 @@
     
   
 
+  
+  
+    
+  
+
 
diff --git a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
index a0ef68ed3021..b8c1d9f36336 100644
--- a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
+++ b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
@@ -46,4 +46,8 @@
     
   
 
+  
+    
+  
+
 
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs
new file mode 100644
index 000000000000..a620627d5de2
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs
@@ -0,0 +1,120 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Text.Json;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.AspNetCore.OpenApi.Build.Tests;
+
+public class GenerateAdditionalXmlFilesForOpenApiTests
+{
+    private static readonly TimeSpan _defaultProcessTimeout = TimeSpan.FromSeconds(45);
+
+    [Fact]
+    public void VerifiesTargetGeneratesXmlFiles()
+    {
+        var projectFile = CreateTestProject();
+        var startInfo = new ProcessStartInfo
+        {
+            FileName = DotNetMuxer.MuxerPathOrDefault(),
+            Arguments = $"build -t:Build -getItem:AdditionalFiles",
+            WorkingDirectory = Path.GetDirectoryName(projectFile),
+            RedirectStandardOutput = true,
+            RedirectStandardError = true,
+            UseShellExecute = false
+        };
+
+        using var process = Process.Start(startInfo);
+        process.WaitForExit(_defaultProcessTimeout);
+        Assert.Equal(0, process.ExitCode);
+
+        var output = process.StandardOutput.ReadToEnd();
+        var result = JsonSerializer.Deserialize(output);
+        var additionalFiles = result.Items.AdditionalFiles;
+        Assert.NotEmpty(additionalFiles);
+
+        // Captures ProjectReferences and PackageReferences in project.
+        var identities = additionalFiles.Select(x => x["Identity"]).ToArray();
+        Assert.Collection(identities,
+            x => Assert.EndsWith("ClassLibrary.xml", x),
+            x => Assert.EndsWith("Microsoft.AspNetCore.OpenApi.xml", x),
+            x => Assert.EndsWith("Microsoft.OpenApi.xml", x)
+        );
+    }
+
+    private static string CreateTestProject()
+    {
+        var classLibTempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+        Directory.CreateDirectory(classLibTempPath);
+
+        // Create a class library project
+        var classLibProjectPath = Path.Combine(classLibTempPath, "ClassLibrary.csproj");
+        var classLibProjectContent = """
+
+  
+    net10.0
+    true
+  
+
+
+""";
+        File.WriteAllText(classLibProjectPath, classLibProjectContent);
+
+        // Create a class library source file
+        var classLibSourcePath = Path.Combine(classLibTempPath, "Class1.cs");
+        var classLibSourceContent = """
+/// 
+/// This is a class
+/// 
+public class Class1
+{
+}
+""";
+        File.WriteAllText(classLibSourcePath, classLibSourceContent);
+
+        var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+        Directory.CreateDirectory(tempPath);
+
+        // Copy the targets file to the temp directory
+        var sourceTargetsPath = Path.Combine(AppContext.BaseDirectory, "Microsoft.AspNetCore.OpenApi.targets");
+        var targetTargetsPath = Path.Combine(tempPath, "Microsoft.AspNetCore.OpenApi.targets");
+        File.Copy(sourceTargetsPath, targetTargetsPath);
+
+        var projectPath = Path.Combine(tempPath, "TestProject.csproj");
+        var projectContent = $$"""
+
+    
+
+  
+    net10.0
+    Exe
+  
+
+  
+    
+    
+  
+
+""";
+        File.WriteAllText(projectPath, projectContent);
+
+        // Create a test source file
+        var sourcePath = Path.Combine(tempPath, "Program.cs");
+        var sourceContent = """
+using Microsoft.AspNetCore.Builder;
+
+var app = WebApplication.Create(args);
+
+app.MapGet("/", () => "Hello World!");
+
+app.Run();
+""";
+        File.WriteAllText(sourcePath, sourceContent);
+
+        return projectPath;
+    }
+
+    private record ItemsResult(AdditionalFilesResult Items);
+    private record AdditionalFilesResult(Dictionary[] AdditionalFiles);
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/Microsoft.AspNetCore.OpenApi.Build.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/Microsoft.AspNetCore.OpenApi.Build.Tests.csproj
new file mode 100644
index 000000000000..4aa6ddf207cf
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/Microsoft.AspNetCore.OpenApi.Build.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+  
+    $(DefaultNetCoreTargetFramework)
+    enable
+  
+
+  
+    
+  
+
+  
+    
+  
+
+  
+    
+  
+
+
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/AddOpenApiTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/AddOpenApiTests.cs
new file mode 100644
index 000000000000..1f9ae3511dd9
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/AddOpenApiTests.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+[UsesVerify]
+public class AddOpenApiTests
+{
+    [Fact]
+    public async Task CanInterceptAddOpenApi()
+    {
+        var source = """
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder();
+
+// No parameters
+builder.Services.AddOpenApi();
+// Name parameter
+builder.Services.AddOpenApi("v2");
+// Configure options parameter
+builder.Services.AddOpenApi(options =>
+{
+    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0;
+});
+// Name and configure options parameters
+builder.Services.AddOpenApi("v2", options =>
+{
+    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0;
+});
+
+var app = builder.Build();
+
+app.MapPost("", () => "Hello world!");
+
+app.Run();
+""";
+
+        var generator = new XmlCommentGenerator();
+        await SnapshotTestHelper.Verify(source, generator, out var compilation);
+        Assert.Empty(compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Warning));
+    }
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
new file mode 100644
index 000000000000..1bad34016d51
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
@@ -0,0 +1,363 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Reflection;
+
+namespace Microsoft.Extensions.Hosting;
+
+internal sealed class HostFactoryResolver
+{
+    private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
+
+    public const string BuildWebHost = nameof(BuildWebHost);
+    public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder);
+    public const string CreateHostBuilder = nameof(CreateHostBuilder);
+    private const string TimeoutEnvironmentKey = "DOTNET_HOST_FACTORY_RESOLVER_DEFAULT_TIMEOUT_IN_SECONDS";
+
+    // The amount of time we wait for the diagnostic source events to fire
+    private static readonly TimeSpan s_defaultWaitTimeout = SetupDefaultTimeout();
+
+    private static TimeSpan SetupDefaultTimeout()
+    {
+        if (Debugger.IsAttached)
+        {
+            return Timeout.InfiniteTimeSpan;
+        }
+
+        if (uint.TryParse(Environment.GetEnvironmentVariable(TimeoutEnvironmentKey), out uint timeoutInSeconds))
+        {
+            return TimeSpan.FromSeconds((int)timeoutInSeconds);
+        }
+
+        return TimeSpan.FromMinutes(5);
+    }
+
+    public static Func ResolveWebHostFactory(Assembly assembly)
+    {
+        return ResolveFactory(assembly, BuildWebHost);
+    }
+
+    public static Func ResolveWebHostBuilderFactory(Assembly assembly)
+    {
+        return ResolveFactory(assembly, CreateWebHostBuilder);
+    }
+
+    public static Func ResolveHostBuilderFactory(Assembly assembly)
+    {
+        return ResolveFactory(assembly, CreateHostBuilder);
+    }
+
+    // This helpers encapsulates all of the complex logic required to:
+    // 1. Execute the entry point of the specified assembly in a different thread.
+    // 2. Wait for the diagnostic source events to fire
+    // 3. Give the caller a chance to execute logic to mutate the IHostBuilder
+    // 4. Resolve the instance of the applications's IHost
+    // 5. Allow the caller to determine if the entry point has completed
+    public static Func ResolveHostFactory(Assembly assembly,
+                                                             TimeSpan waitTimeout = default,
+                                                             bool stopApplication = true,
+                                                             Action configureHostBuilder = null,
+                                                             Action entrypointCompleted = null)
+    {
+        if (assembly.EntryPoint is null)
+        {
+            return null;
+        }
+
+        try
+        {
+            // Attempt to load hosting and check the version to make sure the events
+            // even have a chance of firing (they were added in .NET >= 6)
+            var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting");
+            if (hostingAssembly.GetName().Version is Version version && version.Major < 6)
+            {
+                return null;
+            }
+
+            // We're using a version >= 6 so the events can fire. If they don't fire
+            // then it's because the application isn't using the hosting APIs
+        }
+        catch
+        {
+            // There was an error loading the extensions assembly, return null.
+            return null;
+        }
+
+        return args => new HostingListener(args, assembly.EntryPoint, waitTimeout == default ? s_defaultWaitTimeout : waitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost();
+    }
+
+    private static Func ResolveFactory(Assembly assembly, string name)
+    {
+        var programType = assembly.EntryPoint.DeclaringType;
+        if (programType == null)
+        {
+            return null;
+        }
+
+        var factory = programType.GetMethod(name, DeclaredOnlyLookup);
+        if (!IsFactory(factory))
+        {
+            return null;
+        }
+
+        return args => (T)factory!.Invoke(null, [args])!;
+    }
+
+    // TReturn Factory(string[] args);
+    private static bool IsFactory(MethodInfo factory)
+    {
+        return factory != null
+            && typeof(TReturn).IsAssignableFrom(factory.ReturnType)
+            && factory.GetParameters().Length == 1
+            && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType);
+    }
+
+    // Used by EF tooling without any Hosting references. Looses some return type safety checks.
+    public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan waitTimeout = default)
+    {
+        // Prefer the older patterns by default for back compat.
+        var webHostFactory = ResolveWebHostFactory(assembly);
+        if (webHostFactory != null)
+        {
+            return args =>
+            {
+                var webHost = webHostFactory(args);
+                return GetServiceProvider(webHost);
+            };
+        }
+
+        var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly);
+        if (webHostBuilderFactory != null)
+        {
+            return args =>
+            {
+                var webHostBuilder = webHostBuilderFactory(args);
+                var webHost = Build(webHostBuilder);
+                return GetServiceProvider(webHost);
+            };
+        }
+
+        var hostBuilderFactory = ResolveHostBuilderFactory(assembly);
+        if (hostBuilderFactory != null)
+        {
+            return args =>
+            {
+                var hostBuilder = hostBuilderFactory(args);
+                var host = Build(hostBuilder);
+                return GetServiceProvider(host);
+            };
+        }
+
+        var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout);
+        if (hostFactory != null)
+        {
+            return args =>
+            {
+                static bool IsApplicationNameArg(string arg)
+                    => arg.Equals("--applicationName", StringComparison.OrdinalIgnoreCase) ||
+                        arg.Equals("/applicationName", StringComparison.OrdinalIgnoreCase);
+
+                if (!args.Any(arg => IsApplicationNameArg(arg)) && assembly.GetName().Name is string assemblyName)
+                {
+                    args = [.. args, .. new[] { "--applicationName", assemblyName }];
+                }
+
+                var host = hostFactory(args);
+                return GetServiceProvider(host);
+            };
+        }
+
+        return null;
+    }
+
+    private static object Build(object builder)
+    {
+        var buildMethod = builder.GetType().GetMethod("Build");
+        return buildMethod.Invoke(builder, []);
+    }
+
+    private static IServiceProvider GetServiceProvider(object host)
+    {
+        if (host == null)
+        {
+            return null;
+        }
+        var hostType = host.GetType();
+        var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup);
+        return (IServiceProvider)servicesProperty.GetValue(host);
+    }
+
+    private sealed class HostingListener : IObserver, IObserver>
+    {
+        private readonly string[] _args;
+        private readonly MethodInfo _entryPoint;
+        private readonly TimeSpan _waitTimeout;
+        private readonly bool _stopApplication;
+
+        private readonly TaskCompletionSource _hostTcs = new();
+        private IDisposable _disposable;
+        private readonly Action _configure;
+        private readonly Action _entrypointCompleted;
+        private static readonly AsyncLocal _currentListener = new();
+
+        public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entrypointCompleted)
+        {
+            _args = args;
+            _entryPoint = entryPoint;
+            _waitTimeout = waitTimeout;
+            _stopApplication = stopApplication;
+            _configure = configure;
+            _entrypointCompleted = entrypointCompleted;
+        }
+
+        public object CreateHost()
+        {
+            using var subscription = DiagnosticListener.AllListeners.Subscribe(this);
+
+            // Kick off the entry point on a new thread so we don't block the current one
+            // in case we need to timeout the execution
+            var thread = new Thread(() =>
+            {
+                Exception exception = null;
+
+                try
+                {
+                    // Set the async local to the instance of the HostingListener so we can filter events that
+                    // aren't scoped to this execution of the entry point.
+                    _currentListener.Value = this;
+
+                    var parameters = _entryPoint.GetParameters();
+                    if (parameters.Length == 0)
+                    {
+                        _entryPoint.Invoke(null, []);
+                    }
+                    else
+                    {
+                        _entryPoint.Invoke(null, new object[] { _args });
+                    }
+
+                    // Try to set an exception if the entry point returns gracefully, this will force
+                    // build to throw
+                    _hostTcs.TrySetException(new InvalidOperationException("The entry point exited without ever building an IHost."));
+                }
+                catch (TargetInvocationException tie) when (tie.InnerException.GetType().Name == "HostAbortedException")
+                {
+                    // The host was stopped by our own logic
+                }
+                catch (TargetInvocationException tie)
+                {
+                    exception = tie.InnerException ?? tie;
+
+                    // Another exception happened, propagate that to the caller
+                    _hostTcs.TrySetException(exception);
+                }
+                catch (Exception ex)
+                {
+                    exception = ex;
+
+                    // Another exception happened, propagate that to the caller
+                    _hostTcs.TrySetException(ex);
+                }
+                finally
+                {
+                    // Signal that the entry point is completed
+                    _entrypointCompleted.Invoke(exception);
+                }
+            })
+            {
+                // Make sure this doesn't hang the process
+                IsBackground = true
+            };
+
+            // Start the thread
+            thread.Start();
+
+            try
+            {
+                // Wait before throwing an exception
+                if (!_hostTcs.Task.Wait(_waitTimeout))
+                {
+                    throw new InvalidOperationException($"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable.");
+                }
+            }
+            catch (AggregateException) when (_hostTcs.Task.IsCompleted)
+            {
+                // Lets this propagate out of the call to GetAwaiter().GetResult()
+            }
+
+            Debug.Assert(_hostTcs.Task.IsCompleted);
+
+            return _hostTcs.Task.GetAwaiter().GetResult();
+        }
+
+        public void OnCompleted()
+        {
+            _disposable.Dispose();
+        }
+
+        public void OnError(Exception error)
+        {
+
+        }
+
+        public void OnNext(DiagnosticListener value)
+        {
+            if (_currentListener.Value != this)
+            {
+                // Ignore events that aren't for this listener
+                return;
+            }
+
+            if (value.Name == "Microsoft.Extensions.Hosting")
+            {
+                _disposable = value.Subscribe(this);
+            }
+        }
+
+        public void OnNext(KeyValuePair value)
+        {
+            if (_currentListener.Value != this)
+            {
+                // Ignore events that aren't for this listener
+                return;
+            }
+
+            if (value.Key == "HostBuilding")
+            {
+                _configure.Invoke(value.Value!);
+            }
+
+            if (value.Key == "HostBuilt")
+            {
+                _hostTcs.TrySetResult(value.Value!);
+
+                if (_stopApplication)
+                {
+                    // Stop the host from running further
+                    ThrowHostAborted();
+                }
+            }
+        }
+
+        // HostFactoryResolver is used by tools that explicitly don't want to reference Microsoft.Extensions.Hosting assemblies.
+        // So don't depend on the public HostAbortedException directly. Instead, load the exception type dynamically if it can
+        // be found. If it can't (possibly because the app is using an older version), throw a private exception with the same name.
+        private static void ThrowHostAborted()
+        {
+            var publicHostAbortedExceptionType = Type.GetType("Microsoft.Extensions.Hosting.HostAbortedException, Microsoft.Extensions.Hosting.Abstractions", throwOnError: false);
+            if (publicHostAbortedExceptionType != null)
+            {
+                throw (Exception)Activator.CreateInstance(publicHostAbortedExceptionType)!;
+            }
+            else
+            {
+                throw new HostAbortedException();
+            }
+        }
+
+        private sealed class HostAbortedException : Exception
+        {
+        }
+    }
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
new file mode 100644
index 000000000000..59c7e4b13556
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
@@ -0,0 +1,168 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Loader;
+using System.Text;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Emit;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+[UsesVerify]
+public static class SnapshotTestHelper
+{
+    private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview)
+        .WithFeatures([new KeyValuePair("InterceptorsNamespaces", "Microsoft.AspNetCore.OpenApi.Generated")]);
+
+    public static Task Verify(string source, IIncrementalGenerator generator, out Compilation compilation)
+    {
+        var references = AppDomain.CurrentDomain.GetAssemblies()
+                .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location))
+                .Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
+                .Concat(
+                [
+                    MetadataReference.CreateFromFile(typeof(Builder.WebApplicationBuilder).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(OpenApiOptions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Builder.EndpointRouteBuilderExtensions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Builder.IApplicationBuilder).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Mvc.ApiExplorer.IApiDescriptionProvider).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Mvc.ControllerBase).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(MvcServiceCollectionExtensions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(MvcCoreMvcBuilderExtensions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Http.TypedResults).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(System.Text.Json.Nodes.JsonArray).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location),
+                ]);
+        var inputCompilation = CSharpCompilation.Create("OpenApiXmlCommentGeneratorSample",
+            [CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: "Program.cs")],
+            references,
+            new CSharpCompilationOptions(OutputKind.ConsoleApplication));
+        var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions);
+        return Verifier
+            .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics))
+            .AutoVerify()
+            .UseDirectory("../snapshots");
+    }
+
+    public static async Task VerifyOpenApi(Compilation compilation, Action verifyFunc)
+    {
+        var assemblyName = compilation.AssemblyName;
+        var symbolsName = Path.ChangeExtension(assemblyName, "pdb");
+
+        var output = new MemoryStream();
+        var pdb = new MemoryStream();
+
+        var emitOptions = new EmitOptions(
+            debugInformationFormat: DebugInformationFormat.PortablePdb,
+            pdbFilePath: symbolsName,
+            outputNameOverride: $"TestProject-{Guid.NewGuid()}");
+
+        var embeddedTexts = new List();
+
+        foreach (var syntaxTree in compilation.SyntaxTrees)
+        {
+            var text = syntaxTree.GetText();
+            var encoding = text.Encoding ?? Encoding.UTF8;
+            var buffer = encoding.GetBytes(text.ToString());
+            var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true);
+
+            var syntaxRootNode = (CSharpSyntaxNode)syntaxTree.GetRoot();
+            var newSyntaxTree = CSharpSyntaxTree.Create(syntaxRootNode, options: ParseOptions, encoding: encoding, path: syntaxTree.FilePath);
+
+            compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree);
+
+            embeddedTexts.Add(EmbeddedText.FromSource(syntaxTree.FilePath, sourceText));
+        }
+
+        var result = compilation.Emit(output, pdb, options: emitOptions, embeddedTexts: embeddedTexts);
+
+        Assert.Empty(result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning));
+        Assert.True(result.Success);
+
+        output.Position = 0;
+        pdb.Position = 0;
+
+        var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb);
+
+        void ConfigureHostBuilder(object hostBuilder)
+        {
+            ((IHostBuilder)hostBuilder).ConfigureServices((context, services) =>
+            {
+                services.AddSingleton();
+                services.AddSingleton();
+            });
+        }
+
+        var waitForStartTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+        void OnEntryPointExit(Exception exception)
+        {
+            // If the entry point exited, we'll try to complete the wait
+            if (exception != null)
+            {
+                waitForStartTcs.TrySetException(exception);
+            }
+            else
+            {
+                waitForStartTcs.TrySetResult(0);
+            }
+        }
+
+        var factory = HostFactoryResolver.ResolveHostFactory(assembly,
+            stopApplication: false,
+            configureHostBuilder: ConfigureHostBuilder,
+            entrypointCompleted: OnEntryPointExit);
+
+        if (factory == null)
+        {
+            return;
+        }
+
+        var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services;
+
+        var applicationLifetime = services.GetRequiredService();
+        using (var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(0)))
+        {
+            waitForStartTcs.Task.Wait();
+            var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == "Microsoft.AspNetCore.OpenApi");
+            var serviceType = targetAssembly.GetType("Microsoft.Extensions.ApiDescriptions.IDocumentProvider", throwOnError: false);
+
+            if (serviceType == null)
+            {
+                return;
+            }
+
+            var service = services.GetService(serviceType) ?? throw new InvalidOperationException("Could not resolve IDocumntProvider service.");
+            using var stream = new MemoryStream();
+            var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+            using var writer = new StreamWriter(stream, encoding, bufferSize: 1024, leaveOpen: true);
+            var targetMethod = serviceType.GetMethod("GenerateAsync", [typeof(string), typeof(TextWriter)]) ?? throw new InvalidOperationException("Could not resolve GenerateAsync method.");
+            targetMethod.Invoke(service, ["v1", writer]);
+            stream.Position = 0;
+            var (document, _) = await OpenApiDocument.LoadAsync(stream, "json");
+            verifyFunc(document);
+        }
+    }
+
+    private sealed class NoopHostLifetime : IHostLifetime
+    {
+        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+    }
+
+    private sealed class NoopServer : IServer
+    {
+        public IFeatureCollection Features { get; } = new FeatureCollection();
+        public void Dispose() { }
+        public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext: notnull => Task.CompletedTask;
+        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+    }
+
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
new file mode 100644
index 000000000000..e80fc854c839
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+  
+    $(DefaultNetCoreTargetFramework)
+    enable
+    false
+    false
+    true
+  
+
+  
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+    
+  
+
+  
+    
+  
+
+  
+    
+  
+
+
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/ModuleInitializer.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/ModuleInitializer.cs
new file mode 100644
index 000000000000..bf5defe4b9fc
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/ModuleInitializer.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+public static class ModuleInitializer
+{
+    [ModuleInitializer]
+    public static void Init() =>
+        VerifySourceGenerators.Initialize();
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.Controllers.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.Controllers.cs
new file mode 100644
index 000000000000..4de1532d6930
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.Controllers.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+public partial class OperationTests
+{
+    [Fact]
+    public async Task SupportsXmlCommentsOnOperationsFromControllers()
+    {
+        var source = """
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services
+    .AddControllers()
+    .AddApplicationPart(typeof(TestController).Assembly)
+    .AddApplicationPart(typeof(Test2Controller).Assembly);
+builder.Services.AddOpenApi();
+
+var app = builder.Build();
+
+app.MapControllers();
+
+app.Run();
+
+[ApiController]
+[Route("[controller]")]
+public class TestController : ControllerBase
+{
+    /// 
+    /// A summary of the action.
+    /// 
+    /// 
+    /// A description of the action.
+    /// 
+    [HttpGet]
+    public string Get()
+    {
+        return "Hello, World!";
+    }
+}
+
+[ApiController]
+[Route("[controller]")]
+public class Test2Controller : ControllerBase
+{
+    /// The name of the person.
+    /// Returns the greeting.
+    [HttpGet]
+    public string Get(string name)
+    {
+        return $"Hello, {name}!";
+    }
+
+    /// The todo to insert into the database.
+    [HttpPost]
+    public string Post(Todo todo)
+    {
+        return $"Hello, {todo.Title}!";
+    }
+}
+
+public partial class Program {}
+
+public record Todo(int Id, string Title, bool Completed);
+""";
+        var generator = new XmlCommentGenerator();
+        await SnapshotTestHelper.Verify(source, generator, out var compilation);
+        await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
+        {
+            var path = document.Paths["/Test"].Operations[OperationType.Get];
+            Assert.Equal("A summary of the action.", path.Summary);
+            Assert.Equal("A description of the action.", path.Description);
+
+            var path2 = document.Paths["/Test2"].Operations[OperationType.Get];
+            Assert.Equal("The name of the person.", path2.Parameters[0].Description);
+            Assert.Equal("Returns the greeting.", path2.Responses["200"].Description);
+
+            var path3 = document.Paths["/Test2"].Operations[OperationType.Post];
+            Assert.Equal("The todo to insert into the database.", path3.RequestBody.Description);
+        });
+    }
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs
new file mode 100644
index 000000000000..76fd57471901
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json.Nodes;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+[UsesVerify]
+public partial class OperationTests
+{
+    [Fact]
+    public async Task SupportsXmlCommentsOnOperationsFromMinimalApis()
+    {
+        var source = """
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Http;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddOpenApi();
+
+var app = builder.Build();
+
+app.MapGet("/1", RouteHandlerExtensionMethods.Get);
+app.MapGet("/2", RouteHandlerExtensionMethods.Get2);
+app.MapGet("/3", RouteHandlerExtensionMethods.Get3);
+app.MapGet("/4", RouteHandlerExtensionMethods.Get4);
+app.MapGet("/5", RouteHandlerExtensionMethods.Get5);
+app.MapPost("/6", RouteHandlerExtensionMethods.Post6);
+app.MapPut("/7", RouteHandlerExtensionMethods.Put7);
+
+app.Run();
+
+public static class RouteHandlerExtensionMethods
+{
+    /// 
+    /// A summary of the action.
+    /// 
+    /// 
+    /// A description of the action.
+    /// 
+    public static string Get()
+    {
+        return "Hello, World!";
+    }
+
+    /// The name of the person.
+    /// Returns the greeting.
+    public static string Get2(string name)
+    {
+        return $"Hello, {name}!";
+    }
+
+    /// The name of the person.
+    public static string Get3(string name)
+    {
+        return $"Hello, {name}!";
+    }
+
+    /// Indicates that the value was not found.
+    public static NotFound Get4()
+    {
+        return TypedResults.NotFound("Not found!");
+    }
+
+    /// Indicates that the value is even.
+    /// Indicates that the value is less than 50.
+    /// Indicates that the value was not found.
+    public static Results, Ok, Created> Get5()
+    {
+        var randomNumber = Random.Shared.Next();
+        if (randomNumber % 2 == 0)
+        {
+            return TypedResults.Ok("is even");
+        }
+        else if (randomNumber < 50)
+        {
+            return TypedResults.Created("is less than 50");
+        }
+        return TypedResults.NotFound("Not found!");
+    }
+
+    /// 
+    /// Creates a new user.
+    /// 
+    /// 
+    /// Sample request:
+    ///     POST /6
+    ///     {
+    ///         "username": "johndoe",
+    ///         "email": "john@example.com"
+    ///     }
+    /// 
+    /// The user information.
+    /// Successfully created the user.
+    /// If the user data is invalid.
+    public static IResult Post6(User user)
+    {
+        return TypedResults.Created($"/users/{user.Username}", user);
+    }
+
+    /// 
+    /// Updates an existing record.
+    /// 
+    /// Legacy ID parameter - use uuid instead.
+    /// Unique identifier for the record.
+    /// Update successful.
+    /// Legacy response - will be removed.
+    public static IResult Put7(int? id, string uuid)
+    {
+        return TypedResults.NoContent();
+    }
+}
+
+public class User
+{
+    public string Username { get; set; } = string.Empty;
+    public string Email { get; set; } = string.Empty;
+}
+""";
+        var generator = new XmlCommentGenerator();
+        await SnapshotTestHelper.Verify(source, generator, out var compilation);
+        await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
+        {
+            var path = document.Paths["/1"].Operations[OperationType.Get];
+            Assert.Equal("A summary of the action.", path.Summary);
+            Assert.Equal("A description of the action.", path.Description);
+
+            var path2 = document.Paths["/2"].Operations[OperationType.Get];
+            Assert.Equal("The name of the person.", path2.Parameters[0].Description);
+            Assert.Equal("Returns the greeting.", path2.Responses["200"].Description);
+
+            var path3 = document.Paths["/3"].Operations[OperationType.Get];
+            Assert.Equal("The name of the person.", path3.Parameters[0].Description);
+            var example = Assert.IsAssignableFrom(path3.Parameters[0].Example);
+            Assert.Equal("\"Testy McTester\"", example.ToJsonString());
+
+            var path4 = document.Paths["/4"].Operations[OperationType.Get];
+            var response = path4.Responses["404"];
+            Assert.Equal("Indicates that the value was not found.", response.Description);
+
+            var path5 = document.Paths["/5"].Operations[OperationType.Get];
+            Assert.Equal("Indicates that the value was not found.", path5.Responses["404"].Description);
+            Assert.Equal("Indicates that the value is even.", path5.Responses["200"].Description);
+            Assert.Equal("Indicates that the value is less than 50.", path5.Responses["201"].Description);
+
+            var path6 = document.Paths["/6"].Operations[OperationType.Post];
+            Assert.Equal("Creates a new user.", path6.Summary);
+            Assert.Contains("Sample request:", path6.Description);
+            var userParam = path6.RequestBody.Content["application/json"];
+            var userExample = Assert.IsAssignableFrom(userParam.Example);
+            Assert.Equal("johndoe", userExample["username"].GetValue());
+
+            var path7 = document.Paths["/7"].Operations[OperationType.Put];
+            var idParam = path7.Parameters.First(p => p.Name == "id");
+            Assert.True(idParam.Deprecated);
+            Assert.Equal("Legacy ID parameter - use uuid instead.", idParam.Description);
+        });
+    }
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
new file mode 100644
index 000000000000..4fbf085c0589
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
@@ -0,0 +1,223 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+using System.Text.Json.Nodes;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+[UsesVerify]
+public class SchemaTests
+{
+    [Fact]
+    public async Task SupportsXmlCommentsOnSchemas()
+    {
+        var source = """
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddOpenApi();
+
+var app = builder.Build();
+
+app.MapPost("/todo", (Todo todo) => { });
+app.MapPost("/project", (Project project) => { });
+app.MapPost("/board", (ProjectBoard.BoardItem boardItem) => { });
+app.MapPost("/project-record", (ProjectRecord project) => { });
+app.MapPost("/todo-with-description", (TodoWithDescription todo) => { });
+app.MapPost("/type-with-examples", (TypeWithExamples typeWithExamples) => { });
+app.MapPost("/user", (User user) => { });
+
+app.Run();
+
+/// 
+/// This is a todo item.
+/// 
+public class Todo
+{
+    public int Id { get; set; }
+    public string Name { get; set; }
+    public string Description { get; set; }
+}
+
+/// 
+/// The project that contains  items.
+/// 
+public record Project(string Name, string Description);
+
+public class ProjectBoard
+{
+    /// 
+    /// An item on the board.
+    /// 
+    public class BoardItem
+    {
+        public string Name { get; set; }
+    }
+}
+
+/// 
+/// The project that contains  items.
+/// 
+/// The name of the project.
+/// The description of the project.
+public record ProjectRecord(string Name, string Description);
+
+public class TodoWithDescription
+{
+    /// 
+    /// The identifier of the todo.
+    /// 
+    public int Id { get; set; }
+    /// 
+    /// The name of the todo.
+    /// 
+    public string Name { get; set; }
+    /// 
+    /// A description of the the todo.
+    /// 
+    /// Another description of the todo.
+    public string Description { get; set; }
+}
+
+public class TypeWithExamples
+{
+    /// true
+    public bool BooleanType { get; set; }
+    /// 42
+    public int IntegerType { get; set; }
+    /// 1234567890123456789
+    public long LongType { get; set; }
+    /// 3.14
+    public double DoubleType { get; set; }
+    /// 3.14
+    public float FloatType { get; set; }
+    /// 2022-01-01T00:00:00Z
+    public DateTime DateTimeType { get; set; }
+    /// 2022-01-01
+    public DateOnly DateOnlyType { get; set; }
+    /// Hello, World!
+    public string StringType { get; set; }
+    /// 2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d
+    public Guid GuidType { get; set; }
+    /// 12:30:45
+    public TimeOnly TimeOnlyType { get; set; }
+    /// P3DT4H5M
+    public TimeSpan TimeSpanType { get; set; }
+    /// 255
+    public byte ByteType { get; set; }
+    /// 3.14159265359
+    public decimal DecimalType { get; set; }
+    /// https://example.com
+    public Uri UriType { get; set; }
+}
+
+public interface IUser
+{
+    /// 
+    /// The unique identifier for the user.
+    /// 
+    int Id { get; set; }
+
+    /// 
+    /// The user's display name.
+    /// 
+    string Name { get; set; }
+}
+
+/// 
+public class User : IUser
+{
+    /// 
+    public int Id { get; set; }
+
+    /// 
+    public string Name { get; set; }
+}
+""";
+        var generator = new XmlCommentGenerator();
+        await SnapshotTestHelper.Verify(source, generator, out var compilation);
+        await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
+        {
+            var path = document.Paths["/todo"].Operations[OperationType.Post];
+            var todo = path.RequestBody.Content["application/json"].Schema;
+            Assert.Equal("This is a todo item.", todo.Description);
+
+            path = document.Paths["/project"].Operations[OperationType.Post];
+            var project = path.RequestBody.Content["application/json"].Schema;
+            Assert.Equal("The project that contains Todo items.", project.Description);
+
+            path = document.Paths["/board"].Operations[OperationType.Post];
+            var board = path.RequestBody.Content["application/json"].Schema;
+            Assert.Equal("An item on the board.", board.Description);
+
+            path = document.Paths["/project-record"].Operations[OperationType.Post];
+            project = path.RequestBody.Content["application/json"].Schema;
+
+            Assert.Equal("The name of the project.", project.Properties["name"].Description);
+            Assert.Equal("The description of the project.", project.Properties["description"].Description);
+
+            path = document.Paths["/todo-with-description"].Operations[OperationType.Post];
+            todo = path.RequestBody.Content["application/json"].Schema;
+            Assert.Equal("The identifier of the todo.", todo.Properties["id"].Description);
+            Assert.Equal("The name of the todo.", todo.Properties["name"].Description);
+            Assert.Equal("Another description of the todo.", todo.Properties["description"].Description);
+
+            path = document.Paths["/type-with-examples"].Operations[OperationType.Post];
+            var typeWithExamples = path.RequestBody.Content["application/json"].Schema;
+
+            var booleanTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["booleanType"].Example);
+            Assert.True(booleanTypeExample.GetValue());
+
+            var integerTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["integerType"].Example);
+            Assert.Equal(42, integerTypeExample.GetValue());
+
+            var longTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["longType"].Example);
+            Assert.Equal(1234567890123456789, longTypeExample.GetValue());
+
+            // Broken due to https://github.com/microsoft/OpenAPI.NET/issues/2137
+            // var doubleTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["doubleType"].Example);
+            // Assert.Equal("3.14", doubleTypeExample.GetValue());
+
+            // var floatTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["floatType"].Example);
+            // Assert.Equal(3.14f, floatTypeExample.GetValue());
+
+            // var dateTimeTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["dateTimeType"].Example);
+            // Assert.Equal(DateTime.Parse("2022-01-01T00:00:00Z", CultureInfo.InvariantCulture), dateTimeTypeExample.GetValue());
+
+            // var dateOnlyTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["dateOnlyType"].Example);
+            // Assert.Equal(DateOnly.Parse("2022-01-01", CultureInfo.InvariantCulture), dateOnlyTypeExample.GetValue());
+
+            var stringTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["stringType"].Example);
+            Assert.Equal("Hello, World!", stringTypeExample.GetValue());
+
+            var guidTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["guidType"].Example);
+            Assert.Equal("2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d", guidTypeExample.GetValue());
+
+            var byteTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["byteType"].Example);
+            Assert.Equal(255, byteTypeExample.GetValue());
+
+            // Broken due to https://github.com/microsoft/OpenAPI.NET/issues/2137
+            // var timeOnlyTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["timeOnlyType"].Example);
+            // Assert.Equal(TimeOnly.Parse("12:30:45", CultureInfo.InvariantCulture), timeOnlyTypeExample.GetValue());
+
+            // var timeSpanTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["timeSpanType"].Example);
+            // Assert.Equal(TimeSpan.Parse("P3DT4H5M", CultureInfo.InvariantCulture), timeSpanTypeExample.GetValue());
+
+            // var decimalTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["decimalType"].Example);
+            // Assert.Equal(3.14159265359m, decimalTypeExample.GetValue());
+
+            var uriTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["uriType"].Example);
+            Assert.Equal("https://example.com", uriTypeExample.GetValue());
+
+            path = document.Paths["/user"].Operations[OperationType.Post];
+            var user = path.RequestBody.Content["application/json"].Schema;
+            Assert.Equal("The unique identifier for the user.", user.Properties["id"].Description);
+            Assert.Equal("The user's display name.", user.Properties["name"].Description);
+        });
+    }
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
new file mode 100644
index 000000000000..247fbb23330e
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
@@ -0,0 +1,260 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+// 
+//     This code was generated by a tool.
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// 
+//------------------------------------------------------------------------------
+#nullable enable
+
+namespace System.Runtime.CompilerServices
+{
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+    file sealed class InterceptsLocationAttribute : System.Attribute
+    {
+        public InterceptsLocationAttribute(int version, string data)
+        {
+        }
+    }
+}
+
+namespace Microsoft.AspNetCore.OpenApi.Generated
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text.Json;
+    using System.Text.Json.Nodes;
+    using System.Threading;
+    using System.Threading.Tasks;
+    using Microsoft.AspNetCore.OpenApi;
+    using Microsoft.AspNetCore.Mvc.Controllers;
+    using Microsoft.Extensions.DependencyInjection;
+    using Microsoft.OpenApi.Models;
+    using Microsoft.OpenApi.Any;
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlComment(
+        string? Summary,
+        string? Description,
+        string? Remarks,
+        string? Returns,
+        string? Value,
+        bool Deprecated,
+        List? Examples,
+        List? Parameters,
+        List? Responses);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlResponseComment(string Code, string? Description, string? Example);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class XmlCommentCache
+    {
+        private static Dictionary<(Type?, string?), XmlComment>? _cache;
+        public static Dictionary<(Type?, string?), XmlComment> Cache
+        {
+            get
+            {
+                if (_cache is null)
+                {
+                    _cache = GenerateCacheEntries();
+                }
+                return _cache;
+            }
+        }
+
+        private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
+        {
+            var _cache = new Dictionary<(Type?, string?), XmlComment>();
+
+
+            return _cache;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+    {
+        public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+        {
+            var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+                ? controllerActionDescriptor.MethodInfo
+                : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+            if (methodInfo is null)
+            {
+                return Task.CompletedTask;
+            }
+            if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
+            {
+                if (methodComment.Summary is not null)
+                {
+                    operation.Summary = methodComment.Summary;
+                }
+                if (methodComment.Description is not null)
+                {
+                    operation.Description = methodComment.Description;
+                }
+                if (methodComment.Remarks is not null)
+                {
+                    operation.Description = methodComment.Remarks;
+                }
+                if (methodComment.Parameters is { Count: > 0})
+                {
+                    foreach (var parameterComment in methodComment.Parameters)
+                    {
+                        var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+                        var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+                        if (operationParameter is not null)
+                        {
+                            operationParameter.Description = parameterComment.Description;
+                            if (parameterComment.Example is { } jsonString)
+                            {
+                                operationParameter.Example = jsonString.Parse();
+                            }
+                            operationParameter.Deprecated = parameterComment.Deprecated;
+                        }
+                        else
+                        {
+                            var requestBody = operation.RequestBody;
+                            if (requestBody is not null)
+                            {
+                                requestBody.Description = parameterComment.Description;
+                                if (parameterComment.Example is { } jsonString)
+                                {
+                                    foreach (var mediaType in requestBody.Content.Values)
+                                    {
+                                        mediaType.Example = jsonString.Parse();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+                {
+                    foreach (var response in operation.Responses)
+                    {
+                        var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+                        if (responseComment is not null)
+                        {
+                            response.Value.Description = responseComment.Description;
+                        }
+
+                    }
+                }
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+    {
+        public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+        {
+            if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+            {
+                if (XmlCommentCache.Cache.TryGetValue((propertyInfo.DeclaringType, propertyInfo.Name), out var propertyComment))
+                {
+                    schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+                    if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+                    {
+                        schema.Example = jsonString.Parse();
+                    }
+                }
+            }
+            if (XmlCommentCache.Cache.TryGetValue((context.JsonTypeInfo.Type, null), out var typeComment))
+            {
+                schema.Description = typeComment.Summary;
+                if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+                {
+                    schema.Example = jsonString.Parse();
+                }
+            }
+            return Task.CompletedTask;
+        }
+    }
+
+    file static class JsonNodeExtensions
+    {
+        public static JsonNode? Parse(this string? json)
+        {
+            if (json is null)
+            {
+                return null;
+            }
+
+            try
+            {
+                return JsonNode.Parse(json);
+            }
+            catch (JsonException)
+            {
+                try
+                {
+                    // If parsing fails, try wrapping in quotes to make it a valid JSON string
+                    return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+                }
+                catch (JsonException)
+                {
+                    return null;
+                }
+            }
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class GeneratedServiceCollectionExtensions
+    {
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "BrnVZrUWsBWGsQtqcWdZBKYAAABQcm9ncmFtLmNz")]
+        public static IServiceCollection AddOpenApi(this IServiceCollection services)
+        {
+            return services.AddOpenApi("v1", options =>
+            {
+                options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                options.AddOperationTransformer(new XmlCommentOperationTransformer());
+            });
+        }
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "BrnVZrUWsBWGsQtqcWdZBNcAAABQcm9ncmFtLmNz")]
+        public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName)
+        {
+            return services.AddOpenApi(documentName, options =>
+            {
+                options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                options.AddOperationTransformer(new XmlCommentOperationTransformer());
+            });
+        }
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "BrnVZrUWsBWGsQtqcWdZBBkBAABQcm9ncmFtLmNz")]
+        public static IServiceCollection AddOpenApi(this IServiceCollection services, Action configureOptions)
+        {
+            return services.AddOpenApi("v1", options =>
+            {
+                configureOptions(options);
+                options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                options.AddOperationTransformer(new XmlCommentOperationTransformer());
+            });
+        }
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "BrnVZrUWsBWGsQtqcWdZBL0BAABQcm9ncmFtLmNz")]
+        public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName, Action configureOptions)
+        {
+            // This overload is not intercepted.
+            return OpenApiServiceCollectionExtensions.AddOpenApi(services, documentName, options =>
+            {
+                configureOptions(options);
+                options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                options.AddOperationTransformer(new XmlCommentOperationTransformer());
+            });
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
new file mode 100644
index 000000000000..6dde49c49bf9
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
@@ -0,0 +1,233 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+// 
+//     This code was generated by a tool.
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// 
+//------------------------------------------------------------------------------
+#nullable enable
+
+namespace System.Runtime.CompilerServices
+{
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+    file sealed class InterceptsLocationAttribute : System.Attribute
+    {
+        public InterceptsLocationAttribute(int version, string data)
+        {
+        }
+    }
+}
+
+namespace Microsoft.AspNetCore.OpenApi.Generated
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text.Json;
+    using System.Text.Json.Nodes;
+    using System.Threading;
+    using System.Threading.Tasks;
+    using Microsoft.AspNetCore.OpenApi;
+    using Microsoft.AspNetCore.Mvc.Controllers;
+    using Microsoft.Extensions.DependencyInjection;
+    using Microsoft.OpenApi.Models;
+    using Microsoft.OpenApi.Any;
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlComment(
+        string? Summary,
+        string? Description,
+        string? Remarks,
+        string? Returns,
+        string? Value,
+        bool Deprecated,
+        List? Examples,
+        List? Parameters,
+        List? Responses);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlResponseComment(string Code, string? Description, string? Example);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class XmlCommentCache
+    {
+        private static Dictionary<(Type?, string?), XmlComment>? _cache;
+        public static Dictionary<(Type?, string?), XmlComment> Cache
+        {
+            get
+            {
+                if (_cache is null)
+                {
+                    _cache = GenerateCacheEntries();
+                }
+                return _cache;
+            }
+        }
+
+        private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
+        {
+            var _cache = new Dictionary<(Type?, string?), XmlComment>();
+
+            _cache.Add((typeof(global::TestController), "Get"), new XmlComment("""A summary of the action.""", """A description of the action.""", null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::Test2Controller), "Get"), new XmlComment(null,null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"name", @"The name of the person.", null, false), } ,new List{new XmlResponseComment(@"200", @"Returns the greeting.", @""), }));
+            _cache.Add((typeof(global::Test2Controller), "Post"), new XmlComment(null,null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"todo", @"The todo to insert into the database.", null, false), } ,new List{}));
+
+            return _cache;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+    {
+        public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+        {
+            var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+                ? controllerActionDescriptor.MethodInfo
+                : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+            if (methodInfo is null)
+            {
+                return Task.CompletedTask;
+            }
+            if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
+            {
+                if (methodComment.Summary is not null)
+                {
+                    operation.Summary = methodComment.Summary;
+                }
+                if (methodComment.Description is not null)
+                {
+                    operation.Description = methodComment.Description;
+                }
+                if (methodComment.Remarks is not null)
+                {
+                    operation.Description = methodComment.Remarks;
+                }
+                if (methodComment.Parameters is { Count: > 0})
+                {
+                    foreach (var parameterComment in methodComment.Parameters)
+                    {
+                        var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+                        var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+                        if (operationParameter is not null)
+                        {
+                            operationParameter.Description = parameterComment.Description;
+                            if (parameterComment.Example is { } jsonString)
+                            {
+                                operationParameter.Example = jsonString.Parse();
+                            }
+                            operationParameter.Deprecated = parameterComment.Deprecated;
+                        }
+                        else
+                        {
+                            var requestBody = operation.RequestBody;
+                            if (requestBody is not null)
+                            {
+                                requestBody.Description = parameterComment.Description;
+                                if (parameterComment.Example is { } jsonString)
+                                {
+                                    foreach (var mediaType in requestBody.Content.Values)
+                                    {
+                                        mediaType.Example = jsonString.Parse();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+                {
+                    foreach (var response in operation.Responses)
+                    {
+                        var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+                        if (responseComment is not null)
+                        {
+                            response.Value.Description = responseComment.Description;
+                        }
+
+                    }
+                }
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+    {
+        public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+        {
+            if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+            {
+                if (XmlCommentCache.Cache.TryGetValue((propertyInfo.DeclaringType, propertyInfo.Name), out var propertyComment))
+                {
+                    schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+                    if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+                    {
+                        schema.Example = jsonString.Parse();
+                    }
+                }
+            }
+            if (XmlCommentCache.Cache.TryGetValue((context.JsonTypeInfo.Type, null), out var typeComment))
+            {
+                schema.Description = typeComment.Summary;
+                if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+                {
+                    schema.Example = jsonString.Parse();
+                }
+            }
+            return Task.CompletedTask;
+        }
+    }
+
+    file static class JsonNodeExtensions
+    {
+        public static JsonNode? Parse(this string? json)
+        {
+            if (json is null)
+            {
+                return null;
+            }
+
+            try
+            {
+                return JsonNode.Parse(json);
+            }
+            catch (JsonException)
+            {
+                try
+                {
+                    // If parsing fails, try wrapping in quotes to make it a valid JSON string
+                    return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+                }
+                catch (JsonException)
+                {
+                    return null;
+                }
+            }
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class GeneratedServiceCollectionExtensions
+    {
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "djIPELhgcEDP7ZT+7F1K5lABAABQcm9ncmFtLmNz")]
+        public static IServiceCollection AddOpenApi(this IServiceCollection services)
+        {
+            return services.AddOpenApi("v1", options =>
+            {
+                options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                options.AddOperationTransformer(new XmlCommentOperationTransformer());
+            });
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
new file mode 100644
index 000000000000..0a54e1af25e0
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
@@ -0,0 +1,237 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+// 
+//     This code was generated by a tool.
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// 
+//------------------------------------------------------------------------------
+#nullable enable
+
+namespace System.Runtime.CompilerServices
+{
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+    file sealed class InterceptsLocationAttribute : System.Attribute
+    {
+        public InterceptsLocationAttribute(int version, string data)
+        {
+        }
+    }
+}
+
+namespace Microsoft.AspNetCore.OpenApi.Generated
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text.Json;
+    using System.Text.Json.Nodes;
+    using System.Threading;
+    using System.Threading.Tasks;
+    using Microsoft.AspNetCore.OpenApi;
+    using Microsoft.AspNetCore.Mvc.Controllers;
+    using Microsoft.Extensions.DependencyInjection;
+    using Microsoft.OpenApi.Models;
+    using Microsoft.OpenApi.Any;
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlComment(
+        string? Summary,
+        string? Description,
+        string? Remarks,
+        string? Returns,
+        string? Value,
+        bool Deprecated,
+        List? Examples,
+        List? Parameters,
+        List? Responses);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlResponseComment(string Code, string? Description, string? Example);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class XmlCommentCache
+    {
+        private static Dictionary<(Type?, string?), XmlComment>? _cache;
+        public static Dictionary<(Type?, string?), XmlComment> Cache
+        {
+            get
+            {
+                if (_cache is null)
+                {
+                    _cache = GenerateCacheEntries();
+                }
+                return _cache;
+            }
+        }
+
+        private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
+        {
+            var _cache = new Dictionary<(Type?, string?), XmlComment>();
+
+            _cache.Add((typeof(global::RouteHandlerExtensionMethods), "Get"), new XmlComment("""A summary of the action.""", """A description of the action.""", null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::RouteHandlerExtensionMethods), "Get2"), new XmlComment(null,null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"name", @"The name of the person.", null, false), } ,new List{new XmlResponseComment(@"200", @"Returns the greeting.", @""), }));
+            _cache.Add((typeof(global::RouteHandlerExtensionMethods), "Get3"), new XmlComment(null,null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"name", @"The name of the person.", """Testy McTester""", false), } ,new List{}));
+            _cache.Add((typeof(global::RouteHandlerExtensionMethods), "Get4"), new XmlComment(null,null,null,null,null,false, new List{}, new List{} ,new List{new XmlResponseComment(@"404", @"Indicates that the value was not found.", @""), }));
+            _cache.Add((typeof(global::RouteHandlerExtensionMethods), "Get5"), new XmlComment(null,null,null,null,null,false, new List{}, new List{} ,new List{new XmlResponseComment(@"200", @"Indicates that the value is even.", @""), new XmlResponseComment(@"201", @"Indicates that the value is less than 50.", @""), new XmlResponseComment(@"404", @"Indicates that the value was not found.", @""), }));
+            _cache.Add((typeof(global::RouteHandlerExtensionMethods), "Post6"), new XmlComment("""Creates a new user.""", null,"""Sample request:\n    POST /6\n    {\n        \"username\": \"johndoe\",\n        \"email\": \"john@example.com\"\n    }""", null,null,false, new List{}, new List{new XmlParameterComment(@"user", @"The user information.", """{"username": "johndoe", "email": "john@example.com"}""", false), } ,new List{new XmlResponseComment(@"201", @"Successfully created the user.", @""), new XmlResponseComment(@"400", @"If the user data is invalid.", @""), }));
+            _cache.Add((typeof(global::RouteHandlerExtensionMethods), "Put7"), new XmlComment("""Updates an existing record.""", null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"id", @"Legacy ID parameter - use uuid instead.", null, true), new XmlParameterComment(@"uuid", @"Unique identifier for the record.", null, false), } ,new List{new XmlResponseComment(@"204", @"Update successful.", @""), new XmlResponseComment(@"404", @"Legacy response - will be removed.", @""), }));
+
+            return _cache;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+    {
+        public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+        {
+            var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+                ? controllerActionDescriptor.MethodInfo
+                : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+            if (methodInfo is null)
+            {
+                return Task.CompletedTask;
+            }
+            if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
+            {
+                if (methodComment.Summary is not null)
+                {
+                    operation.Summary = methodComment.Summary;
+                }
+                if (methodComment.Description is not null)
+                {
+                    operation.Description = methodComment.Description;
+                }
+                if (methodComment.Remarks is not null)
+                {
+                    operation.Description = methodComment.Remarks;
+                }
+                if (methodComment.Parameters is { Count: > 0})
+                {
+                    foreach (var parameterComment in methodComment.Parameters)
+                    {
+                        var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+                        var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+                        if (operationParameter is not null)
+                        {
+                            operationParameter.Description = parameterComment.Description;
+                            if (parameterComment.Example is { } jsonString)
+                            {
+                                operationParameter.Example = jsonString.Parse();
+                            }
+                            operationParameter.Deprecated = parameterComment.Deprecated;
+                        }
+                        else
+                        {
+                            var requestBody = operation.RequestBody;
+                            if (requestBody is not null)
+                            {
+                                requestBody.Description = parameterComment.Description;
+                                if (parameterComment.Example is { } jsonString)
+                                {
+                                    foreach (var mediaType in requestBody.Content.Values)
+                                    {
+                                        mediaType.Example = jsonString.Parse();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+                {
+                    foreach (var response in operation.Responses)
+                    {
+                        var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+                        if (responseComment is not null)
+                        {
+                            response.Value.Description = responseComment.Description;
+                        }
+
+                    }
+                }
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+    {
+        public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+        {
+            if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+            {
+                if (XmlCommentCache.Cache.TryGetValue((propertyInfo.DeclaringType, propertyInfo.Name), out var propertyComment))
+                {
+                    schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+                    if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+                    {
+                        schema.Example = jsonString.Parse();
+                    }
+                }
+            }
+            if (XmlCommentCache.Cache.TryGetValue((context.JsonTypeInfo.Type, null), out var typeComment))
+            {
+                schema.Description = typeComment.Summary;
+                if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+                {
+                    schema.Example = jsonString.Parse();
+                }
+            }
+            return Task.CompletedTask;
+        }
+    }
+
+    file static class JsonNodeExtensions
+    {
+        public static JsonNode? Parse(this string? json)
+        {
+            if (json is null)
+            {
+                return null;
+            }
+
+            try
+            {
+                return JsonNode.Parse(json);
+            }
+            catch (JsonException)
+            {
+                try
+                {
+                    // If parsing fails, try wrapping in quotes to make it a valid JSON string
+                    return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+                }
+                catch (JsonException)
+                {
+                    return null;
+                }
+            }
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class GeneratedServiceCollectionExtensions
+    {
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "uRCoT8yxN4pg9NiDfOwm3/EAAABQcm9ncmFtLmNz")]
+        public static IServiceCollection AddOpenApi(this IServiceCollection services)
+        {
+            return services.AddOpenApi("v1", options =>
+            {
+                options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                options.AddOperationTransformer(new XmlCommentOperationTransformer());
+            });
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
new file mode 100644
index 000000000000..7b5c9924e5b0
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
@@ -0,0 +1,259 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+// 
+//     This code was generated by a tool.
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// 
+//------------------------------------------------------------------------------
+#nullable enable
+
+namespace System.Runtime.CompilerServices
+{
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+    file sealed class InterceptsLocationAttribute : System.Attribute
+    {
+        public InterceptsLocationAttribute(int version, string data)
+        {
+        }
+    }
+}
+
+namespace Microsoft.AspNetCore.OpenApi.Generated
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Reflection;
+    using System.Text.Json;
+    using System.Text.Json.Nodes;
+    using System.Threading;
+    using System.Threading.Tasks;
+    using Microsoft.AspNetCore.OpenApi;
+    using Microsoft.AspNetCore.Mvc.Controllers;
+    using Microsoft.Extensions.DependencyInjection;
+    using Microsoft.OpenApi.Models;
+    using Microsoft.OpenApi.Any;
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlComment(
+        string? Summary,
+        string? Description,
+        string? Remarks,
+        string? Returns,
+        string? Value,
+        bool Deprecated,
+        List? Examples,
+        List? Parameters,
+        List? Responses);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file record XmlResponseComment(string Code, string? Description, string? Example);
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class XmlCommentCache
+    {
+        private static Dictionary<(Type?, string?), XmlComment>? _cache;
+        public static Dictionary<(Type?, string?), XmlComment> Cache
+        {
+            get
+            {
+                if (_cache is null)
+                {
+                    _cache = GenerateCacheEntries();
+                }
+                return _cache;
+            }
+        }
+
+        private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
+        {
+            var _cache = new Dictionary<(Type?, string?), XmlComment>();
+
+            _cache.Add((typeof(global::Todo), null), new XmlComment("""This is a todo item.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::Project), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::ProjectBoard.BoardItem), null), new XmlComment("""An item on the board.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::ProjectRecord), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false), } ,new List{}));
+            _cache.Add((typeof(global::IUser), null), new XmlComment("""Represents a user in the system.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::User), null), new XmlComment(null,null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::ProjectRecord), "Name"), new XmlComment("""The name of the project.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::ProjectRecord), "Description"), new XmlComment("""The description of the project.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::TodoWithDescription), "Id"), new XmlComment("""The identifier of the todo.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::TodoWithDescription), "Name"), new XmlComment(null,null,null,null,"""The name of the todo.""", false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::TodoWithDescription), "Description"), new XmlComment("""A description of the the todo.""", null,null,null,"""Another description of the todo.""", false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "BooleanType"), new XmlComment(null,null,null,null,null,false, new List{@"true", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "IntegerType"), new XmlComment(null,null,null,null,null,false, new List{@"42", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "LongType"), new XmlComment(null,null,null,null,null,false, new List{@"1234567890123456789", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "DoubleType"), new XmlComment(null,null,null,null,null,false, new List{@"3.14", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "FloatType"), new XmlComment(null,null,null,null,null,false, new List{@"3.14", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "DateTimeType"), new XmlComment(null,null,null,null,null,false, new List{@"2022-01-01T00:00:00Z", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "DateOnlyType"), new XmlComment(null,null,null,null,null,false, new List{@"2022-01-01", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "StringType"), new XmlComment(null,null,null,null,null,false, new List{@"Hello, World!", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "GuidType"), new XmlComment(null,null,null,null,null,false, new List{@"2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "TimeOnlyType"), new XmlComment(null,null,null,null,null,false, new List{@"12:30:45", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "TimeSpanType"), new XmlComment(null,null,null,null,null,false, new List{@"P3DT4H5M", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "ByteType"), new XmlComment(null,null,null,null,null,false, new List{@"255", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "DecimalType"), new XmlComment(null,null,null,null,null,false, new List{@"3.14159265359", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::TypeWithExamples), "UriType"), new XmlComment(null,null,null,null,null,false, new List{@"https://example.com", }, new List{} ,new List{}));
+            _cache.Add((typeof(global::IUser), "Id"), new XmlComment("""The unique identifier for the user.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::IUser), "Name"), new XmlComment("""The user's display name.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::User), "Id"), new XmlComment("""The unique identifier for the user.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::User), "Name"), new XmlComment("""The user's display name.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+
+            return _cache;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+    {
+        public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+        {
+            var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+                ? controllerActionDescriptor.MethodInfo
+                : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+            if (methodInfo is null)
+            {
+                return Task.CompletedTask;
+            }
+            if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
+            {
+                if (methodComment.Summary is not null)
+                {
+                    operation.Summary = methodComment.Summary;
+                }
+                if (methodComment.Description is not null)
+                {
+                    operation.Description = methodComment.Description;
+                }
+                if (methodComment.Remarks is not null)
+                {
+                    operation.Description = methodComment.Remarks;
+                }
+                if (methodComment.Parameters is { Count: > 0})
+                {
+                    foreach (var parameterComment in methodComment.Parameters)
+                    {
+                        var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+                        var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+                        if (operationParameter is not null)
+                        {
+                            operationParameter.Description = parameterComment.Description;
+                            if (parameterComment.Example is { } jsonString)
+                            {
+                                operationParameter.Example = jsonString.Parse();
+                            }
+                            operationParameter.Deprecated = parameterComment.Deprecated;
+                        }
+                        else
+                        {
+                            var requestBody = operation.RequestBody;
+                            if (requestBody is not null)
+                            {
+                                requestBody.Description = parameterComment.Description;
+                                if (parameterComment.Example is { } jsonString)
+                                {
+                                    foreach (var mediaType in requestBody.Content.Values)
+                                    {
+                                        mediaType.Example = jsonString.Parse();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+                {
+                    foreach (var response in operation.Responses)
+                    {
+                        var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+                        if (responseComment is not null)
+                        {
+                            response.Value.Description = responseComment.Description;
+                        }
+
+                    }
+                }
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+    {
+        public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+        {
+            if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+            {
+                if (XmlCommentCache.Cache.TryGetValue((propertyInfo.DeclaringType, propertyInfo.Name), out var propertyComment))
+                {
+                    schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+                    if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+                    {
+                        schema.Example = jsonString.Parse();
+                    }
+                }
+            }
+            if (XmlCommentCache.Cache.TryGetValue((context.JsonTypeInfo.Type, null), out var typeComment))
+            {
+                schema.Description = typeComment.Summary;
+                if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+                {
+                    schema.Example = jsonString.Parse();
+                }
+            }
+            return Task.CompletedTask;
+        }
+    }
+
+    file static class JsonNodeExtensions
+    {
+        public static JsonNode? Parse(this string? json)
+        {
+            if (json is null)
+            {
+                return null;
+            }
+
+            try
+            {
+                return JsonNode.Parse(json);
+            }
+            catch (JsonException)
+            {
+                try
+                {
+                    // If parsing fails, try wrapping in quotes to make it a valid JSON string
+                    return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+                }
+                catch (JsonException)
+                {
+                    return null;
+                }
+            }
+        }
+    }
+
+    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+    file static class GeneratedServiceCollectionExtensions
+    {
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "MUoNMEheN1FO70WBdPr5waMAAABQcm9ncmFtLmNz")]
+        public static IServiceCollection AddOpenApi(this IServiceCollection services)
+        {
+            return services.AddOpenApi("v1", options =>
+            {
+                options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+                options.AddOperationTransformer(new XmlCommentOperationTransformer());
+            });
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs
index 3078ce4745a0..3d02e86239a3 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs
@@ -5,8 +5,6 @@
 using Microsoft.AspNetCore.InternalTesting;
 using Microsoft.AspNetCore.OpenApi;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.OpenApi;
-using Microsoft.OpenApi.Extensions;
 using Microsoft.OpenApi.Models;
 using Microsoft.OpenApi.Writers;
 
@@ -14,31 +12,30 @@
 public sealed class OpenApiDocumentIntegrationTests(SampleAppFixture fixture) : IClassFixture
 {
     [Theory]
-    [InlineData("v1", OpenApiSpecVersion.OpenApi3_0)]
-    [InlineData("v2", OpenApiSpecVersion.OpenApi3_0)]
-    [InlineData("controllers", OpenApiSpecVersion.OpenApi3_0)]
-    [InlineData("responses", OpenApiSpecVersion.OpenApi3_0)]
-    [InlineData("forms", OpenApiSpecVersion.OpenApi3_0)]
-    [InlineData("schemas-by-ref", OpenApiSpecVersion.OpenApi3_0)]
-    [InlineData("v1", OpenApiSpecVersion.OpenApi3_1)]
-    [InlineData("v2", OpenApiSpecVersion.OpenApi3_1)]
-    [InlineData("controllers", OpenApiSpecVersion.OpenApi3_1)]
-    [InlineData("responses", OpenApiSpecVersion.OpenApi3_1)]
-    [InlineData("forms", OpenApiSpecVersion.OpenApi3_1)]
-    [InlineData("schemas-by-ref", OpenApiSpecVersion.OpenApi3_1)]
-    public async Task VerifyOpenApiDocument(string documentName, OpenApiSpecVersion version)
+    [InlineData("v1")]
+    [InlineData("v2")]
+    [InlineData("controllers")]
+    [InlineData("responses")]
+    [InlineData("forms")]
+    [InlineData("schemas-by-ref")]
+    [InlineData("xml")]
+    public async Task VerifyOpenApiDocument(string documentName)
     {
         var documentService = fixture.Services.GetRequiredKeyedService(documentName);
         var scopedServiceProvider = fixture.Services.CreateScope();
         var document = await documentService.GetOpenApiDocumentAsync(scopedServiceProvider.ServiceProvider);
-        var json = await document.SerializeAsJsonAsync(version);
-        var baseSnapshotsDirectory = SkipOnHelixAttribute.OnHelix()
-            ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "Integration", "snapshots")
-            : "snapshots";
-        var outputDirectory = Path.Combine(baseSnapshotsDirectory, version.ToString());
-        await Verifier.Verify(json)
-            .UseDirectory(outputDirectory)
-            .AutoVerify()
+        await Verifier.Verify(GetOpenApiJson(document))
+            .UseDirectory(SkipOnHelixAttribute.OnHelix()
+                ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "Integration", "snapshots")
+                : "snapshots")
             .UseParameters(documentName);
     }
+
+    private static string GetOpenApiJson(OpenApiDocument document)
+    {
+        using var textWriter = new StringWriter(CultureInfo.InvariantCulture);
+        var jsonWriter = new OpenApiJsonWriter(textWriter);
+        document.SerializeAsV31(jsonWriter);
+        return textWriter.ToString();
+    }
 }
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt
new file mode 100644
index 000000000000..9eb758a76e24
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt
@@ -0,0 +1,96 @@
+{
+  "openapi": "3.1.1",
+  "info": {
+    "title": "Sample | controllers",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/getbyidandname/{id}/{name}": {
+      "get": {
+        "tags": [
+          "Test"
+        ],
+        "parameters": [
+          {
+            "name": "Id",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int32"
+            }
+          },
+          {
+            "name": "Name",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "minLength": 5,
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "text/plain": {
+                "schema": {
+                  "type": "string"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              },
+              "text/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/forms": {
+      "post": {
+        "tags": [
+          "Test"
+        ],
+        "requestBody": {
+          "content": {
+            "application/x-www-form-urlencoded": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "Title": {
+                    "type": "string"
+                  },
+                  "Description": {
+                    "type": "string"
+                  },
+                  "IsCompleted": {
+                    "type": "boolean"
+                  }
+                }
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "Test"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=forms.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=forms.verified.txt
new file mode 100644
index 000000000000..c68e4d17c64d
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=forms.verified.txt
@@ -0,0 +1,227 @@
+{
+  "openapi": "3.1.1",
+  "info": {
+    "title": "Sample | forms",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/forms/form-file": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "required": [
+                  "resume"
+                ],
+                "type": "object",
+                "properties": {
+                  "resume": {
+                    "$ref": "#/components/schemas/IFormFile"
+                  }
+                }
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/forms/form-files": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "required": [
+                  "files"
+                ],
+                "type": "object",
+                "properties": {
+                  "files": {
+                    "$ref": "#/components/schemas/IFormFileCollection"
+                  }
+                }
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/forms/form-file-multiple": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "required": [
+                  "resume",
+                  "files"
+                ],
+                "type": "object",
+                "allOf": [
+                  {
+                    "type": "object",
+                    "properties": {
+                      "resume": {
+                        "$ref": "#/components/schemas/IFormFile"
+                      }
+                    }
+                  },
+                  {
+                    "type": "object",
+                    "properties": {
+                      "files": {
+                        "$ref": "#/components/schemas/IFormFileCollection"
+                      }
+                    }
+                  }
+                ]
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/forms/form-todo": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "$ref": "#/components/schemas/Todo"
+              }
+            },
+            "application/x-www-form-urlencoded": {
+              "schema": {
+                "$ref": "#/components/schemas/Todo"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/forms/forms-pocos-and-files": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "required": [
+                  "file"
+                ],
+                "type": "object",
+                "allOf": [
+                  {
+                    "$ref": "#/components/schemas/Todo"
+                  },
+                  {
+                    "type": "object",
+                    "properties": {
+                      "file": {
+                        "$ref": "#/components/schemas/IFormFile"
+                      }
+                    }
+                  }
+                ]
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "IFormFile": {
+        "type": "string",
+        "format": "binary"
+      },
+      "IFormFileCollection": {
+        "type": "array",
+        "items": {
+          "$ref": "#/components/schemas/IFormFile"
+        }
+      },
+      "Todo": {
+        "required": [
+          "id",
+          "title",
+          "completed",
+          "createdAt"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "description": "The unique identifier of the to-do item.",
+            "format": "int32"
+          },
+          "title": {
+            "type": "string",
+            "description": "The title of the to-do item."
+          },
+          "completed": {
+            "type": "boolean",
+            "description": "Indicates whether the to-do item is completed."
+          },
+          "createdAt": {
+            "type": "string",
+            "description": "The date and time when the to-do item was created.",
+            "format": "date-time"
+          }
+        },
+        "description": "Represents a to-do item."
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "Sample"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=responses.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=responses.verified.txt
new file mode 100644
index 000000000000..45a4660aa78c
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=responses.verified.txt
@@ -0,0 +1,208 @@
+{
+  "openapi": "3.1.1",
+  "info": {
+    "title": "Sample | responses",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/responses/200-add-xml": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Todo"
+                }
+              },
+              "text/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/Todo"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/responses/200-only-xml": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "text/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/Todo"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/responses/triangle": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Triangle"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/responses/shape": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Shape"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "Shape": {
+        "required": [
+          "$type"
+        ],
+        "type": "object",
+        "anyOf": [
+          {
+            "$ref": "#/components/schemas/ShapeTriangle"
+          },
+          {
+            "$ref": "#/components/schemas/ShapeSquare"
+          }
+        ],
+        "discriminator": {
+          "propertyName": "$type",
+          "mapping": {
+            "triangle": "#/components/schemas/ShapeTriangle",
+            "square": "#/components/schemas/ShapeSquare"
+          }
+        }
+      },
+      "ShapeSquare": {
+        "properties": {
+          "$type": {
+            "enum": [
+              "square"
+            ],
+            "type": "string"
+          },
+          "area": {
+            "type": "number",
+            "format": "double"
+          },
+          "color": {
+            "type": "string"
+          },
+          "sides": {
+            "type": "integer",
+            "format": "int32"
+          }
+        }
+      },
+      "ShapeTriangle": {
+        "properties": {
+          "$type": {
+            "enum": [
+              "triangle"
+            ],
+            "type": "string"
+          },
+          "hypotenuse": {
+            "type": "number",
+            "format": "double"
+          },
+          "color": {
+            "type": "string"
+          },
+          "sides": {
+            "type": "integer",
+            "format": "int32"
+          }
+        }
+      },
+      "Todo": {
+        "required": [
+          "id",
+          "title",
+          "completed",
+          "createdAt"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "description": "The unique identifier of the to-do item.",
+            "format": "int32"
+          },
+          "title": {
+            "type": "string",
+            "description": "The title of the to-do item."
+          },
+          "completed": {
+            "type": "boolean",
+            "description": "Indicates whether the to-do item is completed."
+          },
+          "createdAt": {
+            "type": "string",
+            "description": "The date and time when the to-do item was created.",
+            "format": "date-time"
+          }
+        },
+        "description": "Represents a to-do item."
+      },
+      "Triangle": {
+        "type": "object",
+        "properties": {
+          "hypotenuse": {
+            "type": "number",
+            "format": "double"
+          },
+          "color": {
+            "type": "string"
+          },
+          "sides": {
+            "type": "integer",
+            "format": "int32"
+          }
+        }
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "Sample"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt
new file mode 100644
index 000000000000..b750396361c0
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt
@@ -0,0 +1,624 @@
+{
+  "openapi": "3.1.1",
+  "info": {
+    "title": "Sample | schemas-by-ref",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/schemas-by-ref/typed-results": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Triangle"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/multiple-results": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Triangle"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Not Found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/iresult-no-produces": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/iresult-with-produces": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "text/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/Triangle"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/primitives": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "parameters": [
+          {
+            "name": "id",
+            "in": "query",
+            "description": "The ID associated with the Todo item.",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int32"
+            }
+          },
+          {
+            "name": "size",
+            "in": "query",
+            "description": "The number of Todos to fetch",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int32"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/product": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Product"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Product"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/account": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Account"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Account"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/array-of-ints": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "array",
+                "items": {
+                  "type": "integer",
+                  "format": "int32"
+                }
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "integer",
+                  "format": "int32"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/list-of-ints": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "array",
+                "items": {
+                  "type": "integer",
+                  "format": "int32"
+                }
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "integer",
+                  "format": "int32"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/ienumerable-of-ints": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "integer",
+                  "format": "int32"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/dictionary-of-ints": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "additionalProperties": {
+                    "type": "integer",
+                    "format": "int32"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/frozen-dictionary-of-ints": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "additionalProperties": {
+                    "type": "integer",
+                    "format": "int32"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/shape": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Shape"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/weatherforecastbase": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/WeatherForecastBase"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/schemas-by-ref/person": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Person"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "Account": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "name": {
+            "type": "string"
+          }
+        }
+      },
+      "Person": {
+        "required": [
+          "discriminator"
+        ],
+        "type": "object",
+        "anyOf": [
+          {
+            "$ref": "#/components/schemas/PersonStudent"
+          },
+          {
+            "$ref": "#/components/schemas/PersonTeacher"
+          }
+        ],
+        "discriminator": {
+          "propertyName": "discriminator",
+          "mapping": {
+            "student": "#/components/schemas/PersonStudent",
+            "teacher": "#/components/schemas/PersonTeacher"
+          }
+        }
+      },
+      "PersonStudent": {
+        "properties": {
+          "discriminator": {
+            "enum": [
+              "student"
+            ],
+            "type": "string"
+          },
+          "gpa": {
+            "type": "number",
+            "format": "double"
+          }
+        }
+      },
+      "PersonTeacher": {
+        "required": [
+          "subject"
+        ],
+        "properties": {
+          "discriminator": {
+            "enum": [
+              "teacher"
+            ],
+            "type": "string"
+          },
+          "subject": {
+            "type": "string"
+          }
+        }
+      },
+      "Product": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "name": {
+            "type": "string"
+          }
+        }
+      },
+      "Shape": {
+        "required": [
+          "$type"
+        ],
+        "type": "object",
+        "anyOf": [
+          {
+            "$ref": "#/components/schemas/ShapeTriangle"
+          },
+          {
+            "$ref": "#/components/schemas/ShapeSquare"
+          }
+        ],
+        "discriminator": {
+          "propertyName": "$type",
+          "mapping": {
+            "triangle": "#/components/schemas/ShapeTriangle",
+            "square": "#/components/schemas/ShapeSquare"
+          }
+        }
+      },
+      "ShapeSquare": {
+        "properties": {
+          "$type": {
+            "enum": [
+              "square"
+            ],
+            "type": "string"
+          },
+          "area": {
+            "type": "number",
+            "format": "double"
+          },
+          "color": {
+            "type": "string"
+          },
+          "sides": {
+            "type": "integer",
+            "format": "int32"
+          }
+        }
+      },
+      "ShapeTriangle": {
+        "properties": {
+          "$type": {
+            "enum": [
+              "triangle"
+            ],
+            "type": "string"
+          },
+          "hypotenuse": {
+            "type": "number",
+            "format": "double"
+          },
+          "color": {
+            "type": "string"
+          },
+          "sides": {
+            "type": "integer",
+            "format": "int32"
+          }
+        }
+      },
+      "Triangle": {
+        "type": "object",
+        "properties": {
+          "hypotenuse": {
+            "type": "number",
+            "format": "double"
+          },
+          "color": {
+            "type": "string"
+          },
+          "sides": {
+            "type": "integer",
+            "format": "int32"
+          }
+        }
+      },
+      "WeatherForecastBase": {
+        "required": [
+          "$type"
+        ],
+        "type": "object",
+        "anyOf": [
+          {
+            "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity"
+          },
+          {
+            "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries"
+          },
+          {
+            "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews"
+          }
+        ],
+        "discriminator": {
+          "propertyName": "$type",
+          "mapping": {
+            "0": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity",
+            "1": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries",
+            "2": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews"
+          }
+        }
+      },
+      "WeatherForecastBaseWeatherForecastWithCity": {
+        "required": [
+          "city"
+        ],
+        "properties": {
+          "$type": {
+            "enum": [
+              0
+            ],
+            "type": "integer"
+          },
+          "city": {
+            "type": "string"
+          }
+        }
+      },
+      "WeatherForecastBaseWeatherForecastWithLocalNews": {
+        "required": [
+          "news"
+        ],
+        "properties": {
+          "$type": {
+            "enum": [
+              2
+            ],
+            "type": "integer"
+          },
+          "news": {
+            "type": "string"
+          }
+        }
+      },
+      "WeatherForecastBaseWeatherForecastWithTimeSeries": {
+        "required": [
+          "summary"
+        ],
+        "properties": {
+          "$type": {
+            "enum": [
+              1
+            ],
+            "type": "integer"
+          },
+          "date": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "temperatureC": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "summary": {
+            "type": "string"
+          }
+        }
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "Sample"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v1.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v1.verified.txt
new file mode 100644
index 000000000000..abbe8732d74f
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v1.verified.txt
@@ -0,0 +1,208 @@
+{
+  "openapi": "3.1.1",
+  "info": {
+    "title": "Sample | v1",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/v1/array-of-guids": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "parameters": [
+          {
+            "name": "guids",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "format": "uuid"
+              }
+            }
+          },
+          {
+            "name": "X-Version",
+            "in": "header",
+            "schema": {
+              "type": "string",
+              "default": "1.0"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "type": "string",
+                    "format": "uuid"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/todos": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "summary": "Creates a new todo item.",
+        "parameters": [
+          {
+            "name": "X-Version",
+            "in": "header",
+            "schema": {
+              "type": "string",
+              "default": "1.0"
+            }
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Todo"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/v1/todos/{id}": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "description": "Returns a specific todo item.",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int32"
+            }
+          },
+          {
+            "name": "X-Version",
+            "in": "header",
+            "schema": {
+              "type": "string",
+              "default": "1.0"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TodoWithDueDate"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "Todo": {
+        "required": [
+          "id",
+          "title",
+          "completed",
+          "createdAt"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "description": "The unique identifier of the to-do item.",
+            "format": "int32"
+          },
+          "title": {
+            "type": "string",
+            "description": "The title of the to-do item."
+          },
+          "completed": {
+            "type": "boolean",
+            "description": "Indicates whether the to-do item is completed."
+          },
+          "createdAt": {
+            "type": "string",
+            "description": "The date and time when the to-do item was created.",
+            "format": "date-time"
+          }
+        },
+        "description": "Represents a to-do item."
+      },
+      "TodoWithDueDate": {
+        "required": [
+          "dueDate",
+          "id",
+          "title",
+          "completed",
+          "createdAt"
+        ],
+        "type": "object",
+        "properties": {
+          "dueDate": {
+            "type": "string",
+            "description": "The due date of the to-do item.",
+            "format": "date-time"
+          },
+          "id": {
+            "type": "integer",
+            "description": "The unique identifier of the to-do item.",
+            "format": "int32"
+          },
+          "title": {
+            "type": "string",
+            "description": "The title of the to-do item."
+          },
+          "completed": {
+            "type": "boolean",
+            "description": "Indicates whether the to-do item is completed."
+          },
+          "createdAt": {
+            "type": "string",
+            "description": "The date and time when the to-do item was created.",
+            "format": "date-time"
+          }
+        },
+        "description": "Represents a to-do item with a due date."
+      }
+    },
+    "securitySchemes": {
+      "Bearer": {
+        "type": "http",
+        "scheme": "bearer",
+        "bearerFormat": "Json Web Token"
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "Sample"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v2.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v2.verified.txt
new file mode 100644
index 000000000000..fed56ba97790
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v2.verified.txt
@@ -0,0 +1,69 @@
+{
+  "openapi": "3.1.1",
+  "info": {
+    "title": "Sample | v2",
+    "contact": {
+      "name": "OpenAPI Enthusiast",
+      "email": "iloveopenapi@example.com"
+    },
+    "license": {
+      "name": "MIT"
+    },
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/v2/users": {
+      "get": {
+        "tags": [
+          "users"
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "type": "string",
+                    "externalDocs": {
+                      "description": "Documentation for this OpenAPI schema",
+                      "url": "https://example.com/api/docs/schemas/string"
+                    }
+                  },
+                  "externalDocs": {
+                    "description": "Documentation for this OpenAPI schema",
+                    "url": "https://example.com/api/docs/schemas/array"
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "externalDocs": {
+          "description": "Documentation for this OpenAPI endpoint",
+          "url": "https://example.com/api/docs/operations/CreateUser"
+        },
+        "operationId": "CreateUser",
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "users"
+    },
+    {
+      "name": "Sample"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt
new file mode 100644
index 000000000000..fa9d390162a0
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt
@@ -0,0 +1,397 @@
+{
+  "openapi": "3.1.1",
+  "info": {
+    "title": "Sample | xml",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/xml/type-with-examples": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/TypeWithExamples"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TypeWithExamples"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/xml/todo": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/TodoFomInterface"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/xml/project": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Project"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/xml/board": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/BoardItem"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/xml/project-record": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/ProjectRecord"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/xml/todo-with-description": {
+      "post": {
+        "tags": [
+          "Sample"
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/TodoWithDescription"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/Xml": {
+      "get": {
+        "tags": [
+          "Xml"
+        ],
+        "parameters": [
+          {
+            "name": "name",
+            "in": "query",
+            "description": "The name of the person.",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Returns the greeting.",
+            "content": {
+              "text/plain": {
+                "schema": {
+                  "type": "string"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              },
+              "text/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post": {
+        "tags": [
+          "Xml"
+        ],
+        "requestBody": {
+          "description": "The todo to insert into the database.",
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Todo"
+              }
+            },
+            "text/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Todo"
+              }
+            },
+            "application/*+json": {
+              "schema": {
+                "$ref": "#/components/schemas/Todo"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "text/plain": {
+                "schema": {
+                  "type": "string"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              },
+              "text/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "BoardItem": {
+        "required": [
+          "name"
+        ],
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string"
+          }
+        },
+        "description": "An item on the board."
+      },
+      "Project": {
+        "required": [
+          "name",
+          "description"
+        ],
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string"
+          },
+          "description": {
+            "type": "string"
+          }
+        },
+        "description": "The project that contains Todo items."
+      },
+      "ProjectRecord": {
+        "required": [
+          "name",
+          "description"
+        ],
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "The name of the project."
+          },
+          "description": {
+            "type": "string",
+            "description": "The description of the project."
+          }
+        },
+        "description": "The project that contains Todo items."
+      },
+      "Todo": {
+        "required": [
+          "id",
+          "title",
+          "completed"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "title": {
+            "type": "string"
+          },
+          "completed": {
+            "type": "boolean"
+          }
+        }
+      },
+      "TodoFomInterface": {
+        "required": [
+          "name",
+          "description"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "description": "The identifier of the todo.",
+            "format": "int32"
+          },
+          "name": {
+            "type": "string",
+            "description": "The name of the todo."
+          },
+          "description": {
+            "type": "string",
+            "description": "A description of the todo."
+          }
+        },
+        "description": "This is a todo item."
+      },
+      "TodoWithDescription": {
+        "required": [
+          "name",
+          "description"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "description": "The identifier of the todo, overridden.",
+            "format": "int32"
+          },
+          "name": {
+            "type": "string",
+            "description": "The name of the todo, overridden."
+          },
+          "description": {
+            "type": "string",
+            "description": "Another description of the todo."
+          }
+        }
+      },
+      "TypeWithExamples": {
+        "type": "object",
+        "properties": {
+          "booleanType": {
+            "type": "boolean",
+            "example": true
+          },
+          "integerType": {
+            "type": "integer",
+            "format": "int32",
+            "example": 42
+          },
+          "longType": {
+            "type": "integer",
+            "format": "int64",
+            "example": 1234567890123456789
+          },
+          "doubleType": {
+            "type": "number",
+            "format": "double",
+            "example": 3.14
+          },
+          "floatType": {
+            "type": "number",
+            "format": "float",
+            "example": 3.14
+          },
+          "dateTimeType": {
+            "type": "string",
+            "format": "date-time",
+            "example": "2022-01-01T00:00:00.0000000+00:00"
+          },
+          "dateOnlyType": {
+            "type": "string",
+            "format": "date",
+            "example": "2022-01-01T00:00:00.0000000-08:00"
+          }
+        }
+      }
+    }
+  },
+  "tags": [
+    {
+      "name": "Sample"
+    },
+    {
+      "name": "Xml"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Shared/SharedTypes.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Shared/SharedTypes.cs
index 0f9050ea995f..73e85f0458eb 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Shared/SharedTypes.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Shared/SharedTypes.cs
@@ -10,52 +10,159 @@
 using System.Text.Json.Serialization;
 using Microsoft.AspNetCore.Http;
 
+/// 
+/// Represents a to-do item.
+/// 
+/// The unique identifier of the to-do item.
+/// The title of the to-do item.
+/// Indicates whether the to-do item is completed.
+/// The date and time when the to-do item was created.
 internal record Todo(int Id, string Title, bool Completed, DateTime CreatedAt);
 
+/// 
+/// Represents a to-do item with a due date.
+/// 
+/// The unique identifier of the to-do item.
+/// The title of the to-do item.
+/// Indicates whether the to-do item is completed.
+/// The date and time when the to-do item was created.
+/// The due date of the to-do item.
 internal record TodoWithDueDate(int Id, string Title, bool Completed, DateTime CreatedAt, DateTime DueDate) : Todo(Id, Title, Completed, CreatedAt);
 
+/// 
+/// Represents an error.
+/// 
+/// The error code.
+/// The error message.
 internal record Error(int Code, string Message);
 
+/// 
+/// Represents a resume upload.
+/// 
+/// The name of the resume.
+/// The description of the resume.
+/// The resume file.
 internal record ResumeUpload(string Name, string Description, IFormFile Resume);
 
+/// 
+/// Represents a result of an operation.
+/// 
+/// The type of the value.
+/// Indicates whether the operation was successful.
+/// The value of the result.
+/// The error associated with the result, if any.
 internal record Result(bool IsSuccessful, T Value, Error Error);
 
+/// 
+/// Represents a vehicle.
+/// 
 internal class Vehicle
 {
+    /// 
+    /// Gets or sets the number of wheels.
+    /// 
     public int Wheels { get; set; }
+
+    /// 
+    /// Gets or sets the make of the vehicle.
+    /// 
     public string Make { get; set; } = string.Empty;
 }
 
+/// 
+/// Represents a car.
+/// 
 internal class Car : Vehicle
 {
+    /// 
+    /// Gets or sets the number of doors.
+    /// 
     public int Doors { get; set; }
 }
 
+/// 
+/// Represents a boat.
+/// 
 internal class Boat : Vehicle
 {
+    /// 
+    /// Gets or sets the length of the boat.
+    /// 
     public double Length { get; set; }
 }
 
+/// 
+/// Represents the status of an operation.
+/// 
 [JsonConverter(typeof(JsonStringEnumConverter))]
 internal enum Status
 {
+    /// 
+    /// The operation is pending.
+    /// 
     Pending,
+
+    /// 
+    /// The operation is approved.
+    /// 
     Approved,
+
+    /// 
+    /// The operation is rejected.
+    /// 
     Rejected
 }
 
+/// 
+/// Represents a proposal.
+/// 
 internal class Proposal
 {
+    /// 
+    /// Gets or sets the proposal element.
+    /// 
     public required Proposal ProposalElement { get; set; }
+
+    /// 
+    /// Gets or sets the stream associated with the proposal.
+    /// 
     public required Stream Stream { get; set; }
 }
 
+/// 
+/// Represents a paginated collection of items.
+/// 
+/// The type of items contained in the collection. Must be a reference type.
+/// The current page index (zero-based).
+/// The number of items per page.
+/// The total number of items in the collection.
+/// The total number of pages available.
+/// The collection of items for the current page.
 internal class PaginatedItems(int pageIndex, int pageSize, long totalItems, int totalPages, IEnumerable items) where T : class
 {
+    /// 
+    /// Gets or sets the current page index (zero-based).
+    /// 
     public int PageIndex { get; set; } = pageIndex;
+
+    /// 
+    /// Gets or sets the number of items per page.
+    /// 
     public int PageSize { get; set; } = pageSize;
+
+    /// 
+    /// Gets or sets the total number of items in the collection.
+    /// 
     public long TotalItems { get; set; } = totalItems;
+
+    /// 
+    /// Gets or sets the total number of pages available.
+    /// 
     public int TotalPages { get; set; } = totalPages;
+
+    /// 
+    /// Gets or sets the collection of items for the current page.
+    /// 
     public IEnumerable Items { get; set; } = items;
 }
 

From 6cabfc8015580cb305d800b753446fcd3fc7c317 Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Tue, 11 Feb 2025 17:38:06 -0800
Subject: [PATCH 02/13] Fix snapshot test path on Helix

---
 .../Helpers/SnapshotTestHelper.cs                           | 6 ++++--
 ...crosoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj | 4 ++++
 ...OnSchemas#OpenApiXmlCommentSupport.generated.verified.cs | 3 +--
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
index 59c7e4b13556..147d3fb837a2 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
@@ -5,6 +5,7 @@
 using System.Text;
 using Microsoft.AspNetCore.Hosting.Server;
 using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.InternalTesting;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.Emit;
@@ -48,8 +49,9 @@ public static Task Verify(string source, IIncrementalGenerator generator, out Co
         var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions);
         return Verifier
             .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics))
-            .AutoVerify()
-            .UseDirectory("../snapshots");
+            .UseDirectory(SkipOnHelixAttribute.OnHelix()
+                ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "snapshots")
+                : "../snapshots");
     }
 
     public static async Task VerifyOpenApi(Compilation compilation, Action verifyFunc)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
index e80fc854c839..a98ce789b5cb 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
@@ -30,4 +30,8 @@
     
   
 
+  
+    
+  
+
 
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
index 7b5c9924e5b0..34ebd3b0c88a 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
@@ -79,7 +79,6 @@ file static class XmlCommentCache
             _cache.Add((typeof(global::Project), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::ProjectBoard.BoardItem), null), new XmlComment("""An item on the board.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::ProjectRecord), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false), } ,new List{}));
-            _cache.Add((typeof(global::IUser), null), new XmlComment("""Represents a user in the system.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::User), null), new XmlComment(null,null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::ProjectRecord), "Name"), new XmlComment("""The name of the project.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::ProjectRecord), "Description"), new XmlComment("""The description of the project.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
@@ -245,7 +244,7 @@ file static class JsonNodeExtensions
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
     file static class GeneratedServiceCollectionExtensions
     {
-        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "MUoNMEheN1FO70WBdPr5waMAAABQcm9ncmFtLmNz")]
+        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "UZZADqQajYC9HbHNRWW5LaMAAABQcm9ncmFtLmNz")]
         public static IServiceCollection AddOpenApi(this IServiceCollection services)
         {
             return services.AddOpenApi("v1", options =>

From cb35db3f9eaa1a124b9e08f29cf43f88c4f69914 Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Tue, 11 Feb 2025 18:02:44 -0800
Subject: [PATCH 03/13] Add source generator to NuGet package

---
 .../gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj   | 1 +
 src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj            | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj b/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj
index 77f31b901e50..8ca8ad11dd21 100644
--- a/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj
+++ b/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj
@@ -9,6 +9,7 @@
     enable
     true
     false
+    false
   
 
   
diff --git a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
index b8c1d9f36336..6c767af127ff 100644
--- a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
+++ b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
@@ -46,7 +46,10 @@
     
   
 
+  
   
+    
+    
     
   
 

From 6c667cbb80d413f439676f74fc54bab1da399dea Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Tue, 11 Feb 2025 18:39:21 -0800
Subject: [PATCH 04/13] Simplify cref resolution handling

---
 src/OpenApi/gen/XmlComments/XmlComment.cs     | 51 ++++++++-----------
 .../SchemaTests.cs                            |  2 +-
 ...ApiXmlCommentSupport.generated.verified.cs |  4 +-
 ...nApiDocument_documentName=xml.verified.txt |  4 +-
 4 files changed, 25 insertions(+), 36 deletions(-)

diff --git a/src/OpenApi/gen/XmlComments/XmlComment.cs b/src/OpenApi/gen/XmlComments/XmlComment.cs
index 321a23bf032b..64f541b9cb23 100644
--- a/src/OpenApi/gen/XmlComments/XmlComment.cs
+++ b/src/OpenApi/gen/XmlComments/XmlComment.cs
@@ -7,7 +7,6 @@
 using System.Collections.Immutable;
 using Microsoft.AspNetCore.Analyzers.Infrastructure;
 using System.Linq;
-using System.Text.RegularExpressions;
 using System.Threading;
 using System.Xml;
 using System.Xml.Linq;
@@ -18,8 +17,6 @@ namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
 
 internal sealed class XmlComment
 {
-    private const string IdSelector = @"((?![0-9])[\w_])+[\w\(\)\.\{\}\[\]\|\*\^~#@!`,_<>:]*";
-    private static readonly Regex CommentIdRegex = new(@"^(?N|T|M|P|F|E|Overload):(?" + IdSelector + ")$", RegexOptions.Compiled);
     public string? Summary { get; internal set; }
     public string? Description { get; internal set; }
     public string? Value { get; internal set; }
@@ -30,7 +27,7 @@ internal sealed class XmlComment
     public List Parameters { get; internal set; } = [];
     public List Responses { get; internal set; } = [];
 
-    private XmlComment(string xml)
+    private XmlComment(Compilation compilation, string xml)
     {
         // Treat  as 
         if (xml.StartsWith("", StringComparison.InvariantCulture) && xml.EndsWith("", StringComparison.InvariantCulture))
@@ -42,8 +39,8 @@ private XmlComment(string xml)
         // Transform triple slash comment
         var doc = XDocument.Parse(xml, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
 
-        ResolveCrefLink(doc, "//seealso[@cref]");
-        ResolveCrefLink(doc, "//see[@cref]");
+        ResolveCrefLink(compilation, doc, $"//{DocumentationCommentXmlNames.SeeAlsoElementName}[@cref]");
+        ResolveCrefLink(compilation, doc, $"//{DocumentationCommentXmlNames.SeeElementName}[@cref]");
 
         var nav = doc.CreateNavigator();
         Summary = GetSingleNodeValue(nav, "/member/summary");
@@ -68,7 +65,7 @@ private XmlComment(string xml)
         }
 
         var resolvedComment = GetDocumentationComment(symbol, xmlText, [], compilation, cancellationToken);
-        return !string.IsNullOrEmpty(resolvedComment) ? new XmlComment(resolvedComment!) : null;
+        return !string.IsNullOrEmpty(resolvedComment) ? new XmlComment(compilation, resolvedComment!) : null;
     }
 
     private static string? GetDocumentationComment(ISymbol symbol, string xmlText, HashSet? visitedSymbols, Compilation compilation, CancellationToken cancellationToken)
@@ -419,7 +416,13 @@ static string BuildXPathForElement(XElement element)
     private static bool ElementNameIs(XElement element, string name)
         => string.IsNullOrEmpty(element.Name.NamespaceName) && DocumentationCommentXmlNames.ElementEquals(element.Name.LocalName, name);
 
-    private static void ResolveCrefLink(XNode node, string nodeSelector)
+    /// 
+    /// Resolves the cref links in the XML documentation into type names.
+    /// 
+    /// The compilation to resolve type symbol declarations from.
+    /// The target node to process crefs in.
+    /// The node type to process crefs for, can be `see` or `seealso`.
+    private static void ResolveCrefLink(Compilation compilation, XNode node, string nodeSelector)
     {
         if (node == null || string.IsNullOrEmpty(nodeSelector))
         {
@@ -429,31 +432,17 @@ private static void ResolveCrefLink(XNode node, string nodeSelector)
         var nodes = node.XPathSelectElements(nodeSelector + "[@cref]").ToList();
         foreach (var item in nodes)
         {
-            var cref = item.Attribute("cref").Value;
-
-            // Strict check is needed as value could be an invalid href,
-            // e.g. !:Dictionary<TKey, string> when user manually changed the intellisensed generic type
-            var match = CommentIdRegex.Match(cref);
-            if (match.Success)
+            var cref = item.Attribute(DocumentationCommentXmlNames.CrefAttributeName).Value;
+            if (string.IsNullOrEmpty(cref))
             {
-                var id = match.Groups["id"].Value;
-                var type = match.Groups["type"].Value;
-
-                if (type == "Overload")
-                {
-                    id += '*';
-                }
+                continue;
+            }
 
-                // When see and seealso are top level nodes in triple slash comments, do not convert it into xref node
-                if (item.Parent?.Parent != null)
-                {
-                    XElement replacement;
-                    if (type == "T")
-                    {
-                        replacement = XElement.Parse($"""{id}""");
-                        item.ReplaceWith(replacement);
-                    }
-                }
+            var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(cref, compilation);
+            if (symbol is not null)
+            {
+                var type = symbol.ToDisplayString();
+                item.ReplaceWith(new XText(type));
             }
         }
     }
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
index 4fbf085c0589..19760ed60d63 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
@@ -149,7 +149,7 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
 
             path = document.Paths["/project"].Operations[OperationType.Post];
             var project = path.RequestBody.Content["application/json"].Schema;
-            Assert.Equal("The project that contains Todo items.", project.Description);
+            Assert.Equal("The project that contains Todo items.", project.Description);
 
             path = document.Paths["/board"].Operations[OperationType.Post];
             var board = path.RequestBody.Content["application/json"].Schema;
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
index 34ebd3b0c88a..42d13eaa6cef 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
@@ -76,9 +76,9 @@ file static class XmlCommentCache
             var _cache = new Dictionary<(Type?, string?), XmlComment>();
 
             _cache.Add((typeof(global::Todo), null), new XmlComment("""This is a todo item.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
-            _cache.Add((typeof(global::Project), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
+            _cache.Add((typeof(global::Project), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::ProjectBoard.BoardItem), null), new XmlComment("""An item on the board.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
-            _cache.Add((typeof(global::ProjectRecord), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false), } ,new List{}));
+            _cache.Add((typeof(global::ProjectRecord), null), new XmlComment("""The project that contains Todo items.""", null,null,null,null,false, new List{}, new List{new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false), } ,new List{}));
             _cache.Add((typeof(global::User), null), new XmlComment(null,null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::ProjectRecord), "Name"), new XmlComment("""The name of the project.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
             _cache.Add((typeof(global::ProjectRecord), "Description"), new XmlComment("""The description of the project.""", null,null,null,null,false, new List{}, new List{} ,new List{}));
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt
index fa9d390162a0..49df3605e157 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=xml.verified.txt
@@ -260,7 +260,7 @@
             "type": "string"
           }
         },
-        "description": "The project that contains Todo items."
+        "description": "The project that contains Todo items."
       },
       "ProjectRecord": {
         "required": [
@@ -278,7 +278,7 @@
             "description": "The description of the project."
           }
         },
-        "description": "The project that contains Todo items."
+        "description": "The project that contains Todo items."
       },
       "Todo": {
         "required": [

From 4bff0d2bccaeba44af4d035d15a6b196c384cd48 Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Tue, 11 Feb 2025 21:12:07 -0800
Subject: [PATCH 05/13] Fix packaging path and address feedback

---
 src/OpenApi/gen/XmlComments/XmlComment.cs     |  2 +-
 .../sample/Endpoints/MapXmlEndpoints.cs       |  2 ++
 .../src/Microsoft.AspNetCore.OpenApi.csproj   |  5 ++++-
 .../Helpers/HostFactoryResolver.cs            | 19 -------------------
 4 files changed, 7 insertions(+), 21 deletions(-)

diff --git a/src/OpenApi/gen/XmlComments/XmlComment.cs b/src/OpenApi/gen/XmlComments/XmlComment.cs
index 64f541b9cb23..fae78ab345ed 100644
--- a/src/OpenApi/gen/XmlComments/XmlComment.cs
+++ b/src/OpenApi/gen/XmlComments/XmlComment.cs
@@ -474,7 +474,7 @@ private static TNode Copy(TNode node, bool copyAttributeAnnotations)
         // Documents can't be added to containers, so our usual copy trick won't work.
         if (node.NodeType == XmlNodeType.Document)
         {
-            copy = new XDocument(((XDocument)(object)node));
+            copy = new XDocument((XDocument)(object)node);
         }
         else
         {
diff --git a/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs b/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs
index 3c3e2d74df7f..fa34c306ef67 100644
--- a/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs
+++ b/src/OpenApi/sample/Endpoints/MapXmlEndpoints.cs
@@ -99,10 +99,12 @@ public class TodoWithDescription : ITodo
         /// The identifier of the todo, overridden.
         /// 
         public int Id { get; set; }
+
         /// 
         /// The name of the todo, overridden.
         /// 
         public required string Name { get; set; }
+
         /// 
         /// A description of the the todo.
         /// 
diff --git a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
index 6c767af127ff..83bf4a785c74 100644
--- a/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
+++ b/src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
@@ -49,7 +49,10 @@
   
   
     
-    
+    
     
   
 
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
index 1bad34016d51..d3101da3066a 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
@@ -65,25 +65,6 @@ public static Func ResolveHostFactory(Assembly assembly,
             return null;
         }
 
-        try
-        {
-            // Attempt to load hosting and check the version to make sure the events
-            // even have a chance of firing (they were added in .NET >= 6)
-            var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting");
-            if (hostingAssembly.GetName().Version is Version version && version.Major < 6)
-            {
-                return null;
-            }
-
-            // We're using a version >= 6 so the events can fire. If they don't fire
-            // then it's because the application isn't using the hosting APIs
-        }
-        catch
-        {
-            // There was an error loading the extensions assembly, return null.
-            return null;
-        }
-
         return args => new HostingListener(args, assembly.EntryPoint, waitTimeout == default ? s_defaultWaitTimeout : waitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost();
     }
 

From 28dccf5d47ee1d90edc7f5416fdc929925ce5293 Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Wed, 12 Feb 2025 15:29:46 -0800
Subject: [PATCH 06/13] Address feedback

---
 .../Microsoft.AspNetCore.OpenApi.targets      |  1 -
 .../gen/Helpers/AddOpenApiOverloadVariant.cs  |  1 +
 .../gen/XmlCommentGenerator.Emitter.cs        | 26 ++++++-------------
 src/OpenApi/gen/XmlCommentGenerator.Parser.cs | 13 +++++++++-
 ...Core.OpenApi.SourceGenerators.Tests.csproj |  4 ++-
 ...ApiXmlCommentSupport.generated.verified.cs | 24 +++++------------
 ...ApiXmlCommentSupport.generated.verified.cs | 24 +++++------------
 ...ApiXmlCommentSupport.generated.verified.cs | 24 +++++------------
 ...ApiXmlCommentSupport.generated.verified.cs | 24 +++++------------
 .../Microsoft.AspNetCore.OpenApi.Tests.csproj |  4 ++-
 10 files changed, 55 insertions(+), 90 deletions(-)

diff --git a/src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets b/src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets
index c046f1ca157e..9bb4072a48b5 100644
--- a/src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets
+++ b/src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets
@@ -8,7 +8,6 @@
   
     
-      
        throw new InvalidOperationException("Invalid overload variant for `AddOpenApi`.")
+        _ => string.Empty // Effectively no-op for AddOpenApi invocations that do not conform to a variant
     };
 
     internal static string GenerateAddOpenApiInterceptions(ImmutableArray<(AddOpenApiInvocation Source, int Index, ImmutableArray Elements)> groupedAddOpenApiInvocations)
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
index ff674eb26698..5fbf644cddd9 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
@@ -101,7 +101,7 @@ private static string ReplaceGenericArguments(string typeName)
                 // Replace everything between < and > with empty strings separated by commas
                 var segment = result.ToString(start + 1, i - start - 1);
                 var commaCount = segment.Count(c => c == ',');
-                var replacement = string.Join(",", Enumerable.Repeat("", commaCount + 1));
+                var replacement = new string(',', commaCount);
                 result.Remove(start + 1, i - start - 1);
                 result.Insert(start + 1, replacement);
                 i = start + replacement.Length + 1;
@@ -143,6 +143,17 @@ internal static bool FilterInvocations(SyntaxNode node, CancellationToken _)
     internal static AddOpenApiInvocation GetAddOpenApiOverloadVariant(GeneratorSyntaxContext context, CancellationToken cancellationToken)
     {
         var invocationExpression = (InvocationExpressionSyntax)context.Node;
+
+        // Soft check to validate that the method is from the OpenApiServiceCollectionExtensions class
+        // in the Microsoft.AspNetCore.OpenApi assembly.
+        var symbol = context.SemanticModel.GetSymbolInfo(invocationExpression, cancellationToken).Symbol;
+        if (symbol is not IMethodSymbol methodSymbol
+            || methodSymbol.ContainingType.Name != "OpenApiServiceCollectionExtensions"
+            || methodSymbol.ContainingAssembly.Name != "Microsoft.AspNetCore.OpenApi")
+        {
+            return new(AddOpenApiOverloadVariant.Unknown, invocationExpression, null);
+        }
+
         var interceptableLocation = context.SemanticModel.GetInterceptableLocation(invocationExpression, cancellationToken);
         var argumentsCount = invocationExpression.ArgumentList.Arguments.Count;
         if (argumentsCount == 0)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
index a98ce789b5cb..e4c1fa2bb27c 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
@@ -31,7 +31,9 @@
   
 
   
-    
+    
   
 
 
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
index 247fbb23330e..9b7a54360ad1 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
@@ -59,17 +59,7 @@ file record XmlComment(
     file static class XmlCommentCache
     {
         private static Dictionary<(Type?, string?), XmlComment>? _cache;
-        public static Dictionary<(Type?, string?), XmlComment> Cache
-        {
-            get
-            {
-                if (_cache is null)
-                {
-                    _cache = GenerateCacheEntries();
-                }
-                return _cache;
-            }
-        }
+        public static Dictionary<(Type?, string?), XmlComment> Cache => _cache ??= GenerateCacheEntries();
 
         private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
         {
@@ -95,17 +85,17 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
             }
             if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
             {
-                if (methodComment.Summary is not null)
+                if (methodComment.Summary is { } summary)
                 {
-                    operation.Summary = methodComment.Summary;
+                    operation.Summary = summary;
                 }
-                if (methodComment.Description is not null)
+                if (methodComment.Description is { } description)
                 {
-                    operation.Description = methodComment.Description;
+                    operation.Description = description;
                 }
-                if (methodComment.Remarks is not null)
+                if (methodComment.Remarks is { } remarks)
                 {
-                    operation.Description = methodComment.Remarks;
+                    operation.Description = remarks;
                 }
                 if (methodComment.Parameters is { Count: > 0})
                 {
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
index 6dde49c49bf9..da44411e9a7a 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
@@ -59,17 +59,7 @@ file record XmlComment(
     file static class XmlCommentCache
     {
         private static Dictionary<(Type?, string?), XmlComment>? _cache;
-        public static Dictionary<(Type?, string?), XmlComment> Cache
-        {
-            get
-            {
-                if (_cache is null)
-                {
-                    _cache = GenerateCacheEntries();
-                }
-                return _cache;
-            }
-        }
+        public static Dictionary<(Type?, string?), XmlComment> Cache => _cache ??= GenerateCacheEntries();
 
         private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
         {
@@ -98,17 +88,17 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
             }
             if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
             {
-                if (methodComment.Summary is not null)
+                if (methodComment.Summary is { } summary)
                 {
-                    operation.Summary = methodComment.Summary;
+                    operation.Summary = summary;
                 }
-                if (methodComment.Description is not null)
+                if (methodComment.Description is { } description)
                 {
-                    operation.Description = methodComment.Description;
+                    operation.Description = description;
                 }
-                if (methodComment.Remarks is not null)
+                if (methodComment.Remarks is { } remarks)
                 {
-                    operation.Description = methodComment.Remarks;
+                    operation.Description = remarks;
                 }
                 if (methodComment.Parameters is { Count: > 0})
                 {
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
index 0a54e1af25e0..fae70892edd4 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
@@ -59,17 +59,7 @@ file record XmlComment(
     file static class XmlCommentCache
     {
         private static Dictionary<(Type?, string?), XmlComment>? _cache;
-        public static Dictionary<(Type?, string?), XmlComment> Cache
-        {
-            get
-            {
-                if (_cache is null)
-                {
-                    _cache = GenerateCacheEntries();
-                }
-                return _cache;
-            }
-        }
+        public static Dictionary<(Type?, string?), XmlComment> Cache => _cache ??= GenerateCacheEntries();
 
         private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
         {
@@ -102,17 +92,17 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
             }
             if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
             {
-                if (methodComment.Summary is not null)
+                if (methodComment.Summary is { } summary)
                 {
-                    operation.Summary = methodComment.Summary;
+                    operation.Summary = summary;
                 }
-                if (methodComment.Description is not null)
+                if (methodComment.Description is { } description)
                 {
-                    operation.Description = methodComment.Description;
+                    operation.Description = description;
                 }
-                if (methodComment.Remarks is not null)
+                if (methodComment.Remarks is { } remarks)
                 {
-                    operation.Description = methodComment.Remarks;
+                    operation.Description = remarks;
                 }
                 if (methodComment.Parameters is { Count: > 0})
                 {
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
index 42d13eaa6cef..2947f336dc25 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
@@ -59,17 +59,7 @@ file record XmlComment(
     file static class XmlCommentCache
     {
         private static Dictionary<(Type?, string?), XmlComment>? _cache;
-        public static Dictionary<(Type?, string?), XmlComment> Cache
-        {
-            get
-            {
-                if (_cache is null)
-                {
-                    _cache = GenerateCacheEntries();
-                }
-                return _cache;
-            }
-        }
+        public static Dictionary<(Type?, string?), XmlComment> Cache => _cache ??= GenerateCacheEntries();
 
         private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
         {
@@ -123,17 +113,17 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
             }
             if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
             {
-                if (methodComment.Summary is not null)
+                if (methodComment.Summary is { } summary)
                 {
-                    operation.Summary = methodComment.Summary;
+                    operation.Summary = summary;
                 }
-                if (methodComment.Description is not null)
+                if (methodComment.Description is { } description)
                 {
-                    operation.Description = methodComment.Description;
+                    operation.Description = description;
                 }
-                if (methodComment.Remarks is not null)
+                if (methodComment.Remarks is { } remarks)
                 {
-                    operation.Description = methodComment.Remarks;
+                    operation.Description = remarks;
                 }
                 if (methodComment.Parameters is { Count: > 0})
                 {
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Microsoft.AspNetCore.OpenApi.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Microsoft.AspNetCore.OpenApi.Tests.csproj
index 3dd050f9bded..25e04bc98a6f 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Microsoft.AspNetCore.OpenApi.Tests.csproj
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Microsoft.AspNetCore.OpenApi.Tests.csproj
@@ -36,6 +36,8 @@
   
 
   
-    
+    
   
 

From d3eaad1596efdc5dc72523ef8aa5b63873d8b691 Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Wed, 12 Feb 2025 15:52:51 -0800
Subject: [PATCH 07/13] Move SnapshotsTestHelper to top-level

---
 .../Helpers/HostFactoryResolver.cs            | 344 ------------
 .../Helpers/SnapshotTestHelper.cs             | 170 ------
 .../SnapshotTestHelper.cs                     | 509 ++++++++++++++++++
 3 files changed, 509 insertions(+), 514 deletions(-)
 delete mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
 delete mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
 create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs

diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
deleted file mode 100644
index d3101da3066a..000000000000
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/HostFactoryResolver.cs
+++ /dev/null
@@ -1,344 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Diagnostics;
-using System.Reflection;
-
-namespace Microsoft.Extensions.Hosting;
-
-internal sealed class HostFactoryResolver
-{
-    private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
-
-    public const string BuildWebHost = nameof(BuildWebHost);
-    public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder);
-    public const string CreateHostBuilder = nameof(CreateHostBuilder);
-    private const string TimeoutEnvironmentKey = "DOTNET_HOST_FACTORY_RESOLVER_DEFAULT_TIMEOUT_IN_SECONDS";
-
-    // The amount of time we wait for the diagnostic source events to fire
-    private static readonly TimeSpan s_defaultWaitTimeout = SetupDefaultTimeout();
-
-    private static TimeSpan SetupDefaultTimeout()
-    {
-        if (Debugger.IsAttached)
-        {
-            return Timeout.InfiniteTimeSpan;
-        }
-
-        if (uint.TryParse(Environment.GetEnvironmentVariable(TimeoutEnvironmentKey), out uint timeoutInSeconds))
-        {
-            return TimeSpan.FromSeconds((int)timeoutInSeconds);
-        }
-
-        return TimeSpan.FromMinutes(5);
-    }
-
-    public static Func ResolveWebHostFactory(Assembly assembly)
-    {
-        return ResolveFactory(assembly, BuildWebHost);
-    }
-
-    public static Func ResolveWebHostBuilderFactory(Assembly assembly)
-    {
-        return ResolveFactory(assembly, CreateWebHostBuilder);
-    }
-
-    public static Func ResolveHostBuilderFactory(Assembly assembly)
-    {
-        return ResolveFactory(assembly, CreateHostBuilder);
-    }
-
-    // This helpers encapsulates all of the complex logic required to:
-    // 1. Execute the entry point of the specified assembly in a different thread.
-    // 2. Wait for the diagnostic source events to fire
-    // 3. Give the caller a chance to execute logic to mutate the IHostBuilder
-    // 4. Resolve the instance of the applications's IHost
-    // 5. Allow the caller to determine if the entry point has completed
-    public static Func ResolveHostFactory(Assembly assembly,
-                                                             TimeSpan waitTimeout = default,
-                                                             bool stopApplication = true,
-                                                             Action configureHostBuilder = null,
-                                                             Action entrypointCompleted = null)
-    {
-        if (assembly.EntryPoint is null)
-        {
-            return null;
-        }
-
-        return args => new HostingListener(args, assembly.EntryPoint, waitTimeout == default ? s_defaultWaitTimeout : waitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost();
-    }
-
-    private static Func ResolveFactory(Assembly assembly, string name)
-    {
-        var programType = assembly.EntryPoint.DeclaringType;
-        if (programType == null)
-        {
-            return null;
-        }
-
-        var factory = programType.GetMethod(name, DeclaredOnlyLookup);
-        if (!IsFactory(factory))
-        {
-            return null;
-        }
-
-        return args => (T)factory!.Invoke(null, [args])!;
-    }
-
-    // TReturn Factory(string[] args);
-    private static bool IsFactory(MethodInfo factory)
-    {
-        return factory != null
-            && typeof(TReturn).IsAssignableFrom(factory.ReturnType)
-            && factory.GetParameters().Length == 1
-            && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType);
-    }
-
-    // Used by EF tooling without any Hosting references. Looses some return type safety checks.
-    public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan waitTimeout = default)
-    {
-        // Prefer the older patterns by default for back compat.
-        var webHostFactory = ResolveWebHostFactory(assembly);
-        if (webHostFactory != null)
-        {
-            return args =>
-            {
-                var webHost = webHostFactory(args);
-                return GetServiceProvider(webHost);
-            };
-        }
-
-        var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly);
-        if (webHostBuilderFactory != null)
-        {
-            return args =>
-            {
-                var webHostBuilder = webHostBuilderFactory(args);
-                var webHost = Build(webHostBuilder);
-                return GetServiceProvider(webHost);
-            };
-        }
-
-        var hostBuilderFactory = ResolveHostBuilderFactory(assembly);
-        if (hostBuilderFactory != null)
-        {
-            return args =>
-            {
-                var hostBuilder = hostBuilderFactory(args);
-                var host = Build(hostBuilder);
-                return GetServiceProvider(host);
-            };
-        }
-
-        var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout);
-        if (hostFactory != null)
-        {
-            return args =>
-            {
-                static bool IsApplicationNameArg(string arg)
-                    => arg.Equals("--applicationName", StringComparison.OrdinalIgnoreCase) ||
-                        arg.Equals("/applicationName", StringComparison.OrdinalIgnoreCase);
-
-                if (!args.Any(arg => IsApplicationNameArg(arg)) && assembly.GetName().Name is string assemblyName)
-                {
-                    args = [.. args, .. new[] { "--applicationName", assemblyName }];
-                }
-
-                var host = hostFactory(args);
-                return GetServiceProvider(host);
-            };
-        }
-
-        return null;
-    }
-
-    private static object Build(object builder)
-    {
-        var buildMethod = builder.GetType().GetMethod("Build");
-        return buildMethod.Invoke(builder, []);
-    }
-
-    private static IServiceProvider GetServiceProvider(object host)
-    {
-        if (host == null)
-        {
-            return null;
-        }
-        var hostType = host.GetType();
-        var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup);
-        return (IServiceProvider)servicesProperty.GetValue(host);
-    }
-
-    private sealed class HostingListener : IObserver, IObserver>
-    {
-        private readonly string[] _args;
-        private readonly MethodInfo _entryPoint;
-        private readonly TimeSpan _waitTimeout;
-        private readonly bool _stopApplication;
-
-        private readonly TaskCompletionSource _hostTcs = new();
-        private IDisposable _disposable;
-        private readonly Action _configure;
-        private readonly Action _entrypointCompleted;
-        private static readonly AsyncLocal _currentListener = new();
-
-        public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entrypointCompleted)
-        {
-            _args = args;
-            _entryPoint = entryPoint;
-            _waitTimeout = waitTimeout;
-            _stopApplication = stopApplication;
-            _configure = configure;
-            _entrypointCompleted = entrypointCompleted;
-        }
-
-        public object CreateHost()
-        {
-            using var subscription = DiagnosticListener.AllListeners.Subscribe(this);
-
-            // Kick off the entry point on a new thread so we don't block the current one
-            // in case we need to timeout the execution
-            var thread = new Thread(() =>
-            {
-                Exception exception = null;
-
-                try
-                {
-                    // Set the async local to the instance of the HostingListener so we can filter events that
-                    // aren't scoped to this execution of the entry point.
-                    _currentListener.Value = this;
-
-                    var parameters = _entryPoint.GetParameters();
-                    if (parameters.Length == 0)
-                    {
-                        _entryPoint.Invoke(null, []);
-                    }
-                    else
-                    {
-                        _entryPoint.Invoke(null, new object[] { _args });
-                    }
-
-                    // Try to set an exception if the entry point returns gracefully, this will force
-                    // build to throw
-                    _hostTcs.TrySetException(new InvalidOperationException("The entry point exited without ever building an IHost."));
-                }
-                catch (TargetInvocationException tie) when (tie.InnerException.GetType().Name == "HostAbortedException")
-                {
-                    // The host was stopped by our own logic
-                }
-                catch (TargetInvocationException tie)
-                {
-                    exception = tie.InnerException ?? tie;
-
-                    // Another exception happened, propagate that to the caller
-                    _hostTcs.TrySetException(exception);
-                }
-                catch (Exception ex)
-                {
-                    exception = ex;
-
-                    // Another exception happened, propagate that to the caller
-                    _hostTcs.TrySetException(ex);
-                }
-                finally
-                {
-                    // Signal that the entry point is completed
-                    _entrypointCompleted.Invoke(exception);
-                }
-            })
-            {
-                // Make sure this doesn't hang the process
-                IsBackground = true
-            };
-
-            // Start the thread
-            thread.Start();
-
-            try
-            {
-                // Wait before throwing an exception
-                if (!_hostTcs.Task.Wait(_waitTimeout))
-                {
-                    throw new InvalidOperationException($"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable.");
-                }
-            }
-            catch (AggregateException) when (_hostTcs.Task.IsCompleted)
-            {
-                // Lets this propagate out of the call to GetAwaiter().GetResult()
-            }
-
-            Debug.Assert(_hostTcs.Task.IsCompleted);
-
-            return _hostTcs.Task.GetAwaiter().GetResult();
-        }
-
-        public void OnCompleted()
-        {
-            _disposable.Dispose();
-        }
-
-        public void OnError(Exception error)
-        {
-
-        }
-
-        public void OnNext(DiagnosticListener value)
-        {
-            if (_currentListener.Value != this)
-            {
-                // Ignore events that aren't for this listener
-                return;
-            }
-
-            if (value.Name == "Microsoft.Extensions.Hosting")
-            {
-                _disposable = value.Subscribe(this);
-            }
-        }
-
-        public void OnNext(KeyValuePair value)
-        {
-            if (_currentListener.Value != this)
-            {
-                // Ignore events that aren't for this listener
-                return;
-            }
-
-            if (value.Key == "HostBuilding")
-            {
-                _configure.Invoke(value.Value!);
-            }
-
-            if (value.Key == "HostBuilt")
-            {
-                _hostTcs.TrySetResult(value.Value!);
-
-                if (_stopApplication)
-                {
-                    // Stop the host from running further
-                    ThrowHostAborted();
-                }
-            }
-        }
-
-        // HostFactoryResolver is used by tools that explicitly don't want to reference Microsoft.Extensions.Hosting assemblies.
-        // So don't depend on the public HostAbortedException directly. Instead, load the exception type dynamically if it can
-        // be found. If it can't (possibly because the app is using an older version), throw a private exception with the same name.
-        private static void ThrowHostAborted()
-        {
-            var publicHostAbortedExceptionType = Type.GetType("Microsoft.Extensions.Hosting.HostAbortedException, Microsoft.Extensions.Hosting.Abstractions", throwOnError: false);
-            if (publicHostAbortedExceptionType != null)
-            {
-                throw (Exception)Activator.CreateInstance(publicHostAbortedExceptionType)!;
-            }
-            else
-            {
-                throw new HostAbortedException();
-            }
-        }
-
-        private sealed class HostAbortedException : Exception
-        {
-        }
-    }
-}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
deleted file mode 100644
index 147d3fb837a2..000000000000
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Helpers/SnapshotTestHelper.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Runtime.Loader;
-using System.Text;
-using Microsoft.AspNetCore.Hosting.Server;
-using Microsoft.AspNetCore.Http.Features;
-using Microsoft.AspNetCore.InternalTesting;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Emit;
-using Microsoft.CodeAnalysis.Text;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.OpenApi.Models;
-
-namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
-
-[UsesVerify]
-public static class SnapshotTestHelper
-{
-    private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview)
-        .WithFeatures([new KeyValuePair("InterceptorsNamespaces", "Microsoft.AspNetCore.OpenApi.Generated")]);
-
-    public static Task Verify(string source, IIncrementalGenerator generator, out Compilation compilation)
-    {
-        var references = AppDomain.CurrentDomain.GetAssemblies()
-                .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location))
-                .Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
-                .Concat(
-                [
-                    MetadataReference.CreateFromFile(typeof(Builder.WebApplicationBuilder).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(OpenApiOptions).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(Builder.EndpointRouteBuilderExtensions).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(Builder.IApplicationBuilder).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(Mvc.ApiExplorer.IApiDescriptionProvider).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(Mvc.ControllerBase).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(MvcServiceCollectionExtensions).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(MvcCoreMvcBuilderExtensions).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(Http.TypedResults).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(System.Text.Json.Nodes.JsonArray).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
-                    MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location),
-                ]);
-        var inputCompilation = CSharpCompilation.Create("OpenApiXmlCommentGeneratorSample",
-            [CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: "Program.cs")],
-            references,
-            new CSharpCompilationOptions(OutputKind.ConsoleApplication));
-        var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions);
-        return Verifier
-            .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics))
-            .UseDirectory(SkipOnHelixAttribute.OnHelix()
-                ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "snapshots")
-                : "../snapshots");
-    }
-
-    public static async Task VerifyOpenApi(Compilation compilation, Action verifyFunc)
-    {
-        var assemblyName = compilation.AssemblyName;
-        var symbolsName = Path.ChangeExtension(assemblyName, "pdb");
-
-        var output = new MemoryStream();
-        var pdb = new MemoryStream();
-
-        var emitOptions = new EmitOptions(
-            debugInformationFormat: DebugInformationFormat.PortablePdb,
-            pdbFilePath: symbolsName,
-            outputNameOverride: $"TestProject-{Guid.NewGuid()}");
-
-        var embeddedTexts = new List();
-
-        foreach (var syntaxTree in compilation.SyntaxTrees)
-        {
-            var text = syntaxTree.GetText();
-            var encoding = text.Encoding ?? Encoding.UTF8;
-            var buffer = encoding.GetBytes(text.ToString());
-            var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true);
-
-            var syntaxRootNode = (CSharpSyntaxNode)syntaxTree.GetRoot();
-            var newSyntaxTree = CSharpSyntaxTree.Create(syntaxRootNode, options: ParseOptions, encoding: encoding, path: syntaxTree.FilePath);
-
-            compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree);
-
-            embeddedTexts.Add(EmbeddedText.FromSource(syntaxTree.FilePath, sourceText));
-        }
-
-        var result = compilation.Emit(output, pdb, options: emitOptions, embeddedTexts: embeddedTexts);
-
-        Assert.Empty(result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning));
-        Assert.True(result.Success);
-
-        output.Position = 0;
-        pdb.Position = 0;
-
-        var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb);
-
-        void ConfigureHostBuilder(object hostBuilder)
-        {
-            ((IHostBuilder)hostBuilder).ConfigureServices((context, services) =>
-            {
-                services.AddSingleton();
-                services.AddSingleton();
-            });
-        }
-
-        var waitForStartTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
-        void OnEntryPointExit(Exception exception)
-        {
-            // If the entry point exited, we'll try to complete the wait
-            if (exception != null)
-            {
-                waitForStartTcs.TrySetException(exception);
-            }
-            else
-            {
-                waitForStartTcs.TrySetResult(0);
-            }
-        }
-
-        var factory = HostFactoryResolver.ResolveHostFactory(assembly,
-            stopApplication: false,
-            configureHostBuilder: ConfigureHostBuilder,
-            entrypointCompleted: OnEntryPointExit);
-
-        if (factory == null)
-        {
-            return;
-        }
-
-        var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services;
-
-        var applicationLifetime = services.GetRequiredService();
-        using (var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(0)))
-        {
-            waitForStartTcs.Task.Wait();
-            var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == "Microsoft.AspNetCore.OpenApi");
-            var serviceType = targetAssembly.GetType("Microsoft.Extensions.ApiDescriptions.IDocumentProvider", throwOnError: false);
-
-            if (serviceType == null)
-            {
-                return;
-            }
-
-            var service = services.GetService(serviceType) ?? throw new InvalidOperationException("Could not resolve IDocumntProvider service.");
-            using var stream = new MemoryStream();
-            var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
-            using var writer = new StreamWriter(stream, encoding, bufferSize: 1024, leaveOpen: true);
-            var targetMethod = serviceType.GetMethod("GenerateAsync", [typeof(string), typeof(TextWriter)]) ?? throw new InvalidOperationException("Could not resolve GenerateAsync method.");
-            targetMethod.Invoke(service, ["v1", writer]);
-            stream.Position = 0;
-            var (document, _) = await OpenApiDocument.LoadAsync(stream, "json");
-            verifyFunc(document);
-        }
-    }
-
-    private sealed class NoopHostLifetime : IHostLifetime
-    {
-        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-        public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-    }
-
-    private sealed class NoopServer : IServer
-    {
-        public IFeatureCollection Features { get; } = new FeatureCollection();
-        public void Dispose() { }
-        public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext: notnull => Task.CompletedTask;
-        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-    }
-
-}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs
new file mode 100644
index 000000000000..a711b862147b
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs
@@ -0,0 +1,509 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Text;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.InternalTesting;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Emit;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+[UsesVerify]
+public static class SnapshotTestHelper
+{
+    private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview)
+        .WithFeatures([new KeyValuePair("InterceptorsNamespaces", "Microsoft.AspNetCore.OpenApi.Generated")]);
+
+    public static Task Verify(string source, IIncrementalGenerator generator, out Compilation compilation)
+    {
+        var references = AppDomain.CurrentDomain.GetAssemblies()
+                .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location))
+                .Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
+                .Concat(
+                [
+                    MetadataReference.CreateFromFile(typeof(Builder.WebApplicationBuilder).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(OpenApiOptions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Builder.EndpointRouteBuilderExtensions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Builder.IApplicationBuilder).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Mvc.ApiExplorer.IApiDescriptionProvider).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Mvc.ControllerBase).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(MvcServiceCollectionExtensions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(MvcCoreMvcBuilderExtensions).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Http.TypedResults).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(System.Text.Json.Nodes.JsonArray).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
+                    MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location),
+                ]);
+        var inputCompilation = CSharpCompilation.Create("OpenApiXmlCommentGeneratorSample",
+            [CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: "Program.cs")],
+            references,
+            new CSharpCompilationOptions(OutputKind.ConsoleApplication));
+        var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions);
+        return Verifier
+            .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics))
+            .UseDirectory(SkipOnHelixAttribute.OnHelix()
+                ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "snapshots")
+                : "snapshots");
+    }
+
+    public static async Task VerifyOpenApi(Compilation compilation, Action verifyFunc)
+    {
+        var assemblyName = compilation.AssemblyName;
+        var symbolsName = Path.ChangeExtension(assemblyName, "pdb");
+
+        var output = new MemoryStream();
+        var pdb = new MemoryStream();
+
+        var emitOptions = new EmitOptions(
+            debugInformationFormat: DebugInformationFormat.PortablePdb,
+            pdbFilePath: symbolsName,
+            outputNameOverride: $"TestProject-{Guid.NewGuid()}");
+
+        var embeddedTexts = new List();
+
+        foreach (var syntaxTree in compilation.SyntaxTrees)
+        {
+            var text = syntaxTree.GetText();
+            var encoding = text.Encoding ?? Encoding.UTF8;
+            var buffer = encoding.GetBytes(text.ToString());
+            var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true);
+
+            var syntaxRootNode = (CSharpSyntaxNode)syntaxTree.GetRoot();
+            var newSyntaxTree = CSharpSyntaxTree.Create(syntaxRootNode, options: ParseOptions, encoding: encoding, path: syntaxTree.FilePath);
+
+            compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree);
+
+            embeddedTexts.Add(EmbeddedText.FromSource(syntaxTree.FilePath, sourceText));
+        }
+
+        var result = compilation.Emit(output, pdb, options: emitOptions, embeddedTexts: embeddedTexts);
+
+        Assert.Empty(result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning));
+        Assert.True(result.Success);
+
+        output.Position = 0;
+        pdb.Position = 0;
+
+        var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb);
+
+        void ConfigureHostBuilder(object hostBuilder)
+        {
+            ((IHostBuilder)hostBuilder).ConfigureServices((context, services) =>
+            {
+                services.AddSingleton();
+                services.AddSingleton();
+            });
+        }
+
+        var waitForStartTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+        void OnEntryPointExit(Exception exception)
+        {
+            // If the entry point exited, we'll try to complete the wait
+            if (exception != null)
+            {
+                waitForStartTcs.TrySetException(exception);
+            }
+            else
+            {
+                waitForStartTcs.TrySetResult(0);
+            }
+        }
+
+        var factory = HostFactoryResolver.ResolveHostFactory(assembly,
+            stopApplication: false,
+            configureHostBuilder: ConfigureHostBuilder,
+            entrypointCompleted: OnEntryPointExit);
+
+        if (factory == null)
+        {
+            return;
+        }
+
+        var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services;
+
+        var applicationLifetime = services.GetRequiredService();
+        using (var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(0)))
+        {
+            waitForStartTcs.Task.Wait();
+            var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == "Microsoft.AspNetCore.OpenApi");
+            var serviceType = targetAssembly.GetType("Microsoft.Extensions.ApiDescriptions.IDocumentProvider", throwOnError: false);
+
+            if (serviceType == null)
+            {
+                return;
+            }
+
+            var service = services.GetService(serviceType) ?? throw new InvalidOperationException("Could not resolve IDocumntProvider service.");
+            using var stream = new MemoryStream();
+            var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+            using var writer = new StreamWriter(stream, encoding, bufferSize: 1024, leaveOpen: true);
+            var targetMethod = serviceType.GetMethod("GenerateAsync", [typeof(string), typeof(TextWriter)]) ?? throw new InvalidOperationException("Could not resolve GenerateAsync method.");
+            targetMethod.Invoke(service, ["v1", writer]);
+            stream.Position = 0;
+            var (document, _) = await OpenApiDocument.LoadAsync(stream, "json");
+            verifyFunc(document);
+        }
+    }
+
+    private sealed class NoopHostLifetime : IHostLifetime
+    {
+        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+    }
+
+    private sealed class NoopServer : IServer
+    {
+        public IFeatureCollection Features { get; } = new FeatureCollection();
+        public void Dispose() { }
+        public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull => Task.CompletedTask;
+        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+    }
+
+    private sealed class HostFactoryResolver
+    {
+        private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
+
+        public const string BuildWebHost = nameof(BuildWebHost);
+        public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder);
+        public const string CreateHostBuilder = nameof(CreateHostBuilder);
+        private const string TimeoutEnvironmentKey = "DOTNET_HOST_FACTORY_RESOLVER_DEFAULT_TIMEOUT_IN_SECONDS";
+
+        // The amount of time we wait for the diagnostic source events to fire
+        private static readonly TimeSpan s_defaultWaitTimeout = SetupDefaultTimeout();
+
+        private static TimeSpan SetupDefaultTimeout()
+        {
+            if (Debugger.IsAttached)
+            {
+                return Timeout.InfiniteTimeSpan;
+            }
+
+            if (uint.TryParse(Environment.GetEnvironmentVariable(TimeoutEnvironmentKey), out uint timeoutInSeconds))
+            {
+                return TimeSpan.FromSeconds((int)timeoutInSeconds);
+            }
+
+            return TimeSpan.FromMinutes(5);
+        }
+
+        public static Func ResolveWebHostFactory(Assembly assembly)
+        {
+            return ResolveFactory(assembly, BuildWebHost);
+        }
+
+        public static Func ResolveWebHostBuilderFactory(Assembly assembly)
+        {
+            return ResolveFactory(assembly, CreateWebHostBuilder);
+        }
+
+        public static Func ResolveHostBuilderFactory(Assembly assembly)
+        {
+            return ResolveFactory(assembly, CreateHostBuilder);
+        }
+
+        // This helpers encapsulates all of the complex logic required to:
+        // 1. Execute the entry point of the specified assembly in a different thread.
+        // 2. Wait for the diagnostic source events to fire
+        // 3. Give the caller a chance to execute logic to mutate the IHostBuilder
+        // 4. Resolve the instance of the applications's IHost
+        // 5. Allow the caller to determine if the entry point has completed
+        public static Func ResolveHostFactory(Assembly assembly,
+                                                                 TimeSpan waitTimeout = default,
+                                                                 bool stopApplication = true,
+                                                                 Action configureHostBuilder = null,
+                                                                 Action entrypointCompleted = null)
+        {
+            if (assembly.EntryPoint is null)
+            {
+                return null;
+            }
+
+            return args => new HostingListener(args, assembly.EntryPoint, waitTimeout == default ? s_defaultWaitTimeout : waitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost();
+        }
+
+        private static Func ResolveFactory(Assembly assembly, string name)
+        {
+            var programType = assembly.EntryPoint.DeclaringType;
+            if (programType == null)
+            {
+                return null;
+            }
+
+            var factory = programType.GetMethod(name, DeclaredOnlyLookup);
+            if (!IsFactory(factory))
+            {
+                return null;
+            }
+
+            return args => (T)factory!.Invoke(null, [args])!;
+        }
+
+        // TReturn Factory(string[] args);
+        private static bool IsFactory(MethodInfo factory)
+        {
+            return factory != null
+                && typeof(TReturn).IsAssignableFrom(factory.ReturnType)
+                && factory.GetParameters().Length == 1
+                && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType);
+        }
+
+        // Used by EF tooling without any Hosting references. Looses some return type safety checks.
+        public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan waitTimeout = default)
+        {
+            // Prefer the older patterns by default for back compat.
+            var webHostFactory = ResolveWebHostFactory(assembly);
+            if (webHostFactory != null)
+            {
+                return args =>
+                {
+                    var webHost = webHostFactory(args);
+                    return GetServiceProvider(webHost);
+                };
+            }
+
+            var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly);
+            if (webHostBuilderFactory != null)
+            {
+                return args =>
+                {
+                    var webHostBuilder = webHostBuilderFactory(args);
+                    var webHost = Build(webHostBuilder);
+                    return GetServiceProvider(webHost);
+                };
+            }
+
+            var hostBuilderFactory = ResolveHostBuilderFactory(assembly);
+            if (hostBuilderFactory != null)
+            {
+                return args =>
+                {
+                    var hostBuilder = hostBuilderFactory(args);
+                    var host = Build(hostBuilder);
+                    return GetServiceProvider(host);
+                };
+            }
+
+            var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout);
+            if (hostFactory != null)
+            {
+                return args =>
+                {
+                    static bool IsApplicationNameArg(string arg)
+                        => arg.Equals("--applicationName", StringComparison.OrdinalIgnoreCase) ||
+                            arg.Equals("/applicationName", StringComparison.OrdinalIgnoreCase);
+
+                    if (!args.Any(arg => IsApplicationNameArg(arg)) && assembly.GetName().Name is string assemblyName)
+                    {
+                        args = [.. args, .. new[] { "--applicationName", assemblyName }];
+                    }
+
+                    var host = hostFactory(args);
+                    return GetServiceProvider(host);
+                };
+            }
+
+            return null;
+        }
+
+        private static object Build(object builder)
+        {
+            var buildMethod = builder.GetType().GetMethod("Build");
+            return buildMethod.Invoke(builder, []);
+        }
+
+        private static IServiceProvider GetServiceProvider(object host)
+        {
+            if (host == null)
+            {
+                return null;
+            }
+            var hostType = host.GetType();
+            var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup);
+            return (IServiceProvider)servicesProperty.GetValue(host);
+        }
+
+        private sealed class HostingListener : IObserver, IObserver>
+        {
+            private readonly string[] _args;
+            private readonly MethodInfo _entryPoint;
+            private readonly TimeSpan _waitTimeout;
+            private readonly bool _stopApplication;
+
+            private readonly TaskCompletionSource _hostTcs = new();
+            private IDisposable _disposable;
+            private readonly Action _configure;
+            private readonly Action _entrypointCompleted;
+            private static readonly AsyncLocal _currentListener = new();
+
+            public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entrypointCompleted)
+            {
+                _args = args;
+                _entryPoint = entryPoint;
+                _waitTimeout = waitTimeout;
+                _stopApplication = stopApplication;
+                _configure = configure;
+                _entrypointCompleted = entrypointCompleted;
+            }
+
+            public object CreateHost()
+            {
+                using var subscription = DiagnosticListener.AllListeners.Subscribe(this);
+
+                // Kick off the entry point on a new thread so we don't block the current one
+                // in case we need to timeout the execution
+                var thread = new Thread(() =>
+                {
+                    Exception exception = null;
+
+                    try
+                    {
+                        // Set the async local to the instance of the HostingListener so we can filter events that
+                        // aren't scoped to this execution of the entry point.
+                        _currentListener.Value = this;
+
+                        var parameters = _entryPoint.GetParameters();
+                        if (parameters.Length == 0)
+                        {
+                            _entryPoint.Invoke(null, []);
+                        }
+                        else
+                        {
+                            _entryPoint.Invoke(null, new object[] { _args });
+                        }
+
+                        // Try to set an exception if the entry point returns gracefully, this will force
+                        // build to throw
+                        _hostTcs.TrySetException(new InvalidOperationException("The entry point exited without ever building an IHost."));
+                    }
+                    catch (TargetInvocationException tie) when (tie.InnerException.GetType().Name == "HostAbortedException")
+                    {
+                        // The host was stopped by our own logic
+                    }
+                    catch (TargetInvocationException tie)
+                    {
+                        exception = tie.InnerException ?? tie;
+
+                        // Another exception happened, propagate that to the caller
+                        _hostTcs.TrySetException(exception);
+                    }
+                    catch (Exception ex)
+                    {
+                        exception = ex;
+
+                        // Another exception happened, propagate that to the caller
+                        _hostTcs.TrySetException(ex);
+                    }
+                    finally
+                    {
+                        // Signal that the entry point is completed
+                        _entrypointCompleted.Invoke(exception);
+                    }
+                })
+                {
+                    // Make sure this doesn't hang the process
+                    IsBackground = true
+                };
+
+                // Start the thread
+                thread.Start();
+
+                try
+                {
+                    // Wait before throwing an exception
+                    if (!_hostTcs.Task.Wait(_waitTimeout))
+                    {
+                        throw new InvalidOperationException($"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable.");
+                    }
+                }
+                catch (AggregateException) when (_hostTcs.Task.IsCompleted)
+                {
+                    // Lets this propagate out of the call to GetAwaiter().GetResult()
+                }
+
+                Debug.Assert(_hostTcs.Task.IsCompleted);
+
+                return _hostTcs.Task.GetAwaiter().GetResult();
+            }
+
+            public void OnCompleted()
+            {
+                _disposable.Dispose();
+            }
+
+            public void OnError(Exception error)
+            {
+
+            }
+
+            public void OnNext(DiagnosticListener value)
+            {
+                if (_currentListener.Value != this)
+                {
+                    // Ignore events that aren't for this listener
+                    return;
+                }
+
+                if (value.Name == "Microsoft.Extensions.Hosting")
+                {
+                    _disposable = value.Subscribe(this);
+                }
+            }
+
+            public void OnNext(KeyValuePair value)
+            {
+                if (_currentListener.Value != this)
+                {
+                    // Ignore events that aren't for this listener
+                    return;
+                }
+
+                if (value.Key == "HostBuilding")
+                {
+                    _configure.Invoke(value.Value!);
+                }
+
+                if (value.Key == "HostBuilt")
+                {
+                    _hostTcs.TrySetResult(value.Value!);
+
+                    if (_stopApplication)
+                    {
+                        // Stop the host from running further
+                        ThrowHostAborted();
+                    }
+                }
+            }
+
+            // HostFactoryResolver is used by tools that explicitly don't want to reference Microsoft.Extensions.Hosting assemblies.
+            // So don't depend on the public HostAbortedException directly. Instead, load the exception type dynamically if it can
+            // be found. If it can't (possibly because the app is using an older version), throw a private exception with the same name.
+            private static void ThrowHostAborted()
+            {
+                var publicHostAbortedExceptionType = Type.GetType("Microsoft.Extensions.Hosting.HostAbortedException, Microsoft.Extensions.Hosting.Abstractions", throwOnError: false);
+                if (publicHostAbortedExceptionType != null)
+                {
+                    throw (Exception)Activator.CreateInstance(publicHostAbortedExceptionType)!;
+                }
+                else
+                {
+                    throw new HostAbortedException();
+                }
+            }
+
+            private sealed class HostAbortedException : Exception
+            {
+            }
+        }
+    }
+
+}

From 78bb55c65a8887fccb1a3d0ff4d87b004c409ab6 Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Wed, 12 Feb 2025 20:58:22 -0800
Subject: [PATCH 08/13] Try remove LinkBase attribute for HelixContent

---
 ...Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
index e4c1fa2bb27c..ff3ca4db6229 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests.csproj
@@ -31,9 +31,7 @@
   
 
   
-    
+    
   
 
 

From 320c49f5fc72454776ca10520f3c18c44bbac714 Mon Sep 17 00:00:00 2001
From: Safia Abdalla 
Date: Thu, 13 Feb 2025 10:00:28 -0800
Subject: [PATCH 09/13] Resolve more tag types and split up XmlComment

---
 .../gen/XmlCommentGenerator.Emitter.cs        |  96 ++--
 .../gen/XmlComments/XmlComment.InheritDoc.cs  | 443 +++++++++++++++++
 src/OpenApi/gen/XmlComments/XmlComment.cs     | 469 +++---------------
 .../CompletenessTests.cs                      | 369 ++++++++++++++
 ...ApiXmlCommentSupport.generated.verified.cs |  18 +-
 ...ApiXmlCommentSupport.generated.verified.cs | 329 ++++++++++++
 ...ApiXmlCommentSupport.generated.verified.cs |  24 +-
 ...ApiXmlCommentSupport.generated.verified.cs |  37 +-
 ...ApiXmlCommentSupport.generated.verified.cs |  74 +--
 9 files changed, 1385 insertions(+), 474 deletions(-)
 create mode 100644 src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
 create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs
 create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs

diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
index b88436d62748..bb2a36a60c28 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.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;
 using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.IO;
@@ -44,6 +43,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
 {
     using System;
     using System.Collections.Generic;
+    using System.Diagnostics.CodeAnalysis;
     using System.Linq;
     using System.Reflection;
     using System.Text.Json;
@@ -87,6 +87,16 @@ file static class XmlCommentCache
 {{commentsFromCompilation}}
             return _cache;
         }
+
+        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
+        {
+            if (type is not null && type.IsGenericType)
+            {
+                type = type.GetGenericTypeDefinition();
+            }
+
+            return XmlCommentCache.Cache.TryGetValue((type, memberName), out xmlComment);
+        }
     }
 
     {{GeneratedCodeAttribute}}
@@ -102,7 +112,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.Cache.TryGetValue((methodInfo.DeclaringType, methodInfo.Name), out var methodComment))
+            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo.Name, out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -173,7 +183,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.Cache.TryGetValue((propertyInfo.DeclaringType, propertyInfo.Name), out var propertyComment))
+                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -182,7 +192,8 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
                     }
                 }
             }
-            if (XmlCommentCache.Cache.TryGetValue((context.JsonTypeInfo.Type, null), out var typeComment))
+            System.Diagnostics.Debugger.Break();
+            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, null, out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -312,61 +323,88 @@ internal static string EmitCommentsCache(IEnumerable<(string, string?, XmlCommen
         return writer.ToString();
     }
 
+    private static string FormatStringForCode(string? input)
+    {
+        if (input == null)
+        {
+            return "null";
+        }
+
+        var formatted = input
+            .Replace("\"", "\"\""); // Escape double quotes
+
+        return $"@\"{formatted}\"";
+    }
+
     internal static string EmitSourceGeneratedXmlComment(XmlComment comment)
     {
         var writer = new StringWriter();
         var codeWriter = new CodeWriter(writer, baseIndent: 0);
         codeWriter.Write($"new XmlComment(");
-        codeWriter.Write(comment.Summary is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Summary, true)}\"\", ");
-        codeWriter.Write(comment.Description is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Description, true)}\"\", ");
-        codeWriter.Write(comment.Remarks is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Remarks, true)}\"\", ");
-        codeWriter.Write(comment.Returns is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Returns, true)}\"\", ");
-        codeWriter.Write(comment.Value is null ? "null," : $"\"\"{SymbolDisplay.FormatLiteral(comment.Value, true)}\"\", ");
-        codeWriter.Write(comment.Deprecated is null ? "false," : $"{SymbolDisplay.FormatPrimitive(comment.Deprecated == true ? "true" : "false", false, false)}, ");
-        if (comment.Examples is null)
+        codeWriter.Write(FormatStringForCode(comment.Summary) + ", ");
+        codeWriter.Write(FormatStringForCode(comment.Description) + ", ");
+        codeWriter.Write(FormatStringForCode(comment.Remarks) + ", ");
+        codeWriter.Write(FormatStringForCode(comment.Returns) + ", ");
+        codeWriter.Write(FormatStringForCode(comment.Value) + ", ");
+        codeWriter.Write(comment.Deprecated == true ? "true" : "false" + ", ");
+        if (comment.Examples is null || comment.Examples.Count == 0)
         {
             codeWriter.Write("null, ");
         }
         else
         {
-            codeWriter.Write("new List");
-            codeWriter.Write("{");
-            foreach (var example in comment.Examples)
+            codeWriter.Write("[");
+            for (int i = 0; i < comment.Examples.Count; i++)
             {
-                codeWriter.Write($"@\"{example}\", ");
+                var example = comment.Examples[i];
+                codeWriter.Write(FormatStringForCode(example));
+                if (i < comment.Examples.Count - 1)
+                {
+                    codeWriter.Write(", ");
+                }
             }
-            codeWriter.Write("}, ");
+            codeWriter.Write("], ");
         }
-        if (comment.Parameters is null)
+
+        if (comment.Parameters is null || comment.Parameters.Count == 0)
         {
             codeWriter.Write("null, ");
         }
         else
         {
-            codeWriter.Write("new List");
-            codeWriter.Write("{");
-            foreach (var parameter in comment.Parameters)
+            codeWriter.Write("[");
+            for (int i = 0; i < comment.Parameters.Count; i++)
             {
+                var parameter = comment.Parameters[i];
                 var exampleLiteral = string.IsNullOrEmpty(parameter.Example)
                     ? "null"
-                    : $"\"\"\"{parameter.Example!}\"\"\"";
-                codeWriter.Write($"new XmlParameterComment(@\"{parameter.Name}\", @\"{parameter.Description}\", {exampleLiteral}, {(parameter.Deprecated == true ? "true" : "false")}), ");
+                    : FormatStringForCode(parameter.Example!);
+                codeWriter.Write($"new XmlParameterComment(@\"{parameter.Name}\", @\"{parameter.Description}\", {exampleLiteral}, {(parameter.Deprecated == true ? "true" : "false")})");
+                if (i < comment.Parameters.Count - 1)
+                {
+                    codeWriter.Write(", ");
+                }
             }
-            codeWriter.Write("} ,");
+            codeWriter.Write("], ");
         }
-        if (comment.Responses is null)
+
+        if (comment.Responses is null || comment.Responses.Count == 0)
         {
             codeWriter.Write("null");
         }
         else
         {
-            codeWriter.Write("new List");
-            codeWriter.Write("{");
-            foreach (var response in comment.Responses)
+            codeWriter.Write("[");
+            for (int i = 0; i < comment.Responses.Count; i++)
             {
-                codeWriter.Write($"new XmlResponseComment(@\"{response.Code}\", @\"{response.Description}\", @\"{response.Example}\"), ");
+                var response = comment.Responses[i];
+                codeWriter.Write($"new XmlResponseComment(@\"{response.Code}\", @\"{response.Description}\", {(response.Example is null ? "null" : FormatStringForCode(response.Example))})");
+                if (i < comment.Responses.Count - 1)
+                {
+                    codeWriter.Write(", ");
+                }
             }
-            codeWriter.Write("}");
+            codeWriter.Write("]");
         }
         codeWriter.Write(")");
         return writer.ToString();
diff --git a/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs b/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
new file mode 100644
index 000000000000..ee6d503449a0
--- /dev/null
+++ b/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
@@ -0,0 +1,443 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using Microsoft.AspNetCore.Analyzers.Infrastructure;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+
+/// 
+/// Source code in this class is derived from Roslyn's Xml documentation comment processing
+/// to support the full resolution of  tags.
+/// For the original code, see https://github.com/dotnet/roslyn/blob/ef1d7fe925c94e96a93e4c9af50983e0f675a9fd/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs.
+/// 
+internal sealed partial class XmlComment
+{
+    private static string? GetDocumentationComment(ISymbol symbol, string xmlText, HashSet? visitedSymbols, Compilation compilation, CancellationToken cancellationToken)
+    {
+        try
+        {
+            if (string.IsNullOrEmpty(xmlText))
+            {
+                if (IsEligibleForAutomaticInheritdoc(symbol))
+                {
+                    xmlText = $@"";
+                }
+                else
+                {
+                    return string.Empty;
+                }
+            }
+
+            try
+            {
+                var element = XElement.Parse(xmlText, LoadOptions.PreserveWhitespace);
+                element.ReplaceNodes(RewriteMany(symbol, visitedSymbols, compilation, [.. element.Nodes()], cancellationToken));
+                xmlText = element.ToString(SaveOptions.DisableFormatting);
+            }
+            catch (Exception)
+            {
+                return null;
+            }
+            return xmlText;
+        }
+        catch (XmlException)
+        {
+            return null;
+        }
+    }
+
+    private static bool IsEligibleForAutomaticInheritdoc(ISymbol symbol)
+    {
+        // Only the following symbols are eligible to inherit documentation without an  element:
+        //
+        // * Members that override an inherited member
+        // * Members that implement an interface member
+        if (symbol.IsOverride)
+        {
+            return true;
+        }
+
+        if (symbol.ContainingType is null)
+        {
+            // Observed with certain implicit operators, such as operator==(void*, void*).
+            return false;
+        }
+
+        switch (symbol.Kind)
+        {
+            case SymbolKind.Method:
+            case SymbolKind.Property:
+            case SymbolKind.Event:
+                if (symbol.ExplicitOrImplicitInterfaceImplementations().Any())
+                {
+                    return true;
+                }
+
+                break;
+
+            default:
+                break;
+        }
+
+        return false;
+    }
+
+    private static XNode[] RewriteMany(ISymbol symbol, HashSet? visitedSymbols, Compilation compilation, XNode[] nodes, CancellationToken cancellationToken)
+    {
+        var result = new List();
+        foreach (var child in nodes)
+        {
+            result.AddRange(RewriteInheritdocElements(symbol, visitedSymbols, compilation, child, cancellationToken));
+        }
+
+        return [.. result];
+    }
+
+    private static XNode[] RewriteInheritdocElements(ISymbol symbol, HashSet? visitedSymbols, Compilation compilation, XNode node, CancellationToken cancellationToken)
+    {
+        if (node.NodeType == XmlNodeType.Element)
+        {
+            var element = (XElement)node;
+            if (ElementNameIs(element, DocumentationCommentXmlNames.InheritdocElementName))
+            {
+                var rewritten = RewriteInheritdocElement(symbol, visitedSymbols, compilation, element, cancellationToken);
+                if (rewritten is not null)
+                {
+                    return rewritten;
+                }
+            }
+        }
+
+        if (node is not XContainer container)
+        {
+            return [Copy(node, copyAttributeAnnotations: false)];
+        }
+
+        var oldNodes = container.Nodes();
+
+        // Do this after grabbing the nodes, so we don't see copies of them.
+        container = Copy(container, copyAttributeAnnotations: false);
+
+        // WARN: don't use node after this point - use container since it's already been copied.
+
+        if (oldNodes != null)
+        {
+            var rewritten = RewriteMany(symbol, visitedSymbols, compilation, [.. oldNodes], cancellationToken);
+            container.ReplaceNodes(rewritten);
+        }
+
+        return [container];
+    }
+
+    private static XNode[]? RewriteInheritdocElement(ISymbol memberSymbol, HashSet? visitedSymbols, Compilation compilation, XElement element, CancellationToken cancellationToken)
+    {
+        var crefAttribute = element.Attribute(XName.Get(DocumentationCommentXmlNames.CrefAttributeName));
+        var pathAttribute = element.Attribute(XName.Get(DocumentationCommentXmlNames.PathAttributeName));
+
+        var candidate = GetCandidateSymbol(memberSymbol);
+        var hasCandidateCref = candidate is object;
+
+        var hasCrefAttribute = crefAttribute is not null;
+        var hasPathAttribute = pathAttribute is not null;
+        if (!hasCrefAttribute && !hasCandidateCref)
+        {
+            // No cref available
+            return null;
+        }
+
+        ISymbol? symbol;
+        if (crefAttribute is null)
+        {
+            if (candidate is null)
+            {
+                return null;
+            }
+            symbol = candidate;
+        }
+        else
+        {
+            var crefValue = crefAttribute.Value;
+            symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefValue, compilation);
+            if (symbol is null)
+            {
+                return null;
+            }
+        }
+
+        visitedSymbols ??= [];
+        if (!visitedSymbols.Add(symbol!))
+        {
+            // Prevent recursion
+            return null;
+        }
+
+        try
+        {
+            var xmlDocumentation = symbol.GetDocumentationCommentXml(cancellationToken: cancellationToken);
+            if (xmlDocumentation is null)
+            {
+                return [];
+            }
+            var inheritedDocumentation = GetDocumentationComment(symbol, xmlDocumentation, visitedSymbols, compilation, cancellationToken);
+            if (inheritedDocumentation == string.Empty)
+            {
+                return [];
+            }
+
+            var document = XDocument.Parse(inheritedDocumentation, LoadOptions.PreserveWhitespace);
+            string xpathValue;
+            if (string.IsNullOrEmpty(pathAttribute?.Value))
+            {
+                xpathValue = BuildXPathForElement(element.Parent!);
+            }
+            else
+            {
+                xpathValue = pathAttribute!.Value;
+                if (xpathValue.StartsWith("/", StringComparison.InvariantCulture))
+                {
+                    // Account for the root  or  element
+                    xpathValue = "/*" + xpathValue;
+                }
+            }
+
+            // Consider the following code, we want Test.Clone to say "Clones a Test" instead of "Clones a int", thus
+            // we rewrite `typeparamref`s as cref pointing to the correct type:
+            /*
+                public class Test : ICloneable>
+                {
+                    /// 
+                    public Test Clone() => new();
+                }
+
+                /// A type that has clonable instances.
+                /// The type of instances that can be cloned.
+                public interface ICloneable
+                {
+                    /// Clones a .
+                    public T Clone();
+                }
+            */
+            // Note: there is no way to cref an instantiated generic type. See https://github.com/dotnet/csharplang/issues/401
+            var typeParameterRefs = document.Descendants(DocumentationCommentXmlNames.TypeParameterReferenceElementName).ToImmutableArray();
+            foreach (var typeParameterRef in typeParameterRefs)
+            {
+                if (typeParameterRef.Attribute(DocumentationCommentXmlNames.NameAttributeName) is XAttribute typeParamName)
+                {
+                    var targetTypeParameter = symbol.GetAllTypeParameters().FirstOrDefault(p => p.Name == typeParamName.Value);
+                    if (targetTypeParameter is not null
+                        && symbol.OriginalDefinition.GetAllTypeParameters().IndexOf(targetTypeParameter) is int index
+                        && index >= 0)
+                    {
+                        var typeArgs = symbol.GetAllTypeArguments();
+                        if (index < typeArgs.Length)
+                        {
+                            var docId = typeArgs[index].GetDocumentationCommentId();
+                            if (docId != null && !docId.StartsWith("!", StringComparison.OrdinalIgnoreCase))
+                            {
+                                var replacement = new XElement(DocumentationCommentXmlNames.SeeElementName);
+                                replacement.SetAttributeValue(DocumentationCommentXmlNames.CrefAttributeName, docId);
+                                typeParameterRef.ReplaceWith(replacement);
+                            }
+                        }
+                    }
+                }
+            }
+
+            var loadedElements = TrySelectNodes(document, xpathValue);
+            return loadedElements ?? [];
+        }
+        catch (XmlException)
+        {
+            return [];
+        }
+        finally
+        {
+            visitedSymbols.Remove(symbol);
+        }
+
+        // Local functions
+        static ISymbol? GetCandidateSymbol(ISymbol memberSymbol)
+        {
+            if (memberSymbol.ExplicitInterfaceImplementations().Any())
+            {
+                return memberSymbol.ExplicitInterfaceImplementations().First();
+            }
+            else if (memberSymbol.IsOverride)
+            {
+                return memberSymbol.GetOverriddenMember();
+            }
+
+            if (memberSymbol is IMethodSymbol methodSymbol)
+            {
+                if (methodSymbol.MethodKind is MethodKind.Constructor or MethodKind.StaticConstructor)
+                {
+                    var baseType = memberSymbol.ContainingType.BaseType;
+#nullable disable // Can 'baseType' be null here? https://github.com/dotnet/roslyn/issues/39166
+                    return baseType.Constructors.Where(c => IsSameSignature(methodSymbol, c)).FirstOrDefault();
+#nullable enable
+                }
+                else
+                {
+                    // check for implicit interface
+                    return methodSymbol.ExplicitOrImplicitInterfaceImplementations().FirstOrDefault();
+                }
+            }
+            else if (memberSymbol is INamedTypeSymbol typeSymbol)
+            {
+                if (typeSymbol.TypeKind == TypeKind.Class)
+                {
+                    // Classes use the base type as the default inheritance candidate. A different target (e.g. an
+                    // interface) can be provided via the 'path' attribute.
+                    return typeSymbol.BaseType;
+                }
+                else if (typeSymbol.TypeKind == TypeKind.Interface)
+                {
+                    return typeSymbol.Interfaces.FirstOrDefault();
+                }
+                else
+                {
+                    // This includes structs, enums, and delegates as mentioned in the inheritdoc spec
+                    return null;
+                }
+            }
+
+            return memberSymbol.ExplicitOrImplicitInterfaceImplementations().FirstOrDefault();
+        }
+
+        static bool IsSameSignature(IMethodSymbol left, IMethodSymbol right)
+        {
+            if (left.Parameters.Length != right.Parameters.Length)
+            {
+                return false;
+            }
+
+            if (left.IsStatic != right.IsStatic)
+            {
+                return false;
+            }
+
+            if (!SymbolEqualityComparer.Default.Equals(left.ReturnType, right.ReturnType))
+            {
+                return false;
+            }
+
+            for (var i = 0; i < left.Parameters.Length; i++)
+            {
+                if (!SymbolEqualityComparer.Default.Equals(left.Parameters[i].Type, right.Parameters[i].Type))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        static string BuildXPathForElement(XElement element)
+        {
+            if (ElementNameIs(element, "member") || ElementNameIs(element, "doc"))
+            {
+                // Avoid string concatenation allocations for inheritdoc as a top-level element
+                return "/*/node()[not(self::overloads)]";
+            }
+
+            var path = "/node()[not(self::overloads)]";
+            for (var current = element; current != null; current = current.Parent)
+            {
+                var currentName = current.Name.ToString();
+                if (ElementNameIs(current, "member") || ElementNameIs(current, "doc"))
+                {
+                    // Allow  and  to be used interchangeably
+                    currentName = "*";
+                }
+
+                path = "/" + currentName + path;
+            }
+
+            return path;
+        }
+    }
+
+    private static bool ElementNameIs(XElement element, string name)
+        => string.IsNullOrEmpty(element.Name.NamespaceName) && DocumentationCommentXmlNames.ElementEquals(element.Name.LocalName, name);
+
+    private static TNode Copy(TNode node, bool copyAttributeAnnotations)
+        where TNode : XNode
+    {
+        XNode copy;
+
+        // Documents can't be added to containers, so our usual copy trick won't work.
+        if (node.NodeType == XmlNodeType.Document)
+        {
+            copy = new XDocument((XDocument)(object)node);
+        }
+        else
+        {
+            XContainer temp = new XElement("temp");
+            temp.Add(node);
+            copy = temp.LastNode!;
+            temp.RemoveNodes();
+        }
+
+        AnalyzerDebug.Assert(copy != node);
+        AnalyzerDebug.Assert(copy.Parent == null); // Otherwise, when we give it one, it will be copied.
+
+        // Copy annotations, the above doesn't preserve them.
+        // We need to preserve Location annotations as well as line position annotations.
+        CopyAnnotations(node, copy);
+
+        // We also need to preserve line position annotations for all attributes
+        // since we report errors with attribute locations.
+        if (copyAttributeAnnotations && node.NodeType == XmlNodeType.Element)
+        {
+            var sourceElement = (XElement)(object)node;
+            var targetElement = (XElement)copy;
+
+            var sourceAttributes = sourceElement.Attributes().GetEnumerator();
+            var targetAttributes = targetElement.Attributes().GetEnumerator();
+            while (sourceAttributes.MoveNext() && targetAttributes.MoveNext())
+            {
+                AnalyzerDebug.Assert(sourceAttributes.Current.Name == targetAttributes.Current.Name);
+                CopyAnnotations(sourceAttributes.Current, targetAttributes.Current);
+            }
+        }
+
+        return (TNode)copy;
+    }
+
+    private static void CopyAnnotations(XObject source, XObject target)
+    {
+        foreach (var annotation in source.Annotations())
+        {
+            target.AddAnnotation(annotation);
+        }
+    }
+
+    private static XNode[]? TrySelectNodes(XNode node, string xpath)
+    {
+        try
+        {
+            var xpathResult = (IEnumerable)System.Xml.XPath.Extensions.XPathEvaluate(node, xpath);
+
+            // Throws InvalidOperationException if the result of the XPath is an XDocument:
+            return xpathResult?.Cast().ToArray();
+        }
+        catch (InvalidOperationException)
+        {
+            return null;
+        }
+        catch (XPathException)
+        {
+            return null;
+        }
+    }
+}
diff --git a/src/OpenApi/gen/XmlComments/XmlComment.cs b/src/OpenApi/gen/XmlComments/XmlComment.cs
index fae78ab345ed..e1ece052e315 100644
--- a/src/OpenApi/gen/XmlComments/XmlComment.cs
+++ b/src/OpenApi/gen/XmlComments/XmlComment.cs
@@ -2,20 +2,16 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
-using System.Collections;
 using System.Collections.Generic;
-using System.Collections.Immutable;
-using Microsoft.AspNetCore.Analyzers.Infrastructure;
 using System.Linq;
 using System.Threading;
-using System.Xml;
 using System.Xml.Linq;
 using System.Xml.XPath;
 using Microsoft.CodeAnalysis;
 
 namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
 
-internal sealed class XmlComment
+internal sealed partial class XmlComment
 {
     public string? Summary { get; internal set; }
     public string? Description { get; internal set; }
@@ -41,6 +37,16 @@ private XmlComment(Compilation compilation, string xml)
 
         ResolveCrefLink(compilation, doc, $"//{DocumentationCommentXmlNames.SeeAlsoElementName}[@cref]");
         ResolveCrefLink(compilation, doc, $"//{DocumentationCommentXmlNames.SeeElementName}[@cref]");
+        // Resolve  and  tags into bullets
+        ResolveListTags(doc);
+        // Resolve  tags into code blocks
+        ResolveCodeTags(doc, DocumentationCommentXmlNames.CodeElementName, "```");
+        // Resolve  and typeparamref tags into parameter names
+        ResolveParamRefTags(doc);
+        // Resolve  tags into underlying content
+        ResolveParaTags(doc);
+        // Resolve  tags into inline code blocks
+        ResolveCodeTags(doc, DocumentationCommentXmlNames.CElementName, "`");
 
         var nav = doc.CreateNavigator();
         Summary = GetSingleNodeValue(nav, "/member/summary");
@@ -55,367 +61,111 @@ private XmlComment(Compilation compilation, string xml)
         Responses = XmlResponseComment.GetXmlResponseCommentList(nav, "/member/response");
     }
 
-    public static XmlComment? Parse(ISymbol symbol, Compilation compilation, string xmlText, CancellationToken cancellationToken)
-    {
-        // Avoid processing empty or malformed XML comments.
-        if (string.IsNullOrEmpty(xmlText) ||
-            xmlText.StartsWith("