# OpenAPI XmlComments > Keeps the ASP.NET Core OpenAPI XML-comment generator working when one documentation ID arrives twice. An MSBuild package for APIs that reference Microsoft.AspNetCore.OpenApi. The XML-comment source generator builds one dictionary from the documentation of every project reference and of the API's own compilation, and de-duplicates neither, so a documented type that arrives from more than one assembly makes /openapi/v1.json answer 500 with "An item with the same key has already been added". This package removes every colliding documentation ID from the referenced XML documentation before compile, and warns (POAX001) about an ID it can only see once the API has been built. The generator stays on, so every XML description stays in the OpenAPI document. ## Documentation version - Channel: live (default) - Source: purview-dev/openapi-xmlcomments@main ## Resources - Repository: https://github.com/purview-dev/openapi-xmlcomments - Package: https://www.nuget.org/packages/Purview.OpenAPI.XmlComments - Project page: https://purview.dev/projects/openapi-xmlcomments/ - Documentation: https://purview.dev/docs/openapi-xmlcomments/ - Full machine-readable content: https://purview.dev/projects/openapi-xmlcomments/llms-full.txt - Concise machine-readable index: https://purview.dev/projects/openapi-xmlcomments/llms.txt # Getting Started `Purview.OpenAPI.XmlComments` keeps the `Microsoft.AspNetCore.OpenApi` XML-comment source generator working when the same documentation ID comes from more than one assembly. Without it, `/openapi/v1.json` answers `500` with: ```text System.ArgumentException: An item with the same key has already been added. Key: T:Some.Namespace.SomeType at ...XmlCommentCache.GenerateCacheEntries() ``` ## Do you need it? You do if all of these are true: - the API project references `Microsoft.AspNetCore.OpenApi` and its XML-comment generator is on (the default); - the API references projects that generate XML documentation (`GenerateDocumentationFile`); - a documented type with the same full name is compiled into more than one of them - typically an attribute or polyfill a source generator injects into every assembly, such as `Purview.Results.GenerateResultAttribute`. The key in the exception names the type - but only the first collision. To list every colliding ID and where each copy comes from, run the diagnostic in [Troubleshooting](troubleshooting/); AI agents get the same procedure from the [`purview-openapi-xmlcomments-duplicates`](agent-skills/) skill. ## Install ```xml ``` or once for every project in the repository (it does nothing where there is no OpenAPI package): ```xml ``` Then remove any workaround that turns the generator off, such as ```xml ``` ## Build Build as usual. If the injected type is carried by only **one** referenced project, the build warns [`POAX001`](diagnostics/) and names the line to add: ```xml ``` Next: [How It Works](how-it-works/), [Configuration](configuration/) and, if anything still fails, [Troubleshooting](troubleshooting/). --- # How It Works ## The generator's cache `Microsoft.AspNetCore.OpenApi` ships a source generator that turns XML documentation into OpenAPI descriptions. It emits a cache, built on first use, with one `Dictionary.Add` per documentation ID, from two sources: 1. **Project-reference documentation.** The package's `GenerateAdditionalXmlFilesForOpenApi` target adds the XML documentation file of each project reference to `AdditionalFiles`, and the generator adds every `` in each one. There is no accessibility filter: `internal` members are in those files too. 2. **The API's own compilation.** Every public, documented symbol - including the output of other source generators, whose post-initialization sources are visible to it. Neither is de-duplicated ([dotnet/aspnetcore#67192](https://github.com/dotnet/aspnetcore/issues/67192)). A type injected into every assembly is added once per referenced project and once more from the API, and the second `Add` throws when the first schema is transformed. ## What the package changes ```text ResolveReferences GenerateAdditionalXmlFilesForOpenApi (Microsoft.AspNetCore.OpenApi: adds project-reference XML) PurviewDeduplicateOpenAPIXmlComments (this package: filters it) CoreCompile (the generator runs here, on the filtered copies) PurviewCheckOpenAPIXmlCommentOverlap (this package: POAX001) ``` **Before compile**, `PurviewDeduplicateOpenAPIXmlComments`: 1. reads every `.xml` item in `AdditionalFiles` that is an XML documentation file (a `` root holding ``); anything else, or anything unreadable, is passed through untouched; 2. keys each `` the way the generator does, stripping a method's `~ReturnType` suffix except on conversion operators; 3. selects every ID that occurs more than once, plus every `PurviewOpenAPIXmlCommentExclude` ID; 4. removes **every** occurrence of each, writes a filtered copy of each file it changed to `obj///purview-openapi-xml/`, and swaps the copies into `AdditionalFiles`. Every occurrence goes, not all but the first, because the API normally declares the injected type as well: its own copy is then the only one left, and keeping a reference's copy would still collide with it. A duplicated ID is ambient code, not API surface, so no endpoint or schema description is lost. A copy is only rewritten when its content changes, so a no-change build still skips `CoreCompile`. **After compile**, `PurviewCheckOpenAPIXmlCommentOverlap` compares the API's fresh documentation file with the filtered reference documentation and reports [`POAX001`](../diagnostics/) for any ID still in both - the case where only one referenced project carries the type, so step 3 never saw a duplicate. ## What it cannot do - See the API's own symbols before they are compiled. That is why `POAX001` exists, and why its fix applies to the next build rather than the current one. - Fix a collision that never passes through `AdditionalFiles`. --- # Configuration Nothing needs configuring for the common case. Everything below is MSBuild, set in the API project or in `Directory.Build.props`. | Setting | Default | Effect | | --- | --- | --- | | `false` | `true` | Turns the package off: neither target runs. | | `` | none | Removes that ID from every referenced documentation file even when it occurs once. Normalised like every other ID, so a method ID may be written with or without its `~ReturnType` suffix. | ## Warnings `POAX001` is an ordinary MSBuild warning (see [Diagnostics](../diagnostics/)): ```xml $(MSBuildWarningsAsErrors);POAX001 $(MSBuildWarningsAsMessages);POAX001 ``` ## Seeing what it did Every removal is logged at normal verbosity: ```text dotnet build -v:n ... OpenAPI XML comments: removed 'T:Ambient.InjectedAttribute' from Lib.A, Lib.B (duplicate). ``` The filtered copies are in `obj///purview-openapi-xml/`. With `true`, the generator's cache is in `obj/.../generated/Microsoft.AspNetCore.OpenApi.SourceGenerators/.../OpenApiXmlCommentSupport.generated.cs`, where each ID should now appear once. --- # Diagnostics ## POAX001 ```text warning POAX001: 'T:Ambient.InjectedAttribute' is documented both in this project and in Lib.A.xml. If this project's copy is public, the OpenAPI XML-comment cache throws "An item with the same key has already been added" at run time; add to drop the reference's copy. ``` **Cause.** The ID is documented in the API project itself and in exactly one referenced project. It is not a duplicate among the referenced documentation, so it was not removed before compile, but the generator also adds the API's own public documented symbols, so it will collide. **Fix.** Add the item the warning names to the API project: ```xml ``` The overlap is only visible after the API compiles, so the exclude takes effect on the next build. **False positives.** The generator only reads the API's *public* symbols, so an overlap with an `internal` type in the API does not fail. The warning cannot tell the two apart from the documentation file; silence it with `MSBuildWarningsAsMessages` if it is one of those (see [Configuration](../configuration/)). --- # Troubleshooting ## Find what collides The repository ships a diagnostic script with the [`purview-openapi-xmlcomments-duplicates`](../agent-skills/) skill. It asks MSBuild for exactly the documentation files the OpenAPI generator reads, and reports every colliding ID, where each copy comes from, whether the current configuration handles it, and what to add when it does not: ```bash pwsh skills/purview-openapi-xmlcomments-duplicates/scripts/find-duplicate-documentation-ids.ps1 -Project path/to/Api ``` From a checkout of this repository, `just find-duplicates path/to/Api` runs the same. Options: `-TargetFramework` for a multi-targeted project, `-Configuration`, and `-Json` for machine-readable output. It exits `0` when nothing would throw, `1` when something would, and `2` when it could not inspect the project. Build the API first so it can also check the API's own documentation and the generated cache (`-p:EmitCompilerGeneratedFiles=true`). ```text Project: src/Api/Api.csproj (net10.0) Generator present: True Package installed: True (enabled: True) Excluded IDs: none Reference docs: 1 file(s) Project docs read: True [NOT HANDLED] ReferenceAndProject T:Purview.Results.GenerateResultAttribute: Domain.xml + this project Next: - Add to the API project (these are each in one reference and in the API itself, so they are not duplicates among the references): Result: /openapi/v1.json would still throw. ``` | Finding | Meaning | Fix | | --- | --- | --- | | `BetweenReferences` | In two or more referenced projects' documentation | Install the package; automatic | | `ReferenceAndProject` | In one referenced project and in the API itself | `PurviewOpenAPIXmlCommentExclude` (what [`POAX001`](../diagnostics/) reports) | | Generated cache key added more than once | The last build throws | Rebuild after changing the configuration | ## Common situations **Still `500` after installing the package.** Rebuild: the generated cache is only regenerated on compile. If it persists, run the script; an unhandled `ReferenceAndProject` finding needs the exclude it prints, and `POAX001` was reported on the build that found it. **`POAX001` keeps appearing after adding the exclude.** Check the ID matches the warning exactly and that the item is in the API project (or a `Directory.Build.props` that covers it), not in a library. **The script finds nothing, but the cache still has a duplicate.** Both copies come from the API's own compilation, not from referenced documentation, so the package cannot help. Look the key up in the generated cache (each referenced file's entries come first, the API's last) and remove the second declaration. **The duplicated type is real API.** Two referenced projects declare the same public type in the same namespace. The package removes both copies' descriptions, which is safe but loses them; rename one, since consumers referencing both see an ambiguous type anyway. **The package does not seem to run.** `dotnet msbuild path/to/Api/Api.csproj -getProperty:PurviewOpenAPIXmlCommentDeduplication` should print `true`. A normal-verbosity build (`-v:n`) logs `OpenAPI XML comments: removed '...'` for every removal. It only runs in a project that references `Microsoft.AspNetCore.OpenApi`. **A workaround removes the generator.** Delete it once the script reports nothing would throw: it drops every XML description. The script lists any project file that looks like one. ## By hand Without `pwsh`, the generated cache shows every key: ```bash dotnet build path/to/Api -p:EmitCompilerGeneratedFiles=true grep -o 'cache\.Add(@"[^"]*"' path/to/Api/obj/Debug/*/generated/Microsoft.AspNetCore.OpenApi.SourceGenerators/*/OpenApiXmlCommentSupport.generated.cs | sort | uniq -d ``` and the documentation files the generator reads are ```bash dotnet msbuild path/to/Api/Api.csproj -t:ResolveReferences -getItem:AdditionalFiles -p:PurviewOpenAPIXmlCommentDeduplication=false ``` The skill's `references/diagnosing.md` walks through classifying the result, finding which generator injects the type, and reading a binary log. --- # Agent Skills This repository authors an **agent skill** so AI coding agents can diagnose and fix duplicate OpenAPI XML-comment members without rediscovering how the generator works. Skills are authored at `skills//SKILL.md` and installed with the skills CLI, which clones this git repository - **no NuGet package ships them**. ```bash npx skills add purview-dev/openapi-xmlcomments ``` ## Skills published by this repository | Skill | Covers | | --- | --- | | `skills/purview-openapi-xmlcomments-duplicates` | Recognising the `An item with the same key has already been added` failure and `POAX001`, finding every colliding documentation ID and its sources, installing and configuring `Purview.OpenAPI.XmlComments`, and verifying the fix | It bundles: - `scripts/find-duplicate-documentation-ids.ps1` - the diagnostic described in [Troubleshooting](../troubleshooting/). The integration tests run it against real consumers, so the skill's instructions stay true to its output. - `references/diagnosing.md` - the same diagnosis by hand: the generated cache, the `AdditionalFiles` query, finding the generator that injects the type, and binary logs. - `references/configuration.md` - every configuration form: `PackageReference`, Central Package Management, `GlobalPackageReference`, excludes for one or several APIs, warnings and turning it off. ## When an agent should load it - `/openapi/v1.json` answers `500`, or a log shows `XmlCommentCache.GenerateCacheEntries` with a duplicate key. - A build warns `POAX001`. - A project removes the `Microsoft.AspNetCore.OpenApi.SourceGenerators` analyzer. ## Authoring rules - Every skill name starts with `purview-openapi-xmlcomments`: skills install into one flat folder per agent, so an unprefixed name collides with somebody else's. - A skill links only inside its own folder, because only that folder is installed. - `src/tests/Repository.UnitTests` validates every skill on every test run against the Agent Skills specification through `Purview.Skills.Testing`. --- # Testing Four layers, each covering what the one before cannot. | Layer | Where | Proves | | --- | --- | --- | | Unit | `src/tests/XmlComments.UnitTests` | The rules (ID normalisation, which IDs are removed, overlap detection) and both tasks, run in-process against a mocked `IBuildEngine`. | | Integration | `src/tests/XmlComments.IntegrationTests` | Real consumer solutions, built with the real OpenAPI generator on `net10.0` and `net11.0`, importing the props and targets from source. Each API requests its own `/openapi/v1.json`. The same consumers are diagnosed with the skill's script, so its report stays accurate. | | Repository | `src/tests/Repository.UnitTests` | Every authored skill conforms to the Agent Skills specification and carries the product prefix. | | Packed consumer | `scripts/test-packed-consumer.ps1` | The `.nupkg` itself, through `PackageReference` alone: the `buildTransitive` import, the packed task path, and nothing left in the build output. | The integration suite includes `Build_GivenDeduplicationDisabled_ReproducesTheDuplicateKeyFailure`, which turns the package off and asserts the generator still fails. It is what shows the other tests would notice a regression. ```text just test # every tier just test-unit # unit + repository just test-integration just smoke-packed-consumer ``` The integration tier needs `pwsh` on the path for the script tests; GitHub's hosted runners have it. Every consumer compiles the same documented public attribute into the API and each library, which is what a source generator's post-initialization output does. The tests therefore depend on no particular generator. --- # Release Flow - `package.json` is the authoritative version. `Purview.BuildSdk` reads it, so no project file carries a version. - A pull request runs the shared `purview-dev/build` pipeline (restore, build, lint, every test tier, pack and pack validation) and the packed-consumer gate. - A push to `main` runs the release action with `Release:Mode=NuGet`: it packs, publishes to nuget.org through Trusted Publishing, and creates the `v` GitHub release, only when that tag does not already exist. Bump `package.json` and add a `CHANGELOG.md` entry to release. - `.config/purview-build.json`'s `PackValidation.RequiredContent` is the exhaustive list of what the package ships; `just pipeline-pack-validate` runs the same validation locally. To try a build in another repository before releasing it: ```text just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/ ``` Publish a new version each time: NuGet reuses an already-extracted package with the same id and version. --- # Contributing 1. Read [AGENTS.md](https://github.com/purview-dev/openapi-xmlcomments/blob/main/AGENTS.md): it lists the behavioural invariants and the packaging rules. 2. Restore the local tools: `./scripts/restore-dotnet-tools.ps1`. 3. Make the change with its tests. A change to the props or targets needs an integration test; a change to the skill's script needs a `FindDuplicateDocumentationIdsScriptTests` case. 4. Run `just lint-fix`, `just test` and, for anything touching packaging, `just smoke-packed-consumer` and `just pipeline-pack-validate`. 5. Use Conventional Commits (`feat:`, `fix:`, `docs:`, ...); `lefthook` and `commitlint` check them. When the MSBuild surface changes - a property, item, target name, warning or default - update the package README (`src/src/XmlComments/Sdk/README.md`), the root `README.md`, these pages, the skill (`skills/purview-openapi-xmlcomments-duplicates`, including its `references/` and script), `CHANGELOG.md` and `AGENTS.md` in the same commit. ## Skills The authored skill lives in `skills/`. Keep `SKILL.md` under 500 lines with detail in `references/`, link only inside the skill's own folder, and keep the `purview-openapi-xmlcomments` prefix; `Repository.UnitTests` enforces the specification. See [Agent Skills](../agent-skills/).