diff --git a/.agents/agents/other-package-agent.md b/.agents/agents/other-package-agent.md
new file mode 100644
index 0000000..bbb47a8
--- /dev/null
+++ b/.agents/agents/other-package-agent.md
@@ -0,0 +1 @@
+# Other package agent
diff --git a/.agents/agents/release-prep.md b/.agents/agents/release-prep.md
new file mode 100644
index 0000000..a2bcfe5
--- /dev/null
+++ b/.agents/agents/release-prep.md
@@ -0,0 +1,42 @@
+# release-prep (generic agent spec)
+
+## Goal
+
+Prepare a safe prerelease-ready update with minimal churn and clear validation.
+
+## Workflow
+
+1. Read current version from `package.json` and inspect pending `.changeset/*.md` files.
+2. Ensure release notes describe user-visible changes succinctly.
+3. Run repository validation commands:
+ - `just lint-check`
+ - `just build`
+ - `just test`
+4. If tests fail due to filtering invocation, use MTP-compatible `--treenode-filter` semantics.
+5. Summarize changed files, validation output, and any follow-up actions.
+
+## Constraints
+
+- Keep edits narrowly scoped to release prep.
+- Do not rewrite unrelated docs/code.
+- Follow existing conventional-commit and changesets conventions in this repository.
+
+## Available skills in this repository
+
+When relevant, explicitly use the matching skill guidance:
+
+- `changesets-prerelease` — prerelease bump and changeset/changelog quality.
+- `dotnet-tunit` — TUnit test-authoring conventions and assertion style.
+- `tunit-test-runner` — deep TUnit/MTP execution and troubleshooting guidance.
+- `tunit-filtering` — concise `--treenode-filter` syntax and examples for this repo.
+- `git-conventional-commits` — commit hygiene and commit message conventions.
+- `lefthook-integration` — local Git hook strategy and setup guidance.
+
+Paths:
+
+- `../skills/changesets-prerelease/SKILL.md`
+- `../skills/dotnet-tunit/SKILL.md`
+- `../skills/tunit-test-runner/SKILL.md`
+- `../skills/tunit-filtering/SKILL.md`
+- `../skills/git-conventional-commits/SKILL.md`
+- `../skills/lefthook-integration/SKILL.md`
diff --git a/.agents/agents/sdk-consumer-setup.md b/.agents/agents/sdk-consumer-setup.md
new file mode 100644
index 0000000..b356a41
--- /dev/null
+++ b/.agents/agents/sdk-consumer-setup.md
@@ -0,0 +1,34 @@
+# sdk-consumer-setup (generic agent spec)
+
+## Goal
+
+Help a consuming repository adopt or troubleshoot `Purview.DotNetProjectSdk` correctly, without breaking existing build behaviour.
+
+## Workflow
+
+1. Confirm the SDK is imported in `Directory.Build.props`/`Directory.Build.targets` via
+ `` and the matching `Sdk.targets` import.
+2. Check pre-import bootstrap properties are set **before** the `Sdk.props` import when they must affect
+ evaluation: `NamespacePrefix`, `UsePackageJsonVersion`, `RootPackageJson`.
+3. If version resolution looks wrong, verify `package.json` discovery: explicit `RootPackageJson`, then CI
+ variables, `.git` root, or a nearby `package.json`. `UsePackageJsonVersion=Strict` fails fast instead of
+ silently skipping resolution.
+4. If the bundled `.agents/**` content isn't appearing in the repo root, check `EnableAgentFolderInPackage`
+ (default `true`) and `AgentPackDestinationFolder` (default `.agents`) — the copy runs before build via
+ `EnsureAgentFolderInPackageTarget`.
+5. For test-framework or project-shape questions, confirm the project follows repo naming and placement
+ conventions the SDK expects, rather than introducing bespoke structure.
+6. Re-run `dotnet build` (or the repo's canonical build command) after each configuration change to confirm
+ the fix.
+
+## Constraints
+
+- Prefer minimal, targeted property changes over broad `Directory.Build.props` rewrites.
+- Do not disable `PurviewAutoSdkPack` or `EnableAgentFolderInPackage` unless the consumer explicitly asks to
+ opt out.
+- Do not duplicate SDK-managed properties in individual project files unless the scenario is intentionally
+ project-specific.
+
+## Related skill
+
+See `../skills/sdk-configuration-reference/SKILL.md` for the full property reference.
diff --git a/.agents/prompts/project-harness-test.md b/.agents/prompts/project-harness-test.md
new file mode 100644
index 0000000..27cd1de
--- /dev/null
+++ b/.agents/prompts/project-harness-test.md
@@ -0,0 +1,25 @@
+# project-harness-test (generic prompt spec)
+
+Create or update an integration test in `src/tests/DotNetProjectSdk.IntegrationTests/Tests/` using `ProjectHarness`.
+
+## Required behaviour
+
+1. Keep scope to one behaviour (single focused scenario).
+2. Use `ProjectHarness` (`Harness/ProjectHarness.cs`) to construct a throwaway project for the scenario.
+3. Prefer evaluation helpers before build-log parsing:
+ - `GetPropertyAsync` / `GetPropertiesAsync`
+ - `GetItemIdentitiesAsync` / `GetProjectReferencesAsync`
+ - `GetPreprocessProjectAsync` when import/evaluation order matters
+4. Use `BuildAsync(restore: true)` only when the scenario requires restore/build behaviour.
+5. Follow TUnit conventions used in this repo:
+ - `[Test]` method attributes
+ - awaited assertions (e.g., `await Assert.That(actual).IsEqualTo(expected);`)
+6. Keep setup deterministic and minimal; avoid unrelated package/config changes.
+
+## Suggested output structure
+
+- Add/modify one test file under `src/tests/DotNetProjectSdk.IntegrationTests/Tests/`.
+- Include a short test comment describing **Given / When / Then** intent.
+- Validate by running targeted tests first, then broader tests if needed.
+
+If multiple test ideas are possible, pick the one with the smallest diff that still validates the intended SDK behaviour.
diff --git a/.agents/prompts/sdk-diagnose-agent-folder-copy.md b/.agents/prompts/sdk-diagnose-agent-folder-copy.md
new file mode 100644
index 0000000..49ad1c2
--- /dev/null
+++ b/.agents/prompts/sdk-diagnose-agent-folder-copy.md
@@ -0,0 +1,26 @@
+# sdk-diagnose-agent-folder-copy (generic prompt spec)
+
+Diagnose why the bundled `.agents/**` folder from `Purview.DotNetProjectSdk` did not appear at the expected
+destination in a consuming repository.
+
+## Required behaviour
+
+1. Confirm the NuGet package actually contains `.agents/**` content (inspect the `.nupkg` if available).
+2. Confirm the consuming project is packable/buildable and imports the SDK via
+ `Sdk.props`/`Sdk.targets`, since the copy runs in `EnsureAgentFolderInPackageTarget` before build.
+3. Check `EnableAgentFolderInPackage` is not set to `false` anywhere in the build (project file,
+ `Directory.Build.props`, or command-line `-p:` overrides).
+4. Confirm the destination folder: default is `.agents` at the repo root, overridable per-build with
+ `-p:AgentPackDestinationFolder=`.
+5. Verify repo-root discovery succeeded: explicit `RepoRoot`, then a nearby `AGENTS.md`, then source-control
+ root metadata.
+6. Re-run the build and confirm the destination folder now contains the copied files (including the
+ generated `.gitignore` for skill/prompt/agent subfolders).
+
+## Suggested output
+
+- A short root-cause explanation (missing import, disabled flag, wrong destination override, or repo-root
+ discovery miss).
+- The exact command used to reproduce/verify the fix (for example
+ `dotnet build -p:AgentPackDestinationFolder=`).
+- Confirmation that the expected files exist at the resolved destination path.
diff --git a/.agents/skills/changesets-prerelease/SKILL.md b/.agents/skills/changesets-prerelease/SKILL.md
new file mode 100644
index 0000000..d3bb879
--- /dev/null
+++ b/.agents/skills/changesets-prerelease/SKILL.md
@@ -0,0 +1,36 @@
+---
+name: changesets-prerelease
+description: Create and apply a prerelease bump using Changesets CLI, with the new changeset markdown summarizing the actual changes since the last shipped release.
+---
+
+# Changesets Prerelease Skill
+
+Use this skill when preparing the next prerelease version in a project that uses Changesets.
+
+## Steps
+
+1. Add a new changeset:
+ - `npx @changesets/cli add --empty --message ""`
+ - update the generated `.changeset/*.md` frontmatter with the correct target package and bump type used by the repository.
+ - replace the placeholder body with a concise summary of the **actual user-facing changes since the last shipped release**, not just a generic "prepare prerelease" note.
+ - if the immediately previous prerelease number (for example `.24`) has **not** been released yet and you are preparing `.25`, the new markdown should still describe the cumulative changes that matter for the next published prerelease.
+
+2. Bump versions/changelog:
+ - `npx @changesets/cli version`
+3. Commit the resulting changes (`package.json`, `CHANGELOG.md`, and consumed `.changeset` files).
+
+## Writing the new `.changeset/*.md` body
+
+- Summarize the functional changes that should appear in the changelog for the next published prerelease.
+- Prefer short release-note language such as:
+ - fixed SQL snapshot translation for directly mapped complex mirror properties
+ - clarified provider documentation for scalar value object query behaviour
+ - aligned repo-local agent skills and instructions
+- Do **not** leave the body as a procedural placeholder such as `Prepare next prerelease` unless there were truly no meaningful changes.
+- If multiple unreleased prerelease bumps exist locally, write the new file so the generated changelog entry remains useful to an external consumer reading the next shipped release.
+
+## Notes
+
+- If the repository uses prerelease mode, check `.changeset/pre.json` for the active prerelease tag.
+- Ensure release notes/changelog are generated from actual changes, not placeholder text.
+- In prerelease mode, `changeset version` can record a changeset in `.changeset/pre.json` even if the new file was created with placeholder/empty content first. If that happens, correct the `.md` frontmatter/body before finalizing the release notes so the next shipped prerelease entry is accurate.
diff --git a/.agents/skills/dotnet-tunit/SKILL.md b/.agents/skills/dotnet-tunit/SKILL.md
new file mode 100644
index 0000000..274d53c
--- /dev/null
+++ b/.agents/skills/dotnet-tunit/SKILL.md
@@ -0,0 +1,56 @@
+---
+name: dotnet-tunit
+description: Use when creating or refactoring .NET tests with TUnit conventions (AAA pattern, naming, async assertions, and CancellationToken usage).
+category: dotnet
+roles:
+ - dotnet
+ - dotnet-tunit
+ - coding
+tags:
+ - dotnet
+ - csharp
+ - tunit
+ - tests
+---
+
+# dotnet-tunit Skill
+
+Use this skill when creating or refactoring tests in a .NET codebase that uses TUnit.
+
+## Authoring and refactoring rules (mandatory)
+
+- Always use TUnit (`TUnit.Core`, `TUnit.Assertions`).
+- Do not introduce xUnit, NUnit, MSTest, or FluentAssertions unless explicitly requested.
+- Test methods must have `[Test]`.
+- Assertion calls must be awaited.
+- Prefer `await Assert.That(actual).IsEqualTo(expected);`.
+
+### Arrange / Act / Assert structure
+
+- Always use Arrange / Act / Assert.
+- Always include explicit section comments in the test body:
+ - `// Arrange`
+ - `// Act`
+ - `// Assert`
+
+### Naming conventions
+
+- Test classes must be named `{ModuleOrClassBeingTested}Tests`.
+- Test classes must be in the same namespace as the module/class being tested (identical namespace).
+- Test methods must follow `{SubjectUnderTest}_{Scenario}_{Expectation}`.
+ - `SubjectUnderTest` is usually the method name.
+ - Use `ctor` for constructor-focused tests when appropriate.
+
+### CancellationToken rule
+
+- Always include `CancellationToken cancellationToken` as the last parameter when any called method in the test body supports a `CancellationToken` parameter.
+- Pass that token through in the call under test (for example: `var result = await sut.ProcessAsync(cancellationToken);`).
+
+## Refactoring expectation
+
+- When refactoring existing tests, bring them into compliance with all rules above (structure, naming, and cancellation-token usage), while preserving test intent.
+
+## Execution reminder
+
+- Run relevant tests before completion.
+- For execution/filtering details, use `../tunit-test-runner/SKILL.md`.
diff --git a/.agents/skills/git-conventional-commits/SKILL.md b/.agents/skills/git-conventional-commits/SKILL.md
new file mode 100644
index 0000000..6896ae6
--- /dev/null
+++ b/.agents/skills/git-conventional-commits/SKILL.md
@@ -0,0 +1,16 @@
+---
+name: git-conventional-commits
+description: Git workflow and conventional commit guidance.
+---
+
+# Git Conventional Commits Skill
+
+Use this skill when applying Git workflow and commit hygiene practices.
+
+## Rules
+
+- Use Conventional Commits.
+- Prefer small, reviewable commits.
+- Commit types include `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `build`, and `ci`.
+- Do not mix unrelated changes.
+- Explain validation commands before completion.
diff --git a/.agents/skills/lefthook-integration/SKILL.md b/.agents/skills/lefthook-integration/SKILL.md
new file mode 100644
index 0000000..42320ec
--- /dev/null
+++ b/.agents/skills/lefthook-integration/SKILL.md
@@ -0,0 +1,16 @@
+---
+name: lefthook-integration
+description: Lefthook Git hook integration.
+---
+
+# Lefthook Integration Skill
+
+Use this skill when configuring repository-local Git hook setup.
+
+## Rules
+
+- Prefer small, fast checks in pre-commit.
+- Put slower checks in pre-push.
+- Do not block commits with network-dependent checks.
+- Keep commands cross-platform where possible.
+- Document how to install and run hooks locally.
diff --git a/.agents/skills/project-placement-defaults/.gitignore b/.agents/skills/project-placement-defaults/.gitignore
new file mode 100644
index 0000000..2799754
--- /dev/null
+++ b/.agents/skills/project-placement-defaults/.gitignore
@@ -0,0 +1,8 @@
+# Ignore all files
+*
+
+# Don't ignore directories, so Git can traverse them
+!*/
+
+# Keep this file
+!.gitignore
\ No newline at end of file
diff --git a/.agents/skills/sdk-configuration-reference/.gitignore b/.agents/skills/sdk-configuration-reference/.gitignore
new file mode 100644
index 0000000..2799754
--- /dev/null
+++ b/.agents/skills/sdk-configuration-reference/.gitignore
@@ -0,0 +1,8 @@
+# Ignore all files
+*
+
+# Don't ignore directories, so Git can traverse them
+!*/
+
+# Keep this file
+!.gitignore
\ No newline at end of file
diff --git a/.agents/skills/sdk-project-behavior-and-detection/.gitignore b/.agents/skills/sdk-project-behavior-and-detection/.gitignore
new file mode 100644
index 0000000..2799754
--- /dev/null
+++ b/.agents/skills/sdk-project-behavior-and-detection/.gitignore
@@ -0,0 +1,8 @@
+# Ignore all files
+*
+
+# Don't ignore directories, so Git can traverse them
+!*/
+
+# Keep this file
+!.gitignore
\ No newline at end of file
diff --git a/.agents/skills/tunit-filtering/SKILL.md b/.agents/skills/tunit-filtering/SKILL.md
new file mode 100644
index 0000000..a01143f
--- /dev/null
+++ b/.agents/skills/tunit-filtering/SKILL.md
@@ -0,0 +1,146 @@
+---
+name: tunit-filtering
+description: "Use for quick TUnit --treenode-filter patterns."
+---
+
+# TUnit Filtering (quick reference)
+
+Use this skill when you already know you need filtering syntax and want concise examples.
+
+For full execution and troubleshooting guidance, use `../tunit-test-runner/SKILL.md`.
+
+## Defaults
+
+- Test runner is Microsoft.Testing.Platform (MTP), not VSTest.
+- Use `--treenode-filter` for narrowing tests (do not use `--filter`).
+- Prefer your repository's standard full-suite test command for broad runs.
+
+## `--treenode-filter`
+
+Select tests by tree path:
+
+`////`
+
+## Tree segments
+
+Each segment maps to one level:
+
+- 1st segment: Assembly
+- 2nd segment: Namespace
+- 3rd segment: Class
+- 4th segment: Test name
+
+Examples:
+
+- `/*/*/LoginTests/*` → all tests in class `LoginTests`
+- `/*/*/*/AcceptCookiesTest` → a single test by name
+- `/*/MyProject.Tests.Integration/*/*` → tests in a namespace
+
+## Operators and filter options
+
+### `*` wildcard
+
+Matches any value in a segment, or part of a value.
+
+Examples:
+
+- `/*/*/LoginTests*/*`
+- `/*/*/MyProject.Tests.Api*/*`
+
+### `=` equality
+
+Matches an exact property value.
+
+Example:
+
+- `/*/*/*/*[Category=Unit]`
+
+### `!=` not equal
+
+Excludes a property value.
+
+Example:
+
+- `[Category!=Slow]`
+
+### `&` AND
+
+Combines multiple conditions in the same segment or property group.
+
+Examples:
+
+- `/**[(Category=Unit)&(Priority=High)]`
+- `/*/*/*/*[(Category=Unit)&(Priority=High)]`
+
+### `|` OR
+
+Matches either condition, inside a single parenthesized group.
+
+Examples:
+
+- `/*/*/(LoginTests)|(SignupTests)/*`
+- `/**[(Category=Unit)|(Priority=High)]`
+
+### `**` match-all
+
+Matches any path depth, but it must appear at the end of the path.
+
+Examples:
+
+- `/**`
+- `/MyAssembly/**`
+
+## Property filtering
+
+You can filter on custom properties in the last segment using `[...]`.
+
+Examples:
+
+- `/*/*/*/*[Category=Unit]`
+- `/*/*/*/*[Owner=*Team-Backend*]`
+- `/*/*/*/*[Category!=Slow]`
+
+## Important rules
+
+- Only one property group `[...]` is allowed per path segment.
+- If you need multiple property conditions, combine them inside the same brackets with `&` or `|`.
+- Separate brackets like `[Category=Smoke]|[Priority=High]` are not valid.
+- `**` must be at the end; `/**/Path` is not allowed.
+
+## Common examples
+
+- All tests: `/*/*/*/*`
+- All unit tests: `/*/*/*/*[Category=Unit]`
+- High-priority unit tests: `/*/*/*/*[(Category=Unit)&(Priority=High)]`
+- Integration tests with priority: `/*/MyProject.Tests.Integration/*/*[Priority=Critical]`
+
+## `dotnet test` note
+
+TUnit does not use the usual VSTest `--filter` syntax. Use `--treenode-filter` instead.
+
+Preferred: `dotnet test --treenode-filter "..."`
+
+Compatibility form (older SDKs): `dotnet test -- --treenode-filter "..."`
+
+## Quick starts
+
+Use path-like tree-node filters for common flows:
+
+- `/*/*/*/*/` — all tests
+- `/*/*/*/MyFeatureTests/*` — class-scoped
+- `/*/*/*/*/MyScenario*` — name pattern
+
+## Zero-tests troubleshooting
+
+If zero tests run:
+
+1. Verify `--treenode-filter` is used (not `--filter`).
+2. Try `dotnet test --treenode-filter "..."` first; if needed for your SDK version, use `dotnet test -- --treenode-filter "..."`.
+3. Broaden filter first (`/*/*/*/*/`), then narrow.
+4. Confirm target project contains `[Test]` methods.
+5. Confirm property predicates are in one bracket group (e.g. `[(A=1)&(B=2)]`).
+
+## See also
+
+- `../tunit-test-runner/SKILL.md`
+- `../dotnet-tunit/SKILL.md`
diff --git a/.agents/skills/tunit-test-runner/SKILL.md b/.agents/skills/tunit-test-runner/SKILL.md
new file mode 100644
index 0000000..d0a2108
--- /dev/null
+++ b/.agents/skills/tunit-test-runner/SKILL.md
@@ -0,0 +1,112 @@
+---
+name: tunit-test-runner
+description: >-
+ Run, filter, and select TUnit tests through `dotnet test`. Use this whenever
+ executing .NET tests in a project that depends on TUnit (built on
+ Microsoft.Testing.Platform / MTP, not VSTest), when `dotnet test --filter`
+ reports "Zero tests ran", or when tests must be narrowed by assembly,
+ namespace, class, test name, [Category], or other custom properties. Covers
+ the `--treenode-filter` path-based query syntax, its operators, the `--`
+ separator rules across SDK versions, and common 0-tests troubleshooting.
+license: MIT
+---
+
+# Running TUnit tests with `dotnet test`
+
+Use this skill for execution, filtering, and troubleshooting.
+
+For test creation/refactoring conventions (AAA pattern, naming, cancellation tokens), use `../dotnet-tunit/SKILL.md`.
+
+## The one rule that matters most
+
+TUnit runs on **Microsoft.Testing.Platform (MTP)**, not VSTest. The reflexive
+`dotnet test --filter "Category=X"` **does not work**: MTP silently rejects the
+`--filter` flag, prints its own help text, and exits with `Zero tests ran`. That
+looks like a passing-but-empty run or a config failure, but it is just an
+unrecognised flag. **Never reach for `--filter` on a TUnit project.** Use
+`--treenode-filter` instead.
+
+| Other frameworks (VSTest) | TUnit (MTP) |
+| ------------------------------------------ | ---------------------------------------------------- |
+| `--filter "Category=Integration"` | `--treenode-filter "/*/*/*/*[Category=Integration]"` |
+| `--filter "FullyQualifiedName~LoginTests"` | `--treenode-filter "/*/*/LoginTests/*"` |
+| `--filter "Name=AcceptCookiesTest"` | `--treenode-filter "/*/*/*/AcceptCookiesTest"` |
+
+## How to invoke it
+
+Prefer `dotnet test` over `dotnet run`: `dotnet test` builds and runs every
+targeted TFM automatically and works against a `.csproj`, `.sln`, or `.slnx`,
+whereas `dotnet run` only runs a single TFM.
+
+The catch is the `--` separator, which depends on the SDK:
+
+```bash
+# Universal form — works on every SDK. Use this by default.
+dotnet test -- --treenode-filter "/*/*/LoginTests/*"
+
+# .NET 10+ SDK only — the platform flag can be passed directly.
+dotnet test --treenode-filter "/*/*/LoginTests/*"
+```
+
+Anything after `--` is passed through to the TUnit test runner rather than to
+the `dotnet test` command itself. Flags from extension packages
+(`--coverage`, `--report-trx`, `--results-directory`, etc.) **must** also sit
+after the `--`:
+
+```bash
+dotnet test --configuration Release --no-build \
+ -- --treenode-filter "/*/*/*/*[Category=Unit]" --coverage --report-trx
+```
+
+> Run with no filter to execute everything: `dotnet test`.
+
+## The `--treenode-filter` syntax
+
+A filter is a path with four segments, optionally annotated with a property
+group on any segment:
+
+```
+////[Property=Value]
+```
+
+Use `*` as a wildcard in any segment. The classic "run all tests" filter is
+`/*/*/*/*` — four wildcards, one per level.
+
+### Operators
+
+| Operator | Meaning | Example |
+| -------- | --------------------------------------------- | -------------------------------------- |
+| `*` | Wildcard within a segment | `/*/*/LoginTests*/*` |
+| `=` | Property equals (exact) | `/*/*/*/*[Category=Unit]` |
+| `!=` | Property not equal (exclude) | `/*/*/*/*[Category!=Slow]` |
+| `&` | AND — within one segment / property group | `/**[(Category=Unit)&(Priority=High)]` |
+| `\|` | OR — within one segment / property group | `/*/*/(LoginTests)\|(SignupTests)/*` |
+| `**` | Match any path depth (must be at the **end**) | `/MyAssembly/**` |
+
+### Two grammar rules that are easy to get wrong
+
+1. **`&` and `|` operate _inside a single segment or property group_, and each
+ side must be wrapped in parentheses.** They do not join two complete paths.
+2. **Only one property group `[...]` is allowed per path segment.** Combine
+ conditions _inside_ the single bracket — do not chain brackets.
+3. **`**` must terminate the path.** `/MyAssembly/**` is valid; `/**/Class/*`
+ is not.
+
+## Troubleshooting: "Zero tests ran" / 0 tests discovered
+
+Check, in order:
+
+1. **`--filter` was used instead of `--treenode-filter`.** This is the most common cause.
+2. **`Microsoft.NET.Test.Sdk` is still referenced.** It conflicts with the TUnit MTP platform.
+3. **TUnit package missing.** Ensure ``.
+4. **Missing `[Test]` attribute** or unsupported method shape.
+5. **Wrong `OutputType`.** A `hostfxr.dll could not be found` error means the project needs `Exe`.
+6. **Bad filter shape.** Remember the path has exactly four segments.
+
+## References
+
+- TUnit — Test Filters: https://tunit.dev/docs/execution/test-filters/
+- TUnit — Troubleshooting & FAQ: https://tunit.dev/docs/troubleshooting/
+- TUnit — CI/CD pipelines: https://tunit.dev/docs/examples/tunit-ci-pipeline/
+- TUnit — Explicit tests: https://tunit.dev/docs/writing-tests/explicit/
+- MTP graph-query filtering spec: https://github.com/microsoft/testfx/blob/main/docs/mstest-runner-graphqueryfiltering/graph-query-filtering.md
diff --git a/.changeset/breezy-suits-teach.md b/.changeset/breezy-suits-teach.md
new file mode 100644
index 0000000..ac47e04
--- /dev/null
+++ b/.changeset/breezy-suits-teach.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+Fixed default variable issue
diff --git a/.changeset/busy-apes-type.md b/.changeset/busy-apes-type.md
new file mode 100644
index 0000000..4e1efbb
--- /dev/null
+++ b/.changeset/busy-apes-type.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+Fixed incorrect .git folder detection
diff --git a/.changeset/dry-crabs-join.md b/.changeset/dry-crabs-join.md
new file mode 100644
index 0000000..2d90785
--- /dev/null
+++ b/.changeset/dry-crabs-join.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+added versioning caching per-session
diff --git a/.changeset/eager-doodles-strive.md b/.changeset/eager-doodles-strive.md
new file mode 100644
index 0000000..9afa9f0
--- /dev/null
+++ b/.changeset/eager-doodles-strive.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+now copying .editorConfig and global.json into project structure
diff --git a/.changeset/eleven-pandas-agree.md b/.changeset/eleven-pandas-agree.md
new file mode 100644
index 0000000..6fa5077
--- /dev/null
+++ b/.changeset/eleven-pandas-agree.md
@@ -0,0 +1,6 @@
+---
+"changeops": patch
+---
+
+- Added support for auto including Shared*.csproj files
+- Added Strict mode on package.json version
diff --git a/.changeset/four-cycles-doubt.md b/.changeset/four-cycles-doubt.md
new file mode 100644
index 0000000..73d5cf5
--- /dev/null
+++ b/.changeset/four-cycles-doubt.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+fixed IsCLIProject properties
diff --git a/.changeset/fruity-stars-check.md b/.changeset/fruity-stars-check.md
new file mode 100644
index 0000000..b0d7443
--- /dev/null
+++ b/.changeset/fruity-stars-check.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+hardenend version detection
diff --git a/.changeset/funky-rats-hug.md b/.changeset/funky-rats-hug.md
new file mode 100644
index 0000000..afe888f
--- /dev/null
+++ b/.changeset/funky-rats-hug.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+fixed editorconfig props not exposed to VS correctly
diff --git a/.changeset/gentle-tires-attend.md b/.changeset/gentle-tires-attend.md
new file mode 100644
index 0000000..45ef5aa
--- /dev/null
+++ b/.changeset/gentle-tires-attend.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+updated to support different substitite frameworks
diff --git a/.changeset/large-buses-poke.md b/.changeset/large-buses-poke.md
new file mode 100644
index 0000000..422c09d
--- /dev/null
+++ b/.changeset/large-buses-poke.md
@@ -0,0 +1,5 @@
+---
+"changeops": major
+---
+
+initial release
diff --git a/.changeset/lucky-glasses-battle.md b/.changeset/lucky-glasses-battle.md
new file mode 100644
index 0000000..d4cee1d
--- /dev/null
+++ b/.changeset/lucky-glasses-battle.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+hardended aspire host defaults
diff --git a/.changeset/olive-poets-live.md b/.changeset/olive-poets-live.md
new file mode 100644
index 0000000..4fbd49e
--- /dev/null
+++ b/.changeset/olive-poets-live.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+fixed issues with sdk file location when packaged
diff --git a/.changeset/petite-rabbits-hide.md b/.changeset/petite-rabbits-hide.md
new file mode 100644
index 0000000..42bd9fb
--- /dev/null
+++ b/.changeset/petite-rabbits-hide.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+expanded support for CLI
diff --git a/.changeset/pre.json b/.changeset/pre.json
new file mode 100644
index 0000000..a943644
--- /dev/null
+++ b/.changeset/pre.json
@@ -0,0 +1,27 @@
+{
+ "mode": "pre",
+ "tag": "prerelease",
+ "initialVersions": {
+ "changeops": "0.0.0"
+ },
+ "changesets": [
+ "breezy-suits-teach",
+ "busy-apes-type",
+ "dry-crabs-join",
+ "eager-doodles-strive",
+ "eleven-pandas-agree",
+ "four-cycles-doubt",
+ "fruity-stars-check",
+ "funky-rats-hug",
+ "gentle-tires-attend",
+ "large-buses-poke",
+ "lucky-glasses-battle",
+ "olive-poets-live",
+ "petite-rabbits-hide",
+ "rich-candles-relax",
+ "strict-schools-chew",
+ "thick-snails-bathe",
+ "tiny-pears-burn",
+ "warm-phones-repair"
+ ]
+}
diff --git a/.changeset/rich-candles-relax.md b/.changeset/rich-candles-relax.md
new file mode 100644
index 0000000..e34077a
--- /dev/null
+++ b/.changeset/rich-candles-relax.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+added embedded attribute gen"
diff --git a/.changeset/strict-schools-chew.md b/.changeset/strict-schools-chew.md
new file mode 100644
index 0000000..55c45e1
--- /dev/null
+++ b/.changeset/strict-schools-chew.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+InternalsVisibleTo for shared testing infra
diff --git a/.changeset/thick-snails-bathe.md b/.changeset/thick-snails-bathe.md
new file mode 100644
index 0000000..33789a6
--- /dev/null
+++ b/.changeset/thick-snails-bathe.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+fixed test project namespace generation
diff --git a/.changeset/tiny-pears-burn.md b/.changeset/tiny-pears-burn.md
new file mode 100644
index 0000000..304167a
--- /dev/null
+++ b/.changeset/tiny-pears-burn.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+fixing default values
diff --git a/.changeset/warm-phones-repair.md b/.changeset/warm-phones-repair.md
new file mode 100644
index 0000000..0c1e840
--- /dev/null
+++ b/.changeset/warm-phones-repair.md
@@ -0,0 +1,5 @@
+---
+"changeops": patch
+---
+
+Make SDK property conditions explicit and harden executable project detection for top-level Program.cs projects.
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index c42490a..fe21bde 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -2,17 +2,17 @@
"version": 1,
"isRoot": true,
"tools": {
- "dotnet-inspect": {
- "version": "0.7.8",
+ "csharpier": {
+ "version": "1.3.0",
"commands": [
- "dotnet-inspect"
+ "csharpier"
],
"rollForward": false
},
- "csharpier": {
- "version": "1.2.6",
+ "dotnet-inspect": {
+ "version": "0.23.0",
"commands": [
- "csharpier"
+ "dotnet-inspect"
],
"rollForward": false
}
diff --git a/.config/lefthook.yml b/.config/lefthook.yml
index bb1f4e4..056bb5a 100644
--- a/.config/lefthook.yml
+++ b/.config/lefthook.yml
@@ -1,7 +1,7 @@
pre-commit:
jobs:
- name: csharpier check
- run: dotnet csharpier check .
+ run: just lint-check
commit-msg:
jobs:
diff --git a/.csharpierignore b/.csharpierignore
new file mode 100644
index 0000000..786c06d
--- /dev/null
+++ b/.csharpierignore
@@ -0,0 +1,3 @@
+**/bin/
+**/obj/
+**/generated/**
diff --git a/.editorconfig b/.editorconfig
index a0073c1..95d30c0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,416 +1,2621 @@
-root = true
+root = true
-# All files
+# Base Configuration - General Settings, File Formatting, and Naming Conventions
+
+# All Files
[*]
+charset = utf-8
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_top_level_statements = true:silent
+end_of_line = lf
+indent_size = 4
+indent_style = tab
+insert_final_newline = true
+tab_width = 4
+trim_trailing_whitespace = true
+max_line_length = 120
+
+# Type members
+dotnet_hide_advanced_members = false
+dotnet_member_insertion_location = with_other_members_of_the_same_kind
+dotnet_property_generation_behavior = prefer_throwing_properties
+
+# Symbol search
+dotnet_search_reference_assemblies = true
+
+# Nullability settings
+dotnet_build_property.Nullable = enable
+
+# Enable or disable the analyzers
+dotnet_analyzer_diagnostic.severity = warning
+
+# Visual Studio XML Project Files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML Configuration Files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,xml,stylecop}]
+indent_size = 2
+
+# JSON Files
+[*.{json,json5,webmanifest}]
+indent_size = 2
+
+# YAML Files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Markdown Files
+[*.{md,mdx}]
+trim_trailing_whitespace = false
+
+# Bash Files
+[*.sh]
+end_of_line = lf
+
+# Batch Files
+[*.{cmd,bat}]
+end_of_line = crlf
+
+# Web Files
+[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}]
+indent_size = 2
+insert_final_newline = true
+
+# Makefiles
+[Makefile]
indent_style = tab
-# Xml files
-[*.xml]
-indent_size = 2
+# .NET Style Rules - Naming Conventions
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules
+
+[*.{cs,csx,cake,vb,vbx}]
+
+# Non-private static fields are PascalCase
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = warning
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
+dotnet_naming_rule.private_fields.severity = warning
+dotnet_naming_rule.private_fields.style = camel_case_underscore
+dotnet_naming_rule.private_fields.symbols = private_fields
+dotnet_naming_rule.private_fields_style.severity = warning
+dotnet_naming_rule.private_fields_style.style = camel_case
+dotnet_naming_rule.private_fields_style.symbols = private_fields
+dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
+
+# Constants are PascalCase
+dotnet_naming_rule.constants_should_be_pascal_case.severity = warning
+dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style
+dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
+dotnet_naming_style.constant_style.capitalization = pascal_case
+dotnet_naming_symbols.constants.applicable_kinds = field, local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+# Locals and parameters are camelCase
+dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
+dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
+dotnet_naming_rule.locals_should_be_camel_case.severity = warning
+
+# camel_case_style - Define the camelCase style
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+dotnet_naming_style.static_field_style.required_prefix = s_
+dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
+dotnet_naming_symbols.static_fields.required_modifiers = static
+
+# first_upper_style - The first character must start with an upper-case character
+dotnet_naming_style.first_upper_style.capitalization = first_word_upper
+
+# prefix_interface_with_i_style - Interfaces must be PascalCase and first character must be 'I'
+dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I
+dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case
+
+# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and start with 'T'
+dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case
+dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
+
+# disallowed_style - Anything that has this style applied is marked as disallowed
+dotnet_naming_style.disallowed_style.capitalization = pascal_case
+dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
+dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
+
+# internal_error_style - This style should never occur
+dotnet_naming_style.internal_error_style.capitalization = pascal_case
+dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____
+dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____
+
+# All public/protected/protected_internal constant fields must be PascalCase
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group
+dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field
+dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const
+
+# All public/protected/protected_internal static readonly fields must be PascalCase
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly
+
+# No other public/protected/protected_internal fields are allowed
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group
+dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field
+
+# StyleCop Field Naming Rules
+
+# All constant fields must be PascalCase
+dotnet_naming_rule.private_or_internal_field_should_be__fieldname.severity = warning
+dotnet_naming_rule.private_or_internal_field_should_be__fieldname.style = _fieldname
+dotnet_naming_rule.private_or_internal_field_should_be__fieldname.symbols = private_or_internal_field
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group
+dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
+dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field
+dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const
+
+# All static readonly fields must be PascalCase
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly
+
+# No non-private instance fields are allowed
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group
+dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
+dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field
+
+# Private fields must be camelCase
+dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning
+dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style
+dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group
+dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private
+dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field
+
+# Local variables must be camelCase
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group
+dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local
+dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local
+
+# Sanity check - uncovered field case
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group
+dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = *
+dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field
+
+# All elements (namespaces, classes, enums, etc.) must be PascalCase
+dotnet_naming_rule.element_rule.severity = warning
+dotnet_naming_rule.element_rule.style = non_private_static_field_style
+dotnet_naming_rule.element_rule.symbols = element_group
+dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property
+
+# Interfaces use PascalCase and are prefixed with uppercase 'I'
+dotnet_naming_rule.interface_rule.severity = warning
+dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style
+dotnet_naming_rule.interface_rule.symbols = interface_group
+dotnet_naming_symbols.interface_group.applicable_kinds = interface
+
+# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T'
+dotnet_naming_rule.type_parameter_rule.severity = warning
+dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style
+dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group
+dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter
+
+# Function parameters use camelCase
+dotnet_naming_rule.parameters_rule.severity = warning
+dotnet_naming_rule.parameters_rule.style = camel_case_style
+dotnet_naming_rule.parameters_rule.symbols = parameters_group
+dotnet_naming_symbols.parameters_group.applicable_kinds = parameter
+
+# Type Parameters
+dotnet_naming_rule.type_parameter_naming.severity = warning
+dotnet_naming_rule.type_parameter_naming.style = type_parameter_style
+dotnet_naming_rule.type_parameter_naming.symbols = type_parameter_symbol
+dotnet_naming_style.type_parameter_style.capitalization = pascal_case
+dotnet_naming_style.type_parameter_style.required_prefix = T
+dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameter_symbol.applicable_kinds = type_parameter
+
+# Instance fields are camelCase and start with _
+dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
+dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
+dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.instance_fields_should_be_camel_case.style = camel_case_underscore_style
+dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.instance_field_style.capitalization = camel_case
+dotnet_naming_style.instance_field_style.required_prefix = _
+dotnet_naming_symbols.instance_fields.applicable_kinds = field
+dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
+dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
+
+# Local functions are PascalCase
+dotnet_naming_rule.local_functions_should_be_pascal_case.severity = warning
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style
+dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = all_members
+dotnet_naming_style.local_function_style.capitalization = pascal_case
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+
+# "this." and "Me." qualifiers
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Undocumented
+dotnet_style_operator_placement_when_wrapping = end_of_line
+
+# Naming styles
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
+dotnet_naming_rule.interface_should_be_begins_with_i.style = prefix_interface_with_i_style
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.types_should_be_pascal_case.severity = warning
+dotnet_naming_rule.types_should_be_pascal_case.style = non_private_static_field_style
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+
+# By default, name items with PascalCase
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = non_private_static_field_style
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+
+# pascal_case_style - Define the PascalCase style
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+dotnet_naming_symbols.all_members.applicable_kinds = *
+
+# Symbol specifications
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.required_modifiers = *
+
+# Naming styles
+dotnet_naming_style._fieldname.capitalization = camel_case
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+
+# Simplify interpolation
+dotnet_diagnostic.IDE0071.severity = warning
+
+# Accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = warning
+
+# Allow multiple blank lines
+dotnet_diagnostic.IDE2000.severity = silent
+
+# Code Quality Settings
+dotnet_code_quality.enable_nullable_reference_types = true
+dotnet_code_quality.enableNETAnalyzers = true
+dotnet_code_quality.interpolated_string_composite_format = true
+dotnet_code_quality.prefer_auto_properties = false
+dotnet_code_quality.prefer_const = true
+dotnet_code_quality.prefer_inferred_anonymous_type_member_names = true
+dotnet_code_quality.prefer_inferred_tuple_names = true
+dotnet_code_quality.prefer_readonly = true
+dotnet_code_quality.require_accessibility_modifiers = true
+dotnet_code_quality.require_explicit_type_arguments = true
+dotnet_code_quality.require_explicit_visibility = true
+dotnet_code_quality.require_variable_declaration_for_explicit_type = false
+dotnet_code_quality_unused_parameters = all:warning
+dotnet_enable_roslyn_analyzers = true
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# RS0016: Add public types and members to the declared API
+dotnet_public_api_analyzer.require_api_files = true
+
+# Do not use generic CodeAction.Create to create CodeAction
+dotnet_diagnostic.RS0005.severity = none
+
+# C# Specific Settings
+
+[*.{cs,csx,cake}]
+
+# Newline options
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# C# Unnecessary code rules
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# Set the C# language version
+csharp_language_version = latest
+
+# Prefer var for inferred expressions such as method calls.
+# PDS0003 supplies the narrower explicit-type rule for object creation.
+csharp_style_var_elsewhere = true:warning
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+dotnet_style_readonly_field = true:warning
+dotnet_style_require_accessibility_modifiers = omit_if_default:warning
+
+# Code-block preferences
+csharp_prefer_braces = when_multiline:suggestion
+csharp_prefer_simple_using_statement = false:silent
+csharp_prefer_system_threading_lock = true
+dotnet_style_prefer_collection_expression = true:suggestion
+
+# Enable foreach explicit cast preference
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning
+
+# Prefer System.Threading.Lock
+csharp_prefer_system_threading_lock = true:warning
+
+# Prefer parameter null checking
+csharp_style_prefer_parameter_null_checking = true:suggestion
+
+# Prefer extended property pattern
+csharp_style_prefer_extended_property_pattern = true:suggestion
+
+# Prefer UTF-8 string literals
+csharp_style_prefer_utf8_string_literals = true:suggestion
+
+# Prefer tuple swap
+csharp_style_prefer_tuple_swap = true:suggestion
+
+# Prefer local over anonymous function
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+
+# Prefer unbound generic type in nameof
+csharp_style_prefer_unbound_generic_type_in_nameof = true:warning
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_null_check_over_type_check = true:warning
+csharp_style_throw_expression = true:warning
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:warning
+
+# New line preferences
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
+
+# Expression-Bodied members
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_lambdas = true:warning
+csharp_style_expression_bodied_local_functions = true:warning
+csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_prefer_not_pattern = true:warning
+csharp_style_prefer_pattern_matching = true:warning
+csharp_style_prefer_switch_expression = true:warning
+csharp_style_prefer_explicit_this = true:warning
+
+# "Null" checking preferences
+csharp_style_conditional_delegate_call = true:warning
+
+# Spacing options
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrap options
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = false
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Namespace options
+csharp_style_namespace_declarations = file_scoped:warning
+
+# Visual Basic Specific Settings
+
+[*.{vb}]
+
+visual_basic_preferred_modifier_order = Partial, Default, Private, Protected, Public, Friend, NotOverridable, Overridable, MustOverride, Overloads, Overrides, MustInherit, NotInheritable, Static, Shared, Shadows, ReadOnly, WriteOnly, Dim, Const, WithEvents, Widening, Narrowing, Custom, Async:warning
+visual_basic_style_prefer_simplified_object_creation = all : suggestion
+visual_basic_style_prefer_isnot_expression = true : suggestion
+
+# .NET Style Preferences
+
+[*.{cs,csx,cake,vb,vbx}]
+
+# Language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
+
+dotnet_style_collection_initializer = true:warning
+dotnet_style_explicit_tuple_names = true:warning
+dotnet_style_object_initializer = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_compound_assignment = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_inferred_tuple_names = true:warning
+dotnet_style_prefer_simplified_boolean_expressions = false:silent
+dotnet_style_prefer_simplified_interpolation = true:warning
+
+# Expression-level preferences
+dotnet_prefer_system_hash_code = true
+
+# Null-checking preferences
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+
+[*.g.cs]
+# Missing XML comment for publicly visible type or member 'Type_or_Member'
+dotnet_diagnostic.CS1591.severity = none
+
+# Design Rules (CA1000-CA1070)
+# Design rules support adherence to the .NET Framework design guidelines
+
+
+[*.{cs,vb}]
+
+# Do not declare static members on generic types
+dotnet_diagnostic.CA1000.severity = suggestion
+
+# Types that own disposable fields should be disposable
+dotnet_diagnostic.CA1001.severity = error
+
+# Do not expose generic lists
+dotnet_diagnostic.CA1002.severity = error
+
+# Use generic event handler instances
+dotnet_diagnostic.CA1003.severity = warning
+
+# Avoid excessive parameters on generic types
+dotnet_diagnostic.CA1005.severity = warning
+
+# Enums should have zero value
+dotnet_diagnostic.CA1008.severity = error
+
+# Collections should implement generic interface
+dotnet_diagnostic.CA1010.severity = warning
+
+# Abstract types should not have public constructors
+dotnet_diagnostic.CA1012.severity = error
+
+# Mark assemblies with CLSCompliantAttribute
+dotnet_diagnostic.CA1014.severity = silent
+
+# Mark assemblies with AssemblyVersionAttribute
+dotnet_diagnostic.CA1016.severity = error
+
+# Mark assemblies with ComVisibleAttribute
+dotnet_diagnostic.CA1017.severity = silent
+
+# Mark attributes with AttributeUsageAttribute
+dotnet_diagnostic.CA1018.severity = suggestion
+
+# Define accessors for attribute arguments
+dotnet_diagnostic.CA1019.severity = suggestion
+
+# Avoid out parameters
+dotnet_diagnostic.CA1021.severity = suggestion
+
+# Use properties where appropriate
+dotnet_diagnostic.CA1024.severity = suggestion
+
+# Mark enums with FlagsAttribute
+dotnet_diagnostic.CA1027.severity = error
+
+# Enum storage should be Int32
+dotnet_diagnostic.CA1028.severity = error
+
+# Use events where appropriate
+dotnet_diagnostic.CA1030.severity = suggestion
+
+# Do not catch general exception types
+dotnet_diagnostic.CA1031.severity = error
+
+# Implement standard exception constructors
+dotnet_diagnostic.CA1032.severity = suggestion
+
+# Interface methods should be callable by child types
+dotnet_diagnostic.CA1033.severity = suggestion
+
+# Nested types should not be visible
+dotnet_diagnostic.CA1034.severity = error
+
+# Override methods on comparable types
+dotnet_diagnostic.CA1036.severity = suggestion
+
+# Avoid empty interfaces
+dotnet_diagnostic.CA1040.severity = suggestion
+
+# Provide ObsoleteAttribute message
+dotnet_diagnostic.CA1041.severity = warning
+
+# Use integral or string argument for indexers
+dotnet_diagnostic.CA1043.severity = suggestion
+
+# Properties should not be write only
+dotnet_diagnostic.CA1044.severity = error
+
+# Do not pass types by reference
+dotnet_diagnostic.CA1045.severity = warning
+
+# Do not overload operator equals on reference types
+dotnet_diagnostic.CA1046.severity = warning
+
+# Do not declare protected members in sealed types
+dotnet_diagnostic.CA1047.severity = error
+
+# Declare types in namespaces
+dotnet_diagnostic.CA1050.severity = error
+
+# Do not declare visible instance fields
+dotnet_diagnostic.CA1051.severity = error
+
+# Static holder types should be Static or NotInheritable
+dotnet_diagnostic.CA1052.severity = warning
+
+# Static holder types should not have default constructors
+dotnet_diagnostic.CA1053.severity = warning
+
+# URI parameters should not be strings
+dotnet_diagnostic.CA1054.severity = warning
+
+# URI return values should not be strings
+dotnet_diagnostic.CA1055.severity = warning
+
+# URI properties should not be strings
+dotnet_diagnostic.CA1056.severity = warning
+
+# Types should not extend certain base types
+dotnet_diagnostic.CA1058.severity = warning
+
+# Move P/Invokes to NativeMethods class
+dotnet_diagnostic.CA1060.severity = warning
+
+# Do not hide base class methods
+dotnet_diagnostic.CA1061.severity = warning
+
+# Validate arguments of public methods
+dotnet_code_quality.CA1062.api_surface = public, protected
+dotnet_diagnostic.CA1062.severity = error
+
+# Implement IDisposable correctly
+dotnet_diagnostic.CA1063.severity = error
+
+# Exceptions should be public
+dotnet_diagnostic.CA1064.severity = warning
+
+# Do not raise exceptions in unexpected locations
+dotnet_diagnostic.CA1065.severity = warning
+
+# Implement IEquatable when overriding Equals
+dotnet_diagnostic.CA1066.severity = warning
+
+# Override Equals when implementing IEquatable
+dotnet_diagnostic.CA1067.severity = warning
+
+# CancellationToken parameters must come last
+dotnet_diagnostic.CA1068.severity = warning
+
+# Enums should not have duplicate values
+dotnet_diagnostic.CA1069.severity = error
+
+# Do not declare event fields as virtual
+dotnet_diagnostic.CA1070.severity = warning
+
+# Documentation Rules (CA1200)
+# Documentation rules support writing well-documented libraries
+
+
+[*.{cs,vb}]
+
+# Avoid using cref tags with a prefix
+dotnet_diagnostic.CA1200.severity = suggestion
+
+# Globalization Rules (CA1303-CA1311 and CA2101)
+# Globalization rules support world-ready libraries and applications
+
+
+[*.{cs,vb}]
+
+# Do not pass literals as localized parameters
+dotnet_code_quality.CA1303.use_naming_heuristic = true
+dotnet_diagnostic.CA1303.severity = none
+
+# Specify CultureInfo
+dotnet_diagnostic.CA1304.severity = error
+
+# Specify IFormatProvider
+dotnet_diagnostic.CA1305.severity = error
+
+# Specify StringComparison for clarity
+dotnet_diagnostic.CA1307.severity = error
+
+# Normalize strings to uppercase
+dotnet_diagnostic.CA1308.severity = warning
+
+# Use ordinal StringComparison
+dotnet_diagnostic.CA1309.severity = warning
+
+# Specify StringComparison for correctness
+dotnet_diagnostic.CA1310.severity = warning
+
+# Specify a culture or use an invariant version
+dotnet_diagnostic.CA1311.severity = error
+
+# Specify marshaling for P/Invoke string arguments
+dotnet_diagnostic.CA2101.severity = warning
+
+# Interoperability Rules (CA1400-CA1422)
+# Portability rules support portability across different platforms
+# Interoperability rules support interaction with COM clients
+
+
+[*.{cs,vb}]
+
+# P/Invokes should not be visible
+dotnet_diagnostic.CA1401.severity = error
+
+# Validate platform compatibility
+dotnet_diagnostic.CA1416.severity = suggestion
+
+# Do not use OutAttribute on string parameters for P/Invokes
+dotnet_diagnostic.CA1417.severity = suggestion
+
+# Validate platform compatibility
+dotnet_diagnostic.CA1418.severity = suggestion
+
+# Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'
+dotnet_diagnostic.CA1419.severity = suggestion
+
+# Property, type, or attribute requires runtime marshalling
+dotnet_diagnostic.CA1420.severity = suggestion
+
+# Method uses runtime marshalling when DisableRuntimeMarshallingAttribute is applied
+dotnet_diagnostic.CA1421.severity = warning
+
+# Validate platform compatibility - obsoleted APIs
+dotnet_diagnostic.CA1422.severity = warning
+
+# Maintainability Rules (CA1501-CA1516)
+# Maintainability rules support library and application maintenance
+
+
+[*.{cs,vb}]
+
+# Avoid excessive inheritance
+dotnet_diagnostic.CA1501.severity = warning
+
+# Avoid excessive complexity
+dotnet_diagnostic.CA1502.severity = error
+
+# Avoid unmaintainable code
+dotnet_diagnostic.CA1505.severity = error
+
+# Avoid excessive class coupling
+dotnet_diagnostic.CA1506.severity = error
+
+# Use nameof in place of string
+dotnet_diagnostic.CA1507.severity = warning
+
+# Avoid dead conditional code
+dotnet_diagnostic.CA1508.severity = error
+
+# Invalid entry in code metrics configuration file
+dotnet_diagnostic.CA1509.severity = error
+
+# Use ArgumentNullException throw helper
+dotnet_diagnostic.CA1510.severity = warning
+
+# Use ArgumentException throw helper
+dotnet_diagnostic.CA1511.severity = warning
+
+# Use ArgumentOutOfRangeException throw helper
+dotnet_diagnostic.CA1512.severity = warning
+
+# Use ObjectDisposedException throw helper
+dotnet_diagnostic.CA1513.severity = warning
+
+# Avoid redundant length argument
+dotnet_diagnostic.CA1514.severity = warning
+
+# Consider making public types internal
+dotnet_diagnostic.CA1515.severity = suggestion
+
+# Naming Rules (CA1700-CA1727 and IDE0130)
+# Naming rules support adherence to the naming conventions of the .NET design guidelines
+
+
+[*.{cs,vb}]
+
+# Namespace does not match folder structure
+dotnet_diagnostic.IDE0130.severity = warning
+
+# Do not name enum values 'Reserved'
+dotnet_code_quality.CA1700.api_surface = public, protected
+dotnet_diagnostic.CA1700.severity = error
+
+# Identifiers should not contain underscores
+dotnet_code_quality.CA1707.api_surface = public, protected
+dotnet_diagnostic.CA1707.severity = error
+
+# Identifiers should differ by more than case
+dotnet_diagnostic.CA1708.severity = error
+
+# Identifiers should have correct suffix
+dotnet_diagnostic.CA1710.severity = error
+
+# Identifiers should not have incorrect suffix
+dotnet_diagnostic.CA1711.severity = error
+
+# Do not prefix enum values with type name
+dotnet_diagnostic.CA1712.severity = error
+
+# Events should not have Before or After prefix
+dotnet_diagnostic.CA1713.severity = error
+
+# Flags enums should have plural names
+dotnet_diagnostic.CA1714.severity = error
+
+# Identifiers should have correct prefix
+dotnet_diagnostic.CA1715.severity = error
+
+# Identifiers should not match keywords
+dotnet_diagnostic.CA1716.severity = error
+
+# Only FlagsAttribute enums should have plural names
+dotnet_diagnostic.CA1717.severity = error
+
+# Identifiers should not contain type names
+dotnet_diagnostic.CA1720.severity = warning
+
+# Property names should not match get methods
+dotnet_diagnostic.CA1721.severity = error
+
+# Type names should not match namespaces
+dotnet_diagnostic.CA1724.severity = warning
+
+# Parameter names should match base declaration
+dotnet_diagnostic.CA1725.severity = warning
+
+# Use PascalCase for named placeholders
+dotnet_diagnostic.CA1727.severity = error
+
+# Performance Rules (CA1802-CA1877)
+# Performance rules support high-performance libraries and applications
+
+
+
+[*.{cs,vb}]
+
+# Use Literals Where Appropriate
+dotnet_diagnostic.CA1802.severity = suggestion
+
+# Do not initialize unnecessarily
+dotnet_diagnostic.CA1805.severity = error
+
+# Do not ignore method results
+dotnet_diagnostic.CA1806.severity = error
+
+# Initialize reference type static fields inline
+dotnet_diagnostic.CA1810.severity = suggestion
+
+# Avoid uninstantiated internal classes
+dotnet_diagnostic.CA1812.severity = suggestion
+
+# Avoid unsealed attributes
+dotnet_diagnostic.CA1813.severity = suggestion
+
+# Prefer jagged arrays over multidimensional
+dotnet_diagnostic.CA1814.severity = warning
+
+# Override equals and operator equals on value types
+dotnet_diagnostic.CA1815.severity = warning
+
+# Properties should not return arrays
+dotnet_diagnostic.CA1819.severity = warning
+dotnet_code_quality.CA1819.api_surface = public, protected
+
+# Test for empty strings using string length
+dotnet_diagnostic.CA1820.severity = error
+
+# Remove empty finalizers
+dotnet_diagnostic.CA1821.severity = error
+
+# Mark members as static
+dotnet_diagnostic.CA1822.severity = suggestion
+
+# Avoid unused private fields
+dotnet_diagnostic.CA1823.severity = error
+
+# Mark assemblies with NeutralResourcesLanguageAttribute
+dotnet_diagnostic.CA1824.severity = suggestion
+
+# Avoid zero-length array allocations
+dotnet_diagnostic.CA1825.severity = suggestion
+
+# Use property instead of Linq Enumerable method
+dotnet_diagnostic.CA1826.severity = error
+
+# Do not use Count/LongCount when Any can be used
+dotnet_diagnostic.CA1827.severity = silent
+
+# Do not use CountAsync/LongCountAsync when AnyAsync can be used
+dotnet_diagnostic.CA1828.severity = silent
+
+# Use Length/Count property instead of Enumerable.Count() method
+dotnet_diagnostic.CA1829.severity = error
+
+# Prefer strongly-typed Append and Insert method overloads on StringBuilder
+dotnet_diagnostic.CA1830.severity = warning
+
+# Use AsSpan instead of Range-based indexers for string when appropriate
+dotnet_diagnostic.CA1831.severity = error
+
+# Use AsSpan or AsMemory instead of Range-based indexers for getting ReadOnlySpan or ReadOnlyMemory portion of an array *
+dotnet_diagnostic.CA1832.severity = error
+
+# Use AsSpan or AsMemory instead of Range-based indexers for getting Span or Memory portion of an array
+dotnet_diagnostic.CA1833.severity = error
+
+# Use StringBuilder.Append(char) for single character strings
+dotnet_diagnostic.CA1834.severity = suggestion
+
+# Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes
+dotnet_diagnostic.CA1835.severity = warning
+
+# Prefer IsEmpty over Count when available
+dotnet_diagnostic.CA1836.severity = error
+
+# Use Environment.ProcessId instead of Process.GetCurrentProcess().Id
+dotnet_diagnostic.CA1837.severity = suggestion
+
+# Avoid StringBuilder parameters for P/Invokes
+dotnet_diagnostic.CA1838.severity = warning
+
+# Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName
+dotnet_diagnostic.CA1839.severity = error
+
+# Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId
+dotnet_diagnostic.CA1840.severity = silent
+
+# Prefer Dictionary Contains methods
+dotnet_diagnostic.CA1841.severity = silent
+
+# Do not use 'WhenAll' with a single task
+ *
+dotnet_diagnostic.CA1842.severity = error
+
+# Do not use 'WaitAll' with a single task
+ *
+dotnet_diagnostic.CA1843.severity = error
+
+# Provide memory-based overrides of async methods when subclassing 'Stream' *
+dotnet_diagnostic.CA1844.severity = warning
+
+# Use span-based 'string.Concat'
+dotnet_diagnostic.CA1845.severity = error
+
+# Prefer AsSpan over Substring
+dotnet_diagnostic.CA1846.severity = error
+
+
+# Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks
+dotnet_diagnostic.CA2352.severity = error
+
+# Unsafe DataSet or DataTable in serializable type
+dotnet_diagnostic.CA2353.severity = error
+
+# Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attack
+dotnet_diagnostic.CA2354.severity = error
+
+# Unsafe DataSet or DataTable in deserialized object graph
+dotnet_diagnostic.CA2355.severity = error
+
+# Unsafe DataSet or DataTable type in web deserialized object graph
+dotnet_diagnostic.CA2356.severity = error
+
+# Ensure autogenerated class containing DataSet.ReadXml() is not used with untrusted data
+dotnet_diagnostic.CA2361.severity = error
+
+# Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks
+dotnet_diagnostic.CA2362.severity = error
+
+# Review code for SQL injection vulnerabilities
+dotnet_diagnostic.CA3001.severity = warning
+
+# Review code for XSS vulnerabilities
+dotnet_diagnostic.CA3002.severity = warning
+
+# Review code for file path injection vulnerabilities
+dotnet_diagnostic.CA3003.severity = warning
+
+# Review code for information disclosure vulnerabilities
+dotnet_diagnostic.CA3004.severity = warning
+
+# Review code for LDAP injection vulnerabilities
+dotnet_diagnostic.CA3005.severity = error
+
+# Review code for process command injection vulnerabilities
+dotnet_diagnostic.CA3006.severity = error
+
+# Review code for open redirect vulnerabilities
+dotnet_diagnostic.CA3007.severity = warning
+
+# Review code for XPath injection vulnerabilities
+dotnet_diagnostic.CA3008.severity = error
+
+# Review code for XML injection vulnerabilities
+dotnet_diagnostic.CA3009.severity = error
+
+# Review code for XAML injection vulnerabilities
+dotnet_diagnostic.CA3010.severity = error
+
+# Review code for DLL injection vulnerabilities
+dotnet_diagnostic.CA3011.severity = warning
+
+# Review code for regex injection vulnerabilities
+dotnet_diagnostic.CA3012.severity = warning
+
+# Do Not Add Schema By URL
+dotnet_diagnostic.CA3061.severity = warning
+
+# Insecure DTD processing in XML
+dotnet_diagnostic.CA3075.severity = warning
+
+# Insecure XSLT script processing
+dotnet_diagnostic.CA3076.severity = warning
+
+# Insecure Processing in API Design, XmlDocument and XmlTextReader
+dotnet_diagnostic.CA3077.severity = warning
+
+# Mark Verb Handlers With Validate Antiforgery Token
+dotnet_diagnostic.CA3147.severity = warning
+
+# Do Not Use Weak Cryptographic Algorithms
+dotnet_diagnostic.CA5350.severity = warning
+
+# Do Not Use Broken Cryptographic Algorithms
+dotnet_diagnostic.CA5351.severity = error
+
+# Do Not Use Unsafe Cipher Modes
+dotnet_diagnostic.CA5358.severity = warning
+
+# Do Not Disable Certificate Validation
+dotnet_diagnostic.CA5359.severity = warning
+
+# Do Not Call Dangerous Methods In Deserialization
+dotnet_diagnostic.CA5360.severity = warning
+
+# Do Not Disable SChannel Use of Strong Crypto
+dotnet_diagnostic.CA5361.severity = warning
+
+# Do Not Refer Self In Serializable Class
+dotnet_diagnostic.CA5362.severity = error
+
+# Do Not Disable Request Validation
+dotnet_diagnostic.CA5363.severity = warning
+
+# Do Not Use Deprecated Security Protocols
+dotnet_diagnostic.CA5364.severity = error
+
+# Do Not Disable HTTP Header Checking
+dotnet_diagnostic.CA5365.severity = warning
+
+# Use XmlReader For DataSet Read Xml
+dotnet_diagnostic.CA5366.severity = warning
+
+# Do Not Serialize Types With Pointer Fields
+dotnet_diagnostic.CA5367.severity = warning
+
+# Set ViewStateUserKey For Classes Derived From Page
+dotnet_diagnostic.CA5368.severity = warning
+
+# Use XmlReader For Deserialize
+dotnet_diagnostic.CA5369.severity = warning
+
+# Use XmlReader For Validating Reader
+dotnet_diagnostic.CA5370.severity = warning
+
+# Use XmlReader For Schema Read
+dotnet_diagnostic.CA5371.severity = warning
+
+# Use XmlReader For XPathDocument
+dotnet_diagnostic.CA5372.severity = warning
+
+# Do not use obsolete key derivation function
+dotnet_diagnostic.CA5373.severity = warning
+
+# Do Not Use XslTransform
+dotnet_diagnostic.CA5374.severity = warning
+
+# Do Not Use Account Shared Access Signature
+dotnet_diagnostic.CA5375.severity = warning
+
+# Use SharedAccessProtocol HttpsOnly
+dotnet_diagnostic.CA5376.severity = warning
+
+# Use Container Level Access Policy
+dotnet_diagnostic.CA5377.severity = warning
+
+# Do not disable ServicePointManagerSecurityProtocols
+dotnet_diagnostic.CA5378.severity = warning
+
+# Do Not Use Weak Key Derivation Function Algorithm
+dotnet_diagnostic.CA5379.severity = warning
+
+# Do Not Add Certificates To Root Store
+dotnet_diagnostic.CA5380.severity = warning
+
+# Ensure Certificates Are Not Added To Root Store
+dotnet_diagnostic.CA5381.severity = warning
+
+# Use Secure Cookies In ASP.Net Core
+dotnet_diagnostic.CA5382.severity = warning
+
+# Ensure Use Secure Cookies In ASP.Net Core
+dotnet_diagnostic.CA5383.severity = warning
+
+# Do Not Use Digital Signature Algorithm (DSA)
+dotnet_diagnostic.CA5384.severity = warning
+
+# Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size
+dotnet_diagnostic.CA5385.severity = warning
+
+# Avoid hardcoding SecurityProtocolType value
+dotnet_diagnostic.CA5386.severity = warning
+
+# Do Not Use Weak Key Derivation Function With Insufficient Iteration Count
+dotnet_diagnostic.CA5387.severity = warning
+
+# Ensure Sufficient Iteration Count When Using Weak Key Derivation Function
+dotnet_diagnostic.CA5388.severity = warning
+
+# Do Not Add Archive Item's Path To The Target File System Path
+dotnet_diagnostic.CA5389.severity = warning
+
+# Do Not Hard Code Encryption Key
+dotnet_diagnostic.CA5390.severity = error
+
+# Use antiforgery tokens in ASP.NET Core MVC controllers
+dotnet_diagnostic.CA5391.severity = warning
+
+# Use DefaultDllImportSearchPaths attribute for P/Invokes
+dotnet_diagnostic.CA5392.severity = warning
+
+# Do not use unsafe DllImportSearchPath value
+dotnet_diagnostic.CA5393.severity = warning
+
+# Do not use insecure randomness
+dotnet_diagnostic.CA5394.severity = warning
+
+# Miss HttpVerb attribute for action methods
+dotnet_diagnostic.CA5395.severity = warning
+
+# Set HttpOnly to true for HttpCookie
+dotnet_diagnostic.CA5396.severity = warning
+
+# Do not use deprecated SslProtocols values
+dotnet_diagnostic.CA5397.severity = warning
+
+# Avoid hardcoded SslProtocols values
+dotnet_diagnostic.CA5398.severity = warning
+
+# Definitely disable HttpClient certificate revocation list check
+dotnet_diagnostic.CA5399.severity = warning
+
+# Ensure HttpClient certificate revocation list check is not disabled
+dotnet_diagnostic.CA5400.severity = warning
+
+# Do not use CreateEncryptor with non-default IV
+dotnet_diagnostic.CA5401.severity = suggestion
+
+# Use CreateEncryptor with the default IV
+dotnet_diagnostic.CA5402.severity = warning
+
+# Do not hard-code certificate
+dotnet_diagnostic.CA5403.severity = error
+
+# Do not disable token validation checks
+dotnet_diagnostic.CA5404.severity = error
+
+# Do not always skip token validation in delegates
+dotnet_diagnostic.CA5405.severity = error
+
+dotnet_diagnostic.SEC001.severity = error
+
+# SingleFile Rules (IL3000-IL3005)
+# Single-file rules support single-file applications
+
+
+[*.{cs,vb}]
+
+# Avoid using accessing Assembly file path when publishing as a single-file
+dotnet_diagnostic.IL3000.severity = error
+
+# Avoid accessing Assembly file path when publishing as a single file
+dotnet_diagnostic.IL3001.severity = error
+
+# Avoid calling members annotated with 'RequiresAssemblyFilesAttribute' when publishing as a single file
+dotnet_diagnostic.IL3002.severity = error
+
+# RequiresAssemblyFilesAttribute annotations must match across all interface implementations or overrides
+dotnet_diagnostic.IL3003.severity = error
+
+# RequiresAssemblyFilesAttribute cannot be placed directly on application entry point
+dotnet_diagnostic.IL3005.severity = error
+
+# Style Rules (IDE0001-IDE0380)
+# Style rules support consistent code style in your codebase
+
+
+[*.{cs,vb}]
+
+# Simplify name
+dotnet_diagnostic.IDE0001.severity = warning
+
+# Simplify member access
+dotnet_diagnostic.IDE0002.severity = warning
+
+# this and Me preferences
+dotnet_diagnostic.IDE0003.severity = suggestion
+dotnet_diagnostic.IDE0009.severity = suggestion
+
+# Remove unnecessary cast
+dotnet_diagnostic.IDE0004.severity = error
+
+# Remove unnecessary import
+dotnet_diagnostic.IDE0005.severity = warning
+
+# Add missing cases to switch statement
+dotnet_diagnostic.IDE0010.severity = warning
+
+# Add braces
+dotnet_diagnostic.IDE0011.severity = silent
+
+# Use throw expression
+dotnet_diagnostic.IDE0016.severity = warning
+
+# Use object initializers
+dotnet_diagnostic.IDE0017.severity = suggestion
+
+# Inline variable declaration
+dotnet_diagnostic.IDE0018.severity = suggestion
+
+# Use pattern matching to avoid 'as' followed by a 'null' check
+dotnet_diagnostic.IDE0019.severity = warning
+
+# Use pattern matching to avoid 'is' check followed by a cast
+dotnet_diagnostic.IDE0020.severity = warning
+
+# Use expression body for constructors
+dotnet_diagnostic.IDE0021.severity = silent
+
+# Use expression body for methods
+dotnet_diagnostic.IDE0022.severity = silent
+
+# Use expression body for operators
+dotnet_diagnostic.IDE0023.severity = silent
+dotnet_diagnostic.IDE0024.severity = silent
+
+# Use expression body for properties
+dotnet_diagnostic.IDE0025.severity = silent
+
+# Use expression body for indexers
+dotnet_diagnostic.IDE0026.severity = suggestion
+
+# Use expression body for accessors
+dotnet_diagnostic.IDE0027.severity = silent
+
+# Use collection initializers
+dotnet_diagnostic.IDE0028.severity = suggestion
+
+# Use coalesce expression
+dotnet_diagnostic.IDE0029.severity = suggestion
+
+# Use coalesce expression (nullable types)
+dotnet_diagnostic.IDE0030.severity = suggestion
+
+# Use null propagation
+dotnet_diagnostic.IDE0031.severity = suggestion
+
+# Use auto property
+dotnet_diagnostic.IDE0032.severity = suggestion
+
+# Use explicitly provided tuple name
+dotnet_diagnostic.IDE0033.severity = suggestion
+
+# Simplify 'default' expression
+dotnet_diagnostic.IDE0034.severity = suggestion
+
+# Remove unreachable code
+dotnet_diagnostic.IDE0035.severity = error
+
+# Order modifiers
+dotnet_diagnostic.IDE0036.severity = warning
+
+# Use inferred member name
+dotnet_diagnostic.IDE0037.severity = suggestion
+
+# Use pattern matching to avoid is check followed by a cast (without variable)
+dotnet_diagnostic.IDE0038.severity = warning
+
+# Use local function instead of lambda
+dotnet_diagnostic.IDE0039.severity = suggestion
+
+# Add accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = error
+
+# Use is null check
+dotnet_diagnostic.IDE0041.severity = suggestion
+
+# Deconstruct variable declaration
+dotnet_diagnostic.IDE0042.severity = suggestion
+
+# Format string contains invalid placeholder
+dotnet_diagnostic.IDE0043.severity = warning
+
+# Add readonly modifier
+dotnet_diagnostic.IDE0044.severity = warning
+
+# Use conditional expression for assignment
+dotnet_diagnostic.IDE0045.severity = suggestion
+
+# Use conditional expression for return
+dotnet_diagnostic.IDE0046.severity = suggestion
+
+# Parentheses preferences
+dotnet_diagnostic.IDE0047.severity = warning
+dotnet_diagnostic.IDE0048.severity = warning
+
+# Use language keywords instead of framework type names for type references
+dotnet_diagnostic.IDE0049.severity = warning
+
+# Convert anonymous type to tuple
+dotnet_diagnostic.IDE0050.severity = suggestion
+
+# Remove unused private member
+dotnet_diagnostic.IDE0051.severity = warning
+
+# Remove unread private member
+dotnet_diagnostic.IDE0052.severity = error
+
+# Use expression body for lambdas
+dotnet_diagnostic.IDE0053.severity = suggestion
+
+# Use compound assignment
+dotnet_diagnostic.IDE0054.severity = suggestion
+
+# Fix formatting
+dotnet_diagnostic.IDE0055.severity = suggestion
+
+# Use index operator
+dotnet_diagnostic.IDE0056.severity = suggestion
+
+# Use range operator
+dotnet_diagnostic.IDE0057.severity = suggestion
+
+# Remove unnecessary expression value
+dotnet_diagnostic.IDE0058.severity = silent
+
+# Remove unnecessary value assignment
+dotnet_diagnostic.IDE0059.severity = error
+
+# Remove unused parameter
+dotnet_diagnostic.IDE0060.severity = error
+
+# Use expression body for local functions
+dotnet_diagnostic.IDE0061.severity = suggestion
+
+# Make local function static
+dotnet_diagnostic.IDE0062.severity = suggestion
+
+# Use simple 'using' statement
+dotnet_diagnostic.IDE0063.severity = suggestion
+
+# Make struct fields writable
+dotnet_diagnostic.IDE0064.severity = suggestion
+
+# 'using' directive placement
+dotnet_diagnostic.IDE0065.severity = error
+
+# Use switch expression
+dotnet_diagnostic.IDE0066.severity = warning
+
+# Use 'System.HashCode.Combine'
+dotnet_diagnostic.IDE0070.severity = warning
+
+# Add missing cases to switch expression
+dotnet_diagnostic.IDE0072.severity = suggestion
+
+# Require file header
+dotnet_diagnostic.IDE0073.severity = warning
+
+# Use compound assignment
+dotnet_diagnostic.IDE0074.severity = suggestion
+
+# Simplify conditional expression
+dotnet_diagnostic.IDE0075.severity = suggestion
+
+# Remove invalid global 'SuppressMessageAttribute'
+dotnet_diagnostic.IDE0076.severity = silent
+
+# Avoid legacy format target in global 'SuppressMessageAttribute'
+dotnet_diagnostic.IDE0077.severity = warning
+
+# Use pattern matching
+dotnet_diagnostic.IDE0078.severity = warning
+
+# Remove unnecessary suppression
+dotnet_diagnostic.IDE0079.severity = error
+
+# Remove unnecessary suppression operator
+dotnet_diagnostic.IDE0080.severity = suggestion
+
+# Remove ByVal
+dotnet_diagnostic.IDE0081.severity = error
+
+# Convert typeof to nameof
+dotnet_diagnostic.IDE0082.severity = warning
+
+# Use pattern matching (not operator)
+dotnet_diagnostic.IDE0083.severity = warning
+
+# Use pattern matching (IsNot operator)
+dotnet_diagnostic.IDE0084.severity = suggestion
+
+# Simplify new expression
+dotnet_diagnostic.IDE0090.severity = warning
+
+# Remove unnecessary equality operator
+dotnet_diagnostic.IDE0100.severity = silent
+
+# Use conditional delegate call
+dotnet_diagnostic.IDE0105.severity = warning
+
+# Remove unnecessary discard
+dotnet_diagnostic.IDE0110.severity = warning
+
+# Simplify LINQ expression
+dotnet_diagnostic.IDE0120.severity = warning
+
+# Simplify LINQ type check and cast
+dotnet_diagnostic.IDE0121.severity = warning
+
+# Simplify object creation
+dotnet_diagnostic.IDE0140.severity = suggestion
+
+# Prefer 'null' check over type check
+dotnet_diagnostic.IDE0150.severity = warning
+
+# Use block-scoped namespace
+dotnet_diagnostic.IDE0160.severity = suggestion
+
+# Use file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = suggestion
+
+# Simplify property pattern
+dotnet_diagnostic.IDE0170.severity = warning
+
+# Use tuple to swap values
+dotnet_diagnostic.IDE0180.severity = suggestion
+
+# Unnecessary lambda expression
+dotnet_diagnostic.IDE0200.severity = warning
+
+# Convert to top-level statements
+dotnet_diagnostic.IDE0210.severity = warning
+
+# Convert to 'Program.Main' style program
+dotnet_diagnostic.IDE0211.severity = suggestion
+
+# Add explicit cast in foreach loop
+dotnet_diagnostic.IDE0220.severity = error
+
+# Use UTF-8 string literal
+dotnet_diagnostic.IDE0230.severity = warning
+
+# Nullable directive is redundant
+dotnet_diagnostic.IDE0240.severity = error
+
+# Nullable directive is unnecessary
+dotnet_diagnostic.IDE0241.severity = error
+
+# Struct can be made 'readonly'
+dotnet_diagnostic.IDE0250.severity = warning
+
+# Member can be made 'readonly'
+dotnet_diagnostic.IDE0251.severity = warning
+
+# Use pattern matching
+dotnet_diagnostic.IDE0260.severity = warning
+
+# Null check can be simplified
+dotnet_diagnostic.IDE0270.severity = warning
+
+# Use 'nameof'
+dotnet_diagnostic.IDE0280.severity = error
+
+# Use primary constructor
+dotnet_diagnostic.IDE0290.severity = suggestion
+
+# Use collection expression for array
+dotnet_diagnostic.IDE0300.severity = warning
+
+# Use collection expression for empty
+dotnet_diagnostic.IDE0301.severity = suggestion
+
+# Use collection expression for stackalloc
+dotnet_diagnostic.IDE0302.severity = warning
+
+# Use collection expression for Create()
+dotnet_diagnostic.IDE0303.severity = warning
+
+# Use collection expression for builder
+dotnet_diagnostic.IDE0304.severity = warning
+
+# Use collection expression for fluent
+dotnet_diagnostic.IDE0305.severity = warning
+
+# Use collection expression for new
+dotnet_diagnostic.IDE0306.severity = warning
+
+# Make anonymous function static
+dotnet_diagnostic.IDE0320.severity = warning
+
+# Prefer 'System.Threading.Lock'
+dotnet_diagnostic.IDE0330.severity = warning
+
+# Use unbound generic type
+dotnet_diagnostic.IDE0340.severity = warning
+
+# Use implicitly typed lambda
+dotnet_diagnostic.IDE0350.severity = warning
+
+# Simplify property accessor
+dotnet_diagnostic.IDE0360.severity = suggestion
+
+# Remove unnecessary `unsafe` modifier
+dotnet_diagnostic.IDE0380.severity = warning
+
+# Remove unnecessary suppression (null-forgiving operator)
+dotnet_diagnostic.IDE0370.severity = warning
+
+# Naming rule violation
+dotnet_diagnostic.IDE1006.severity = silent
+
+# Embedded statements must be on their own line
+dotnet_diagnostic.IDE2001.severity = warning
+
+# Consecutive braces must not have blank line between them
+dotnet_diagnostic.IDE2002.severity = warning
+
+# Blank line required between block and subsequent statement
+dotnet_diagnostic.IDE2003.severity = warning
+
+# Blank line not allowed after constructor initializer colon
+dotnet_diagnostic.IDE2004.severity = warning
+
+# Blank line not allowed after conditional expression token
+dotnet_diagnostic.IDE2005.severity = warning
+
+# Blank line not allowed after arrow expression clause token
+dotnet_diagnostic.IDE2006.severity = warning
+
+# C# Style Rules - var preferences
+
+[*.{cs,csx,cake}]
+
+dotnet_diagnostic.IDE0007.severity = warning
+dotnet_diagnostic.IDE0008.severity = none
+dotnet_diagnostic.PDS0003.severity = warning
+
+# Usage Rules (CA1801, CA1816, CA2200-CA2267)
+# Usage rules support proper usage of .NET
+
+
+[*.{cs,vb}]
+
+# Review unused parameters
+dotnet_diagnostic.CA1801.severity = warning
+
+# Call GC.SuppressFinalize correctly
+dotnet_diagnostic.CA1816.severity = error
+
+# Rethrow to preserve stack details
+dotnet_diagnostic.CA2200.severity = error
+
+# Do not raise reserved exception types
+dotnet_diagnostic.CA2201.severity = error
+
+# Initialize value type static fields inline
+dotnet_diagnostic.CA2207.severity = warning
+
+# Instantiate argument exceptions correctly
+dotnet_diagnostic.CA2208.severity = error
+
+# Non-constant fields should not be visible
+dotnet_diagnostic.CA2211.severity = error
+
+# Disposable fields should be disposed
+dotnet_diagnostic.CA2213.severity = error
+
+# Do not call overridable methods in constructors
+dotnet_diagnostic.CA2214.severity = warning
+
+# Dispose methods should call base class dispose
+dotnet_diagnostic.CA2215.severity = error
+
+# Disposable types should declare finalizer
+dotnet_diagnostic.CA2216.severity = warning
+
+# Do not mark enums with FlagsAttribute
+dotnet_diagnostic.CA2217.severity = error
+
+# Override GetHashCode on overriding Equals
+dotnet_diagnostic.CA2218.severity = warning
+
+# Do not raise exceptions in exception clauses
+dotnet_diagnostic.CA2219.severity = error
+
+# Override Equals on overloading operator equals
+dotnet_diagnostic.CA2224.severity = warning
+
+# Operator overloads have named alternates
+dotnet_diagnostic.CA2225.severity = warning
+
+# Operators should have symmetrical overloads
+dotnet_diagnostic.CA2226.severity = warning
+
+# Collection properties should be read only
+dotnet_diagnostic.CA2227.severity = error
+
+# Implement serialization constructors
+dotnet_diagnostic.CA2229.severity = warning
+
+# Overload operator equals on overriding ValueType.Equals
+dotnet_diagnostic.CA2231.severity = warning
+
+# Pass System.Uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = warning
+
+# Mark all non-serializable fields
+dotnet_diagnostic.CA2235.severity = suggestion
+
+# Mark ISerializable types with SerializableAttribute
+dotnet_diagnostic.CA2237.severity = warning
+
+# Provide correct arguments to formatting methods
+dotnet_diagnostic.CA2241.severity = warning
+
+# Test for NaN correctly
+dotnet_diagnostic.CA2242.severity = warning
+
+# Attribute string literals should parse correctly
+dotnet_diagnostic.CA2243.severity = warning
+
+# Do not duplicate indexed element initializations
+dotnet_diagnostic.CA2244.severity = warning
+
+# Do not assign a property to itself
+dotnet_diagnostic.CA2245.severity = error
+
+# Do not assign a symbol and its member in the same statement
+dotnet_diagnostic.CA2246.severity = warning
+
+# Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum
+dotnet_diagnostic.CA2247.severity = warning
+
+# Provide correct enum argument to Enum.HasFlag
+dotnet_diagnostic.CA2248.severity = warning
+
+# Consider using String.Contains instead of String.IndexOf
+dotnet_diagnostic.CA2249.severity = error
+
+# Use ThrowIfCancellationRequested
+dotnet_diagnostic.CA2250.severity = warning
+
+# Use String.Equals over String.Compare
+dotnet_diagnostic.CA2251.severity = warning
+
+# Opt in to preview features
+dotnet_diagnostic.CA2252.severity = suggestion
+
+# Named placeholders should not be numeric values
+dotnet_diagnostic.CA2253.severity = warning
+
+# Template should be a static expression
+dotnet_diagnostic.CA2254.severity = warning
+
+# The ModuleInitializer attribute should not be used in libraries
+dotnet_diagnostic.CA2255.severity = warning
+
+# All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface
+dotnet_diagnostic.CA2256.severity = warning
+
+# Members defined on an interface with 'DynamicInterfaceCastableImplementationAttribute' should be 'static'
+dotnet_diagnostic.CA2257.severity = warning
+
+# Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported
+dotnet_diagnostic.CA2258.severity = warning
+
+# Ensure ThreadStatic is only used with static fields
+dotnet_diagnostic.CA2259.severity = error
+
+# Implement generic math interfaces correctly
+dotnet_diagnostic.CA2260.severity = warning
+
+# Do not use ConfigureAwaitOptions.SuppressThrowing with Task
+dotnet_diagnostic.CA2261.severity = warning
+
+# Set 'MaxResponseHeadersLength' properly
+dotnet_diagnostic.CA2262.severity = warning
+
+# Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'
+dotnet_diagnostic.CA2264.severity = warning
+
+# Do not compare Span to null or default
+dotnet_diagnostic.CA2265.severity = error
+
+# Use correct type parameter
+dotnet_diagnostic.CA2266.severity = warning
+
+# Use correct type parameter for 'IAsyncEnumerable.ConfigureAwait'
+dotnet_diagnostic.CA2267.severity = warning
+
+# External Analyzer Rules
+# Rules from StyleCop, Async, Dispose, Regex, JSON, MSTest, and other third-party analyzers
+
+
+
+[*.{cs,vb}]
+
+# StyleCop Analyzer Rules (SA)
+
+# A violation of this rule occurs when a compilation (project) contains files with DocumentationMode set to None
+dotnet_diagnostic.SA0001.severity = warning
+
+# The spacing around a C# keyword is incorrect
+dotnet_diagnostic.SA1000.severity = warning
+
+# DoNotPrefixCallsWithBaseUnlessLocalImplementationExists
+dotnet_diagnostic.SA1100.severity = warning
+
+# StatementMustNotUseUnnecessaryParenthesis
+dotnet_diagnostic.SA1119.severity = suggestion
+
+# CommentsMustContainText
+dotnet_diagnostic.SA1120.severity = warning
+
+# DoNotUseRegions
+dotnet_diagnostic.SA1124.severity = warning
+
+# UsingDirectivesMustBePlacedCorrectly
+dotnet_diagnostic.SA1200.severity = warning
+
+# ElementsMustAppearInTheCorrectOrder
+dotnet_diagnostic.SA1201.severity = warning
+
+# ElementsMustBeOrderedByAccess
+dotnet_diagnostic.SA1202.severity = warning
+
+# ElementMustBeginWithUpperCaseLetter
+dotnet_diagnostic.SA1300.severity = error
+
+# InterfaceNamesMustBeginWithI
+dotnet_diagnostic.SA1302.severity = error
+
+# ConstFieldNamesMustBeginWithUpperCaseLetter
+dotnet_diagnostic.SA1303.severity = error
+
+# FieldNamesMustNotUseHungarianNotation
+dotnet_diagnostic.SA1305.severity = warning
+
+# VariableNamesMustNotBePrefixed
+dotnet_diagnostic.SA1308.severity = error
+
+# A field name in C# begins with an underscore
+dotnet_diagnostic.SA1309.severity = silent
+
+# VariableNamesMustBeginWithLowerCaseLetter
+dotnet_diagnostic.SA1312.severity = warning
+
+# ParameterNamesMustBeginWithLowerCaseLetter
+dotnet_diagnostic.SA1313.severity = error
+
+# TupleElementNamesShouldUseCorrectCasing
+dotnet_diagnostic.SA1316.severity = warning
+
+# AccessModifierMustBeDeclared
+dotnet_diagnostic.SA1400.severity = warning
+
+# FieldsMustBePrivate
+dotnet_diagnostic.SA1401.severity = error
+
+# FileMayOnlyContainASingleNamespace
+dotnet_diagnostic.SA1403.severity = error
+
+# A Code Analysis SuppressMessage attribute does not include a justification
+dotnet_diagnostic.SA1404.severity = error
+
+# DebugAssertMustProvideMessageText
+dotnet_diagnostic.SA1405.severity = warning
+
+# DebugFailMustProvideMessageText
+dotnet_diagnostic.SA1406.severity = warning
+
+# RemoveUnnecessaryCode
+dotnet_diagnostic.SA1409.severity = error
+
+# BracesForMultiLineStatementsMustNotShareLine
+dotnet_diagnostic.SA1500.severity = error
+
+# ElementMustNotBeOnSingleLine
+dotnet_diagnostic.SA1502.severity = error
+
+# BracesMustNotBeOmitted
+dotnet_diagnostic.SA1503.severity = error
+
+# ClosingBraceMustBeFollowedByBlankLine
+dotnet_diagnostic.SA1513.severity = warning
+
+# UseBracesConsistently
+dotnet_diagnostic.SA1520.severity = error
+
+# File header copyright text should match
+dotnet_diagnostic.SA1636.severity = none
+
+# ElementDocumentationMustBeSpelledCorrectly
+dotnet_diagnostic.SA1650.severity = warning
+
+# Dispose Analyzer Rules (IDISP)
+
+# Dispose created
+dotnet_diagnostic.IDISP001.severity = error
+
+# Dispose member
+dotnet_diagnostic.IDISP002.severity = error
+
+# Dispose previous before re-assigning
+dotnet_diagnostic.IDISP003.severity = error
+
+# Don't ignore created IDisposable
+dotnet_diagnostic.IDISP004.severity = error
+
+# Return type should indicate that the value should be disposed
+dotnet_diagnostic.IDISP005.severity = error
+
+# Implement IDisposable
+dotnet_diagnostic.IDISP006.severity = error
+
+# Don't dispose injected
+dotnet_diagnostic.IDISP007.severity = error
+
+# Don't assign member with injected and created disposables
+dotnet_diagnostic.IDISP008.severity = error
+
+# Add IDisposable interface
+dotnet_diagnostic.IDISP009.severity = error
+
+# Call base.Dispose(disposing)
+dotnet_diagnostic.IDISP010.severity = error
+
+# Don't return disposed instance
+dotnet_diagnostic.IDISP011.severity = warning
+
+# Property should not return created disposable
+dotnet_diagnostic.IDISP012.severity = error
+
+# Await in using
+dotnet_diagnostic.IDISP013.severity = error
+
+# Use a single instance of HttpClient
+dotnet_diagnostic.IDISP014.severity = warning
+
+# Member should not return created and cached instance
+dotnet_diagnostic.IDISP015.severity = error
-# C# files
-[*.cs]
+# Don't use disposed instance
+dotnet_diagnostic.IDISP016.severity = error
-#### Core EditorConfig Options ####
+# Prefer using
+dotnet_diagnostic.IDISP017.severity = error
-max_line_length = 120
-trim_trailing_whitespace = true
+# Inline variable declaration
+dotnet_diagnostic.IDISP018.severity = error
-# Indentation and spacing
-indent_size = 4
-tab_width = 4
+# Call SuppressFinalize
+dotnet_diagnostic.IDISP019.severity = error
-# New line preferences
-end_of_line = lf
-insert_final_newline = true
+# Use pattern matching to avoid is check followed by a cast (with variable)
+dotnet_diagnostic.IDISP020.severity = error
-#### .NET Coding Conventions ####
-[*.{cs,vb}]
+# Call this.Dispose(true)
+dotnet_diagnostic.IDISP021.severity = error
-dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
-dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
-dotnet_naming_rule.private_members_with_underscore.severity = warning
+# Call this.Dispose(false)
+dotnet_diagnostic.IDISP022.severity = error
-dotnet_naming_symbols.private_fields.applicable_kinds = field
-dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+# Don't use reference types in finalizer context
+dotnet_diagnostic.IDISP023.severity = error
-dotnet_naming_style.prefix_underscore.capitalization = camel_case
-dotnet_naming_style.prefix_underscore.required_prefix = _
+# Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer
+dotnet_diagnostic.IDISP024.severity = error
-# Organize usings
-dotnet_separate_import_directive_groups = false
-dotnet_sort_system_directives_first = true
-file_header_template = unset
+# Class with no virtual dispose method should be sealed
+dotnet_diagnostic.IDISP025.severity = error
-# this. and Me. preferences
-dotnet_style_qualification_for_event = false:silent
-dotnet_style_qualification_for_field = false:silent
-dotnet_style_qualification_for_method = false:silent
-dotnet_style_qualification_for_property = false:silent
+# Class with no virtual DisposeAsyncCore method should be sealed
+dotnet_diagnostic.IDISP026.severity = error
-# Language keywords vs BCL types preferences
-dotnet_style_predefined_type_for_locals_parameters_members = true:silent
-dotnet_style_predefined_type_for_member_access = true:silent
+# Async Analyzer Rules (ASYNC, VSTHRD, RCS)
-# Parentheses preferences
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+# Asynchronous method names should end with Async
+dotnet_diagnostic.ASYNC0001.severity = error
-# Modifier preferences
+# Non asynchronous method names should not end with Async
+dotnet_diagnostic.ASYNC0002.severity = error
-# “internal/private not necessary” is typically an IDE diagnostic; set it to error if you want it blocking
-dotnet_diagnostic.IDE0040.severity = warning
-dotnet_style_require_accessibility_modifiers = omit_if_default:warning
+# Avoid void returning asynchronous method
+dotnet_diagnostic.ASYNC0003.severity = warning
-# Expression-level preferences
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-dotnet_style_prefer_auto_properties = true:suggestion
-dotnet_style_prefer_compound_assignment = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
-dotnet_style_prefer_conditional_expression_over_return = true:suggestion
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
-dotnet_style_prefer_inferred_tuple_names = true:suggestion
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
-dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
-dotnet_style_prefer_simplified_interpolation = true:suggestion
+# Use ConfigureAwait(false) on await expression
+dotnet_diagnostic.ASYNC0004.severity = error
-# Field preferences
-dotnet_style_readonly_field = true:warning
+# Do not use blocking call (make method async)
+dotnet_diagnostic.MA0045.severity = none
-# Parameter preferences
-dotnet_code_quality_unused_parameters = all:suggestion
+# Call 'ConfigureAwait(false)'
+dotnet_diagnostic.RCS1090.severity = error
-# Suppression preferences
-dotnet_remove_unnecessary_suppression_exclusions = none
+# Return completed task instead of returning null
+dotnet_diagnostic.RCS1210.severity = error
-#### C# Coding Conventions ####
-[*.cs]
+# AsyncifyInvocation: Use Task Async
+dotnet_diagnostic.AsyncifyInvocation.severity = error
-# var preferences
+# AsyncifyVariable: Use Task Async
+dotnet_diagnostic.AsyncifyVariable.severity = error
-# Suppress IDE0008 - var is acceptable for method calls, otherwise `var x = Guid.Parse(...)` looks givens warning.
-dotnet_diagnostic.IDE0008.severity = silent
+# Avoid legacy thread switching methods
+dotnet_diagnostic.VSTHRD001.severity = error
-csharp_style_var_for_built_in_types = true:silent
-csharp_style_var_when_type_is_apparent = false:warning
-csharp_style_var_elsewhere = true:silent
+# Avoid problematic synchronous waits
+dotnet_diagnostic.VSTHRD002.severity = error
-# Expression-bodied members
-csharp_style_expression_bodied_accessors = true:silent
-csharp_style_expression_bodied_constructors = when_possible:suggestion
-csharp_style_expression_bodied_indexers = true:silent
-csharp_style_expression_bodied_lambdas = true:suggestion
-csharp_style_expression_bodied_local_functions = when_possible:suggestion
-csharp_style_expression_bodied_methods = when_possible:suggestion
-csharp_style_expression_bodied_operators = when_possible:suggestion
-csharp_style_expression_bodied_properties = true:suggestion
+# Avoid awaiting foreign Tasks
+dotnet_diagnostic.VSTHRD003.severity = error
-# Pattern matching preferences
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_prefer_not_pattern = true:suggestion
-csharp_style_prefer_pattern_matching = true:silent
-csharp_style_prefer_switch_expression = true:suggestion
+# Await SwitchToMainThreadAsync
+dotnet_diagnostic.VSTHRD004.severity = error
-# Null-checking preferences
-csharp_style_conditional_delegate_call = true:suggestion
+# Invoke single-threaded types on Main thread
+dotnet_diagnostic.VSTHRD010.severity = error
-# Modifier preferences
-csharp_prefer_static_local_function = true:warning
-csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
+# Use AsyncLazy
+dotnet_diagnostic.VSTHRD011.severity = error
-# Code-block preferences
-csharp_prefer_braces = when_possible:error
-csharp_prefer_simple_using_statement = true:suggestion
+# Provide JoinableTaskFactory where allowed
+dotnet_diagnostic.VSTHRD012.severity = error
-# Expression-level preferences
-csharp_prefer_simple_default_expression = true:suggestion
-csharp_style_deconstructed_variable_declaration = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_pattern_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_index_operator = true:suggestion
-csharp_style_prefer_range_operator = true:suggestion
-csharp_style_throw_expression = true:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
-csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+# Avoid async void methods
+dotnet_diagnostic.VSTHRD100.severity = error
-# 'using' directive preferences
-csharp_using_directive_placement = outside_namespace:silent
+# Avoid unsupported async delegates
+dotnet_diagnostic.VSTHRD101.severity = error
-#### C# Formatting Rules ####
+# Implement internal logic asynchronously
+dotnet_diagnostic.VSTHRD102.severity = error
-# New line preferences
-csharp_new_line_before_catch = true
-csharp_new_line_before_else = true
-csharp_new_line_before_finally = true
-csharp_new_line_before_members_in_anonymous_types = true
-csharp_new_line_before_members_in_object_initializers = true
-csharp_new_line_before_open_brace = all
-csharp_new_line_between_query_expression_clauses = true
+# Call async methods when in an async method
+dotnet_diagnostic.VSTHRD103.severity = error
-# Indentation preferences
-csharp_indent_block_contents = true
-csharp_indent_braces = false
-csharp_indent_case_contents = true
-csharp_indent_case_contents_when_block = true
-csharp_indent_labels = one_less_than_current
-csharp_indent_switch_labels = true
+# Offer async option
+dotnet_diagnostic.VSTHRD104.severity = warning
-# Space preferences
-csharp_space_after_cast = false
-csharp_space_after_colon_in_inheritance_clause = true
-csharp_space_after_comma = true
-csharp_space_after_dot = false
-csharp_space_after_keywords_in_control_flow_statements = true
-csharp_space_after_semicolon_in_for_statement = true
-csharp_space_around_binary_operators = before_and_after
-csharp_space_around_declaration_statements = false
-csharp_space_before_colon_in_inheritance_clause = true
-csharp_space_before_comma = false
-csharp_space_before_dot = false
-csharp_space_before_open_square_brackets = false
-csharp_space_before_semicolon_in_for_statement = false
-csharp_space_between_empty_square_brackets = false
-csharp_space_between_method_call_empty_parameter_list_parentheses = false
-csharp_space_between_method_call_name_and_opening_parenthesis = false
-csharp_space_between_method_call_parameter_list_parentheses = false
-csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
-csharp_space_between_method_declaration_name_and_open_parenthesis = false
-csharp_space_between_method_declaration_parameter_list_parentheses = false
-csharp_space_between_parentheses = false
-csharp_space_between_square_brackets = false
+# Avoid method overloads that assume TaskScheduler.Current
+dotnet_diagnostic.VSTHRD105.severity = warning
-# Wrapping preferences
-csharp_preserve_single_line_blocks = true
-csharp_preserve_single_line_statements = true
-csharp_style_namespace_declarations = file_scoped:silent
-csharp_style_prefer_method_group_conversion = true:silent
-csharp_style_prefer_top_level_statements = true:silent
-csharp_style_prefer_primary_constructors = true:suggestion
-csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning
-csharp_prefer_system_threading_lock = true:suggestion
-csharp_style_allow_embedded_statements_on_same_line_experimental = false:error
-csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
-csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
-csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
-csharp_style_prefer_null_check_over_type_check = true:suggestion
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
+# Use InvokeAsync to raise async events
+dotnet_diagnostic.VSTHRD106.severity = error
+
+# Await Task within using expression
+dotnet_diagnostic.VSTHRD107.severity = error
+
+# Assert thread affinity unconditionally
+dotnet_diagnostic.VSTHRD108.severity = error
+
+# Switch instead of assert in async methods
+dotnet_diagnostic.VSTHRD109.severity = error
+
+# Observe result of async calls
+dotnet_diagnostic.VSTHRD110.severity = error
+
+# Use .ConfigureAwait(bool)
+dotnet_diagnostic.VSTHRD111.severity = error
+
+# Implement System.IAsyncDisposable
+dotnet_diagnostic.VSTHRD112.severity = error
+
+# Check for System.IAsyncDisposable
+dotnet_diagnostic.VSTHRD113.severity = error
+
+# Avoid returning a null Task
+dotnet_diagnostic.VSTHRD114.severity = error
+
+# Use "Async" suffix for async methods
+dotnet_diagnostic.VSTHRD200.severity = error
+
+# Asynchronous method name should end with 'Async'
+dotnet_diagnostic.RCS1046.severity = error
+
+# Non-asynchronous method name should not end with 'Async'
+dotnet_diagnostic.RCS1047.severity = error
+
+# Class Analyzer Rules
+
+# Seal Class
+dotnet_diagnostic.CLASS0001.severity = warning
+
+# Enum Analyzer Rules
+
+# Default switch label
+dotnet_diagnostic.ENUM0001.severity = error
+
+# Merge switch sections
+dotnet_diagnostic.ENUM0002.severity = warning
+
+# Populate switch
+dotnet_diagnostic.ENUM0003.severity = warning
+
+# Regex Analyzer Rules (RE)
+
+# Invalid regex pattern
+dotnet_diagnostic.RE0001.severity = warning
+
+# JSON Analyzer Rules
+
+# Invalid JSON pattern
+dotnet_diagnostic.JSON001.severity = suggestion
+
+# Probable JSON string detected
+dotnet_diagnostic.JSON002.severity = suggestion
+
+# Return Analyzer Rules
+
+# Do not return null
+dotnet_diagnostic.RETURN0001.severity = warning
+
+# Roslynator Analyzer Rules (RCS)
+
+# Simplify boolean comparison
+dotnet_diagnostic.RCS1049.severity = silent
+
+# Remove unnecessary 'Imports' or 'using' directive
+dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = warning
+
+# MSTest Analyzer Rules (MSTEST)
+
+# Explicitly enable or disable tests parallelization
+dotnet_diagnostic.MSTEST0001.severity = warning
+
+# Test classes should have valid layout
+dotnet_diagnostic.MSTEST0002.severity = warning
+
+# Test methods should have valid layout
+dotnet_diagnostic.MSTEST0003.severity = warning
+
+# Public types should be test classes
+dotnet_diagnostic.MSTEST0004.severity = none
+
+# Test context property should have valid layout
+dotnet_diagnostic.MSTEST0005.severity = warning
+
+# Avoid [ExpectedException]
+dotnet_diagnostic.MSTEST0006.severity = warning
+
+# Use test attributes only on test methods
+dotnet_diagnostic.MSTEST0007.severity = warning
+
+# TestInitialize method should have valid layout
+dotnet_diagnostic.MSTEST0008.severity = warning
+
+# TestCleanup method should have valid layout
+dotnet_diagnostic.MSTEST0009.severity = warning
+
+# ClassInitialize method should have valid layout
+dotnet_diagnostic.MSTEST0010.severity = warning
+
+# ClassCleanup method should have valid layout
+dotnet_diagnostic.MSTEST0011.severity = warning
+
+# AssemblyInitialize method should have valid layout
+dotnet_diagnostic.MSTEST0012.severity = warning
+
+# AssemblyCleanup method should have valid layout
+dotnet_diagnostic.MSTEST0013.severity = warning
+
+# DataRow should be valid
+dotnet_diagnostic.MSTEST0014.severity = warning
+
+# Test method should not be ignored
+dotnet_diagnostic.MSTEST0015.severity = warning
+
+# Test class should have test method
+dotnet_diagnostic.MSTEST0016.severity = warning
+
+# Assertion arguments should be passed in the correct order
+dotnet_diagnostic.MSTEST0017.severity = warning
+
+# DynamicData should be valid
+dotnet_diagnostic.MSTEST0018.severity = warning
+
+# Prefer TestInitialize methods over constructors
+dotnet_diagnostic.MSTEST0019.severity = warning
+
+# Prefer constructors over TestInitialize methods
+dotnet_diagnostic.MSTEST0020.severity = warning
+
+# Prefer Dispose over TestCleanup methods
+dotnet_diagnostic.MSTEST0021.severity = warning
+
+# Prefer TestCleanup methods over Dispose
+dotnet_diagnostic.MSTEST0022.severity = warning
+
+# Do not negate boolean assertions
+dotnet_diagnostic.MSTEST0023.severity = warning
+
+# Do not store TestContext in a static member
+dotnet_diagnostic.MSTEST0024.severity = warning
+
+# Use 'Assert.Fail' instead of an always-failing assert
+dotnet_diagnostic.MSTEST0025.severity = warning
+
+# Avoid conditional access in assertions
+dotnet_diagnostic.MSTEST0026.severity = warning
+
+# Non-nullable reference not initialized suppressor
+dotnet_diagnostic.MSTEST0027.severity = none
+
+# Non-nullable reference not initialized suppressor
+dotnet_diagnostic.MSTEST0028.severity = none
+
+# Public method should be test method
+dotnet_diagnostic.MSTEST0029.severity = warning
+
+# Type containing [TestMethod] should be marked with [TestClass]
+dotnet_diagnostic.MSTEST0030.severity = warning
+
+# System.ComponentModel.DescriptionAttribute has no effect on test methods
+dotnet_diagnostic.MSTEST0031.severity = warning
+
+# Review or remove the assertion as its condition is known to be always true
+dotnet_diagnostic.MSTEST0032.severity = warning
+
+# Non-nullable reference not initialized suppressor
+dotnet_diagnostic.MSTEST0033.severity = none
+
+# Use ClassCleanupBehavior.EndOfClass with the [ClassCleanup]
+dotnet_diagnostic.MSTEST0034.severity = warning
+
+# [DeploymentItem] can be specified only on test class or test method
+dotnet_diagnostic.MSTEST0035.severity = warning
+
+# Do not use shadowing inside test class
+dotnet_diagnostic.MSTEST0036.severity = warning
+
+# Use proper 'Assert' methods
+dotnet_diagnostic.MSTEST0037.severity = warning
+
+# Don't use 'Assert.AreSame' or 'Assert.AreNotSame' with value types
+dotnet_diagnostic.MSTEST0038.severity = warning
+
+# Use newer 'Assert.Throws' methods
+dotnet_diagnostic.MSTEST0039.severity = warning
+
+# Do not assert inside 'async void' contexts
+dotnet_diagnostic.MSTEST0040.severity = warning
+
+# Use 'ConditionBaseAttribute' on test classes
+dotnet_diagnostic.MSTEST0041.severity = warning
+
+# Obsolete API Rules (SYSLIB)
+# Rules for obsolete .NET APIs and platform-specific warnings
-# Prefer target-typed new(): Type x = new();
-csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
-csharp_style_prefer_tuple_swap = true:suggestion
-csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
-csharp_style_prefer_utf8_string_literals = true:suggestion
-csharp_prefer_static_anonymous_function = true:suggestion
-csharp_style_prefer_readonly_struct = true:suggestion
-csharp_style_prefer_readonly_struct_member = true:suggestion
-#### Naming styles ####
[*.{cs,vb}]
-# Naming rules
+# The UTF-7 encoding is insecure
+dotnet_diagnostic.SYSLIB0001.severity = error
-dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
-dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
+# PrincipalPermissionAttribute is obsolete
+dotnet_diagnostic.SYSLIB0002.severity = error
-dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
-dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
-dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
+# Code access security is not supported
+dotnet_diagnostic.SYSLIB0003.severity = error
-dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
-dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
-dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
+# The constrained execution region (CER) feature is not supported
+dotnet_diagnostic.SYSLIB0004.severity = error
-dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
-dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
+# The global assembly cache (GAC) is not supported
+dotnet_diagnostic.SYSLIB0005.severity = error
-dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
-dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
+# Thread.Abort is not supported
+dotnet_diagnostic.SYSLIB0006.severity = error
-dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.events_should_be_pascalcase.symbols = events
-dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
+# Default implementations of cryptography algorithms not supported
+dotnet_diagnostic.SYSLIB0007.severity = error
-dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
-dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
-dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
+# CreatePdbGenerator is not supported
+dotnet_diagnostic.SYSLIB0008.severity = error
-dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
-dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
-dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
+# The AuthenticationManager Authenticate and PreAuthenticate methods are not supported
+dotnet_diagnostic.SYSLIB0009.severity = error
-dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
-dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
-dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
+# Unsupported remoting APIs
+dotnet_diagnostic.SYSLIB0010.severity = error
-dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
-dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
+# BinaryFormatter serialization is obsolete
+dotnet_diagnostic.SYSLIB0011.severity = error
-dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
-dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
-dotnet_naming_rule.private_fields_should_be__camelcase.style = prefix_underscore
+# Type or member is obsolete
+dotnet_diagnostic.SYSLIB0012.severity = error
-dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
-dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
-dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
+# EscapeUriString is obsolete
+dotnet_diagnostic.SYSLIB0013.severity = error
-dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
-dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
+# WebRequest, HttpWebRequest, ServicePoint, WebClient are obsolete
+dotnet_diagnostic.SYSLIB0014.severity = error
-dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
-dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
+# DisablePrivateReflectionAttribute is obsolete
+dotnet_diagnostic.SYSLIB0015.severity = error
-dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
-dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
+# GetContextInfo() is obsolete
+dotnet_diagnostic.SYSLIB0016.severity = error
-dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
-dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
+# Strong-name signing is not supported and throws PlatformNotSupportedException
+dotnet_diagnostic.SYSLIB0017.severity = error
-dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
-dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
+# Reflection-only loading is not supported and throws PlatformNotSupportedException
+dotnet_diagnostic.SYSLIB0018.severity = error
-dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
-dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
+# Some RuntimeEnvironment APIs are obsolete
+dotnet_diagnostic.SYSLIB0019.severity = error
-dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
-dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
+# IgnoreNullValues is obsolete
+dotnet_diagnostic.SYSLIB0020.severity = error
-# Symbol specifications
+# Derived cryptographic types are obsolete
+dotnet_diagnostic.SYSLIB0021.severity = error
-dotnet_naming_symbols.interfaces.applicable_kinds = interface
-dotnet_naming_symbols.interfaces.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.interfaces.required_modifiers =
+# The Rijndael and RijndaelManaged types are obsolete
+dotnet_diagnostic.SYSLIB0022.severity = error
-dotnet_naming_symbols.enums.applicable_kinds = enum
-dotnet_naming_symbols.enums.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.enums.required_modifiers =
+# RNGCryptoServiceProvider is obsolete
+dotnet_diagnostic.SYSLIB0023.severity = error
-dotnet_naming_symbols.events.applicable_kinds = event
-dotnet_naming_symbols.events.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.events.required_modifiers =
+# Creating and unloading AppDomains is not supported and throws an exception
+dotnet_diagnostic.SYSLIB0024.severity = error
-dotnet_naming_symbols.methods.applicable_kinds = method
-dotnet_naming_symbols.methods.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.methods.required_modifiers =
+# SuppressIldasmAttribute is obsolete
+dotnet_diagnostic.SYSLIB0025.severity = error
-dotnet_naming_symbols.properties.applicable_kinds = property
-dotnet_naming_symbols.properties.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.properties.required_modifiers =
+# X509Certificate and X509Certificate2 are immutable
+dotnet_diagnostic.SYSLIB0026.severity = error
-dotnet_naming_symbols.public_fields.applicable_kinds = field
-dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
-dotnet_naming_symbols.public_fields.required_modifiers =
+# PublicKey.Key is obsolete
+dotnet_diagnostic.SYSLIB0027.severity = error
-dotnet_naming_symbols.private_fields.applicable_kinds = field
-dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_fields.required_modifiers =
+# X509Certificate2.PrivateKey is obsolete
+dotnet_diagnostic.SYSLIB0028.severity = error
-dotnet_naming_symbols.private_static_fields.applicable_kinds = field
-dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_static_fields.required_modifiers = static
+# ProduceLegacyHmacValues is obsolete
+dotnet_diagnostic.SYSLIB0029.severity = error
-dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
-dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.types_and_namespaces.required_modifiers =
+# HMACSHA1 always uses the algorithm implementation provided by the platform
+dotnet_diagnostic.SYSLIB0030.severity = error
-dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
-dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.non_field_members.required_modifiers =
+# EncodeOID is obsolete
+dotnet_diagnostic.SYSLIB0031.severity = error
-dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
-dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
-dotnet_naming_symbols.type_parameters.required_modifiers =
+# Recovery from corrupted process state exceptions is not supported
+dotnet_diagnostic.SYSLIB0032.severity = error
-dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
-dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_constant_fields.required_modifiers = const
+# Rfc2898DeriveBytes.CryptDeriveKey is obsolete
+dotnet_diagnostic.SYSLIB0033.severity = error
-dotnet_naming_symbols.local_variables.applicable_kinds = local
-dotnet_naming_symbols.local_variables.applicable_accessibilities = local
-dotnet_naming_symbols.local_variables.required_modifiers =
+# CmsSigner(CspParameters) constructor is obsolete
+dotnet_diagnostic.SYSLIB0034.severity = error
-dotnet_naming_symbols.local_constants.applicable_kinds = local
-dotnet_naming_symbols.local_constants.applicable_accessibilities = local
-dotnet_naming_symbols.local_constants.required_modifiers = const
+# ComputeCounterSignature without specifying a CmsSigner is obsolete
+dotnet_diagnostic.SYSLIB0035.severity = error
-dotnet_naming_symbols.parameters.applicable_kinds = parameter
-dotnet_naming_symbols.parameters.applicable_accessibilities = *
-dotnet_naming_symbols.parameters.required_modifiers =
+# Regex.CompileToAssembly is obsolete
+dotnet_diagnostic.SYSLIB0036.severity = error
-dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
-dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
-dotnet_naming_symbols.public_constant_fields.required_modifiers = const
+# AssemblyName members HashAlgorithm, ProcessorArchitecture, and VersionCompatibility are obsolete
+dotnet_diagnostic.SYSLIB0037.severity = error
-dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
-dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
-dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
+# SerializationFormat.Binary is obsolete
+dotnet_diagnostic.SYSLIB0038.severity = error
-dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
-dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
+# SslProtocols.Tls and SslProtocols.Tls11 are obsolete
+dotnet_diagnostic.SYSLIB0039.severity = error
-dotnet_naming_symbols.local_functions.applicable_kinds = local_function
-dotnet_naming_symbols.local_functions.applicable_accessibilities = *
-dotnet_naming_symbols.local_functions.required_modifiers =
+# EncryptionPolicy.NoEncryption and EncryptionPolicy.AllowNoEncryption are obsolete
+dotnet_diagnostic.SYSLIB0040.severity = error
-# Naming styles
+# Some Rfc2898DeriveBytes constructors are obsolete
+dotnet_diagnostic.SYSLIB0041.severity = error
-dotnet_naming_style.pascalcase.required_prefix =
-dotnet_naming_style.pascalcase.required_suffix =
-dotnet_naming_style.pascalcase.word_separator =
-dotnet_naming_style.pascalcase.capitalization = pascal_case
-
-dotnet_naming_style.ipascalcase.required_prefix = I
-dotnet_naming_style.ipascalcase.required_suffix =
-dotnet_naming_style.ipascalcase.word_separator =
-dotnet_naming_style.ipascalcase.capitalization = pascal_case
-
-dotnet_naming_style.tpascalcase.required_prefix = T
-dotnet_naming_style.tpascalcase.required_suffix =
-dotnet_naming_style.tpascalcase.word_separator =
-dotnet_naming_style.tpascalcase.capitalization = pascal_case
-
-dotnet_naming_style._camelcase.required_prefix = _
-dotnet_naming_style._camelcase.required_suffix =
-dotnet_naming_style._camelcase.word_separator =
-dotnet_naming_style._camelcase.capitalization = camel_case
-
-dotnet_naming_style.camelcase.required_prefix =
-dotnet_naming_style.camelcase.required_suffix =
-dotnet_naming_style.camelcase.word_separator =
-dotnet_naming_style.camelcase.capitalization = camel_case
-
-dotnet_naming_style.s_camelcase.required_prefix = s_
-dotnet_naming_style.s_camelcase.required_suffix =
-dotnet_naming_style.s_camelcase.word_separator =
-dotnet_naming_style.s_camelcase.capitalization = camel_case
-tab_width = 4
-indent_size = 4
-end_of_line = crlf
-dotnet_style_allow_multiple_blank_lines_experimental = false:warning
-dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
-dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
-dotnet_style_namespace_match_folder = true:suggestion
+# FromXmlString and ToXmlString on ECC types are obsolete
+dotnet_diagnostic.SYSLIB0042.severity = error
+
+# ECDiffieHellmanPublicKey.ToByteArray is obsolete
+dotnet_diagnostic.SYSLIB0043.severity = error
+
+# AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete
+dotnet_diagnostic.SYSLIB0044.severity = error
+
+# Some cryptographic factory methods are obsolete
+dotnet_diagnostic.SYSLIB0045.severity = error
+
+# ControlledExecution.Run should not be used
+dotnet_diagnostic.SYSLIB0046.severity = error
+
+# XmlSecureResolver is obsolete
+dotnet_diagnostic.SYSLIB0047.severity = error
+
+# RSA.EncryptValue(Byte[]) and RSA.DecryptValue(Byte[]) are obsolete
+dotnet_diagnostic.SYSLIB0048.severity = error
+
+# JsonSerializerOptions.AddContext is obsolete
+dotnet_diagnostic.SYSLIB0049.severity = error
+
+# Formatter-based serialization is obsolete
+dotnet_diagnostic.SYSLIB0050.severity = error
+
+# APIs that support obsolete formatter-based serialization are obsolete
+dotnet_diagnostic.SYSLIB0051.severity = error
+
+# APIs that support obsolete mechanisms for Regex extensibility are obsolete
+dotnet_diagnostic.SYSLIB0052.severity = error
+
+# AesGcm should indicate the required tag size for encryption and decryption
+dotnet_diagnostic.SYSLIB0053.severity = error
+
+# Thread.VolatileRead and Thread.VolatileWrite are obsolete
+dotnet_diagnostic.SYSLIB0054.severity = warning
+
+# AdvSimd.ShiftRightLogicalRoundedNarrowingSaturate* methods with signed parameters are obsolete
+dotnet_diagnostic.SYSLIB0055.severity = warning
+
+# Assembly.LoadFrom that takes an AssemblyHashAlgorithm is obsolete
+dotnet_diagnostic.SYSLIB0056.severity = warning
+
+# X509Certificate2 and X509Certificate constructors for binary and file content are obsolete
+dotnet_diagnostic.SYSLIB0057.severity = warning
+
+# The KeyExchangeAlgorithm, KeyExchangeStrength, CipherAlgorithm, CipherAlgorithmStrength, HashAlgorithm, and HashStrength properties of SslStream are obsolete
+dotnet_diagnostic.SYSLIB0058.severity = warning
+
+# SystemEvents.EventsThreadShutdown callbacks aren't run before the process exits
+dotnet_diagnostic.SYSLIB0059.severity = warning
+
+# Rfc2898DeriveBytes constructors are obsolete
+dotnet_diagnostic.SYSLIB0060.severity = error
+
+# System.Linq.Queryable.MaxBy and System.Linq.Queryable.MinBy taking an IComparer are obsolete
+dotnet_diagnostic.SYSLIB0061.severity = error
+
+# Source Generator Diagnostics (SYSLIB1xxx)
+
+# Logging method names can't start with an underscore
+dotnet_diagnostic.SYSLIB1001.severity = error
+
+# Don't include log level parameters as templates in the logging message
+dotnet_diagnostic.SYSLIB1002.severity = error
+
+# Logging method parameter names can't start with an underscore
+dotnet_diagnostic.SYSLIB1003.severity = error
+
+# Could not find a required type definition
+dotnet_diagnostic.SYSLIB1005.severity = error
+
+# Multiple logging methods cannot use the same event ID
+dotnet_diagnostic.SYSLIB1006.severity = error
+
+# Logging methods must return void
+dotnet_diagnostic.SYSLIB1007.severity = error
+
+# One of the arguments to a logging method must implement the ILogger interface
+dotnet_diagnostic.SYSLIB1008.severity = error
+
+# Logging methods must be static
+dotnet_diagnostic.SYSLIB1009.severity = error
+
+# Logging methods must be partial
+dotnet_diagnostic.SYSLIB1010.severity = error
+
+# Logging methods cannot be generic
+dotnet_diagnostic.SYSLIB1011.severity = error
+
+# Redundant qualifier in logging message
+dotnet_diagnostic.SYSLIB1012.severity = error
+
+# Don't include exception parameters as templates in the logging message
+dotnet_diagnostic.SYSLIB1013.severity = error
+
+# Logging template has no corresponding method argument
+dotnet_diagnostic.SYSLIB1014.severity = error
+
+# Argument is not referenced from the logging message
+dotnet_diagnostic.SYSLIB1015.severity = error
+
+# Logging methods cannot have a body
+dotnet_diagnostic.SYSLIB1016.severity = error
+
+# A LogLevel value must be supplied in the LoggerMessage attribute or as a parameter to the logging method
+dotnet_diagnostic.SYSLIB1017.severity = error
+
+# Don't include logger parameters as templates in the logging message
+dotnet_diagnostic.SYSLIB1018.severity = error
-# Verify
+# Couldn't find a field of type ILogger
+dotnet_diagnostic.SYSLIB1019.severity = error
+
+# Found multiple fields of type ILogger
+dotnet_diagnostic.SYSLIB1020.severity = error
+
+# Multiple message-template item names differ only by case
+dotnet_diagnostic.SYSLIB1021.severity = error
+
+# Can't have malformed format strings
+dotnet_diagnostic.SYSLIB1022.severity = error
+
+# Generating more than six arguments is not supported
+dotnet_diagnostic.SYSLIB1023.severity = error
+
+# System.Text.Json source generator did not generate output for type
+dotnet_diagnostic.SYSLIB1030.severity = error
+
+# System.Text.Json source generator encountered a duplicate type info property name
+dotnet_diagnostic.SYSLIB1031.severity = error
+
+# Context classes to be augmented by the System.Text.Json source generator must be declared as partial
+dotnet_diagnostic.SYSLIB1032.severity = error
+
+# System.Text.Json source generator encountered a type with multiple [JsonConstructor] annotations
+dotnet_diagnostic.SYSLIB1033.severity = warning
+
+# JsonSourceGenerator encountered a [JsonStringEnumConverter] annotation
+dotnet_diagnostic.SYSLIB1034.severity = warning
+
+# System.Text.Json source generator encountered a type with multiple [JsonExtensionData] annotations
+dotnet_diagnostic.SYSLIB1035.severity = error
+
+# System.Text.Json source generator encountered an invalid [JsonExtensionData] annotation
+dotnet_diagnostic.SYSLIB1036.severity = error
+
+# System.Text.Json source generator encountered a type with init-only properties which are not supported for deserialization
+dotnet_diagnostic.SYSLIB1037.severity = error
+
+# System.Text.Json source generator encountered a property annotated with [JsonInclude] but with inaccessible accessors
+dotnet_diagnostic.SYSLIB1038.severity = error
+
+# Invalid GeneratedRegexAttribute usage
+dotnet_diagnostic.SYSLIB1040.severity = error
+
+# Multiple GeneratedRegexAttribute attributes were applied to the same method, but only one is allowed
+dotnet_diagnostic.SYSLIB1041.severity = error
+
+# The specified regular expression is invalid
+dotnet_diagnostic.SYSLIB1042.severity = error
+
+# A GeneratedRegexAttribute method must be partial, parameterless, non-generic, and non-abstract, and return Regex
+dotnet_diagnostic.SYSLIB1043.severity = error
+
+# The regex generator couldn't generate a complete source implementation for the specified regular expression due to an internal limitation
+dotnet_diagnostic.SYSLIB1044.severity = error
+
+# Use GeneratedRegexAttribute to generate the regular expression implementation at compile time
+dotnet_diagnostic.SYSLIB1045.severity = warning
+
+# Invalid LibraryImportAttribute usage
+dotnet_diagnostic.SYSLIB1050.severity = error
+
+# The specified type is not supported by source-generated p/invokes
+dotnet_diagnostic.SYSLIB1051.severity = error
+
+# The specified configuration is not supported by source-generated p/invokes
+dotnet_diagnostic.SYSLIB1052.severity = error
+
+# The specified LibraryImportAttribute arguments cannot be forwarded to DllImportAttribute
+dotnet_diagnostic.SYSLIB1053.severity = error
+
+# Use LibraryImportAttribute instead of DllImportAttribute to generate p/invoke marshalling code at compile time
+dotnet_diagnostic.SYSLIB1054.severity = error
+
+# Invalid CustomMarshallerAttribute usage
+dotnet_diagnostic.SYSLIB1055.severity = error
+
+# The specified native type is invalid
+dotnet_diagnostic.SYSLIB1056.severity = error
+
+# The marshaller type does not have the required shape
+dotnet_diagnostic.SYSLIB1057.severity = error
+
+# Invalid NativeMarshallingAttribute usage
+dotnet_diagnostic.SYSLIB1058.severity = error
+
+# The marshaller type does not support an allocating constructor
+dotnet_diagnostic.SYSLIB1059.severity = error
+
+# The specified marshaller type is invalid
+dotnet_diagnostic.SYSLIB1060.severity = error
+
+# The marshaller type has incompatible method signatures
+dotnet_diagnostic.SYSLIB1061.severity = error
+
+# The project must be updated with true
+dotnet_diagnostic.SYSLIB1062.severity = error
+
+# Invalid JSImportAttribute usage
+dotnet_diagnostic.SYSLIB1070.severity = error
+
+# Invalid JSExportAttribute usage
+dotnet_diagnostic.SYSLIB1071.severity = error
+
+# The specified type is not supported by source-generated JavaScript interop
+dotnet_diagnostic.SYSLIB1072.severity = error
+
+# The specified configuration is not supported by source-generated JavaScript interop
+dotnet_diagnostic.SYSLIB1073.severity = error
+
+# JSImportAttribute requires unsafe code
+dotnet_diagnostic.SYSLIB1074.severity = error
+
+# JSExportAttribute requires unsafe code
+dotnet_diagnostic.SYSLIB1075.severity = error
+
+# Invalid GeneratedComInterfaceAttribute usage
+dotnet_diagnostic.SYSLIB1090.severity = error
+
+# Method is declared in different partial declaration than the GeneratedComInterface attribute
+dotnet_diagnostic.SYSLIB1091.severity = error
+
+# Usage of LibraryImport or GeneratedComInterface attribute does not follow recommendation
+dotnet_diagnostic.SYSLIB1092.severity = error
+
+# Analysis for COM interface generation has failed
+dotnet_diagnostic.SYSLIB1093.severity = error
+
+# The base COM interface failed to generate source. Code will not be generated for this interface
+dotnet_diagnostic.SYSLIB1094.severity = error
+
+# Invalid GeneratedComClassAttribute usage
+dotnet_diagnostic.SYSLIB1095.severity = error
+
+# Use GeneratedComInterfaceAttribute instead of ComImportAttribute to generate COM marshalling code at compile time
+dotnet_diagnostic.SYSLIB1096.severity = error
+
+# This type implements at least one type with the GeneratedComInterfaceAttribute attribute
+dotnet_diagnostic.SYSLIB1097.severity = error
+
+# .NET COM hosting with EnableComHosting only supports built-in COM interop
+dotnet_diagnostic.SYSLIB1098.severity = error
+
+# COM Interop APIs on System.Runtime.InteropServices.Marshal do not support source-generated COM and will fail at run time
+dotnet_diagnostic.SYSLIB1099.severity = error
+
+# Type is not supported
+dotnet_diagnostic.SYSLIB1100.severity = error
+
+# Property on type is not supported
+dotnet_diagnostic.SYSLIB1101.severity = error
+
+# Project's language version must be at least C# 11
+dotnet_diagnostic.SYSLIB1102.severity = error
+
+# Value types are invalid inputs to configuration 'Bind' methods
+dotnet_diagnostic.SYSLIB1103.severity = error
+
+# Generator cannot determine the target configuration type
+dotnet_diagnostic.SYSLIB1104.severity = error
+
+# Can't use ValidateObjectMembersAttribute or ValidateEnumeratedItemsAttribute on fields or properties with open generic types
+dotnet_diagnostic.SYSLIB1201.severity = error
+
+# A member type has no fields or properties to validate
+dotnet_diagnostic.SYSLIB1202.severity = error
+
+# A type has no fields or properties to validate
+dotnet_diagnostic.SYSLIB1203.severity = error
+
+# Deriving from a GeneratedComInterface-attributed interface defined in another assembly is not supported
+dotnet_diagnostic.SYSLIB1230.severity = error
+
+# Other Platform Diagnostics
+
+# SVE is a preview feature can be used by enabling EnablePreviewFeatures flag
+dotnet_diagnostic.SYSLIB5003.severity = error
+
+# CS9035 - should be warning or error
+dotnet_diagnostic.CS9035.severity = none
+
+# For Verify package based content
[*.{received,verified}.{cs,txt}]
charset = "utf-8-bom"
end_of_line = lf
@@ -420,6 +2625,12 @@ insert_final_newline = false
tab_width = unset
trim_trailing_whitespace = false
+# The following sections are about
+# ignoring certain rules for specific files or folders, such as settings, models, DTOs, etc.
+# including when using the Extensions/ folder for matching non-local namespace extensions such as
+# - Extensions/System/Linq/EnumerableExtensions.cs
+# - Extensions/Microsoft/Extensions/DepdendencyInjection/ServiceCollectionExtensions.cs
+
# If it's a settings, model, dto, etc file, ignore the 'properties' cannot have arrays
# and other annoying rules - the following section is duplicated
[**/*{ValueObjects,Settings,Options,Model,Models,DTO,Entity,Response,Request}.{cs,vb},]
@@ -453,5 +2664,8 @@ dotnet_diagnostic.RCS1205.severity = none
dotnet_diagnostic.IDE0130.severity = none
dotnet_diagnostic.CA1034.severity = none
+[**/{Extension,Extensions}.{cs,vb}]
+dotnet_diagnostic.CA1034.severity = none
+
[**/Generated/**/*.{cs,vb}]
-dotnet_diagnostic.CS8602.severity = none
\ No newline at end of file
+dotnet_diagnostic.CS8602.severity = none
diff --git a/.gitattributes b/.gitattributes
index 134192a..8d48b3e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,58 +1,108 @@
-# Set default behaviors to automatically normalize line endings
+# Default: auto-detect text and normalise to LF in the index.
+# Everything below only overrides the *working tree* checkout behaviour.
* text=auto
-# Handle line endings for specific file types
-*.cs text eol=crlf
-*.csproj text eol=crlf
-*.sln text eol=crlf
-*.xaml text eol=crlf
-*.json text eol=lf
-*.js text eol=lf
-*.jsx text eol=lf
-*.ts text eol=lf
-*.tsx text eol=lf
-*.html text eol=lf
-*.css text eol=lf
-*.scss text eol=lf
-*.xml text eol=lf
-*.yml text eol=lf
-*.yaml text eol=lf
-*.md text eol=lf
-*.sh text eol=lf
-*.bat text eol=crlf
+# ---------------------------------------------------------------------------
+# .NET source and project files
+# No eol= means each dev's core.autocrlf / core.eol decides. Set eol=lf here
+# instead if you want to hard-enforce LF everywhere -- but keep it in sync
+# with end_of_line in .editorconfig or `dotnet format --verify-no-changes`
+# will fail in CI.
+# ---------------------------------------------------------------------------
+*.cs text diff=csharp
+*.csx text diff=csharp
+*.vb text
+*.fs text
+*.fsx text
+*.csproj text
+*.fsproj text
+*.vbproj text
+*.props text
+*.targets text
+*.resx text
+*.config text
+*.ruleset text
+*.xaml text
+*.cshtml text diff=html
+*.razor text diff=html
+*.sln text eol=lf
+*.slnx text eol=lf
-# Handle encoding for common text files
-*.cs text eol=crlf working-tree-encoding=UTF-8
-*.js text eol=lf working-tree-encoding=UTF-8
+# ---------------------------------------------------------------------------
+# Data, config and docs
+# ---------------------------------------------------------------------------
+*.json text
+*.xml text
+*.yml text
+*.yaml text
+*.toml text
+*.md text diff=markdown
+*.svg text
+*.editorconfig text
+.gitattributes text
+.gitignore text
-# Treat binary files as binary
-*.png binary
-*.jpg binary
-*.jpeg binary
-*.gif binary
-*.ico binary
-*.svg binary
-*.mp4 binary
-*.zip binary
-*.exe binary
-*.dll binary
-*.gz binary
-*.lockb binary
+# ---------------------------------------------------------------------------
+# Web assets (only relevant if the repo ships any)
+# ---------------------------------------------------------------------------
+*.js text
+*.jsx text
+*.ts text
+*.tsx text
+*.html text diff=html
+*.css text
+*.scss text
-# Treat specific build artifacts and IDE files as binary or text
-*.pdb binary
-*.obj binary
-*.log text
-*.vsix binary
+# ---------------------------------------------------------------------------
+# Scripts -- these MUST be pinned regardless of platform.
+# cmd.exe misparses LF in multi-line blocks and labels; sh needs LF.
+# ---------------------------------------------------------------------------
+*.sh text eol=lf
+*.bash text eol=lf
+*.zsh text eol=lf
+*.bat text eol=crlf
+*.cmd text eol=crlf
+*.ps1 text eol=lf
+*.psm1 text eol=lf
+*.psd1 text eol=lf
+Dockerfile text eol=lf
+*.dockerfile text eol=lf
+justfile text eol=lf
+Makefile text eol=lf
-# Handle .NET dependencies and other common binary formats
-*.nupkg binary
-*.snk binary
+# Patches carry their own endings -- never touch them.
+*.patch -text
-# Ensure scripts have correct line endings
-*.sh text eol=lf
-*.bat text eol=crlf
+# ---------------------------------------------------------------------------
+# Binary
+# ---------------------------------------------------------------------------
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.webp binary
+*.mp4 binary
+*.zip binary
+*.gz binary
+*.7z binary
+*.tar binary
+*.exe binary
+*.dll binary
+*.pdb binary
+*.obj binary
+*.vsix binary
+*.nupkg binary
+*.snupkg binary
+*.snk binary
+*.pfx binary
+*.lockb binary
+*.woff binary
+*.woff2 binary
-# Prevent certain generated files from showing as modified due to line endings
-*.g.dart text eol=lf
-*.generated.cs text eol=lf
+# ---------------------------------------------------------------------------
+# Keep repo scaffolding out of `git archive` / source packages
+# ---------------------------------------------------------------------------
+.gitattributes export-ignore
+.gitignore export-ignore
+.github/ export-ignore
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..6aac13e
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,19 @@
+# GitHub Copilot Instructions
+
+## Primary instruction source
+
+Use the repository root [`AGENTS.md`](../AGENTS.md) as the **primary** source of truth for behavior, architecture context, testing standards, and completion criteria.
+
+If this file and `AGENTS.md` appear to conflict, prefer `AGENTS.md` unless this file explicitly states a GitHub Copilot-only exception.
+
+## Copilot-specific guidance
+
+This file should only contain **GitHub Copilot-specific** instruction details.
+Keep product, architecture, and general engineering standards centralized in `AGENTS.md`.
+
+## Operating expectations for Copilot
+
+- Apply the `AGENTS.md` testing bar strictly (TUnit/TUnit.Mocks, AAA comments, naming, cancellation token rule).
+- Treat work as incomplete until relevant tests pass.
+- Consult the repository `.agents/` folder for additional skills/workflows that may improve execution quality.
+- Keep edits minimal, focused, and aligned with existing SDK and repository conventions.
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
new file mode 100644
index 0000000..d91a39c
--- /dev/null
+++ b/.github/workflows/pr.yml
@@ -0,0 +1,83 @@
+name: PR
+
+on:
+ pull_request:
+ branches: [main]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ DOTNET_NOLOGO: true
+
+jobs:
+ build:
+ name: Build, lint & package
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.nuget/packages
+ ~/.local/share/NuGet
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: Run PR pipeline (without tests)
+ env:
+ Build__RunTests: "false"
+ run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release
+
+ tests:
+ name: Tests (${{ matrix.project }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ project:
+ - Analyzers.UnitTests.csproj
+ - Analyzers.IntegrationTests.csproj
+ - DotNetProjectSdk.IntegrationTests.csproj
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.nuget/packages
+ ~/.local/share/NuGet
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: Run tests for ${{ matrix.project }}
+ env:
+ Build__RunTests: "true"
+ Build__TestProjects: ${{ matrix.project }}
+ Build__RunLint: "false"
+ Build__RunPack: "false"
+ run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..70d8a62
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,60 @@
+name: Release
+
+on:
+ push:
+ branches: [main]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ release:
+ name: Release packages
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.nuget/packages
+ ~/.local/share/NuGet
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: Check for version bump
+ id: version
+ shell: bash
+ run: |
+ VERSION=$(node -p "require('./package.json').version")
+ TAG="v$VERSION"
+ if git rev-parse "$TAG" >/dev/null 2>&1; then
+ echo "Version $VERSION is already tagged as $TAG. Skipping release."
+ echo "should_publish=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "New version $VERSION detected. Releasing $TAG."
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Run release pipeline
+ if: steps.version.outputs.should_publish == 'true'
+ env:
+ Release__ShouldPublish: true
+ NuGet__ApiKey: ${{ secrets.NUGET__APIKEY }}
+ run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release
diff --git a/.gitignore b/.gitignore
index 917df8d..76ebe1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -656,3 +656,8 @@ sketch
# End of https://www.toptal.com/developers/gitignore/api/openframeworks+visualstudio,visualstudiocode,jetbrains,react,node,astro,aspnetcore,ncrunch,go
!scripts/*
+
+*.binlog
+
+!build/
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..1269822
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "cSpell.words": [
+ "Analyzer",
+ "Analyzers",
+ "msbuild",
+ "nsubstitute",
+ "tunit",
+ "tunitmock",
+ "Xunit"
+ ]
+}
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9b87dd9
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,162 @@
+# AGENTS.md
+
+Guidance for AI coding agents working in this repository.
+
+## Customization source of truth
+
+Use `.agents/` as the default location for **generic agentic information** shared across agent runtimes.
+
+- `.agents/skills/` — canonical, cross-agent skills
+- `.agents/agents/` — canonical, cross-agent agent workflow specs
+- `.agents/prompts/` — canonical, cross-agent prompt specs
+
+Use `.github/` customization files only as VS Code/Copilot wrappers or registration points when required by tooling.
+
+## Purpose
+
+This repo builds and tests `Purview.DotNetProjectSdk`, a reusable MSBuild SDK package plus analyzer and tests.
+
+For full product behaviour and configuration, read [`README.md`](./README.md). Keep edits minimal, targeted, and convention-driven.
+
+Repo-specific agent content lives under `src/src/DotNetProjectSdk/Sdk/.agents/` and is packed into the NuGet package as `.agents/**` by the standard `PurviewAutoSdkPack` `Sdk/` packaging logic. Add new skills under that path so they automatically flow into consuming repositories without hardcoding individual skill names.
+
+## AgentPack folder and downstream impact
+
+**Hard requirement:** This SDK must pack the contents of `Sdk/` into the NuGet package so that downstream consumers of `Purview.DotNetProjectSdk` receive the same `Sdk/**` files. The `PurviewAutoSdkPack` feature is the mechanism that delivers this for standard consuming projects. Do not implement `Sdk/` packaging only for the `DotNetProjectSdk` project itself.
+
+For packable projects, `PurviewAutoSdkPack` (default `true`) automatically adds `Sdk/**/*` as `None` items with `Pack="true"` and `Visible="true"`, mapping each file to the correct location in the package:
+
+- `Sdk/.agents/**` → `.agents/**`
+- `Sdk/.github/**` → `.github/**`
+- `Sdk/build/**` → `build/**`
+- `Sdk/buildTransitive/**` → `buildTransitive/**`
+- `Sdk/buildMultiTargeting/**` → `buildMultiTargeting/**`
+- `Sdk/*.md`, `Sdk/*.png`, `Sdk/*.jpg`, etc. → package root
+- everything else under `Sdk/` → `Sdk/`
+
+The `DotNetProjectSdk.csproj` itself is an MSBuild SDK, so it disables `PurviewAutoSdkPack` and explicitly packs its `Sdk/` contents instead. This is an exception for the SDK project only; every other project that consumes this SDK relies on `PurviewAutoSdkPack` to ship its `Sdk/` folder. Consuming repositories that use this SDK get the bundled agent folder copied into `$(AgentPackDestinationFolder)/` (default `.agents/`) before build when `EnableAgentFolderInPackage` is `true` (default).
+
+During packaging, the SDK injects a `.gitignore` file into each second-level folder under `Sdk/.agents` with the content `# Ignore all files\n*\n# Don't ignore directories, so Git can traverse them\n!*/\n# Keep this file\n!.gitignore`, so the copied folder is ignored by Git in consuming repositories while keeping the folder structure discoverable.
+
+Any edit, addition, or deletion in `src/src/DotNetProjectSdk/Sdk/.agents/` therefore changes the contents delivered to every repository that consumes this SDK.
+
+Tests for this feature live in `src/tests/DotNetProjectSdk.IntegrationTests/Tests/AgentPackFolderTests.cs`.
+
+## Repository map
+
+- `src/src/DotNetProjectSdk/` — packable MSBuild SDK package (`Purview.DotNetProjectSdk`)
+- `src/src/Analyzers/` — Roslyn analyzer/source-generator assembly
+- `src/src/CodeFixers/` — Roslyn code-fix assembly (`Purview.DotNetProjectSdk.CodeFixers`)
+- `src/tests/Analyzers.UnitTests/` — analyzer-focused unit tests
+- `src/tests/Analyzers.IntegrationTests/` — analyzer integration tests (Roslyn end-to-end analyzer/suppressor/code-fix behavior)
+- `src/tests/DotNetProjectSdk.IntegrationTests/` — integration harness validating SDK behaviour
+- `src/DotNetProjectSdk.slnx` — solution entry point
+
+## Test project placement and namespace conventions
+
+When adding or changing tests in this repository:
+
+- Keep **analyzer unit tests** (pure algorithm/utility or direct Roslyn compilation assertions) in
+ `src/tests/Analyzers.UnitTests/`.
+- Keep **analyzer integration tests** (behavior spanning analyzer diagnostics, suppressors, and code fixes)
+ in `src/tests/Analyzers.IntegrationTests/`.
+- Keep **SDK integration harness tests** in `src/tests/DotNetProjectSdk.IntegrationTests/`.
+
+Namespace expectations:
+
+- `Analyzers.UnitTests` sources use `Purview.DotNetProjectSdk.Analyzers`
+- `Analyzers.IntegrationTests` sources use
+ `Purview.DotNetProjectSdk.Analyzers`
+- `DotNetProjectSdk.IntegrationTests` sources use `Purview.DotNetProjectSdk`
+
+Do not mix analyzer unit/integration tests in the same project unless explicitly requested.
+
+## Canonical commands
+
+Prefer `just` tasks:
+
+- `just restore`
+- `just build`
+- `just test`
+- `just lint-check`
+- `just lint-fix`
+- `just pack`
+
+`dotnet` fallback uses `src/DotNetProjectSdk.slnx` and `Release`.
+
+## Testing rules (important)
+
+This repo uses **Microsoft.Testing.Platform** (`global.json`) and **TUnit** conventions.
+
+- Prefer `just test` first.
+- If filtering tests, use `--treenode-filter` (not `--filter`).
+- When passing test-runner options, keep the `--` separator with `dotnet test`.
+
+### Cross-platform requirement (non-negotiable)
+
+**All tests and features must work identically on Windows, Linux, and macOS.** CI runs the full
+suite on `ubuntu-latest`, so a test that only passes on Windows is a failing PR. This is a hard
+requirement, not a preference:
+
+- **Never hardcode platform-specific paths** in tests, analyzer configs, or fixtures — no
+ Windows drive paths (`C:\...`), no backslash-only separators, no case-insensitive path assumptions.
+- **Build paths with platform APIs**: `Path.Combine`, `Path.GetTempPath()`, `Path.DirectorySeparatorChar`,
+ `Path.GetFullPath()`. When a test needs a fixed fake path, normalize Windows-style literals to the
+ current platform (see `AnalyzerTestInfrastructure.NormalizeFakePath` and the local
+ `NamespaceCalculatorTests.NormalizeFakePath` helpers) instead of passing them raw.
+- **Feed production path-math code only native-format paths.** Code like
+ `ExtensionsNamespaceHelper` uses `Path`/`Uri` relative-path logic; any non-native separator or
+ drive-letter literal on Linux makes it return wrong results.
+- **Do not rely on environment specifics** such as case-insensitive filesystems, a `C:` drive, or
+ trailing-separator behaviour — they differ per OS.
+- When writing analyzer/compiler tests, keep `SyntaxTree` file paths and `build_property.*` values
+ (e.g. `ProjectDir`) consistent and native on every platform.
+- The harness (`ProjectHarness`) already creates throwaway projects under `Path.GetTempPath()` —
+ keep it that way; never introduce fixed absolute or Windows-style paths.
+
+For filtering syntax and troubleshooting, see:
+
+- [`tunit-test-runner` skill](./.agents/skills/tunit-test-runner/SKILL.md)
+- [`dotnet-tunit` skill](./.agents/skills/dotnet-tunit/SKILL.md)
+- [`tunit-filtering` skill](./.agents/skills/tunit-filtering/SKILL.md)
+
+### Integration harness for complex validation
+
+Use `src/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs` when validating behaviour that depends on MSBuild evaluation, import order, or generated project state.
+
+- Create throwaway projects with `ProjectHarness.For(...).BuildAsync()` (or `CreateAsync`/`CreateWithContentAsync`).
+- Prefer harness evaluation helpers over brittle log parsing:
+ - `GetPropertyAsync` / `GetPropertiesAsync`
+ - `GetItemIdentitiesAsync` / `GetProjectReferencesAsync`
+ - `GetPreprocessProjectAsync` for evaluated project inspection
+- Use `BuildAsync(restore: true)` when package restore/build behaviour is part of the scenario.
+- Keep scenarios minimal and deterministic; isolate one behaviour per test.
+- For import-time behaviour, set required pre-import properties in harness setup (before `Sdk.props` import).
+
+Supporting files:
+
+- `src/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.Builder.cs`
+- `src/tests/DotNetProjectSdk.IntegrationTests/TestHelpers.cs`
+
+## Conventions to preserve
+
+- Keep project naming aligned with repo conventions in [`README.md`](./README.md#project-naming-guide).
+- Respect Central Package Management in `Directory.Packages.props`.
+- Avoid unrelated refactors or formatting-only churn unless requested.
+- Follow existing style and keep changes small and testable.
+
+## Release and commit workflow
+
+- Versioning is driven by `package.json` + Changesets.
+- Use existing Changesets and commit conventions:
+ - [`changesets-prerelease` skill](./.agents/skills/changesets-prerelease/SKILL.md)
+ - [`git-conventional-commits` skill](./.agents/skills/git-conventional-commits/SKILL.md)
+ - [`lefthook-integration` skill](./.agents/skills/lefthook-integration/SKILL.md)
+- Generic release agent workflow source: `./.agents/agents/release-prep.md`
+
+## Practical guardrails for agents
+
+- Validate changes with targeted tests first, then broader suite as needed.
+- When touching test behaviour, verify with MTP/TUnit-compatible invocation.
+- Prefer linking users to existing docs over duplicating long explanations in chat.
+- Keep generic guidance in `.agents/`; keep `.github/` copies thin and referential.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1759f6a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,109 @@
+# changeops
+
+## 1.0.0-prerelease.17
+
+### Patch Changes
+
+- fixed test project namespace generation
+
+## 1.0.0-prerelease.16
+
+### Patch Changes
+
+- updated to support different substitite frameworks
+
+## 1.0.0-prerelease.15
+
+### Patch Changes
+
+- hardended aspire host defaults
+
+## 1.0.0-prerelease.14
+
+### Patch Changes
+
+- hardenend version detection
+
+## 1.0.0-prerelease.13
+
+### Patch Changes
+
+- fixed IsCLIProject properties
+
+## 1.0.0-prerelease.12
+
+### Patch Changes
+
+- expanded support for CLI
+
+## 1.0.0-prerelease.11
+
+### Patch Changes
+
+- Make SDK property conditions explicit and harden executable project detection for top-level Program.cs projects.
+
+## 1.0.0-prerelease.10
+
+### Patch Changes
+
+- fixing default values
+
+## 1.0.0-prerelease.9
+
+### Patch Changes
+
+- added versioning caching per-session
+
+## 1.0.0-prerelease.8
+
+### Patch Changes
+
+- Fixed default variable issue
+
+## 1.0.0-prerelease.7
+
+### Patch Changes
+
+- Fixed incorrect .git folder detection
+
+## 1.0.0-prerelease.6
+
+### Patch Changes
+
+- Added Strict mode on package.json version
+
+## 1.0.0-prerelease.5
+
+### Patch Changes
+
+- now copying .editorConfig and global.json into project structure
+
+## 1.0.0-prerelease.4
+
+### Patch Changes
+
+- added embedded attribute gen"
+
+## 1.0.0-prerelease.3
+
+### Patch Changes
+
+- fixed editorconfig props not exposed to VS correctly
+
+## 1.0.0-prerelease.2
+
+### Patch Changes
+
+- fixed issues with sdk file location when packaged
+
+## 1.0.0-reprelease.1
+
+### Patch Changes
+
+- InternalsVisibleTo for shared testing infra
+
+## 1.0.0-reprelease.0
+
+### Major Changes
+
+- 61dc52c: initial project creation
diff --git a/Directory.Build.targets b/Directory.Build.targets
deleted file mode 100644
index eb33093..0000000
--- a/Directory.Build.targets
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2bb38ec..af95e94 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,15 +5,20 @@
-
+
+
+
+
-
-
+
+
-
-
+
+
+
+
diff --git a/DotNetProjectSdk.slnx b/DotNetProjectSdk.slnx
deleted file mode 100644
index df28643..0000000
--- a/DotNetProjectSdk.slnx
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Justfile b/Justfile
new file mode 100644
index 0000000..c5d8cce
--- /dev/null
+++ b/Justfile
@@ -0,0 +1,111 @@
+set quiet
+
+solution_file := "src/DotNetProjectSdk.slnx"
+build_configuration := "Release"
+artifacts_folder := "./artifacts"
+
+pipeline_solution := "build/Pipeline.slnx"
+pipeline_project := "build/PipelineCLI/PipelineCLI.csproj"
+
+current_version := `node -p "require('./package.json').version"`
+
+[private]
+default:
+ just --list
+
+# Run the PR pipeline (restore, build, lint, tests)
+[group('Pipeline')]
+pipeline-pr *args:
+ echo "Running PR pipeline..."
+ dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} {{ args }}
+
+# Run the build pipeline (restore, build, lint)
+[group('Pipeline')]
+pipeline-build *args:
+ echo "Running build pipeline..."
+ dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Build:RunTests=false --Release:Mode=None {{ args }}
+
+# Run the release pipeline (restore, build, lint, tests, pack, publish, GitHub release)
+[group('Pipeline')]
+pipeline-release *args:
+ echo "Running release pipeline..."
+ dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Release:Mode=NuGet {{ args }}
+
+# Run the release pipeline (restore, build, lint, tests, pack, local nuget publish)
+# Note: `just` runs recipes through the shell, which strips backslashes from unquoted arguments.
+# Always use forward slashes for the feed path, e.g.
+# just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/
+[group('Pipeline')]
+pipeline-local-release *args:
+ echo "Running local release pipeline..."
+ dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Release:Mode=LocalNuGet {{ args }}
+
+# Run the pipeline with tests enabled
+[group('Pipeline')]
+pipeline-tests *args:
+ echo "Running tests pipeline..."
+ dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Build:RunTests=true --Release:Mode=None {{ args }}
+
+# Open the solution in Visual Studio/ Registered application
+[group('Utilities')]
+vs:
+ open {{ solution_file }}
+
+# Open the solution in Visual Studio/ Registered application
+[group('Utilities')]
+vs-pipeline:
+ open {{ pipeline_solution }}
+
+# Build the solution for the specified configuration (default: Release)
+[group('Build and Test')]
+build *args:
+ echo "==> Building {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ GREEN }}{{ current_version }}{{ NORMAL }}) with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}"
+ dotnet build {{ solution_file }} --configuration {{ build_configuration }} {{ args }}
+
+# Cleans the solution for the specified configuration (default: Release)
+[group('Build and Test')]
+clean *args:
+ echo "==> Cleaning {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ GREEN }}{{ current_version }}{{ NORMAL }}) with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}"
+ dotnet clean {{ solution_file }} --configuration {{ build_configuration }} {{ args }}
+
+# Restore local .NET tools
+[group('Utilities')]
+restore-tools:
+ dotnet tool restore
+
+# Restore NuGet packages for the solution
+[group('Build and Test')]
+restore *args:
+ dotnet restore {{ solution_file }} {{ args }}
+
+# Displays the current package version from package.json
+[group('Build and Test')]
+current_version:
+ echo "==> Current version: {{ GREEN }}{{ current_version }}{{ NORMAL }} (defined in package.json and automatically included in the build output through the Purview.DotNetProjectSdk package)"
+
+# Run tests for a specific project with a filter (e.g., "/*/*/*/*/", or "/*/*/*/*[Category=Unit]" to run just unit tests) and configuration (e.g., "Release")
+[group('Build and Test')]
+test filter="/*/*/*/*/" *args:
+ echo "==> Testing {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ GREEN }}{{ build_configuration }}{{ NORMAL }}) with filter {{ YELLOW }}{{ filter }}{{ NORMAL }}"
+ dotnet test --project {{ solution_file }} --configuration {{ build_configuration }} --treenode-filter "{{ filter }}" --ignore-exit-code 8 {{ args }}
+
+# Run agent-pack integration tests in the same Linux SDK environment used by CI
+[group('Build and Test')]
+test-linux:
+ pwsh -NoProfile -File ./scripts/test-linux-docker.ps1
+
+# Pack all packable projects
+[group('Build and Test')]
+pack artifact_folder=artifacts_folder *args:
+ echo "==> Packing {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ GREEN }}{{ current_version }}{{ NORMAL }}) to {{ YELLOW }}{{ artifact_folder }}{{ NORMAL }}"
+ dotnet pack "{{ solution_file }}" --configuration "{{ build_configuration }}" --output "{{ artifact_folder }}" {{ args }}
+
+# Format the code with CSharpier
+[group('Utilities')]
+lint-fix:
+ dotnet csharpier format .
+
+# Check formatting with CSharpier
+[group('Utilities')]
+lint-check:
+ dotnet csharpier check .
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..39a433b
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 KJL Solutions Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index f581f69..b279d16 100644
--- a/README.md
+++ b/README.md
@@ -2,23 +2,29 @@
A reusable MSBuild SDK NuGet package that delivers standardised .NET project defaults, code-style enforcement, test-framework wiring, and Central Package Management integration. Install it once per repo — every project beneath the repo root inherits everything automatically.
+> [!NOTE]
+> This SDK package imposes convention over configuration, enforcing certain styles and automations based on project file names, etc.
+
## What's included
| Feature | Detail |
-|---|---|
+| -- | -- |
| **Project type detection** | `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsContainerProject`, `IsWebSdkProject`, `IsAspireHostProject`, … |
| **C# defaults** | `net10.0` TFM (overridable), `LangVersion=preview`, `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds |
-| **Code style** | `.editorconfig` baked into the package and applied via `EditorConfigFilePath`; `EnforceCodeStyleInBuild=true` |
+| **Code style** | `.editorconfig` baked into the package, applied via `EditorConfigFilePath`, and auto-bootstrapped to repo root if missing; `EnforceCodeStyleInBuild=true`, `EnableNETAnalyzers=true`, `AnalysisLevel=latest`, `AnalysisMode=All` |
+| **NuGet packaging** | `PublishRepositoryUrl=true`, `IncludeSymbols=true`, `SymbolPackageFormat=snupkg`, `EmbedUntrackedSources=true` for packable projects |
+| **Repo bootstrap** | Missing repo-root `.editorconfig` and `global.json` are auto-copied/created by default (disable via `DisableAutoCopySdkFiles=true`) |
| **CI detection** | `ContinuousIntegrationBuild` set automatically when `CI`, `GITHUB_ACTIONS`, or `TF_BUILD` env vars are present |
| **SourceLink** | `Microsoft.SourceLink.GitHub` added to all packable projects (configurable via `SourceLinkPackageName`) |
| **Purview Telemetry** | `Purview.Telemetry.SourceGenerator` + `Microsoft.Extensions.Telemetry.Abstractions` added by default (opt-out) |
-| **Assembly info** | Auto-generated `static class AssemblyInfo` with `RootNamespace`, `Version`, `Company`, etc. |
-| **InternalsVisibleTo** | Generated for all defined `TestType` variants automatically |
+| **Assembly info** | Auto-generated `static partial class AssemblyInfo` with `RootNamespace`, `Version`, `Company`, etc., plus an embedded `Microsoft.CodeAnalysis.EmbeddedAttribute` (can be excluded via `PURVIEW_SDK_EXCLUDE_EMBEDDED`). |
+| **InternalsVisibleTo** | Generated for all `TestType` variants and shared testing projects, using the resolved `$(AssemblyName)` so explicit, generated, and default naming are all handled |
| **Namespace management** | `NamespacePrefix.ProjectName` pattern with suffix stripping (`.Core`, `.Shared`, `.EF`, …) |
-| **Test framework** | **TUnit** by default; switch to **XUnit v3** with one property |
-| **Testing extras** | NSubstitute, Bogus, `Microsoft.Testing.Platform.MSBuild`, Aspire.Hosting — all wired up |
+| **Testing framework** | `TestingFramework`: **TUnit** (default), `Xunit`, or `None` |
+| **Mocking provider** | `SubstituteFramework`: **TUnitMocks** (default), `NSubstitute`, or `None` |
+| **Test data provider** | `TestDataFramework`: **Bogus** (default) or `None` |
+| **Version detection** | Reads `version` from `package.json` and applies it to `Version` and `PackageVersion` automatically; falls back to `0.0.1` |
| **CPM** | `ManagePackageVersionsCentrally=true` — versions live in your `Directory.Packages.props` |
-| **Container projects** | AOT, Linux Docker defaults when a `Dockerfile` is present |
---
@@ -28,7 +34,9 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def
```json
{
- "sdk": { "version": "10.0.202", "rollForward": "latestMinor" },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ },
"msbuild-sdks": {
"Purview.DotNetProjectSdk": "1.0.0"
}
@@ -51,7 +59,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def
### 3. Create `Directory.Build.targets` at repo root
```xml
-
+
```
@@ -64,20 +72,125 @@ Copy `templates/Directory.Packages.props` from this package to your repo root. A
---
+## Project naming guide
+
+The SDK applies several conventions automatically based on the `.csproj` filename and `NamespacePrefix`.
+
+### Defaults (no extra configuration)
+
+By default (`EnableAssemblyNameGeneration=false`), `AssemblyName` follows standard .NET behaviour — it's just the `.csproj` filename. `RootNamespace` is always derived from `$(NamespacePrefix).$(ProjectName)`:
+
+| `.csproj` filename | `AssemblyName` | `RootNamespace` | Detected as |
+| -- | -- | -- | -- |
+| `Api.csproj` | `Api` | `Acme.Api` | Source project |
+| `Api.UnitTests.csproj` | `Api.UnitTests` | `Acme.Api` | `IsTestProject=true`, `TestingType=Unit` |
+| `Api.IntegrationTests.csproj` | `Api.IntegrationTests` | `Acme.Api` | `IsTestProject=true`, `TestingType=Integration` |
+| `SharedTestingFramework.csproj` | `SharedTestingFramework` | `Acme.SharedTestingFramework` | `IsSharedTestingProject=true` |
+
+> **Note:** `InternalsVisibleTo` follows `$(AssemblyName)` — so for `Api.csproj` the SDK generates `Api.UnitTests`, `Api.IntegrationTests`, etc.
+
+### With `EnableAssemblyNameGeneration=true`
+
+When enabled, the SDK derives `AssemblyName` and `PackageId` from `$(PurviewLogicalProjectName)` — the full `$(NamespacePrefix).$(ProjectName)` with deduplication:
+
+| `.csproj` filename | `AssemblyName` | `RootNamespace` |
+| -- | -- | -- |
+| `Api.csproj` | `Acme.Api` | `Acme.Api` |
+| `Api.UnitTests.csproj` | `Acme.Api.UnitTests` | `Acme.Api` |
+| `Core.Infrastructure.csproj` | `Acme.Core.Infrastructure` | `Acme.Core.Infrastructure` |
+
+Use short `.csproj` names in both modes — the SDK handles the prefixing:
+
+```text
+✅ Api.csproj → short name, SDK resolves the rest
+❌ Acme.Api.csproj → redundant prefix, avoid
+```
+
+A build-time check (`PurviewProjectFileNameMismatch`) enforces that the `.csproj` filename matches its parent directory name, preventing inconsistent naming. Set `DisableProjectFileNamingConventionCheck=true` to opt out.
+
+### Recommended structure: `src/` + `tests/`
+
+For larger repos, separate source and test projects into `src/` and `tests/` folders:
+
+```text
+MyRepo/
+├── Directory.Build.props ← NamespacePrefix=Acme
+├── Directory.Build.targets
+├── Directory.Packages.props
+├── global.json
+├── src/
+│ ├── Api/
+│ │ └── Api.csproj
+│ ├── Core/
+│ │ └── Core.csproj
+│ └── SourceGenerator/
+│ └── SourceGenerator.csproj
+├── tests/
+│ ├── Api.UnitTests/
+│ │ └── Api.UnitTests.csproj → IsTestProject=true, TestingType=Unit
+│ ├── Api.IntegrationTests/
+│ │ └── Api.IntegrationTests.csproj
+│ └── SharedTestingFramework/
+│ └── SharedTestingFramework.csproj → IsSharedTestingProject=true
+└── package.json
+```
+
+### Flat structure: everything together
+
+For smaller repos, source and test projects can live side-by-side:
+
+```text
+MyRepo/
+├── Directory.Build.props
+├── Directory.Build.targets
+├── Directory.Packages.props
+├── global.json
+├── Api/
+│ └── Api.csproj
+├── Api.UnitTests/
+│ └── Api.UnitTests.csproj
+├── Core/
+│ └── Core.csproj
+├── Core.IntegrationTests/
+│ └── Core.IntegrationTests.csproj
+└── package.json
+```
+
+Both layouts work identically — the SDK detects test projects by name suffix, not folder location.
+
+### Quick reference
+
+```sh
+# Create a source project
+mkdir src/Api && cd src/Api
+dotnet new classlib -n Api
+
+# Create its unit tests
+mkdir ../../tests/Api.UnitTests && cd ../../tests/Api.UnitTests
+dotnet new classlib -n Api.UnitTests # SDK wires TUnit automatically
+
+# Or flat:
+mkdir Api.UnitTests && cd Api.UnitTests
+dotnet new classlib -n Api.UnitTests
+```
+
+---
+
## Template files
The `templates/` folder contains ready-to-copy starter files for new repos:
| File | Purpose |
-|---|---|
+| -- | -- |
| `Directory.Build.props` | Bootstrapper — copy to repo root and set `NamespacePrefix` |
| `Directory.Build.targets` | Bootstrapper — copy to repo root |
| `Directory.Packages.props` | All default package versions with `*` floating to latest |
-| `global.json` | SDK pin + `msbuild-sdks` entry |
-| `.editorconfig` | Full C# code-style rules (IDE-discoverable copy) |
+| `global.json` | `msbuild-sdks` entry + `Microsoft.Testing.Platform` test runner |
| `.gitignore` | ASP.NET Core + VS + Rider + Node combined gitignore |
| `.gitattributes` | Line-ending normalisation for .cs, .json, .yml, etc. |
-| `.config/dotnet-tools.json` | CSharpier + dotnet-inspect pre-configured |
+| `.config/dotnet-tools.json` | CSharpier tool manifest |
+
+The package also ships bundled agent content under `.agents/**`. During build, the SDK copies it into the consuming repository's `.agents/` folder by default so compatible coding agents can discover repository-aware guidance automatically. The SDK also injects a `.gitignore` file into each second-level agent folder with the content `# Ignore all files\n*\n# Don't ignore directories, so Git can traverse them\n!*/\n# Keep this file\n!.gitignore`, so the copied folder is ignored by Git while keeping the folder structure discoverable.
---
@@ -85,36 +198,166 @@ The `templates/` folder contains ready-to-copy starter files for new repos:
Set any of these properties **before** the `` in your `Directory.Build.props`:
+### Version detection
+
+| Property | Default | Description |
+| -- | -- | -- |
+| `UsePackageJsonVersion` | `true` | `true` enables version detection, `false` disables it, and `Strict` requires version detection to succeed (build fails if no version source can be resolved). |
+| `RootPackageJson` | *(auto-discovered)* | Explicit path to a `package.json`. Relative paths are resolved from the project directory. |
+| `EnableVersionDetectionCache` | `true` | Enables local caching of auto-discovered package.json version results. |
+| `VersionDetectionLogEnabled` | `false` | Emits a high-importance message showing the detected package version. Set to `true` to enable logging.
+
+When `UsePackageJsonVersion=true` (the default) or `UsePackageJsonVersion=Strict`, the SDK:
+
+1. **Explicit path** — if `RootPackageJson` is set, reads that file directly.
+2. **Auto-discovery** — otherwise, walks up from the project directory looking for a `.git` marker to locate the repo root, then reads `package.json` from there.
+
+The extracted `version` field is applied to both `Version` and `PackageVersion`. A build error is raised if the file can't be found or contains no `version` field. With `UsePackageJsonVersion=Strict`, the build also fails when no package.json source can be discovered (for example, no explicit `RootPackageJson` and no discoverable `.git` marker).
+
+Version detection logging is disabled by default. Set `VersionDetectionLogEnabled` to `true` to emit a high-importance message showing the detected package version.
+
+> **Important — set before the import:** Both `UsePackageJsonVersion` and `RootPackageJson` must be set **before** the `` line in your `Directory.Build.props`. The version logic runs during that import and cannot see properties set afterwards (e.g. in individual `.csproj` files).
+>
+> ```xml
+>
+>
+> Acme
+>
+> $(MSBuildThisFileDirectory)package.json
+>
+>
+>
+>
+> ```
+
### General
| Property | Default | Description |
-|---|---|---|
+| -- | -- | -- |
| `NamespacePrefix` | *(required)* | Root namespace prefix, e.g. `Acme`. Results in `Acme.MyProject`. |
| `DisableNamespacePrefixCheck` | `false` | Set to `true` to suppress the build error for missing `NamespacePrefix`. |
-| `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. |
+| `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. |
+| `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults for a single `netstandard2.0` target, analyzer rules, SourceLink, generated-file output, dependency output, symbol packaging, telemetry exclusion, and package build output. |
+| `PackProjectReferencedSourceGenerators` | `true` | Automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Set to `false` to opt out; set `Pack="false"` on an individual reference to exclude only that generator. |
| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | SourceLink provider. Set to `Microsoft.SourceLink.AzureDevOps.Git` for ADO repos. |
+| `DisableSourceLink` | `false` | Set to `true` to stop the SDK from adding the configured SourceLink package automatically. |
+| `EnableAssemblyNameGeneration` | `false` | When `true`, the SDK derives `AssemblyName` (and `PackageId`) from `$(PurviewLogicalProjectName)` — i.e. `$(NamespacePrefix).$(ProjectName)` with deduplication logic. When `false` (default), standard .NET behaviour applies (`$(MSBuildProjectName)`). Explicit `` in a `.csproj` always takes precedence. |
+| `DisableProjectFileNamingConventionCheck` | `false` | Set to `true` to disable the validation that requires `MyProject\MyProject.csproj` naming alignment. |
+| `DisableGenerateAssemblyInfoClass` | `false` | Set to `true` to disable the generated `AssemblyInfo` helper source. |
+| `AutoIncludeUsings` | `true` | Controls SDK-added global usings for `NamespacePrefix` and `RootNamespace`. |
+
+### Repo bootstrap
+
+| Property | Default | Description |
+| -- | -- | -- |
+| `DisableAutoCopySdkFiles` | `false` | Master switch that disables repo-level SDK file bootstrapping. |
+| `BootstrapEditorConfigToRepoRoot` | `true` | Copies the SDK `.editorconfig` to the repository root when missing. |
+| `RepositoryEditorConfigFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `.editorconfig`. |
+| `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. |
+| `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. |
+| `PurviewDotNetProjectSdkVersionForGlobalJson` | *(auto-detected or `1.0.0` fallback)* | Version written to the `msbuild-sdks.Purview.DotNetProjectSdk` entry in a bootstrapped `global.json`. |
+
+### Agent folder
+
+| Property | Default | Description |
+| -- | -- | -- |
+| `PurviewAutoSdkPack` | `true` | When `true`, automatically packs the `Sdk/` folder contents into the NuGet package with the correct root-level paths. Disable this for MSBuild SDK projects. |
+| `EnableAgentFolderInPackage` | `true` | Copies the bundled `.agents/**` folder from the SDK NuGet package into the consuming repository's `.agents/` folder (or `$(AgentPackDestinationFolder)/`) before build. |
+| `AgentPackDestinationFolder` | `.agents` | Repo-relative destination folder that receives the copied agent folder contents when `EnableAgentFolderInPackage` is `true`. |
+
+To disable bundled agent folder copying in a consuming repo, set the opt-out property before importing the SDK:
+
+```xml
+
+ false
+
+```
+
+When a project is packable, the SDK treats any content under `Sdk/` as a pack target with these rules:
+
+| Source path | Package path |
+| -- | -- |
+| `Sdk/.agents/**` | `.agents/**` |
+| `Sdk/.github/**` | `.github/**` |
+| `Sdk/build/**` | `build/**` |
+| `Sdk/buildTransitive/**` | `buildTransitive/**` |
+| `Sdk/buildMultiTargeting/**` | `buildMultiTargeting/**` |
+| `Sdk/*.md`, `Sdk/*.png`, `Sdk/*.jpg`, etc. | package root |
+| everything else under `Sdk/` | `Sdk/` |
+
+The SDK automatically adds a `.gitignore` file into each second-level folder under `Sdk/.agents` with the content `# Ignore all files\n*\n# Don't ignore directories, so Git can traverse them\n!*/\n# Keep this file\n!.gitignore`. This ensures the copied folder structure remains discoverable in consuming repositories while the content itself is ignored by Git.
### Telemetry
| Property | Default | Description |
-|---|---|---|
-| `ExcludePurviewTelemetry` | `false` | Set to `true` to remove `Purview.Telemetry.SourceGenerator` from all projects. |
-| `ExcludeMSTelemetryExtension` | `false` | Set to `true` to remove `Microsoft.Extensions.Telemetry.Abstractions`. |
+| -- | -- | -- |
+| `ExcludePurviewTelemetry` | `false` | Set to `true` to exclude `Purview.Telemetry.SourceGenerator` from all projects. |
+| `ExcludeMSTelemetryExtension` | `false` | Set to `true` to exclude `Microsoft.Extensions.Telemetry.Abstractions`. Note, when `ExcludePurviewTelemetry` is `false` this is excluded anyway. |
### Testing
| Property | Default | Description |
-|---|---|---|
-| `ProjectSdkTestFramework` | `TUnit` | Testing framework. Set to `XUnit` to switch to xunit v3. |
+| -- | -- | -- |
+| `TestingFramework` | `TUnit` | Testing framework. Supported values: `TUnit`, `Xunit`, `None`. |
+| `SubstituteFramework` | `TUnitMocks` | Mocking provider. Supported values: `TUnitMocks`, `NSubstitute`, `None`. |
+| `TestDataFramework` | `Bogus` | Test data provider. Supported values: `Bogus`, `None`. |
| `DisableAutoInternalsVisibleTo` | `false` | Set to `true` to disable automatic `InternalsVisibleTo` generation for test types and shared testing projects. |
-#### Example: switch a repo to XUnit
+### Compiler-visible SDK properties
+
+The SDK now exports its properties via `CompilerVisibleProperty`, so analyzers and source generators can read them through `build_property.`.
+
+| Property | Description |
+| -- | -- |
+| `UsePackageJsonVersion` | Whether version detection from `package.json` is active. |
+| `RootPackageJson` | Resolved path to the `package.json` used for version detection. |
+| `RepoRoot` | Repo root directory found via `.git` auto-discovery. |
+| `Version` | Package/assembly version, sourced from `package.json` when detection is enabled. |
+| `PackageVersion` | NuGet package version, sourced from `package.json` when detection is enabled. |
+| `NamespacePrefix` | Required namespace prefix used to derive `RootNamespace`. |
+| `DisableNamespacePrefixCheck` | Disables the build error for missing `NamespacePrefix`. |
+| `TestingFramework` | Selected testing framework (`TUnit`, `Xunit`, or `None`). |
+| `SubstituteFramework` | Selected mocking provider (`TUnitMocks`, `NSubstitute`, or `None`). |
+| `TestDataFramework` | Selected test data provider (`Bogus` or `None`). |
+| `SourceLinkPackageName` | SourceLink package ID added by the SDK. |
+| `ExcludePurviewTelemetry` | Opt-out for `Purview.Telemetry.SourceGenerator`. |
+| `ExcludeMSTelemetryExtension` | Opt-out for `Microsoft.Extensions.Telemetry.Abstractions`. |
+| `DisableGenerateAssemblyInfoClass` | Disables generated `AssemblyInfo` helper source. |
+| `EnableAssemblyNameGeneration` | When `true`, the SDK derives `AssemblyName` from the logical project name. |
+| `DisableAutoInternalsVisibleTo` | Disables automatic `InternalsVisibleTo` generation. |
+| `AutoIncludeUsings` | Controls SDK-added global usings. |
+| `IsCSharpProject` | True when the project is a `.csproj`. |
+| `IsTestProject` | True when project name ends with a supported test suffix. |
+| `IsSharedTestingProject` | True for known shared testing helper project names. |
+| `TestingType` | Detected test category suffix from project name. |
+| `TargetProjectName` | Inferred target project name for test projects. |
+| `IsContainerProject` | True when Dockerfile markers indicate container defaults. |
+| `IsSdkProject` | True when an SDK value is detected from project/import declaration. |
+| `SdkProjectName` | Detected SDK name (e.g. `Microsoft.NET.Sdk.Web`). |
+| `IsWebProject` | Marker used in SDK web-project behaviour. |
+| `IsWebSdkProject` | True when `SdkProjectName` is `Microsoft.NET.Sdk.Web`. |
+| `IsWorkerSdkProject` | True when `SdkProjectName` is `Microsoft.NET.Sdk.Worker`. |
+| `IsAspireHostProject` | True when SDK starts with `Aspire.Sdk.Host`. |
+| `EditorConfigFilePath` | Path to the SDK-provided `.editorconfig` that is injected into `@(EditorConfigFiles)`. |
+| `RepositoryEditorConfigFilePath` | Destination path for bootstrapping a physical repo-level `.editorconfig` (defaults to git repo root; falls back to `Directory.Build.props` directory). |
+| `BootstrapEditorConfigToRepoRoot` | When `true` (default), copies the SDK `.editorconfig` to `RepositoryEditorConfigFilePath` if missing. |
+| `RepositoryGlobalJsonFilePath` | Destination path for bootstrapping a physical repo-level `global.json` (defaults to git repo root; falls back to `Directory.Build.props` directory). |
+| `BootstrapGlobalJsonToRepoRoot` | When `true` (default), creates `global.json` at `RepositoryGlobalJsonFilePath` if missing. |
+| `PurviewDotNetProjectSdkVersionForGlobalJson` | Version used for `msbuild-sdks.Purview.DotNetProjectSdk` when bootstrapping `global.json` (auto-detected from SDK package path, fallback `1.0.0`). |
+| `DisableAutoCopySdkFiles` | When `true`, disables SDK auto-copy/bootstrap for repo files (`.editorconfig`, `global.json`). |
+| `PurviewAutoSdkPack` | When `true`, automatically packs the `Sdk/` folder contents into the NuGet package with the correct root-level paths. |
+| `CurrentYear` | Current year used in generated assembly metadata. |
+| `AutoGeneratedAssemblyInfoFile` | Relative path to generated AssemblyInfo source file. |
+
+#### Example: switch a repo to Xunit + NSubstitute and disable Bogus
```xml
Acme
- XUnit
+ Xunit
+ NSubstitute
+ None
@@ -127,7 +370,7 @@ Set any of these properties **before** the `` in your `Directory.Build.p
Test projects are automatically detected by their suffix. Supported patterns:
-```
+```text
MyProject.UnitTests → IsTestProject=true, TestingType=Unit
MyProject.IntegrationTests→ IsTestProject=true, TestingType=Integration
MyProject.E2ETests → IsTestProject=true, TestingType=E2E
@@ -143,9 +386,16 @@ Projects named `SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedT
## InternalsVisibleTo
-The SDK automatically generates `[assembly: InternalsVisibleTo("MyProject.UnitTests")]` (and all other TestType variants) for every non-test project. This allows test projects to access internal members. No manual attributes required.
+The SDK automatically generates `[assembly: InternalsVisibleTo("…")]` attributes for every non-test C# project. The friend assembly name is derived from the source project's resolved `$(AssemblyName)`, so all naming modes are handled correctly:
+
+- **Explicit ``** — if a project sets `Custom.Assembly`, the generated attributes use `Custom.Assembly.UnitTests`, `Custom.Assembly.IntegrationTests`, etc.
+- **`EnableAssemblyNameGeneration=true`** — the SDK-derived fully-qualified name is used (e.g. `Acme.MyProject.UnitTests`).
+- **Default** — standard .NET behaviour: `$(MSBuildProjectName)` (e.g. `MyProject.UnitTests`).
+
+Two categories of friend assemblies are generated:
-Additionally, all SharedTesting projects (like `SharedTestingFramework`, `SharedTestingInfrastructure`, etc.) are also granted access to internals, so shared testing infrastructure has full visibility into the projects being tested.
+1. **TestType variants** — one `InternalsVisibleTo` per defined `TestType` (`Unit`, `Integration`, `Architecture`, `Contract`, `Functional`, …), formatted as `$(AssemblyName).{TestType}Tests`.
+2. **SharedTesting projects** — one per known shared testing project name (`SharedTestingFramework`, `SharedTestingInfrastructure`, etc.). When `EnableAssemblyNameGeneration=true` and a `NamespacePrefix` is set, these are prefixed (e.g. `Acme.SharedTestingFramework`); otherwise the raw name is used.
### Disabling automatic InternalsVisibleTo
@@ -159,12 +409,81 @@ To disable automatic InternalsVisibleTo generation, set `DisableAutoInternalsVis
---
+## EmbeddedAttribute generation
+
+When `GenerateAssemblyInfoClassTarget` writes the SDK-generated `AssemblyInfo` source, it also emits:
+
+```csharp
+namespace Microsoft.CodeAnalysis
+{
+ sealed partial class EmbeddedAttribute : System.Attribute { }
+}
+```
+
+This block is guarded by:
+
+```csharp
+#if !PURVIEW_SDK_EXCLUDE_EMBEDDED
+```
+
+`AssemblyInfo` is emitted with `[Microsoft.CodeAnalysis.Embedded]`, so the project must have a matching `Microsoft.CodeAnalysis.EmbeddedAttribute` type available at compile time. The SDK emits that attribute to satisfy the reference and to keep generated metadata/source-generator-facing symbols marked as embedded.
+
+Define `PURVIEW_SDK_EXCLUDE_EMBEDDED` only when your build already provides `Microsoft.CodeAnalysis.EmbeddedAttribute` from another source; otherwise compilation will fail because the attribute used by generated `AssemblyInfo` cannot be resolved.
+
+---
+
## Namespace stripping
Certain suffixes are automatically stripped from `RootNamespace` to avoid awkward namespace names like `Acme.MyProject.Core.Something`:
Stripped suffixes: `Core`, `EF`, `Shared`, `ClientShared`, `ServiceDefaults`, and all shared testing project names.
+### Extensions namespace rule (`PDS0002`)
+
+When a file is placed under a project-root `Extensions/` folder, the analyzer intentionally treats
+that folder as a namespace reset point.
+
+- Scope: only files where the first project-relative segment is exactly `Extensions`
+- Expected namespace: derived from subfolders under `Extensions/` (file name is ignored)
+- `RootNamespace` is deliberately ignored for these files
+
+Examples:
+
+| Project-relative file path | Expected namespace |
+| -- | -- |
+| `Extensions/System/StringExtensions.cs` | `System` |
+| `Extensions/Microsoft/Extensions/Configuration/ConfigurationExtensions.cs` | `Microsoft.Extensions.Configuration` |
+| `Extensions/TopLevel.cs` | *(global namespace)* |
+
+To avoid conflicting guidance, `IDE0130` is suppressed for files in this root `Extensions/` scope.
+Outside this scope, normal `IDE0130` behaviour remains unchanged.
+
+---
+
+## Assembly name generation
+
+By default (`EnableAssemblyNameGeneration=false`), the SDK follows standard .NET behaviour: `AssemblyName` is `$(MSBuildProjectName)`. Set `EnableAssemblyNameGeneration=true` (in `Directory.Build.props` or individual `.csproj`) to have the SDK derive `AssemblyName` from `$(PurviewLogicalProjectName)`:
+
+```xml
+
+ Acme
+ true
+
+```
+
+With this enabled:
+
+| Project name | `NamespacePrefix` | Resolved `AssemblyName` |
+| -- | -- | -- |
+| `Api` | `Acme` | `Acme.Api` |
+| `Acme.Api` | `Acme` | `Acme.Api` (no double-prefix) |
+| `Core.Infrastructure` | `Acme` | `Acme.Core.Infrastructure` |
+| `Acme` | `Acme` | `Acme` |
+
+`PackageId` follows `AssemblyName` (with namespace-remove patterns applied). An explicit `` in a `.csproj` always takes precedence over generation.
+
+> **Note:** `RootNamespace` is derived from `$(PurviewLogicalProjectName)` regardless of this setting — it always reflects `$(NamespacePrefix).$(ProjectName)` with suffix stripping applied. `EnableAssemblyNameGeneration` only controls whether `AssemblyName`/`PackageId` follow suit.
+
---
## Central Package Management
@@ -184,8 +503,9 @@ To add project-specific packages, just append `PackageVersion` entries to your `
## Building the SDK
```sh
-dotnet build src/Purview.DotNetProjectSdk/Purview.DotNetProjectSdk.csproj
-dotnet pack src/Purview.DotNetProjectSdk/Purview.DotNetProjectSdk.csproj -o ./artifacts
+dotnet build src/DotNetProjectSdk.slnx -c Release
+dotnet test src/DotNetProjectSdk.slnx -c Release
+dotnet pack src/src/DotNetProjectSdk/DotNetProjectSdk.csproj -o ./artifacts
```
## License
diff --git a/assets/images/purview-logo.jpg b/assets/images/purview-logo.jpg
new file mode 100644
index 0000000..eef73ed
Binary files /dev/null and b/assets/images/purview-logo.jpg differ
diff --git a/build/Directory.Build.props b/build/Directory.Build.props
new file mode 100644
index 0000000..c0da58b
--- /dev/null
+++ b/build/Directory.Build.props
@@ -0,0 +1,12 @@
+
+
+ Purview.Aspire.ResourceKit
+ true
+
+
+
+
+
+ $(NoWarn);CA1062;CA1515;CA2007;CA1873;
+
+
diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets
new file mode 100644
index 0000000..6d9a3e0
--- /dev/null
+++ b/build/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/build/Directory.Packages.props b/build/Directory.Packages.props
new file mode 100644
index 0000000..4741f63
--- /dev/null
+++ b/build/Directory.Packages.props
@@ -0,0 +1,15 @@
+
+
+
+ [3.2.8,)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/Pipeline.slnx b/build/Pipeline.slnx
new file mode 100644
index 0000000..5c5b98f
--- /dev/null
+++ b/build/Pipeline.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/PipelineCLI/GlobalUsings.cs b/build/PipelineCLI/GlobalUsings.cs
new file mode 100644
index 0000000..0835bee
--- /dev/null
+++ b/build/PipelineCLI/GlobalUsings.cs
@@ -0,0 +1,11 @@
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Logging;
+global using Microsoft.Extensions.Options;
+global using ModularPipelines;
+global using ModularPipelines.Extensions;
+global using Octokit;
+global using Octokit.Internal;
+global using Purview.Aspire.ResourceKit.PipelineCLI.Helpers;
+global using Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+global using Purview.Aspire.ResourceKit.PipelineCLI.Settings;
diff --git a/build/PipelineCLI/Helpers/DotNetCLIOptions.cs b/build/PipelineCLI/Helpers/DotNetCLIOptions.cs
new file mode 100644
index 0000000..7f8dc73
--- /dev/null
+++ b/build/PipelineCLI/Helpers/DotNetCLIOptions.cs
@@ -0,0 +1,9 @@
+using ModularPipelines.Options;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers;
+
+public sealed record DotNetCLIOptions : CommandLineToolOptions
+{
+ public static DotNetCLIOptions Create(params string[] commandParts) =>
+ new() { Tool = "dotnet", CommandParts = commandParts };
+}
diff --git a/build/PipelineCLI/Helpers/PathHelpers.cs b/build/PipelineCLI/Helpers/PathHelpers.cs
new file mode 100644
index 0000000..36e8ba9
--- /dev/null
+++ b/build/PipelineCLI/Helpers/PathHelpers.cs
@@ -0,0 +1,21 @@
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers;
+
+static class PathHelpers
+{
+ public static string FindRepositoryRoot(string? startDirectory = null)
+ {
+ if (string.IsNullOrEmpty(startDirectory))
+ startDirectory = PipelineProjectDirectory.Find();
+
+ DirectoryInfo? directory = new(startDirectory);
+ while (directory is not null)
+ {
+ if (File.Exists(Path.Combine(directory.FullName, "package.json")))
+ return directory.FullName;
+
+ directory = directory.Parent;
+ }
+
+ throw new InvalidOperationException("Could not locate the repository root (no package.json found).");
+ }
+}
diff --git a/build/PipelineCLI/Helpers/TestHelpers.cs b/build/PipelineCLI/Helpers/TestHelpers.cs
new file mode 100644
index 0000000..c5eaa1a
--- /dev/null
+++ b/build/PipelineCLI/Helpers/TestHelpers.cs
@@ -0,0 +1,39 @@
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers;
+
+static class TestHelpers
+{
+ public static string BuildTUnitTreeNodeFilter(
+ string? assembly = null,
+ string? @namespace = null,
+ string? className = null,
+ string? testNameQuery = null
+ )
+ {
+ var filter = "/";
+ filter += assembly switch
+ {
+ null => "*",
+ _ => assembly,
+ };
+
+ filter += @namespace switch
+ {
+ null => "*",
+ _ => @namespace,
+ };
+
+ filter += className switch
+ {
+ null => "*",
+ _ => className,
+ };
+
+ filter += testNameQuery switch
+ {
+ null => "*",
+ _ => testNameQuery,
+ };
+
+ return filter;
+ }
+}
diff --git a/build/PipelineCLI/Modules/BuildModule.cs b/build/PipelineCLI/Modules/BuildModule.cs
new file mode 100644
index 0000000..fddc45b
--- /dev/null
+++ b/build/PipelineCLI/Modules/BuildModule.cs
@@ -0,0 +1,30 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Context;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Build")]
+[DependsOn]
+public class BuildModule(IOptions settings) : Module
+{
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ return await context
+ .DotNet()
+ .Build(
+ new()
+ {
+ ProjectSolution = settings.Value.Solution,
+ Configuration = settings.Value.Configuration,
+ NoRestore = true,
+ },
+ cancellationToken: cancellationToken
+ );
+ }
+}
diff --git a/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs b/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs
new file mode 100644
index 0000000..14de6dd
--- /dev/null
+++ b/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs
@@ -0,0 +1,60 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Configuration;
+using ModularPipelines.Context;
+using ModularPipelines.GitHub.Extensions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Release")]
+[DependsOn]
+[DependsOn]
+public class CreateGitHubReleaseModule(IOptions releaseSettings, IOptions gitSettings)
+ : Module
+{
+ protected override ModuleConfiguration Configure() =>
+ ModuleConfiguration
+ .Create()
+ .WithSkipWhen(_ =>
+ releaseSettings.Value.Mode is ReleaseMode.NuGet or ReleaseMode.GitHubRelease
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip(
+ "Release publishing is disabled. Set Release__Mode=GitHubRelease or Release__Mode=NuGet to create a GitHub release."
+ )
+ )
+ .WithSkipWhen(_ =>
+ string.IsNullOrWhiteSpace(gitSettings.Value.GetGitHubToken())
+ ? SkipDecision.Skip(
+ "GitHub access token is not configured. Set GitHub__AccessToken or GITHUB_TOKEN to create a GitHub release."
+ )
+ : SkipDecision.DoNotSkip
+ )
+ .Build();
+
+ protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ var versionResult = await context.GetModule();
+ var version =
+ versionResult.ValueOrDefault
+ ?? throw new InvalidOperationException("The version was not produced by the version module.");
+
+ var tag = $"v{version}";
+
+ var repositoryIdString = context.GitHub().EnvironmentVariables.RepositoryId;
+ if (!long.TryParse(repositoryIdString, out var repositoryId))
+ {
+ throw new InvalidOperationException(
+ $"Failed to parse RepositoryId '{repositoryIdString}' as a valid long integer."
+ );
+ }
+
+ // Create a new release on GitHub with the specified tag and generate release notes
+ return await context
+ .GitHub()
+ .Client.Repository.Release.Create(
+ repositoryId,
+ new NewRelease(tag) { Name = tag, GenerateReleaseNotes = true }
+ );
+ }
+}
diff --git a/build/PipelineCLI/Modules/LintModule.cs b/build/PipelineCLI/Modules/LintModule.cs
new file mode 100644
index 0000000..4511e1b
--- /dev/null
+++ b/build/PipelineCLI/Modules/LintModule.cs
@@ -0,0 +1,45 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Configuration;
+using ModularPipelines.Context;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Build")]
+public sealed class LintModule(IOptions settings) : Module
+{
+ protected override ModuleConfiguration Configure() =>
+ ModuleConfiguration
+ .Create()
+ .WithSkipWhen(_ =>
+ settings.Value.RunLint
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip("Linting is disabled. Set Build__RunLint=true to enable it.")
+ )
+ .Build();
+
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ var repositoryRoot = PathHelpers.FindRepositoryRoot();
+ var dotnet = context.DotNet();
+ var restoreResult = await dotnet.Tool.Restore(
+ new() { Interactive = false, ToolManifest = Path.Combine(repositoryRoot, ".config", "dotnet-tools.json") },
+ new() { WorkingDirectory = repositoryRoot },
+ cancellationToken
+ );
+ if (restoreResult.ExitCode != 0)
+ return restoreResult;
+
+ // Restore worked, now run the linter
+ return await context.Shell.Command.ExecuteCommandLineTool(
+ DotNetCLIOptions.Create("tool", "run", "csharpier", "check", repositoryRoot),
+ new() { WorkingDirectory = repositoryRoot },
+ cancellationToken: cancellationToken
+ );
+ }
+}
diff --git a/build/PipelineCLI/Modules/PackModule.cs b/build/PipelineCLI/Modules/PackModule.cs
new file mode 100644
index 0000000..5977701
--- /dev/null
+++ b/build/PipelineCLI/Modules/PackModule.cs
@@ -0,0 +1,60 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Configuration;
+using ModularPipelines.Context;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.DotNet.Options;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Build")]
+[DependsOn]
+[DependsOn]
+public sealed class PackModule(IOptions settings, IOptions releaseSettings)
+ : Module
+{
+ protected override ModuleConfiguration Configure() =>
+ ModuleConfiguration
+ .Create()
+ .WithSkipWhen(_ =>
+ settings.Value.RunPack
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip("Packing is disabled. Set Build__RunPack=true to enable it.")
+ )
+ .WithSkipWhen(_ =>
+ releaseSettings.Value.Mode != ReleaseMode.None
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip(
+ "Packing is disabled. Set Release__Mode to something other than None to enable it."
+ )
+ )
+ .Build();
+
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ var versionResult = await context.GetModule();
+ var nugetVersion =
+ versionResult.ValueOrDefault
+ ?? throw new InvalidOperationException("The version was not produced by the version module.");
+
+ Directory.CreateDirectory(settings.Value.ArtifactsFolder);
+
+ var version = nugetVersion.ToString();
+ return await context
+ .DotNet()
+ .Pack(
+ new DotNetPackOptions
+ {
+ ProjectSolution = settings.Value.Solution,
+ Configuration = settings.Value.Configuration,
+ Output = settings.Value.ArtifactsFolder,
+ Properties = [("PackageVersion", version), ("Version", version)],
+ },
+ cancellationToken: cancellationToken
+ );
+ }
+}
diff --git a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs
new file mode 100644
index 0000000..1eddee2
--- /dev/null
+++ b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs
@@ -0,0 +1,200 @@
+using System.ComponentModel.DataAnnotations;
+using ModularPipelines.Attributes;
+using ModularPipelines.Configuration;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+using NuGet.Versioning;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Build")]
+[DependsOn]
+public class PublishLocalNuGetModule(
+ IOptions localNuGetFeedSettings,
+ IOptions releaseSettings,
+ IOptions buildSettings
+) : Module
+{
+ protected override ModuleConfiguration Configure() =>
+ ModuleConfiguration
+ .Create()
+ .WithSkipWhen(ctx =>
+ ctx.IsRunningLocally()
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip("Local NuGet Feed publishing is disabled. This module can only be run locally.")
+ )
+ .WithSkipWhen(_ =>
+ releaseSettings.Value.Mode == ReleaseMode.LocalNuGet
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip(
+ "Local NuGet Feed publishing is disabled. Set Release__Mode=LocalNuGet to enable it."
+ )
+ )
+ .Build();
+
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ var localFeedPath = localNuGetFeedSettings.Value.LocalFeedPath;
+
+ var validationResults = new List();
+ var validationContext = new ValidationContext(localNuGetFeedSettings.Value);
+ if (
+ !Validator.TryValidateObject(
+ localNuGetFeedSettings.Value,
+ validationContext,
+ validationResults,
+ validateAllProperties: true
+ )
+ )
+ {
+ foreach (var validationResult in validationResults)
+ context.Logger.LogError("{Message}", validationResult.ErrorMessage);
+
+ throw new InvalidOperationException(
+ $"Invalid {nameof(PublishLocalNuGetSettings)} configuration for {nameof(PublishLocalNuGetSettings.LocalFeedPath)}. "
+ + "Windows paths with backslashes may have been stripped by the shell; "
+ + "use forward slashes, e.g. --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/."
+ );
+ }
+
+ var fullLocalFeedPath = Path.GetFullPath(localFeedPath);
+ context.Logger.LogInformation("Publishing local NuGet packages to {LocalFeedPath}.", fullLocalFeedPath);
+
+ if (!Directory.Exists(fullLocalFeedPath))
+ Directory.CreateDirectory(fullLocalFeedPath);
+
+ var packages = Directory
+ .GetFiles(buildSettings.Value.ArtifactsFolder, "*.nupkg")
+ .Concat(Directory.GetFiles(buildSettings.Value.ArtifactsFolder, "*.snupkg"))
+ .ToArray();
+ if (packages.Length == 0)
+ {
+ throw new InvalidOperationException(
+ $"No packages found in {buildSettings.Value.ArtifactsFolder}. The local feed was not populated."
+ );
+ }
+
+ List nupkgPackages = [];
+ foreach (var package in packages)
+ {
+ var fileName = Path.GetFileName(package);
+ var destinationPath = Path.Combine(fullLocalFeedPath, fileName);
+
+ if (Path.GetExtension(fileName) == ".nupkg")
+ nupkgPackages.Add(await ParsePackageDetailsAsync(package, cancellationToken));
+
+ if (!localNuGetFeedSettings.Value.OverwriteExistingPackages && File.Exists(destinationPath))
+ {
+ context.Logger.LogInformation("Package {Package} already exists in local feed. Skipping.", fileName);
+ File.Delete(package);
+
+ continue;
+ }
+
+ File.Move(package, destinationPath, true);
+ context.Logger.LogInformation("Copied package {Package} to local feed.", fileName);
+ }
+
+ if (localNuGetFeedSettings.Value.ClearPackageCache)
+ {
+ context.Logger.LogInformation("Clearing local NuGet package cache...");
+
+ var globalPackagesResult = await context.Shell.Command.ExecuteCommandLineTool(
+ DotNetCLIOptions.Create("nuget", "locals", "global-packages", "--list"),
+ cancellationToken: cancellationToken
+ );
+ if (globalPackagesResult.ExitCode != 0)
+ return globalPackagesResult;
+
+ var httpCacheResult = await context.Shell.Command.ExecuteCommandLineTool(
+ DotNetCLIOptions.Create("nuget", "locals", "http-cache", "--list"),
+ cancellationToken: cancellationToken
+ );
+ if (httpCacheResult.ExitCode != 0)
+ return httpCacheResult;
+
+ var globalPackagePaths = globalPackagesResult
+ .StandardOutput.Replace("global-packages: ", "", StringComparison.Ordinal)
+ .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
+ .Where(Directory.Exists);
+
+ var httpCachePaths = httpCacheResult
+ .StandardOutput.Replace("http-cache: ", "", StringComparison.Ordinal)
+ .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
+ .Where(Directory.Exists);
+
+ foreach (var artifact in nupkgPackages)
+ {
+#pragma warning disable CA1308 // Normalize strings to uppercase
+ var loweredPackageId = artifact.PackageId.ToLowerInvariant();
+ var loweredVersion = artifact.Version.ToFullString().ToLowerInvariant();
+#pragma warning restore CA1308 // Normalize strings to uppercase
+
+ foreach (var globalPath in globalPackagePaths)
+ {
+ var packagePath = Path.Combine(globalPath, loweredPackageId, artifact.Version.ToFullString());
+ if (Directory.Exists(packagePath))
+ {
+ Directory.Delete(packagePath, true);
+ context.Logger.LogInformation(
+ "Deleted package {Package} version {Version} from global packages cache.",
+ artifact.PackageId,
+ artifact.Version
+ );
+ }
+ }
+ foreach (var httpCachePath in httpCachePaths)
+ {
+ string[] packagePaths =
+ [
+ Path.Combine(httpCachePath, "list_" + loweredPackageId + ".dat"),
+ Path.Combine(httpCachePath, "list_" + loweredPackageId + "_index.dat"),
+ Path.Combine(httpCachePath, "list_" + loweredPackageId + "_range_*.dat"),
+ Path.Combine(httpCachePath, "nupkg_" + loweredPackageId + "." + loweredVersion + ".dat"),
+ ];
+
+ foreach (var path in packagePaths)
+ {
+ var directory = Path.GetDirectoryName(path);
+ var pattern = Path.GetFileName(path);
+ foreach (var file in Directory.EnumerateFiles(directory!, pattern, SearchOption.AllDirectories))
+ {
+ File.Delete(file);
+ context.Logger.LogInformation(
+ "Deleted package {Package} version {Version} from HTTP cache.",
+ artifact.PackageId,
+ artifact.Version
+ );
+ }
+ }
+ }
+ }
+ }
+
+ if (localNuGetFeedSettings.Value.ShutdownDotnetBuilderServer)
+ {
+ context.Logger.LogInformation("Shutting down dotnet builder server...");
+
+ return await context.Shell.Command.ExecuteCommandLineTool(
+ DotNetCLIOptions.Create("build-server", "shutdown"),
+ cancellationToken: cancellationToken
+ );
+ }
+
+ return null;
+ }
+
+ static async Task ParsePackageDetailsAsync(string artifact, CancellationToken cancellationToken)
+ {
+ using var packageReader = new NuGet.Packaging.PackageArchiveReader(artifact);
+ var packaging = await packageReader.GetNuspecReaderAsync(cancellationToken);
+
+ return new(packaging.GetId(), packaging.GetVersion());
+ }
+}
+
+record struct PackageDetails(string PackageId, NuGetVersion Version);
diff --git a/build/PipelineCLI/Modules/PublishNuGetModule.cs b/build/PipelineCLI/Modules/PublishNuGetModule.cs
new file mode 100644
index 0000000..ae68a4c
--- /dev/null
+++ b/build/PipelineCLI/Modules/PublishNuGetModule.cs
@@ -0,0 +1,69 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Configuration;
+using ModularPipelines.Context;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Release")]
+[DependsOn]
+[DependsOn]
+public class PublishNuGetModule(
+ IOptions buildSettings,
+ IOptions nugetSettings,
+ IOptions releaseSettings
+) : Module
+{
+ protected override ModuleConfiguration Configure() =>
+ ModuleConfiguration
+ .Create()
+ .WithSkipWhen(_ =>
+ releaseSettings.Value.Mode == ReleaseMode.NuGet
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip(
+ "Release publishing is disabled. Set Release__Mode=NuGet to publish packages to nuget.org."
+ )
+ )
+ .WithSkipWhen(_ =>
+ string.IsNullOrWhiteSpace(nugetSettings.Value.GetNuGetAPIKey())
+ ? SkipDecision.Skip(
+ "NuGet API key is not set. Set NuGet__APIKey or NUGET_APIKEY to publish packages."
+ )
+ : SkipDecision.DoNotSkip
+ )
+ .Build();
+
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ var packages = Directory
+ .EnumerateFiles(buildSettings.Value.ArtifactsFolder, "*.nupkg", SearchOption.TopDirectoryOnly)
+ .ToList();
+
+ if (packages.Count == 0)
+ {
+ throw new InvalidOperationException($"No NuGet packages found in {buildSettings.Value.ArtifactsFolder}.");
+ }
+
+ var tasks = packages.Select(package =>
+ context
+ .DotNet()
+ .Nuget.Push(
+ new()
+ {
+ Path = package,
+ Source = nugetSettings.Value.FeedUrl,
+ ApiKey = nugetSettings.Value.GetNuGetAPIKey(),
+ SkipDuplicate = true,
+ },
+ cancellationToken: cancellationToken
+ )
+ );
+
+ return await Task.WhenAll(tasks);
+ }
+}
diff --git a/build/PipelineCLI/Modules/RestoreModule.cs b/build/PipelineCLI/Modules/RestoreModule.cs
new file mode 100644
index 0000000..139883c
--- /dev/null
+++ b/build/PipelineCLI/Modules/RestoreModule.cs
@@ -0,0 +1,25 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Context;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.DotNet.Options;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Build")]
+public class RestoreModule(IOptions settings) : Module
+{
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ return await context
+ .DotNet()
+ .Restore(
+ new DotNetRestoreOptions { ProjectSolution = settings.Value.Solution },
+ cancellationToken: cancellationToken
+ );
+ }
+}
diff --git a/build/PipelineCLI/Modules/RunTestsModule.cs b/build/PipelineCLI/Modules/RunTestsModule.cs
new file mode 100644
index 0000000..d9c2030
--- /dev/null
+++ b/build/PipelineCLI/Modules/RunTestsModule.cs
@@ -0,0 +1,117 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using ModularPipelines.Attributes;
+using ModularPipelines.Configuration;
+using ModularPipelines.Context;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.DotNet.Options;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Build")]
+[DependsOn]
+public class RunTestsModule(IOptions settings) : Module
+{
+ protected override ModuleConfiguration Configure() =>
+ ModuleConfiguration
+ .Create()
+ .WithSkipWhen(_ =>
+ settings.Value.RunTests
+ ? SkipDecision.DoNotSkip
+ : SkipDecision.Skip("Tests are disabled. Set Build__RunTests=true to run them.")
+ )
+ .Build();
+
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ var testProjects = FilterTestProjects(
+ Directory.EnumerateFiles("src/tests", "*Tests.csproj", SearchOption.AllDirectories).ToList(),
+ settings.Value.TestProjects
+ );
+ if (testProjects.Count == 0)
+ {
+ context.Logger.LogWarning(
+ "No test projects matched 'src/tests' (filter: {TestProjects}), despite tests being enabled. Skipping test execution.",
+ settings.Value.TestProjects
+ );
+
+ return [];
+ }
+
+ var timings = new List<(string Project, TimeSpan Elapsed, int ExitCode)>();
+
+ var tasks = testProjects.Select(async project =>
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var result = await context
+ .DotNet()
+ .Test(
+ new DotNetTestOptions
+ {
+ Project = project,
+ Configuration = settings.Value.Configuration,
+ NoBuild = true,
+ NoRestore = true,
+ Arguments = ["--ignore-exit-code", "8", "--treenode-filter", settings.Value.TestFilter],
+ },
+ cancellationToken: cancellationToken
+ );
+ stopwatch.Stop();
+
+ lock (timings)
+ timings.Add((project, stopwatch.Elapsed, result.ExitCode));
+
+ return result;
+ });
+
+ var results = await Task.WhenAll(tasks);
+
+ context.Logger.LogInformation(
+ "Test run timings:{NewLine}{Timings}",
+ Environment.NewLine,
+ string.Join(
+ Environment.NewLine,
+ timings
+ .OrderByDescending(t => t.Elapsed)
+ .Select(t => $" {Path.GetFileName(t.Project)}: {t.Elapsed.TotalSeconds:F1}s (exit {t.ExitCode})")
+ )
+ );
+
+ return results;
+ }
+
+ static IReadOnlyList FilterTestProjects(IReadOnlyList projects, string filter)
+ {
+ if (string.IsNullOrWhiteSpace(filter) || filter.Trim() == "*")
+ return projects;
+
+ var patterns = filter
+ .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(ToRegexPattern)
+ .ToArray();
+
+ return projects
+ .Where(project =>
+ {
+ var fileName = Path.GetFileName(project);
+ return patterns.Any(pattern => Regex.IsMatch(fileName, pattern, RegexOptions.IgnoreCase));
+ })
+ .ToList();
+ }
+
+ static string ToRegexPattern(string entry)
+ {
+ if (entry.Contains('*', StringComparison.Ordinal))
+ {
+ var escaped = Regex.Escape(entry);
+ return "^" + escaped.Replace("\\*", ".*", StringComparison.Ordinal) + "$";
+ }
+
+ return "^" + Regex.Escape(entry) + "$";
+ }
+}
diff --git a/build/PipelineCLI/Modules/VersionModule.cs b/build/PipelineCLI/Modules/VersionModule.cs
new file mode 100644
index 0000000..80aa59f
--- /dev/null
+++ b/build/PipelineCLI/Modules/VersionModule.cs
@@ -0,0 +1,36 @@
+using System.Text.Json;
+using ModularPipelines.Attributes;
+using ModularPipelines.Context;
+using ModularPipelines.Modules;
+using NuGet.Versioning;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;
+
+[ModuleCategory("Build")]
+public class VersionModule : Module
+{
+ protected override async Task ExecuteAsync(
+ IModuleContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ var packageJsonPath = Path.Combine(Environment.CurrentDirectory, "package.json");
+
+ if (!File.Exists(packageJsonPath))
+ throw new FileNotFoundException($"Could not find package.json at {packageJsonPath}");
+
+ var packageJson = await File.ReadAllTextAsync(packageJsonPath, cancellationToken);
+
+ using var document = JsonDocument.Parse(packageJson);
+ var version = document.RootElement.GetProperty("version").GetString();
+
+ if (string.IsNullOrWhiteSpace(version))
+ throw new InvalidOperationException("The version field in package.json is missing or empty.");
+
+ if (!NuGetVersion.TryParse(version, out var nugetVersion))
+ throw new InvalidOperationException($"The version '{version}' in package.json is not a valid SemVer.");
+
+ context.Summary.KeyValue("Version", "Package version", version);
+ return nugetVersion;
+ }
+}
diff --git a/build/PipelineCLI/PipelineCLI.csproj b/build/PipelineCLI/PipelineCLI.csproj
new file mode 100644
index 0000000..0264da8
--- /dev/null
+++ b/build/PipelineCLI/PipelineCLI.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/build/PipelineCLI/PipelineProjectDirectory.cs b/build/PipelineCLI/PipelineProjectDirectory.cs
new file mode 100644
index 0000000..288cda8
--- /dev/null
+++ b/build/PipelineCLI/PipelineProjectDirectory.cs
@@ -0,0 +1,55 @@
+using System.Runtime.CompilerServices;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI;
+
+static class PipelineProjectDirectory
+{
+ const string DirectoryVariable = "MODULAR_PIPELINES_DIRECTORY";
+
+ public static string Find([CallerFilePath] string sourceFilePath = "")
+ {
+ var configuredDirectory = Environment.GetEnvironmentVariable(DirectoryVariable);
+ if (!string.IsNullOrWhiteSpace(configuredDirectory))
+ {
+ return ValidateConfiguredDirectory(configuredDirectory);
+ }
+
+ var sourceDirectory = Path.GetDirectoryName(sourceFilePath);
+ return IsPipelineDirectory(sourceDirectory) ? sourceDirectory! : FindFromBuildOutput();
+ }
+
+ static string ValidateConfiguredDirectory(string configuredDirectory)
+ {
+ var fullPath = Path.GetFullPath(configuredDirectory);
+ return IsPipelineDirectory(fullPath)
+ ? fullPath
+ : throw new InvalidOperationException(
+ $"{DirectoryVariable} must point to a directory containing appsettings.json and a project file."
+ );
+ }
+
+ static string FindFromBuildOutput()
+ {
+ for (
+ var directory = new DirectoryInfo(AppContext.BaseDirectory);
+ directory is not null;
+ directory = directory.Parent
+ )
+ {
+ if (IsPipelineDirectory(directory.FullName))
+ {
+ return directory.FullName;
+ }
+ }
+
+ throw new InvalidOperationException(
+ $"Could not locate the pipeline project directory. Set {DirectoryVariable} to its path."
+ );
+ }
+
+ static bool IsPipelineDirectory(string? directory) =>
+ directory is not null
+ && Directory.Exists(directory)
+ && File.Exists(Path.Combine(directory, "appsettings.json"))
+ && Directory.EnumerateFiles(directory, "*.csproj").Any();
+}
diff --git a/build/PipelineCLI/Program.cs b/build/PipelineCLI/Program.cs
new file mode 100644
index 0000000..5d38362
--- /dev/null
+++ b/build/PipelineCLI/Program.cs
@@ -0,0 +1,42 @@
+var pipelineDirectory = PipelineProjectDirectory.Find();
+var repositoryRoot = PathHelpers.FindRepositoryRoot(pipelineDirectory);
+
+var builder = Pipeline.CreateBuilder(args);
+
+builder
+ .Configuration.AddJsonFile(Path.Combine(pipelineDirectory, "appsettings.json"), optional: false)
+ .AddEnvironmentVariables()
+ .AddCommandLine(args);
+
+builder.Services.Configure(builder.Configuration.GetSection(BuildSettings.SectionName));
+builder.Services.Configure(builder.Configuration.GetSection(NuGetSettings.SectionName));
+builder.Services.Configure(
+ builder.Configuration.GetSection(PublishLocalNuGetSettings.SectionName)
+);
+builder.Services.Configure(builder.Configuration.GetSection(GitHubSettings.SectionName));
+builder.Services.Configure(builder.Configuration.GetSection(ReleaseSettings.SectionName));
+
+builder.Services.AddSingleton(serviceProvider =>
+{
+ var settings = serviceProvider.GetRequiredService>();
+ var accessToken = settings.Value.GetGitHubToken();
+
+ return new GitHubClient(new(settings.Value.ProductHeader), new InMemoryCredentialStore(new(accessToken)));
+});
+
+Environment.CurrentDirectory = repositoryRoot;
+
+builder
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule();
+
+await using var pipeline = await builder.BuildAsync();
+
+await pipeline.RunAsync();
diff --git a/build/PipelineCLI/Properties/launchSettings.json b/build/PipelineCLI/Properties/launchSettings.json
new file mode 100644
index 0000000..d5bbebe
--- /dev/null
+++ b/build/PipelineCLI/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "Run": {
+ "commandName": "Project"
+ },
+ "Local-NuGet": {
+ "commandName": "Project",
+ "commandLineArgs": "--Release:Mode=LocalNuGet\r\n--PublishLocalNuGet:LocalFeedPath=p:\\_sync-projects\\.local-nuget\\"
+ }
+ }
+}
diff --git a/build/PipelineCLI/Settings/BuildSettings.cs b/build/PipelineCLI/Settings/BuildSettings.cs
new file mode 100644
index 0000000..0a7f159
--- /dev/null
+++ b/build/PipelineCLI/Settings/BuildSettings.cs
@@ -0,0 +1,34 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings;
+
+public sealed class BuildSettings
+{
+ public const string SectionName = "Build";
+
+ public LogLevel LogLevel { get; init; } = LogLevel.Warning;
+
+ [Required(AllowEmptyStrings = false)]
+ public string Solution { get; init; } = "src/DotNetProjectSdk.slnx";
+
+ [Required(AllowEmptyStrings = false)]
+ public string Configuration { get; init; } = "Release";
+
+ [Required(AllowEmptyStrings = false)]
+ public string ArtifactsFolder { get; init; } = "artifacts";
+
+ public bool RunTests { get; init; } = true;
+
+ [Required(AllowEmptyStrings = false)]
+ public string TestFilter { get; init; } = "/*/*/*/*/";
+
+ ///
+ /// Comma-separated list of test project file names (or glob patterns) to run.
+ /// Empty or "*" runs every test project under src/tests.
+ ///
+ public string TestProjects { get; init; } = "*";
+
+ public bool RunLint { get; init; } = true;
+
+ public bool RunPack { get; init; } = true;
+}
diff --git a/build/PipelineCLI/Settings/GitHubSettings.cs b/build/PipelineCLI/Settings/GitHubSettings.cs
new file mode 100644
index 0000000..d54f201
--- /dev/null
+++ b/build/PipelineCLI/Settings/GitHubSettings.cs
@@ -0,0 +1,22 @@
+using ModularPipelines.Attributes;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings;
+
+public sealed record GitHubSettings
+{
+ public const string SectionName = "GitHub";
+
+ [SecretValue]
+ public string? AccessToken { get; init; }
+
+ [SecretValue]
+ [ConfigurationKeyName("GITHUB_TOKEN")]
+ public string? EnvAccessToken { get; init; }
+
+ public string ProductHeader { get; init; } = "Purview.SourceGeneratorFramework.Pipeline";
+
+ public string? GetGitHubToken() =>
+ !string.IsNullOrWhiteSpace(AccessToken) ? AccessToken
+ : !string.IsNullOrWhiteSpace(EnvAccessToken) ? EnvAccessToken
+ : null;
+}
diff --git a/build/PipelineCLI/Settings/NuGetSettings.cs b/build/PipelineCLI/Settings/NuGetSettings.cs
new file mode 100644
index 0000000..a2a530b
--- /dev/null
+++ b/build/PipelineCLI/Settings/NuGetSettings.cs
@@ -0,0 +1,22 @@
+using ModularPipelines.Attributes;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings;
+
+public sealed record NuGetSettings
+{
+ public const string SectionName = "NuGet";
+
+ [SecretValue]
+ public string? APIKey { get; set; }
+
+ [SecretValue]
+ [ConfigurationKeyName("NUGET_APIKEY")]
+ public string? EnvAPIKey { get; set; }
+
+ public string FeedUrl { get; init; } = "https://api.nuget.org/v3/index.json";
+
+ public string? GetNuGetAPIKey() =>
+ !string.IsNullOrWhiteSpace(APIKey) ? APIKey
+ : !string.IsNullOrWhiteSpace(EnvAPIKey) ? EnvAPIKey
+ : null;
+}
diff --git a/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs b/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs
new file mode 100644
index 0000000..3c5514e
--- /dev/null
+++ b/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs
@@ -0,0 +1,82 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings;
+
+public sealed record PublishLocalNuGetSettings : IValidatableObject
+{
+ public const string SectionName = "PublishLocalNuGet";
+
+ [Required(AllowEmptyStrings = false)]
+ public string LocalFeedPath { get; init; } = string.Empty;
+
+ public bool OverwriteExistingPackages { get; init; } = true;
+
+ public bool ShutdownDotnetBuilderServer { get; init; } = true;
+
+ public bool ClearPackageCache { get; init; } = true;
+
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ if (string.IsNullOrWhiteSpace(LocalFeedPath))
+ {
+ yield return new ValidationResult("LocalFeedPath is required.", [nameof(LocalFeedPath)]);
+ yield break;
+ }
+
+ // Path.IsPathRooted("p:foo") returns true, but a drive-relative path like "p:foo" is NOT an
+ // absolute path: Path.GetFullPath resolves it against the current directory and can silently
+ // copy packages to an unintended location. This is the classic signature of a Windows path whose
+ // backslashes were stripped by a sh-style shell, e.g. 'p:\_sync-projects\.local-nuget\'.
+ if (LocalFeedPath.Length >= 2 && LocalFeedPath[1] == ':')
+ {
+ var hasSeparatorAfterDrive =
+ LocalFeedPath.Length >= 3
+ && (
+ LocalFeedPath[2] == Path.DirectorySeparatorChar
+ || LocalFeedPath[2] == Path.AltDirectorySeparatorChar
+ );
+ if (!hasSeparatorAfterDrive)
+ {
+ yield return new ValidationResult(
+ $"LocalFeedPath '{LocalFeedPath}' is drive-relative, not an absolute path. "
+ + "This is usually caused by the shell stripping backslashes from a Windows path such as "
+ + $"'p:\\_sync-projects\\.local-nuget\\'. Use forward slashes instead, e.g. "
+ + "'p:/_sync-projects/.local-nuget/'.",
+ [nameof(LocalFeedPath)]
+ );
+ yield break;
+ }
+ }
+
+ if (!Path.IsPathRooted(LocalFeedPath))
+ {
+ yield return new ValidationResult(
+ $"LocalFeedPath must be an absolute path. Received: '{LocalFeedPath}'.",
+ [nameof(LocalFeedPath)]
+ );
+ yield break;
+ }
+
+ var root = Path.GetPathRoot(LocalFeedPath);
+ if (string.IsNullOrEmpty(root))
+ {
+ yield return new ValidationResult(
+ $"LocalFeedPath could not be parsed. Received: '{LocalFeedPath}'.",
+ [nameof(LocalFeedPath)]
+ );
+ yield break;
+ }
+
+ var lastChar = root[^1];
+ if (lastChar == Path.DirectorySeparatorChar || lastChar == Path.AltDirectorySeparatorChar)
+ yield break;
+
+ if (root.StartsWith(@"\\", StringComparison.Ordinal) || root.StartsWith("//", StringComparison.Ordinal))
+ yield break;
+
+ yield return new ValidationResult(
+ $"LocalFeedPath must be an absolute path (e.g. 'C:\\folder' or '\\\\server\\share'). Received: '{LocalFeedPath}'.",
+ [nameof(LocalFeedPath)]
+ );
+ }
+}
diff --git a/build/PipelineCLI/Settings/ReleaseSettings.cs b/build/PipelineCLI/Settings/ReleaseSettings.cs
new file mode 100644
index 0000000..69fa2a7
--- /dev/null
+++ b/build/PipelineCLI/Settings/ReleaseSettings.cs
@@ -0,0 +1,19 @@
+namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings;
+
+public enum ReleaseMode
+{
+ None,
+
+ NuGet,
+
+ GitHubRelease,
+
+ LocalNuGet,
+}
+
+public sealed record ReleaseSettings
+{
+ public const string SectionName = "Release";
+
+ public ReleaseMode Mode { get; set; } = ReleaseMode.None;
+}
diff --git a/build/PipelineCLI/appsettings.json b/build/PipelineCLI/appsettings.json
new file mode 100644
index 0000000..9381c34
--- /dev/null
+++ b/build/PipelineCLI/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "NuGet": {
+ "FeedUrl": "https://api.nuget.org/v3/index.json"
+ },
+ "GitHub": {
+ "AccessToken": null,
+ "ProductHeader": "Purview.SourceGeneratorFramework.Pipeline"
+ },
+ "Release": {
+ "Mode": "None"
+ }
+}
diff --git a/global.json b/global.json
index e0fa617..5274dcb 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "10.0.202",
+ "version": "10.0.400",
"rollForward": "latestMinor",
"allowPrerelease": false
},
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..3a17df0
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index e990496..587cb6b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,9 +1,12 @@
{
- "name": "dotnet-project-sdk",
+ "name": "changeops",
+ "version": "1.0.0-prerelease.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
+ "name": "changeops",
+ "version": "1.0.0-prerelease.10",
"devDependencies": {
"@changesets/cli": "^2.31.0"
}
@@ -392,16 +395,6 @@
"node": ">= 8"
}
},
- "node_modules/@types/node": {
- "version": "25.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
- "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
- "extraneous": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
- }
- },
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -1253,13 +1246,6 @@
"node": ">=8.0"
}
},
- "node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
- "extraneous": true,
- "license": "MIT"
- },
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
diff --git a/package.json b/package.json
index b728460..1ed4921 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
- "name": "changeops",
- "devDependencies": {
- "@changesets/cli": "^2.31.0"
- }
+ "name": "changeops",
+ "version": "1.0.0-prerelease.43",
+ "devDependencies": {
+ "@changesets/cli": "^2.31.0"
+ }
}
diff --git a/scripts/test-linux-docker.ps1 b/scripts/test-linux-docker.ps1
new file mode 100644
index 0000000..351db0f
--- /dev/null
+++ b/scripts/test-linux-docker.ps1
@@ -0,0 +1,23 @@
+[CmdletBinding()]
+param(
+ [string]$Image = "mcr.microsoft.com/dotnet/sdk:10.0.400"
+)
+
+$ErrorActionPreference = "Stop"
+
+if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
+ throw "Docker is required to run the Linux integration tests."
+}
+
+$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
+$containerRepositoryRoot = "/repo"
+
+docker run --rm `
+ --mount "type=bind,source=$repositoryRoot,target=$containerRepositoryRoot,readonly" `
+ --workdir /work `
+ $Image `
+ bash -c "tar --exclude='.git' --exclude='.vs' --exclude='bin' --exclude='obj' --exclude='artifacts' --exclude='TestResults' --exclude='node_modules' -C /repo -cf - . | tar -C /work -xf - && dotnet test src/tests/DotNetProjectSdk.IntegrationTests/DotNetProjectSdk.IntegrationTests.csproj -c Release -- --treenode-filter '/*/*/AgentPackFolderTests/*'"
+
+if ($LASTEXITCODE -ne 0) {
+ throw "Linux integration tests failed with exit code $LASTEXITCODE."
+}
diff --git a/Directory.Build.props b/src/Directory.Build.props
similarity index 62%
rename from Directory.Build.props
rename to src/Directory.Build.props
index 818b5fe..0830715 100644
--- a/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -6,11 +6,13 @@
pattern instead (see README.md).
-->
- Purview
+ Purview.DotNetProjectSdk
true
- true
+
+ false
+
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
new file mode 100644
index 0000000..4a96bb5
--- /dev/null
+++ b/src/Directory.Build.targets
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ <_ReferenceCopyLocalPaths Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))" />
+
+
+
+
+
+
+
diff --git a/src/DotNetProjectSdk.Analyzers/EditorBrowsableSuppressor.cs b/src/DotNetProjectSdk.Analyzers/EditorBrowsableSuppressor.cs
deleted file mode 100644
index a4b90bd..0000000
--- a/src/DotNetProjectSdk.Analyzers/EditorBrowsableSuppressor.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System.Collections.Immutable;
-using System.ComponentModel;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Diagnostics;
-
-namespace Purview.DotNetProjectSdk.Analyzers;
-
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-sealed class EditorBrowsableSuppressor : DiagnosticSuppressor
-{
- static readonly SuppressionDescriptor SuppressMissingXmlDocs = new(
- "PDS0001",
- "CS1591",
- "EditorBrowsable(Never) members do not require XML docs."
- );
-
- public override ImmutableArray SupportedSuppressions => [SuppressMissingXmlDocs];
-
- public override void ReportSuppressions(SuppressionAnalysisContext context)
- {
- foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
- {
- Location location = diagnostic.Location;
- if (!location.IsInSource || location.SourceTree is null)
- {
- continue;
- }
-
- SemanticModel semanticModel = context.GetSemanticModel(location.SourceTree);
- SyntaxNode rootNode = location.SourceTree.GetRoot(context.CancellationToken);
- SyntaxNode node = rootNode.FindNode(location.SourceSpan, getInnermostNodeForTie: true);
- ISymbol? symbol = semanticModel.GetDeclaredSymbol(node, context.CancellationToken);
-
- if (symbol is not null && HasEditorBrowsableNever(symbol))
- {
- context.ReportSuppression(Suppression.Create(SuppressMissingXmlDocs, diagnostic));
- }
- }
- }
-
- static bool HasEditorBrowsableNever(ISymbol symbol)
- {
- foreach (AttributeData attribute in symbol.GetAttributes())
- {
- if (attribute.AttributeClass?.ToDisplayString() != typeof(EditorBrowsableAttribute).FullName)
- {
- continue;
- }
-
- if (
- attribute.ConstructorArguments.Length == 1
- && attribute.ConstructorArguments[0].Value is int editorBrowsableState
- && editorBrowsableState == (int)EditorBrowsableState.Never
- )
- {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/src/DotNetProjectSdk.slnx b/src/DotNetProjectSdk.slnx
new file mode 100644
index 0000000..fed1892
--- /dev/null
+++ b/src/DotNetProjectSdk.slnx
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DotNetProjectSdk/DotNetProjectSdk.csproj b/src/DotNetProjectSdk/DotNetProjectSdk.csproj
deleted file mode 100644
index e230867..0000000
--- a/src/DotNetProjectSdk/DotNetProjectSdk.csproj
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
- Purview.DotNetProjectSdk
- MSBuildSdk
- A reusable MSBuild SDK that provides standardised .NET project defaults, code style enforcement, test framework wiring (TUnit or XUnit), and Central Package Management integration. Drop in via Directory.Build.props to apply uniformly across every project in a repo.
- msbuild;sdk;build;tunit;xunit;csharp;dotnet;central-package-management
- https://github.com/kjldev/purview-dotnet-project-sdk
- MIT
- git
-
-
- net10.0
- false
- false
- true
- true
-
-
- true
- true
-
- $(NoWarn);NU5128
-
- true
-
- false
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/DotNetProjectSdk/Sdk/.editorconfig b/src/DotNetProjectSdk/Sdk/.editorconfig
deleted file mode 100644
index a0073c1..0000000
--- a/src/DotNetProjectSdk/Sdk/.editorconfig
+++ /dev/null
@@ -1,457 +0,0 @@
-root = true
-
-# All files
-[*]
-indent_style = tab
-
-# Xml files
-[*.xml]
-indent_size = 2
-
-# C# files
-[*.cs]
-
-#### Core EditorConfig Options ####
-
-max_line_length = 120
-trim_trailing_whitespace = true
-
-# Indentation and spacing
-indent_size = 4
-tab_width = 4
-
-# New line preferences
-end_of_line = lf
-insert_final_newline = true
-
-#### .NET Coding Conventions ####
-[*.{cs,vb}]
-
-dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
-dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
-dotnet_naming_rule.private_members_with_underscore.severity = warning
-
-dotnet_naming_symbols.private_fields.applicable_kinds = field
-dotnet_naming_symbols.private_fields.applicable_accessibilities = private
-
-dotnet_naming_style.prefix_underscore.capitalization = camel_case
-dotnet_naming_style.prefix_underscore.required_prefix = _
-
-# Organize usings
-dotnet_separate_import_directive_groups = false
-dotnet_sort_system_directives_first = true
-file_header_template = unset
-
-# this. and Me. preferences
-dotnet_style_qualification_for_event = false:silent
-dotnet_style_qualification_for_field = false:silent
-dotnet_style_qualification_for_method = false:silent
-dotnet_style_qualification_for_property = false:silent
-
-# Language keywords vs BCL types preferences
-dotnet_style_predefined_type_for_locals_parameters_members = true:silent
-dotnet_style_predefined_type_for_member_access = true:silent
-
-# Parentheses preferences
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
-
-# Modifier preferences
-
-# “internal/private not necessary” is typically an IDE diagnostic; set it to error if you want it blocking
-dotnet_diagnostic.IDE0040.severity = warning
-dotnet_style_require_accessibility_modifiers = omit_if_default:warning
-
-# Expression-level preferences
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-dotnet_style_prefer_auto_properties = true:suggestion
-dotnet_style_prefer_compound_assignment = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
-dotnet_style_prefer_conditional_expression_over_return = true:suggestion
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
-dotnet_style_prefer_inferred_tuple_names = true:suggestion
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
-dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
-dotnet_style_prefer_simplified_interpolation = true:suggestion
-
-# Field preferences
-dotnet_style_readonly_field = true:warning
-
-# Parameter preferences
-dotnet_code_quality_unused_parameters = all:suggestion
-
-# Suppression preferences
-dotnet_remove_unnecessary_suppression_exclusions = none
-
-#### C# Coding Conventions ####
-[*.cs]
-
-# var preferences
-
-# Suppress IDE0008 - var is acceptable for method calls, otherwise `var x = Guid.Parse(...)` looks givens warning.
-dotnet_diagnostic.IDE0008.severity = silent
-
-csharp_style_var_for_built_in_types = true:silent
-csharp_style_var_when_type_is_apparent = false:warning
-csharp_style_var_elsewhere = true:silent
-
-# Expression-bodied members
-csharp_style_expression_bodied_accessors = true:silent
-csharp_style_expression_bodied_constructors = when_possible:suggestion
-csharp_style_expression_bodied_indexers = true:silent
-csharp_style_expression_bodied_lambdas = true:suggestion
-csharp_style_expression_bodied_local_functions = when_possible:suggestion
-csharp_style_expression_bodied_methods = when_possible:suggestion
-csharp_style_expression_bodied_operators = when_possible:suggestion
-csharp_style_expression_bodied_properties = true:suggestion
-
-# Pattern matching preferences
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_prefer_not_pattern = true:suggestion
-csharp_style_prefer_pattern_matching = true:silent
-csharp_style_prefer_switch_expression = true:suggestion
-
-# Null-checking preferences
-csharp_style_conditional_delegate_call = true:suggestion
-
-# Modifier preferences
-csharp_prefer_static_local_function = true:warning
-csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
-
-# Code-block preferences
-csharp_prefer_braces = when_possible:error
-csharp_prefer_simple_using_statement = true:suggestion
-
-# Expression-level preferences
-csharp_prefer_simple_default_expression = true:suggestion
-csharp_style_deconstructed_variable_declaration = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_pattern_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_index_operator = true:suggestion
-csharp_style_prefer_range_operator = true:suggestion
-csharp_style_throw_expression = true:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
-csharp_style_unused_value_expression_statement_preference = discard_variable:silent
-
-# 'using' directive preferences
-csharp_using_directive_placement = outside_namespace:silent
-
-#### C# Formatting Rules ####
-
-# New line preferences
-csharp_new_line_before_catch = true
-csharp_new_line_before_else = true
-csharp_new_line_before_finally = true
-csharp_new_line_before_members_in_anonymous_types = true
-csharp_new_line_before_members_in_object_initializers = true
-csharp_new_line_before_open_brace = all
-csharp_new_line_between_query_expression_clauses = true
-
-# Indentation preferences
-csharp_indent_block_contents = true
-csharp_indent_braces = false
-csharp_indent_case_contents = true
-csharp_indent_case_contents_when_block = true
-csharp_indent_labels = one_less_than_current
-csharp_indent_switch_labels = true
-
-# Space preferences
-csharp_space_after_cast = false
-csharp_space_after_colon_in_inheritance_clause = true
-csharp_space_after_comma = true
-csharp_space_after_dot = false
-csharp_space_after_keywords_in_control_flow_statements = true
-csharp_space_after_semicolon_in_for_statement = true
-csharp_space_around_binary_operators = before_and_after
-csharp_space_around_declaration_statements = false
-csharp_space_before_colon_in_inheritance_clause = true
-csharp_space_before_comma = false
-csharp_space_before_dot = false
-csharp_space_before_open_square_brackets = false
-csharp_space_before_semicolon_in_for_statement = false
-csharp_space_between_empty_square_brackets = false
-csharp_space_between_method_call_empty_parameter_list_parentheses = false
-csharp_space_between_method_call_name_and_opening_parenthesis = false
-csharp_space_between_method_call_parameter_list_parentheses = false
-csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
-csharp_space_between_method_declaration_name_and_open_parenthesis = false
-csharp_space_between_method_declaration_parameter_list_parentheses = false
-csharp_space_between_parentheses = false
-csharp_space_between_square_brackets = false
-
-# Wrapping preferences
-csharp_preserve_single_line_blocks = true
-csharp_preserve_single_line_statements = true
-csharp_style_namespace_declarations = file_scoped:silent
-csharp_style_prefer_method_group_conversion = true:silent
-csharp_style_prefer_top_level_statements = true:silent
-csharp_style_prefer_primary_constructors = true:suggestion
-csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning
-csharp_prefer_system_threading_lock = true:suggestion
-csharp_style_allow_embedded_statements_on_same_line_experimental = false:error
-csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
-csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
-csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
-csharp_style_prefer_null_check_over_type_check = true:suggestion
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
-
-# Prefer target-typed new(): Type x = new();
-csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
-csharp_style_prefer_tuple_swap = true:suggestion
-csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
-csharp_style_prefer_utf8_string_literals = true:suggestion
-csharp_prefer_static_anonymous_function = true:suggestion
-csharp_style_prefer_readonly_struct = true:suggestion
-csharp_style_prefer_readonly_struct_member = true:suggestion
-
-#### Naming styles ####
-[*.{cs,vb}]
-
-# Naming rules
-
-dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
-dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
-dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
-dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
-
-dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
-dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
-dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
-
-dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
-dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
-dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.events_should_be_pascalcase.symbols = events
-dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
-dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
-dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
-
-dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
-dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
-dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
-
-dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
-dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
-dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
-
-dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
-dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
-dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
-dotnet_naming_rule.private_fields_should_be__camelcase.style = prefix_underscore
-
-dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
-dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
-dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
-
-dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
-dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
-dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
-dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
-dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
-dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
-dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
-
-dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
-dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
-dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
-
-# Symbol specifications
-
-dotnet_naming_symbols.interfaces.applicable_kinds = interface
-dotnet_naming_symbols.interfaces.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.interfaces.required_modifiers =
-
-dotnet_naming_symbols.enums.applicable_kinds = enum
-dotnet_naming_symbols.enums.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.enums.required_modifiers =
-
-dotnet_naming_symbols.events.applicable_kinds = event
-dotnet_naming_symbols.events.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.events.required_modifiers =
-
-dotnet_naming_symbols.methods.applicable_kinds = method
-dotnet_naming_symbols.methods.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.methods.required_modifiers =
-
-dotnet_naming_symbols.properties.applicable_kinds = property
-dotnet_naming_symbols.properties.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.properties.required_modifiers =
-
-dotnet_naming_symbols.public_fields.applicable_kinds = field
-dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
-dotnet_naming_symbols.public_fields.required_modifiers =
-
-dotnet_naming_symbols.private_fields.applicable_kinds = field
-dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_fields.required_modifiers =
-
-dotnet_naming_symbols.private_static_fields.applicable_kinds = field
-dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_static_fields.required_modifiers = static
-
-dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
-dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.types_and_namespaces.required_modifiers =
-
-dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
-dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, protected, protected_internal
-dotnet_naming_symbols.non_field_members.required_modifiers =
-
-dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
-dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
-dotnet_naming_symbols.type_parameters.required_modifiers =
-
-dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
-dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_constant_fields.required_modifiers = const
-
-dotnet_naming_symbols.local_variables.applicable_kinds = local
-dotnet_naming_symbols.local_variables.applicable_accessibilities = local
-dotnet_naming_symbols.local_variables.required_modifiers =
-
-dotnet_naming_symbols.local_constants.applicable_kinds = local
-dotnet_naming_symbols.local_constants.applicable_accessibilities = local
-dotnet_naming_symbols.local_constants.required_modifiers = const
-
-dotnet_naming_symbols.parameters.applicable_kinds = parameter
-dotnet_naming_symbols.parameters.applicable_accessibilities = *
-dotnet_naming_symbols.parameters.required_modifiers =
-
-dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
-dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
-dotnet_naming_symbols.public_constant_fields.required_modifiers = const
-
-dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
-dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
-dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
-
-dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
-dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
-dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
-
-dotnet_naming_symbols.local_functions.applicable_kinds = local_function
-dotnet_naming_symbols.local_functions.applicable_accessibilities = *
-dotnet_naming_symbols.local_functions.required_modifiers =
-
-# Naming styles
-
-dotnet_naming_style.pascalcase.required_prefix =
-dotnet_naming_style.pascalcase.required_suffix =
-dotnet_naming_style.pascalcase.word_separator =
-dotnet_naming_style.pascalcase.capitalization = pascal_case
-
-dotnet_naming_style.ipascalcase.required_prefix = I
-dotnet_naming_style.ipascalcase.required_suffix =
-dotnet_naming_style.ipascalcase.word_separator =
-dotnet_naming_style.ipascalcase.capitalization = pascal_case
-
-dotnet_naming_style.tpascalcase.required_prefix = T
-dotnet_naming_style.tpascalcase.required_suffix =
-dotnet_naming_style.tpascalcase.word_separator =
-dotnet_naming_style.tpascalcase.capitalization = pascal_case
-
-dotnet_naming_style._camelcase.required_prefix = _
-dotnet_naming_style._camelcase.required_suffix =
-dotnet_naming_style._camelcase.word_separator =
-dotnet_naming_style._camelcase.capitalization = camel_case
-
-dotnet_naming_style.camelcase.required_prefix =
-dotnet_naming_style.camelcase.required_suffix =
-dotnet_naming_style.camelcase.word_separator =
-dotnet_naming_style.camelcase.capitalization = camel_case
-
-dotnet_naming_style.s_camelcase.required_prefix = s_
-dotnet_naming_style.s_camelcase.required_suffix =
-dotnet_naming_style.s_camelcase.word_separator =
-dotnet_naming_style.s_camelcase.capitalization = camel_case
-tab_width = 4
-indent_size = 4
-end_of_line = crlf
-dotnet_style_allow_multiple_blank_lines_experimental = false:warning
-dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
-dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
-dotnet_style_namespace_match_folder = true:suggestion
-
-# Verify
-[*.{received,verified}.{cs,txt}]
-charset = "utf-8-bom"
-end_of_line = lf
-indent_size = unset
-indent_style = unset
-insert_final_newline = false
-tab_width = unset
-trim_trailing_whitespace = false
-
-# If it's a settings, model, dto, etc file, ignore the 'properties' cannot have arrays
-# and other annoying rules - the following section is duplicated
-[**/*{ValueObjects,Settings,Options,Model,Models,DTO,Entity,Response,Request}.{cs,vb},]
-dotnet_diagnostic.CA1002.severity = none
-dotnet_diagnostic.CA1024.severity = none
-dotnet_diagnostic.CA1056.severity = none
-dotnet_diagnostic.CA1819.severity = none
-dotnet_diagnostic.CA2227.severity = none
-dotnet_diagnostic.CS8618.severity = none
-dotnet_diagnostic.CS8767.severity = none
-
-[**/{ValueObjects,Migrations,Entities,Models,DTOs,Settings,Options,Configuration,Requests,Responses}/**.{cs,vb}]
-dotnet_diagnostic.CA1002.severity = none
-dotnet_diagnostic.CA1024.severity = none
-dotnet_diagnostic.CA1056.severity = none
-dotnet_diagnostic.CA1819.severity = none
-dotnet_diagnostic.CA2227.severity = none
-dotnet_diagnostic.CS8618.severity = none
-dotnet_diagnostic.CS8767.severity = none
-
-[**/Migrations/**.{cs,vb}]
-dotnet_diagnostic.CA1062.severity = none
-dotnet_diagnostic.CA1825.severity = none
-dotnet_diagnostic.CA1861.severity = none
-dotnet_diagnostic.IDE0053.severity = none
-dotnet_diagnostic.IDE0300.severity = none
-dotnet_diagnostic.RCS1021.severity = none
-dotnet_diagnostic.RCS1205.severity = none
-
-[**/Extensions/**.{cs,vb}]
-dotnet_diagnostic.IDE0130.severity = none
-dotnet_diagnostic.CA1034.severity = none
-
-[**/Generated/**/*.{cs,vb}]
-dotnet_diagnostic.CS8602.severity = none
\ No newline at end of file
diff --git a/src/DotNetProjectSdk/Sdk/Sdk.props b/src/DotNetProjectSdk/Sdk/Sdk.props
deleted file mode 100644
index fc25bb1..0000000
--- a/src/DotNetProjectSdk/Sdk/Sdk.props
+++ /dev/null
@@ -1,413 +0,0 @@
-
-
-
- <_SharedTestingProjectNames>;SharedTestingFramework;SharedTestingInfrastructure;SharedTestingInfra;SharedTestingUtilities;SharedTestingLibrary;SharedTestingLib;SharedTestingHelpers;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- false
- false
- false
- false
- true
-
- false
- false
-
- false
- false
- false
- false
- false
- false
-
- $([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText(`$(MSBuildProjectFullPath)`)), `(?s-i)(?:^|\s|>)(?s-i)(?:^|\s|>)<\s*(?:Project|Import)\s(?:[^>]*?)\s?Sdk\s*="(?<sdkproj>.*?)"`).Groups['sdkproj'].Value)
- true
- true
- true
- true
- true
-
- $([System.Text.RegularExpressions.Regex]::Match($(MSBuildProjectName), `^.*\.([\w|_]\w*)Tests?$`).get_Groups().get_Item(1).ToString())
- true
- true
-
-
-
-
- TUnit
-
-
-
-
- net10.0
- preview
- enable
- enable
- true
- true
-
- true
- true
- true
-
- $(MSBuildThisFileDirectory).editorconfig
-
- Microsoft.SourceLink.GitHub
- false
- false
- false
- true
- true
- AllEnabledByDefault
- Latest
- AD0001;$(NoWarn)
- IDE2001;$(NoWarn)
- CA1014;CA1848;CA2007;CA2201;CA2225;CA2254;$(NoWarn)
- RCS1090;RCS1108;$(NoWarn)
- $([System.DateTime]::Now.Year)
- 0.0.1
-
- $(IntermediateOutputPath)/Properties/GeneratedAssemblyInfo.cs
-
-
-
-
-
- $(NamespacePrefix).$(MSBuildProjectName)
-
-
-
-
- $(RootNamespace.Replace('.$(TestingType)Tests', ''))
- $([System.Text.RegularExpressions.Regex]::Replace($(MSBuildProjectName), `[.]$(TestingType)Tests?$$`, ``))
-
-
-
-
-
-
-
-
-
- <_Parameter1>"DynamicProxyGenAssembly2"
-
-
-
-
-
-
-
-
-
-
-
-
- %(RecursiveDir)%(Filename)%(Extension)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- true
- Linux
- ..\..\
-
-
-
-
-
-
-
- $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated
-
-
-
-
- true
- $(AllowedOutputExtensionsInPackageBuildOutputFolder);.xml
-
-
-
-
- $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
- $(PackageTags);
- true
- true
- true
- snupkg
-
-
-
-
-
- all
- analyzers
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
- $(NoWarn);CA1062;CA1515;CA1707;CA1822;
- $(NoWarn);CS1591;
- false
- false
- false
-
-
- Exe
- 0
- true
- [NSubstitute*]*,[TUnit.*]*,[xunit.*]*,[Microsoft.Testing.*]*,[Microsoft.NET.Test*]*,[Bogus*]*
- System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute
- false
- false
-
- false
- false
-
-
- true
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
- true
- true
- true
-
-
-
- <_Parameter1>$(TestingType)
-
-
-
-
-
-
-
- <_Parameter1>Category
- <_Parameter2>$(TestingType)
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/DotNetProjectSdk/Sdk/Sdk.targets b/src/DotNetProjectSdk/Sdk/Sdk.targets
deleted file mode 100644
index cbd5303..0000000
--- a/src/DotNetProjectSdk/Sdk/Sdk.targets
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
-
-
- <_NsRemovePattern>@(NamespaceRemoveSuffix, '|')
- <_FixedRootNamespace>$([System.Text.RegularExpressions.Regex]::Replace('$(RootNamespace)', '\.(?:$(_NsRemovePattern))(?=\.|$)', ''))
-
-
-
-
-
-
- $(_FixedRootNamespace)
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_Parameter1>$(MSBuildProjectName).%(TestType.Identity)Tests
-
-
-
- <_Parameter1>%(SharedTestingProjectName.Identity)
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/settings.VisualStudio.json b/src/settings.VisualStudio.json
new file mode 100644
index 0000000..194ba35
--- /dev/null
+++ b/src/settings.VisualStudio.json
@@ -0,0 +1,4 @@
+/* Visual Studio Settings File */
+{
+ "csharpier.general.runonsave": true
+}
diff --git a/src/src/Analyzers/AnalyzerReleases.Shipped.md b/src/src/Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..81d62a3
--- /dev/null
+++ b/src/src/Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,7 @@
+## Release 1.0.0
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|------
+PDS0002 | Naming | Warning | Extensions root folder resets namespace
diff --git a/src/src/Analyzers/AnalyzerReleases.Unshipped.md b/src/src/Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..593b428
--- /dev/null
+++ b/src/src/Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,5 @@
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|------
+PDS0003 | Style | Warning | Prefer explicit types with target-typed object creation
diff --git a/src/DotNetProjectSdk.Analyzers/DotNetProjectSdk.Analyzers.csproj b/src/src/Analyzers/Analyzers.csproj
similarity index 64%
rename from src/DotNetProjectSdk.Analyzers/DotNetProjectSdk.Analyzers.csproj
rename to src/src/Analyzers/Analyzers.csproj
index e87e221..daae6f2 100644
--- a/src/DotNetProjectSdk.Analyzers/DotNetProjectSdk.Analyzers.csproj
+++ b/src/src/Analyzers/Analyzers.csproj
@@ -2,9 +2,7 @@
netstandard2.0
false
- true
false
- false
true
true
Purview.DotNetProjectSdk.Analyzers
@@ -14,5 +12,13 @@
all
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
diff --git a/src/src/Analyzers/BuildPropertyKeys.cs b/src/src/Analyzers/BuildPropertyKeys.cs
new file mode 100644
index 0000000..6b01cd8
--- /dev/null
+++ b/src/src/Analyzers/BuildPropertyKeys.cs
@@ -0,0 +1,29 @@
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.DotNetProjectSdk.Analyzers;
+
+///
+/// Analyzer-config property names the SDK exposes to Roslyn as build_property.*.
+///
+static class BuildPropertyKeys
+{
+ public const string ProjectDir = "build_property.ProjectDir";
+}
+
+///
+/// Read helpers for .
+///
+static class AnalyzerConfigOptionsExtensions
+{
+ public static bool TryGetBuildProperty(this AnalyzerConfigOptions options, string key, out string value)
+ {
+ if (options.TryGetValue(key, out var configuredValue) && !string.IsNullOrWhiteSpace(configuredValue))
+ {
+ value = configuredValue;
+ return true;
+ }
+
+ value = string.Empty;
+ return false;
+ }
+}
diff --git a/src/src/Analyzers/EditorBrowsable/EditorBrowsableSuppressor.cs b/src/src/Analyzers/EditorBrowsable/EditorBrowsableSuppressor.cs
new file mode 100644
index 0000000..34432ab
--- /dev/null
+++ b/src/src/Analyzers/EditorBrowsable/EditorBrowsableSuppressor.cs
@@ -0,0 +1,68 @@
+using System.Collections.Immutable;
+using System.ComponentModel;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.DotNetProjectSdk.Analyzers.EditorBrowsable;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class EditorBrowsableSuppressor : DiagnosticSuppressor
+{
+ static readonly SuppressionDescriptor SuppressMissingXmlDocs = new(
+ "PDS0001",
+ "CS1591",
+ "EditorBrowsable(Never) members do not require XML docs."
+ );
+
+ public override ImmutableArray SupportedSuppressions => [SuppressMissingXmlDocs];
+
+ public override void ReportSuppressions(SuppressionAnalysisContext context)
+ {
+ var editorBrowsableType = context.Compilation.GetTypeByMetadataName(typeof(EditorBrowsableAttribute).FullName!);
+ if (editorBrowsableType is null)
+ {
+ return;
+ }
+
+ foreach (var diagnostic in context.ReportedDiagnostics)
+ {
+ var location = diagnostic.Location;
+ if (!location.IsInSource || location.SourceTree is null)
+ {
+ continue;
+ }
+
+ var semanticModel = context.GetSemanticModel(location.SourceTree);
+ var rootNode = location.SourceTree.GetRoot(context.CancellationToken);
+ var node = rootNode.FindNode(location.SourceSpan, getInnermostNodeForTie: true);
+ var symbol = semanticModel.GetDeclaredSymbol(node, context.CancellationToken);
+
+ if (symbol is not null && HasEditorBrowsableNever(symbol, editorBrowsableType))
+ {
+ context.ReportSuppression(Suppression.Create(SuppressMissingXmlDocs, diagnostic));
+ }
+ }
+ }
+
+ static bool HasEditorBrowsableNever(ISymbol symbol, INamedTypeSymbol editorBrowsableType)
+ {
+ foreach (var attribute in symbol.GetAttributes())
+ {
+ if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, editorBrowsableType))
+ {
+ continue;
+ }
+
+ if (
+ attribute.ConstructorArguments.Length == 1
+ && attribute.ConstructorArguments[0].Value is int editorBrowsableState
+ && editorBrowsableState == (int)EditorBrowsableState.Never
+ )
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceAnalyzer.cs b/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceAnalyzer.cs
new file mode 100644
index 0000000..70dbfff
--- /dev/null
+++ b/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceAnalyzer.cs
@@ -0,0 +1,77 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ExtensionsNamespaceAnalyzer : DiagnosticAnalyzer
+{
+ internal const string DiagnosticId = "PDS0002";
+
+ static readonly DiagnosticDescriptor Rule = new(
+ DiagnosticId,
+ "Extensions root folder resets namespace",
+ "Namespace '{0}' does not match expected Extensions namespace '{1}'",
+ "Naming",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Files under the project-root 'Extensions' folder derive their namespace from the folder structure, ignoring RootNamespace."
+ );
+
+ public override ImmutableArray SupportedDiagnostics => [Rule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ if (context is null)
+ throw new ArgumentNullException(nameof(context));
+
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterSyntaxNodeAction(
+ AnalyzeNamespaceDeclaration,
+ SyntaxKind.NamespaceDeclaration,
+ SyntaxKind.FileScopedNamespaceDeclaration
+ );
+ }
+
+ static void AnalyzeNamespaceDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ if (context.Node is not BaseNamespaceDeclarationSyntax namespaceDeclaration)
+ {
+ return;
+ }
+
+ var filePath = context.Node.SyntaxTree.FilePath;
+ if (string.IsNullOrWhiteSpace(filePath))
+ return;
+
+ var options = context.Options.AnalyzerConfigOptionsProvider.GetOptions(context.Node.SyntaxTree);
+ if (!options.TryGetBuildProperty(BuildPropertyKeys.ProjectDir, out var projectDir))
+ return;
+
+ var expectedNamespace = ExtensionsNamespaceHelper.ComputeExpectedNamespace(projectDir, filePath);
+ if (expectedNamespace is null)
+ {
+ return;
+ }
+
+ var actualNamespace = namespaceDeclaration.Name.ToString();
+ if (string.Equals(actualNamespace, expectedNamespace, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ var diagnostic = Diagnostic.Create(
+ Rule,
+ namespaceDeclaration.Name.GetLocation(),
+ actualNamespace,
+ expectedNamespace
+ );
+
+ context.ReportDiagnostic(diagnostic);
+ }
+}
diff --git a/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceHelper.cs b/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceHelper.cs
new file mode 100644
index 0000000..ec45326
--- /dev/null
+++ b/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceHelper.cs
@@ -0,0 +1,118 @@
+using System.Runtime.InteropServices;
+
+namespace Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+
+///
+/// Helper methods for deriving and validating namespace conventions for files under the
+/// project-root Extensions directory.
+///
+static class ExtensionsNamespaceHelper
+{
+ const string ExtensionsRootFolderName = "Extensions";
+
+ static readonly StringComparison PathSegmentComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? StringComparison.OrdinalIgnoreCase
+ : StringComparison.Ordinal;
+
+ static readonly StringComparison FileExtensionComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? StringComparison.OrdinalIgnoreCase
+ : StringComparison.Ordinal;
+ static readonly char[] DirectorySeparators = ['\\', '/'];
+
+ internal static bool IsInExtensionsRootScope(string projectDir, string filePath)
+ {
+ return ComputeExpectedNamespace(projectDir, filePath) is not null;
+ }
+
+ internal static string? ComputeExpectedNamespace(string projectDir, string filePath)
+ {
+ if (string.IsNullOrWhiteSpace(projectDir) || string.IsNullOrWhiteSpace(filePath))
+ {
+ return null;
+ }
+
+ if (!filePath.EndsWith(".cs", FileExtensionComparison))
+ {
+ return null;
+ }
+
+ string fullProjectDir;
+ string fullFilePath;
+
+ try
+ {
+ fullProjectDir = EnsureTrailingDirectorySeparator(Path.GetFullPath(projectDir));
+ fullFilePath = Path.GetFullPath(filePath);
+ }
+ catch (ArgumentException)
+ {
+ return null;
+ }
+ catch (NotSupportedException)
+ {
+ return null;
+ }
+ catch (PathTooLongException)
+ {
+ return null;
+ }
+
+ var relativePath = GetRelativePath(fullProjectDir, fullFilePath);
+ if (string.IsNullOrWhiteSpace(relativePath))
+ {
+ return null;
+ }
+
+ if (relativePath.StartsWith("..", StringComparison.Ordinal) || Path.IsPathRooted(relativePath))
+ {
+ return null;
+ }
+
+ var relativeSegments = relativePath.Split(DirectorySeparators, StringSplitOptions.RemoveEmptyEntries);
+ relativeSegments = [.. relativeSegments.Select(static s => s.Trim()).Where(static s => s.Length > 0)];
+
+ if (relativeSegments.Length < 2)
+ {
+ return null;
+ }
+
+ if (!string.Equals(relativeSegments[0], ExtensionsRootFolderName, PathSegmentComparison))
+ {
+ return null;
+ }
+
+ var folderSegmentsCount = relativeSegments.Length - 2;
+ if (folderSegmentsCount <= 0)
+ {
+ return string.Empty;
+ }
+
+ // The expected namespace is derived from the segments of the relative path that are between the "Extensions" folder and the file name.
+ return string.Join(".", relativeSegments.Skip(1).Take(folderSegmentsCount));
+ }
+
+ static string GetRelativePath(string basePath, string fullPath)
+ {
+ var baseUri = new Uri(EnsureTrailingDirectorySeparator(basePath), UriKind.Absolute);
+ var fullUri = new Uri(fullPath, UriKind.Absolute);
+ var relativeUri = baseUri.MakeRelativeUri(fullUri);
+ return Uri.UnescapeDataString(relativeUri.ToString()).Replace('/', Path.DirectorySeparatorChar);
+ }
+
+ static string EnsureTrailingDirectorySeparator(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+
+ var lastChar = path[path.Length - 1];
+ if (lastChar == Path.DirectorySeparatorChar || lastChar == Path.AltDirectorySeparatorChar)
+ {
+ return path;
+ }
+
+ // Append the platform-specific directory separator character to the end of the path.
+ return path + Path.DirectorySeparatorChar;
+ }
+}
diff --git a/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceSuppressor.cs b/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceSuppressor.cs
new file mode 100644
index 0000000..fc1d1d6
--- /dev/null
+++ b/src/src/Analyzers/ExtensionsNamespace/ExtensionsNamespaceSuppressor.cs
@@ -0,0 +1,46 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ExtensionsNamespaceSuppressor : DiagnosticSuppressor
+{
+ static readonly SuppressionDescriptor SuppressIde0130ForExtensionsNamespaceRule = new(
+ "PDS0003",
+ "IDE0130",
+ "Files rooted under 'Extensions' intentionally derive namespace from the Extensions subtree and ignore RootNamespace."
+ );
+
+ public override ImmutableArray SupportedSuppressions =>
+ [SuppressIde0130ForExtensionsNamespaceRule];
+
+ public override void ReportSuppressions(SuppressionAnalysisContext context)
+ {
+ foreach (var diagnostic in context.ReportedDiagnostics)
+ {
+ if (!string.Equals(diagnostic.Id, "IDE0130", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ var location = diagnostic.Location;
+ if (!location.IsInSource || location.SourceTree is null)
+ {
+ continue;
+ }
+
+ var options = context.Options.AnalyzerConfigOptionsProvider.GetOptions(location.SourceTree);
+ if (!options.TryGetBuildProperty(BuildPropertyKeys.ProjectDir, out var projectDir))
+ {
+ continue;
+ }
+
+ if (ExtensionsNamespaceHelper.IsInExtensionsRootScope(projectDir, location.SourceTree.FilePath))
+ {
+ context.ReportSuppression(Suppression.Create(SuppressIde0130ForExtensionsNamespaceRule, diagnostic));
+ }
+ }
+ }
+}
diff --git a/src/src/Analyzers/TargetTypedObjectCreation/TargetTypedObjectCreationAnalyzer.cs b/src/src/Analyzers/TargetTypedObjectCreation/TargetTypedObjectCreationAnalyzer.cs
new file mode 100644
index 0000000..c763ef1
--- /dev/null
+++ b/src/src/Analyzers/TargetTypedObjectCreation/TargetTypedObjectCreationAnalyzer.cs
@@ -0,0 +1,47 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.DotNetProjectSdk.Analyzers.TargetTypedObjectCreation;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class TargetTypedObjectCreationAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "PDS0003";
+
+ static readonly DiagnosticDescriptor Rule = new(
+ DiagnosticId,
+ "Use an explicit type with target-typed object creation",
+ "Use explicit type instead of 'var' with target-typed object creation",
+ "Style",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Prefer an explicit type combined with a target-typed 'new()' expression over 'var'."
+ );
+
+ public override ImmutableArray SupportedDiagnostics => [Rule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ if (context is null)
+ throw new ArgumentNullException(nameof(context));
+
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeLocalDeclaration, SyntaxKind.LocalDeclarationStatement);
+ }
+
+ static void AnalyzeLocalDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ var declaration = ((LocalDeclarationStatementSyntax)context.Node).Declaration;
+ if (!declaration.Type.IsVar || declaration.Variables.Count != 1)
+ return;
+
+ if (declaration.Variables[0].Initializer?.Value is ObjectCreationExpressionSyntax)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(Rule, declaration.Type.GetLocation()));
+ }
+ }
+}
diff --git a/src/src/CodeFixers/CodeFixers.csproj b/src/src/CodeFixers/CodeFixers.csproj
new file mode 100644
index 0000000..1a232b1
--- /dev/null
+++ b/src/src/CodeFixers/CodeFixers.csproj
@@ -0,0 +1,27 @@
+
+
+ netstandard2.0
+ false
+ false
+ true
+ true
+ Purview.DotNetProjectSdk.CodeFixers
+
+
+
+
+
+
+
+
+ all
+
+
+ all
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
diff --git a/src/src/CodeFixers/ExtensionsNamespace/ExtensionsNamespaceCodeFixProvider.cs b/src/src/CodeFixers/ExtensionsNamespace/ExtensionsNamespaceCodeFixProvider.cs
new file mode 100644
index 0000000..07ccd14
--- /dev/null
+++ b/src/src/CodeFixers/ExtensionsNamespace/ExtensionsNamespaceCodeFixProvider.cs
@@ -0,0 +1,132 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Purview.DotNetProjectSdk.Analyzers;
+using Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+
+namespace Purview.DotNetProjectSdk.CodeFixers.ExtensionsNamespace;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ExtensionsNamespaceCodeFixProvider))]
+[Shared]
+public sealed class ExtensionsNamespaceCodeFixProvider : CodeFixProvider
+{
+ internal const string SyncNamespaceToExtensionsFolderEquivalenceKey = "SyncNamespaceToExtensionsFolder";
+
+ public override ImmutableArray FixableDiagnosticIds => [ExtensionsNamespaceAnalyzer.DiagnosticId];
+
+ public override FixAllProvider GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
+ if (root is null)
+ {
+ return;
+ }
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
+ var namespaceDeclaration = node.FirstAncestorOrSelf();
+ if (namespaceDeclaration is null)
+ {
+ continue;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Sync namespace to Extensions folder structure",
+ ct => ApplyFixAsync(context.Document, namespaceDeclaration, ct),
+ equivalenceKey: SyncNamespaceToExtensionsFolderEquivalenceKey
+ ),
+ diagnostic
+ );
+ }
+ }
+
+ static async Task ApplyFixAsync(
+ Document document,
+ BaseNamespaceDeclarationSyntax namespaceDeclaration,
+ CancellationToken cancellationToken
+ )
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken);
+ if (root is null)
+ {
+ return document;
+ }
+
+ if (!TryGetProjectDir(document, root.SyntaxTree, out var projectDir))
+ {
+ return document;
+ }
+
+ var expectedNamespace = ExtensionsNamespaceHelper.ComputeExpectedNamespace(
+ projectDir,
+ root.SyntaxTree.FilePath
+ );
+ if (expectedNamespace is null)
+ {
+ return document;
+ }
+
+ if (expectedNamespace.Length == 0)
+ {
+ var globalNamespaceDocument = RemoveNamespaceDeclaration(root, namespaceDeclaration);
+ return document.WithSyntaxRoot(globalNamespaceDocument);
+ }
+
+ var expectedNamespaceName = SyntaxFactory
+ .ParseName(expectedNamespace)
+ .WithTriviaFrom(namespaceDeclaration.Name);
+
+ var rewrittenNamespace = namespaceDeclaration switch
+ {
+ FileScopedNamespaceDeclarationSyntax fileScoped => fileScoped.WithName(expectedNamespaceName),
+ NamespaceDeclarationSyntax blockScoped => blockScoped.WithName(expectedNamespaceName),
+ _ => namespaceDeclaration,
+ };
+
+ var newRoot = root.ReplaceNode(namespaceDeclaration, rewrittenNamespace);
+ return document.WithSyntaxRoot(newRoot);
+ }
+
+ static bool TryGetProjectDir(Document document, SyntaxTree syntaxTree, out string projectDir)
+ {
+ var options = document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree);
+ return options.TryGetBuildProperty(BuildPropertyKeys.ProjectDir, out projectDir);
+ }
+
+ static SyntaxNode RemoveNamespaceDeclaration(SyntaxNode root, BaseNamespaceDeclarationSyntax namespaceDeclaration)
+ {
+ if (namespaceDeclaration.Parent is CompilationUnitSyntax compilationUnit)
+ {
+ var index = compilationUnit.Members.IndexOf(namespaceDeclaration);
+ if (index >= 0)
+ {
+ var members = compilationUnit.Members.RemoveAt(index).InsertRange(index, namespaceDeclaration.Members);
+ return compilationUnit.WithMembers(members);
+ }
+ }
+
+ if (namespaceDeclaration.Parent is NamespaceDeclarationSyntax parentNamespace)
+ {
+ var index = parentNamespace.Members.IndexOf(namespaceDeclaration);
+ if (index >= 0)
+ {
+ var members = parentNamespace.Members.RemoveAt(index).InsertRange(index, namespaceDeclaration.Members);
+ var rewrittenParent = parentNamespace.WithMembers(members);
+ return root.ReplaceNode(parentNamespace, rewrittenParent);
+ }
+ }
+
+ return root;
+ }
+}
diff --git a/src/src/CodeFixers/TargetTypedObjectCreation/TargetTypedObjectCreationCodeFixProvider.cs b/src/src/CodeFixers/TargetTypedObjectCreation/TargetTypedObjectCreationCodeFixProvider.cs
new file mode 100644
index 0000000..a75e941
--- /dev/null
+++ b/src/src/CodeFixers/TargetTypedObjectCreation/TargetTypedObjectCreationCodeFixProvider.cs
@@ -0,0 +1,73 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Purview.DotNetProjectSdk.Analyzers.TargetTypedObjectCreation;
+
+namespace Purview.DotNetProjectSdk.CodeFixers.TargetTypedObjectCreation;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TargetTypedObjectCreationCodeFixProvider))]
+[Shared]
+public sealed class TargetTypedObjectCreationCodeFixProvider : CodeFixProvider
+{
+ internal const string UseExplicitTypeAndTargetTypedNewEquivalenceKey = "UseExplicitTypeAndTargetTypedNew";
+
+ public override ImmutableArray FixableDiagnosticIds => [TargetTypedObjectCreationAnalyzer.DiagnosticId];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
+ if (root is null)
+ return;
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var node = root.FindNode(diagnostic.Location.SourceSpan);
+ var declaration = node.FirstAncestorOrSelf();
+ if (declaration is null || !declaration.Type.IsVar)
+ continue;
+
+ if (
+ declaration.Variables.Count != 1
+ || declaration.Variables[0].Initializer?.Value is not ObjectCreationExpressionSyntax
+ )
+ continue;
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Use explicit type and target-typed new",
+ cancellationToken => ApplyFixAsync(context.Document, declaration, cancellationToken),
+ equivalenceKey: UseExplicitTypeAndTargetTypedNewEquivalenceKey
+ ),
+ diagnostic
+ );
+ }
+ }
+
+ static async Task ApplyFixAsync(
+ Document document,
+ VariableDeclarationSyntax declaration,
+ CancellationToken cancellationToken
+ )
+ {
+ if (declaration.Variables[0].Initializer?.Value is not ObjectCreationExpressionSyntax objectCreation)
+ return document;
+
+ var explicitType = objectCreation.Type.WithTriviaFrom(declaration.Type);
+ var targetTypedCreation = SyntaxFactory
+ .ImplicitObjectCreationExpression(
+ objectCreation.ArgumentList ?? SyntaxFactory.ArgumentList(),
+ objectCreation.Initializer
+ )
+ .WithTriviaFrom(objectCreation);
+
+ var rewrittenDeclaration = declaration.ReplaceNode(objectCreation, targetTypedCreation).WithType(explicitType);
+ var root = await document.GetSyntaxRootAsync(cancellationToken);
+ return root is null ? document : document.WithSyntaxRoot(root.ReplaceNode(declaration, rewrittenDeclaration));
+ }
+}
diff --git a/src/src/DotNetProjectSdk/DotNetProjectSdk.csproj b/src/src/DotNetProjectSdk/DotNetProjectSdk.csproj
new file mode 100644
index 0000000..b197bd3
--- /dev/null
+++ b/src/src/DotNetProjectSdk/DotNetProjectSdk.csproj
@@ -0,0 +1,115 @@
+
+
+ Purview.DotNetProjectSdk
+ MSBuildSdk
+ A reusable MSBuild SDK that provides standardised .NET project defaults, code style enforcement, configurable testing framework wiring (TUnit, Xunit, or None), mocking provider wiring (TUnitMocks, NSubstitute, or None), and Central Package Management integration. Drop in via Directory.Build.props to apply uniformly across every project in a repo.
+ msbuild;sdk;build;tunit;xunit;tunitmock;nsubstitute;bogus;csharp;dotnet
+ https://github.com/purview-dev/purview-dotnet-project-sdk
+ MIT
+ README.md
+ git
+ purview-logo.jpg
+
+
+ netstandard2.0
+ false
+ false
+ true
+ true
+
+
+ true
+ true
+
+ false
+ false
+
+ false
+
+ $(NoWarn);NU5128
+
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/DotNetProjectSdk/Sdk/.agents/agents/sdk-consumer-setup.md b/src/src/DotNetProjectSdk/Sdk/.agents/agents/sdk-consumer-setup.md
new file mode 100644
index 0000000..b356a41
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/.agents/agents/sdk-consumer-setup.md
@@ -0,0 +1,34 @@
+# sdk-consumer-setup (generic agent spec)
+
+## Goal
+
+Help a consuming repository adopt or troubleshoot `Purview.DotNetProjectSdk` correctly, without breaking existing build behaviour.
+
+## Workflow
+
+1. Confirm the SDK is imported in `Directory.Build.props`/`Directory.Build.targets` via
+ `` and the matching `Sdk.targets` import.
+2. Check pre-import bootstrap properties are set **before** the `Sdk.props` import when they must affect
+ evaluation: `NamespacePrefix`, `UsePackageJsonVersion`, `RootPackageJson`.
+3. If version resolution looks wrong, verify `package.json` discovery: explicit `RootPackageJson`, then CI
+ variables, `.git` root, or a nearby `package.json`. `UsePackageJsonVersion=Strict` fails fast instead of
+ silently skipping resolution.
+4. If the bundled `.agents/**` content isn't appearing in the repo root, check `EnableAgentFolderInPackage`
+ (default `true`) and `AgentPackDestinationFolder` (default `.agents`) — the copy runs before build via
+ `EnsureAgentFolderInPackageTarget`.
+5. For test-framework or project-shape questions, confirm the project follows repo naming and placement
+ conventions the SDK expects, rather than introducing bespoke structure.
+6. Re-run `dotnet build` (or the repo's canonical build command) after each configuration change to confirm
+ the fix.
+
+## Constraints
+
+- Prefer minimal, targeted property changes over broad `Directory.Build.props` rewrites.
+- Do not disable `PurviewAutoSdkPack` or `EnableAgentFolderInPackage` unless the consumer explicitly asks to
+ opt out.
+- Do not duplicate SDK-managed properties in individual project files unless the scenario is intentionally
+ project-specific.
+
+## Related skill
+
+See `../skills/sdk-configuration-reference/SKILL.md` for the full property reference.
diff --git a/src/src/DotNetProjectSdk/Sdk/.agents/prompts/sdk-diagnose-agent-folder-copy.md b/src/src/DotNetProjectSdk/Sdk/.agents/prompts/sdk-diagnose-agent-folder-copy.md
new file mode 100644
index 0000000..49ad1c2
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/.agents/prompts/sdk-diagnose-agent-folder-copy.md
@@ -0,0 +1,26 @@
+# sdk-diagnose-agent-folder-copy (generic prompt spec)
+
+Diagnose why the bundled `.agents/**` folder from `Purview.DotNetProjectSdk` did not appear at the expected
+destination in a consuming repository.
+
+## Required behaviour
+
+1. Confirm the NuGet package actually contains `.agents/**` content (inspect the `.nupkg` if available).
+2. Confirm the consuming project is packable/buildable and imports the SDK via
+ `Sdk.props`/`Sdk.targets`, since the copy runs in `EnsureAgentFolderInPackageTarget` before build.
+3. Check `EnableAgentFolderInPackage` is not set to `false` anywhere in the build (project file,
+ `Directory.Build.props`, or command-line `-p:` overrides).
+4. Confirm the destination folder: default is `.agents` at the repo root, overridable per-build with
+ `-p:AgentPackDestinationFolder=`.
+5. Verify repo-root discovery succeeded: explicit `RepoRoot`, then a nearby `AGENTS.md`, then source-control
+ root metadata.
+6. Re-run the build and confirm the destination folder now contains the copied files (including the
+ generated `.gitignore` for skill/prompt/agent subfolders).
+
+## Suggested output
+
+- A short root-cause explanation (missing import, disabled flag, wrong destination override, or repo-root
+ discovery miss).
+- The exact command used to reproduce/verify the fix (for example
+ `dotnet build -p:AgentPackDestinationFolder=`).
+- Confirmation that the expected files exist at the resolved destination path.
diff --git a/src/src/DotNetProjectSdk/Sdk/.agents/skills/project-placement-defaults/SKILL.md b/src/src/DotNetProjectSdk/Sdk/.agents/skills/project-placement-defaults/SKILL.md
new file mode 100644
index 0000000..52e0ab4
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/.agents/skills/project-placement-defaults/SKILL.md
@@ -0,0 +1,113 @@
+---
+name: project-placement-defaults
+description: "Use when creating, moving, or splitting projects in a repository that uses Purview.DotNetProjectSdk, especially for src/tests placement, test suffix naming, namespace alignment, and automatic project-reference behavior."
+---
+
+# Project placement defaults for Purview.DotNetProjectSdk
+
+Use this skill whenever a task asks to add, move, split, or create a project in a repository that uses `Purview.DotNetProjectSdk` and you need placement, naming, and reference decisions to remain consistent with the SDK's automatic conventions.
+
+## Core principle
+
+Preserve the host repository's existing layout first; only introduce new structure when no established pattern exists. In Purview-based repos, prefer layouts that let the SDK's naming and auto-reference rules work without extra overrides.
+
+## Placement heuristics
+
+Use the repository's current structure as the source of truth, with these Purview-friendly defaults:
+
+1. Prefer source projects under `src/`.
+2. Prefer test projects under `tests/`.
+3. Place new projects beside similar projects (same language, layer, and test type).
+4. Keep one test type per project by default.
+5. Keep shared helper projects in explicit shared/shared-testing locations when those concepts exist.
+
+When a repo has no clear structure, use these conservative defaults because they align well with the SDK's automatic project-reference search paths:
+
+- Source/library projects under `src/`
+- Test projects under `tests/`
+- Integration/end-to-end tests in explicit sibling projects/folders such as `tests/Api.IntegrationTests/` or `tests/Api.E2ETests/`
+
+## Purview-specific naming rules
+
+The SDK relies heavily on project names.
+
+- Keep the `.csproj` filename equal to its containing directory name unless `DisableProjectFileNamingConventionCheck=true` is explicitly used.
+- Use conventional test suffixes such as `.UnitTests`, `.IntegrationTests`, `.E2ETests`, `.FunctionalTests`, `.ContractTests`, and other supported `*Tests` suffixes.
+- Keep shared helper projects on the SDK's exact recognized names when you want shared behavior:
+ - Shared projects: `Shared`, `SharedFramework`, `SharedInfrastructure`, `SharedInfra`, `SharedUtilities`, `SharedUtils`, `SharedLibrary`, `SharedLib`, `SharedHelpers`
+ - Shared testing projects: `SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedTestingInfra`, `SharedTestingUtilities`, `SharedTestingUtils`, `SharedTestingLibrary`, `SharedTestingLib`, `SharedTestingHelpers`
+- Do not invent near-miss names if you expect the SDK to classify the project automatically.
+
+## Test-type boundaries
+
+Separate tests by behavior and dependency scope:
+
+- **Unit tests**: isolate logic with minimal external dependencies.
+- **Integration tests**: verify behavior across component boundaries (I/O, framework integration, build/evaluation behavior).
+- **End-to-end/system tests**: verify full workflow behavior across the assembled system.
+
+If specialized test categories exist (for example, analyzer diagnostics vs code-fix integration), keep category-specific tests in distinct projects/folders.
+
+The SDK recognizes many test suffixes, including `Unit`, `Integration`, `E2E`, `EndToEnd`, `Acceptance`, `Functional`, `Performance`, `Load`, `Smoke`, `Stress`, `Regression`, `Security`, `Chaos`, `Scenario`, `System`, `Threat`, `BlackBox`, `WhiteBox`, `Accessibility`, `Interactive`, `Environment`, `Architecture`, and `Contract`.
+
+## Naming and namespace defaults
+
+Align identities with existing repository conventions:
+
+- Project names should follow prevailing patterns in sibling projects.
+- Test project names should clearly indicate scope/type with recognized test suffixes.
+- `NamespacePrefix` should remain the root identity source for the repo.
+- `RootNamespace` usually flows from the logical project identity generated by the SDK; avoid custom namespace overrides unless required.
+- When moving files between projects, update namespaces so they match the destination project's conventions.
+
+Do not invent a new naming scheme when an existing one is already in use.
+
+## Project defaults
+
+When creating a new project:
+
+1. Match the SDK/project style used by sibling projects.
+2. Reuse central dependency/version management if present.
+3. Add only dependencies required for the project's scope.
+4. Add the project to the repository solution/workspace entry point.
+5. Keep configuration consistent with neighboring projects (target frameworks, nullable, analyzers, warnings).
+
+When working in a Purview-based repo, also assume:
+
+- `TargetFramework` defaults to `net10.0` if not otherwise set, or `netstandard2.0` when the project explicitly declares `IsRoslynComponent=true`.
+- Test projects receive framework packages and coverage defaults from the SDK.
+- Non-test projects receive SourceLink and telemetry defaults unless explicitly opted out.
+
+## Move/split workflow checklist
+
+When splitting or relocating tests/projects:
+
+1. Create destination project/folder using established layout patterns.
+2. Move files physically.
+3. Update namespaces/imports/references for the destination.
+4. Verify the destination project name still produces the intended `TestingType`, `TargetProjectName`, and `RootNamespace`.
+5. Remove stale dependencies from the source project.
+6. Update solution/workspace membership and project references.
+6. Run build and relevant tests.
+
+## Automatic project-reference behavior to preserve
+
+The SDK automatically searches for project references based on naming and placement.
+
+- Test projects probe for their target project in these relative locations:
+ - `../$(TargetProjectName)/$(TargetProjectName).csproj`
+ - `../../$(TargetProjectName)/$(TargetProjectName).csproj`
+ - `../src/$(TargetProjectName)/$(TargetProjectName).csproj`
+ - `../../src/$(TargetProjectName)/$(TargetProjectName).csproj`
+- Non-test projects automatically look for sibling shared projects via `../Shared*/Shared*.csproj`.
+- Test projects automatically look for sibling shared-testing projects via `../SharedTesting*/SharedTesting*.csproj`.
+
+If you move projects away from these conventions, be prepared to add explicit project references.
+
+## Guardrails
+
+- Prefer minimal, targeted diffs.
+- Avoid cross-cutting renames unrelated to the move/split intent.
+- Keep test intent unchanged while relocating.
+- If structure is ambiguous, infer from nearest sibling projects and document the assumption in the change summary.
+- When in doubt, preserve compatibility with the SDK's automatic naming, namespace, and project-reference behavior.
diff --git a/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md
new file mode 100644
index 0000000..693eea6
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md
@@ -0,0 +1,161 @@
+---
+name: sdk-configuration-reference
+description: "Use when configuring Purview.DotNetProjectSdk through Directory.Build.props or a .csproj, especially for NamespacePrefix, version detection, testing framework selection, telemetry, repo bootstrapping, and embedded agent-skill settings."
+---
+
+# Purview.DotNetProjectSdk configuration reference
+
+Use this skill when a task asks what can be configured in `Purview.DotNetProjectSdk`, where a property must be set, or which defaults the SDK applies automatically.
+
+## First rule: know where a property must be set
+
+Set repo-wide bootstrap properties **before** importing the SDK in `Directory.Build.props` when the value must affect `Sdk.props` evaluation.
+
+Common pre-import properties:
+
+- `NamespacePrefix`
+- `UsePackageJsonVersion`
+- `RootPackageJson`
+- Repo-wide testing framework selection properties when you want every project to inherit them
+
+If a property changes behavior in `Sdk.targets` instead, it can usually be set later (for example in a project file), but prefer repo-wide defaults in `Directory.Build.props` unless the scenario is intentionally project-specific.
+
+## Version detection settings
+
+These properties control package/app version resolution from `package.json`:
+
+- `UsePackageJsonVersion` — default `true`; supported values: `true`, `false`, `Strict`
+- `RootPackageJson` — explicit path to the `package.json` to read
+- `EnableVersionDetectionCache` — default `true`; enables local caching of resolved version data
+- `VersionDetectionCacheFile` — optional explicit cache file path
+- `VersionDetectionLogEnabled` — default `false`; set to `true` to log the detected package version
+
+Behavior rules:
+
+1. If `RootPackageJson` is set, the SDK uses that path.
+2. Otherwise it tries to discover the repo root from CI variables, `.git`, or a nearby `package.json`.
+3. When version detection succeeds, both `Version` and `PackageVersion` are set from the `version` field.
+4. `UsePackageJsonVersion=Strict` should be treated as “fail if discovery/resolution cannot succeed”.
+
+## Core identity and build settings
+
+These are the most important configurable properties exposed by the SDK:
+
+- `NamespacePrefix` — required unless `DisableNamespacePrefixCheck=true`
+- `DisableNamespacePrefixCheck` — default `false`
+- `TargetFramework` — defaults to `net10.0` when neither `TargetFramework` nor `TargetFrameworks` is set; projects explicitly declaring `IsRoslynComponent=true` default to `netstandard2.0`
+- `IsRoslynComponent` — when explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, extended analyzer rules, disabled SourceLink and untracked-source embedding, no dependency file, compiler-generated output under the framework-specific intermediate directory, `symbols.nupkg`, `PackSourceGeneratorSymbols`, telemetry exclusion, and excluded normal build output
+- `PackProjectReferencedSourceGenerators` — default `true`; packable projects automatically include analyzer `ProjectReference` outputs and runtime dependencies under `analyzers/dotnet/cs/`. Set it to `false` globally or use `Pack="false"` on one analyzer reference to opt out.
+- `EnableAssemblyNameGeneration` — default `false`; when `true`, `AssemblyName` and default `PackageId` follow the logical project name
+- `DisableProjectFileNamingConventionCheck` — default `false`; disables the directory-name/file-name match validation
+- `DisableGenerateAssemblyInfoClass` — default `false`; disables generated `AssemblyInfo`
+- `DisableAutoInternalsVisibleTo` — default `false`; disables automatic friend assembly generation
+- `AutoIncludeUsings` — default `true`; controls SDK-added global usings
+- `SourceLinkPackageName` — default `Microsoft.SourceLink.GitHub`
+- `DisableSourceLink` — default `false`
+
+## Telemetry and package-related settings
+
+- `ExcludePurviewTelemetry` — default `false`; removes `Purview.Telemetry.SourceGenerator`
+- `ExcludeMSTelemetryExtension` — default `false`; removes `Microsoft.Extensions.Telemetry.Abstractions`, only relevant if `ExcludePurviewTelemetry` is also `true`
+- `IsPackable` — defaults to `false` if not set elsewhere
+- `PackageTags`, `IncludeSource`, `IncludeSymbols`, `PublishRepositoryUrl`, `SymbolPackageFormat` — standard pack-related settings the SDK participates in for packable projects
+
+## Test framework settings
+
+The SDK supports opinionated testing defaults and validation.
+
+Primary settings:
+
+- `TestingFramework` — default `TUnit`; supported values: `TUnit`, `Xunit`, `None`
+- `SubstituteFramework` — default `TUnitMocks`; supported values: `TUnitMocks`, `NSubstitute`, `None`
+- `TestDataFramework` — default `Bogus`; supported values: `Bogus`, `None`
+
+Related toggles and derived settings:
+
+- `CollectCoverage` — defaults to `true` for detected test projects
+- `EnableStaticNativeInstrumentation` — defaults to `false` for test projects
+- `EnableDynamicNativeInstrumentation` — defaults to `false` for test projects
+- `TestingPlatformDotnetTestSupport`, `UseMicrosoftTestingPlatformRunner`, `EnableMicrosoftTestingPlatform` — enabled automatically for TUnit test projects
+
+## Repo bootstrap and developer-experience settings
+
+These settings control the SDK’s repo-level helper file bootstrapping:
+
+- `DisableAutoCopySdkFiles` — default `false`; master switch for SDK-managed repo file copying
+- `BootstrapEditorConfigToRepoRoot` — default `true`
+- `RepositoryEditorConfigFilePath` — optional override for the destination `.editorconfig`
+- `BootstrapGlobalJsonToRepoRoot` — default `true`
+- `RepositoryGlobalJsonFilePath` — optional override for the destination `global.json`
+- `PurviewDotNetProjectSdkVersionForGlobalJson` — defaults to detected SDK package version, fallback `1.0.0`
+- `PurviewAutoSdkPack` — default `true`; when `true`, automatically packs the `Sdk/` folder contents into the NuGet package with the correct root-level paths
+- `EnableAgentFolderInPackage` — default `true`; copies the bundled `.agents/**` folder from the SDK NuGet package into the consuming repo’s `.agents/`
+- `AgentPackDestinationFolder` — default `.agents`; repo-relative destination folder that receives copied agent content as `$(AgentPackDestinationFolder)/**`
+
+**Hard requirement:** This SDK must pack the contents of `Sdk/` into the NuGet package so that downstream consumers of `Purview.DotNetProjectSdk` receive the same `Sdk/**` files. The `PurviewAutoSdkPack` feature (default `true`) is the mechanism that delivers this for standard consuming projects. When a project is packable, the SDK automatically adds `Sdk/**/*` as package content with the correct root-level paths:
+
+- `Sdk/.agents/**` → `.agents/**`
+- `Sdk/.github/**` → `.github/**`
+- `Sdk/build/**` → `build/**`
+- `Sdk/buildTransitive/**` → `buildTransitive/**`
+- `Sdk/buildMultiTargeting/**` → `buildMultiTargeting/**`
+- `Sdk/*.md`, `Sdk/*.png`, `Sdk/*.jpg`, etc. → package root
+- everything else under `Sdk/` → `Sdk/`
+
+The SDK injects a `.gitignore` file into each second-level folder under `Sdk/.agents` during packaging with the following content:
+
+```text[.gitignore]
+# Ignore all files
+*
+
+# Don't ignore directories, so Git can traverse them
+!*/
+
+# Keep this file
+!.gitignore
+```
+
+This lets consuming repos keep the agent folder structure discoverable while ignoring the copied content in Git.
+
+## Important derived properties you can inspect
+
+When explaining SDK behavior, prefer these derived values over guessing:
+
+- `PurviewLogicalProjectName`
+- `PurviewNamespacePrefix`
+- `PurviewProjectShortName`
+- `PurviewTestType`
+- `RootNamespace`
+- `AssemblyName`
+- `PackageVersion`
+- `TestingType`
+- `TargetProjectName`
+- `RepoRoot`
+- `RootPackageJson`
+
+## Compiler-visible properties
+
+The SDK exports many properties for analyzers and source generators through `build_property.`. When authoring analyzers or generators, prefer those exported properties instead of re-deriving SDK behavior manually.
+
+Especially relevant exported properties include:
+
+- `UsePackageJsonVersion`, `RootPackageJson`, `RepoRoot`, `Version`, `PackageVersion`
+- `NamespacePrefix`, `DisableNamespacePrefixCheck`
+- `TestingFramework`, `SubstituteFramework`, `TestDataFramework`
+- `ExcludePurviewTelemetry`, `ExcludeMSTelemetryExtension`
+- `EnableAssemblyNameGeneration`, `DisableAutoInternalsVisibleTo`, `DisableGenerateAssemblyInfoClass`
+- `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsSharedProject`
+- `TestingType`, `TargetProjectName`
+- `IsContainerProject`, `IsSdkProject`, `SdkProjectName`, `IsWebProject`, `IsWebSdkProject`, `IsWorkerSdkProject`, `IsAspireHostProject`, `IsCLIProject`
+- `EditorConfigFilePath`, `RepositoryEditorConfigFilePath`, `BootstrapEditorConfigToRepoRoot`
+- `RepositoryGlobalJsonFilePath`, `BootstrapGlobalJsonToRepoRoot`, `DisableAutoCopySdkFiles`
+- `PurviewDotNetProjectSdkVersionForGlobalJson`, `CurrentYear`, `AutoGeneratedAssemblyInfoFile`
+
+## Guidance for edits
+
+When changing SDK configuration:
+
+1. Preserve existing defaults unless the task explicitly changes product behavior.
+2. Keep README, SDK property declarations, validation, and any shipped skills aligned.
+3. If you add a new user-facing property, update both the configuration docs and the bundled skills.
+4. If the property affects import-time behavior, document that it must be set before the SDK import.
diff --git a/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-project-behavior-and-detection/SKILL.md b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-project-behavior-and-detection/SKILL.md
new file mode 100644
index 0000000..8da51c3
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-project-behavior-and-detection/SKILL.md
@@ -0,0 +1,183 @@
+---
+name: sdk-project-behavior-and-detection
+description: "Use when explaining why Purview.DotNetProjectSdk classified a project as test, shared, CLI, web, Aspire host, or container, or when reasoning about auto-added packages, project references, namespaces, and naming conventions."
+---
+
+# Purview.DotNetProjectSdk project behavior and detection
+
+Use this skill when a task asks **why** the SDK applied a behavior automatically, or when adding/moving projects in a repo that relies on Purview’s naming and project-type inference.
+
+## Project-type detection rules
+
+The SDK infers behavior from project names, project contents, and SDK declarations.
+
+### Test detection
+
+A project is treated as a test project when its name ends with `*Test` or `*Tests` and the suffix before `Test(s)` matches a supported testing type such as:
+
+- `Unit`
+- `Integration`
+- `E2E`
+- `EndToEnd`
+- `Acceptance`
+- `Functional`
+- `Performance`
+- `Load`
+- `Smoke`
+- `Stress`
+- `Regression`
+- `Security`
+- `Chaos`
+- `Scenario`
+- `System`
+- `Threat`
+- `BlackBox`
+- `WhiteBox`
+- `Accessibility`
+- `Interactive`
+- `Environment`
+- `Architecture`
+- `Contract`
+
+Derived properties:
+
+- `IsTestProject=true`
+- `TestingType=`
+- `PurviewTestType=Tests`
+- `TargetProjectName=`
+
+### Shared project detection
+
+The SDK recognizes shared project names exactly. These are not generic substring matches.
+
+Shared project names:
+
+- `Shared`
+- `SharedFramework`
+- `SharedInfrastructure`
+- `SharedInfra`
+- `SharedUtilities`
+- `SharedUtils`
+- `SharedLibrary`
+- `SharedLib`
+- `SharedHelpers`
+
+Shared testing project names:
+
+- `SharedTestingFramework`
+- `SharedTestingInfrastructure`
+- `SharedTestingInfra`
+- `SharedTestingUtilities`
+- `SharedTestingUtils`
+- `SharedTestingLibrary`
+- `SharedTestingLib`
+- `SharedTestingHelpers`
+
+Derived flags:
+
+- `IsSharedProject`
+- `IsSharedTestingProject`
+
+### SDK/content-based detection
+
+- `IsSdkProject` / `SdkProjectName` come from parsing the project/import `Sdk="..."` declaration
+- `IsWebSdkProject=true` for `Microsoft.NET.Sdk.Web`
+- `IsWorkerSdkProject=true` for `Microsoft.NET.Sdk.Worker`
+- `IsAspireHostProject=true` when the SDK starts with `Aspire.Sdk.Host` or `Aspire.AppHost.Sdk`
+- `IsContainerProject=true` when `Dockerfile`, `dockerfile`, or `Dockerfile.dev` exists in the project directory
+- `IsCLIProject=true` when the project name ends with `CLI`, `Console`, `CommandLine`, `QuickStart`, or `QuickStarts`
+
+## Namespace and identity behavior
+
+The SDK derives the project identity from `NamespacePrefix` and the project name.
+
+Key behavior:
+
+1. `PurviewLogicalProjectName` is built from `NamespacePrefix` plus the project name, with deduplication when the project name already starts with the namespace tail.
+2. `RootNamespace` defaults to `PurviewLogicalProjectName`.
+3. Known suffixes are stripped from `RootNamespace`, including shared/shared-testing names and common segments like `Core`, `EF`, `Shared`, `ClientShared`, and `ServiceDefaults`.
+4. Test suffixes are removed from `RootNamespace`, so `Acme.Api.UnitTests` still maps back to `Acme.Api`.
+
+Do not hand-author alternate namespace conventions unless the repository explicitly opts out of the SDK defaults.
+
+## Automatic project references
+
+The SDK adds project references based on layout conventions.
+
+### Non-test projects
+
+For ordinary non-test, non-shared projects, it automatically looks for sibling shared projects:
+
+- `../Shared*/Shared*.csproj`
+
+It also removes accidental self/shared-testing matches.
+
+### Test projects
+
+For detected test projects, it attempts these target-project paths in order when they exist:
+
+- `../$(TargetProjectName)/$(TargetProjectName).csproj`
+- `../../$(TargetProjectName)/$(TargetProjectName).csproj`
+- `../src/$(TargetProjectName)/$(TargetProjectName).csproj`
+- `../../src/$(TargetProjectName)/$(TargetProjectName).csproj`
+
+It also adds sibling shared-testing project references via:
+
+- `../SharedTesting*/SharedTesting*.csproj`
+
+This is why consistent naming and placement matter so much in repos that use the SDK.
+
+## Automatic framework/package behavior
+
+### For non-test C# projects
+
+- Adds SourceLink unless `DisableSourceLink=true`
+- Adds Purview telemetry packages unless `ExcludePurviewTelemetry=true`
+- Generates documentation files for non-test, non-shared-testing library projects
+- Generates `InternalsVisibleTo` attributes unless `DisableAutoInternalsVisibleTo=true`
+
+### For test and shared-testing projects
+
+- Applies test-friendly `NoWarn` defaults
+- Marks projects as not packable/publishable
+- Adds substitute/test-data/testing packages based on `SubstituteFramework`, `TestDataFramework`, and `TestingFramework`
+- For TUnit test projects, enables Microsoft.Testing.Platform integration properties automatically
+- For shared-testing projects, skips the runnable test package and marks them with a skip/category pattern appropriate to the selected test framework
+
+### For special project types
+
+- CLI projects default to `OutputType=Exe` and include `appsettings*.json` as content
+- Container projects enable `InvariantGlobalization`, `PublishAot`, Linux Docker defaults, and container tooling package references
+- Web SDK projects get `Microsoft.AspNetCore.OpenApi.Generated` added to `InterceptorsNamespaces` unless marked as a separate web-project mode
+- Aspire host projects default to `OutputType=Exe`
+
+## How to reason about surprising behavior
+
+If the SDK “did something unexpected”, inspect these values first:
+
+- `MSBuildProjectName`
+- `NamespacePrefix`
+- `PurviewLogicalProjectName`
+- `RootNamespace`
+- `TestingType`
+- `TargetProjectName`
+- `SdkProjectName`
+- `IsTestProject`
+- `IsSharedProject`
+- `IsSharedTestingProject`
+- `IsContainerProject`
+- `IsCLIProject`
+- `IsWebSdkProject`
+- `IsAspireHostProject`
+
+Prefer explaining behavior from these computed properties rather than from assumptions about folder names alone.
+
+## Guidance for structural changes
+
+When adding or moving projects in a repo using this SDK:
+
+1. Keep the `.csproj` filename equal to its containing directory name unless the repo explicitly disables that validation.
+2. Preserve established `src/` and `tests/`-style layouts whenever possible.
+3. Use test project suffixes intentionally so auto-detection and auto-references work.
+4. Keep shared helpers in exact shared/shared-testing names if you want the corresponding SDK behavior.
+5. If you change a naming rule in the SDK, update the README and the shipped skills together.
diff --git a/src/src/DotNetProjectSdk/Sdk/Props/Defaults.props b/src/src/DotNetProjectSdk/Sdk/Props/Defaults.props
new file mode 100644
index 0000000..bdf85de
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/Props/Defaults.props
@@ -0,0 +1,158 @@
+
+
+ <_SharedProjectNames Condition="'$(_SharedProjectNames)' == ''"
+ >;Shared;SharedFramework;SharedInfrastructure;SharedInfra;SharedUtilities;SharedUtils;SharedLibrary;SharedLib;SharedHelpers;
+ <_SharedTestingProjectNames Condition="'$(_SharedTestingProjectNames)' == ''"
+ >;SharedTestingFramework;SharedTestingInfrastructure;SharedTestingInfra;SharedTestingUtilities;SharedTestingUtils;SharedTestingLibrary;SharedTestingLib;SharedTestingHelpers;
+
+ true
+
+ latest
+ All
+
+ true
+ true
+ true
+ false
+ false
+
+ true
+ false
+ false
+ false
+ false
+ false
+
+
+
+ true
+ true
+ false
+ false
+ true
+ false
+ true
+ false
+ .agents
+
+
+ false
+ false
+ false
+ false
+ false
+ enable
+ false
+ true
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ preview
+ true
+
+
+ enable
+
+ false
+
+ $(Version)
+
+ TUnit
+ TUnitMocks
+ Bogus
+
+
+
+
+ false
+ true
+ 1.0.0
+
+
+
+
+ Microsoft.SourceLink.GitHub
+ snupkg
+
+
+ false
+
+ false
+ 0.0.1
+
+ <_PurviewSdkContainerDirectory
+ Condition="'$(_PurviewSdkContainerDirectory)' == ''"
+ >
+ <_PurviewSdkContainerDirectoryName
+ Condition="'$(_PurviewSdkContainerDirectoryName)' == ''"
+ >
+ <_PackageJsonVersion Condition="'$(_PackageJsonVersion)' == ''">
+ <_RootPackageJsonText Condition="'$(_RootPackageJsonText)' == ''">
+ <_RootPackageJsonWasSpecified Condition="'$(_RootPackageJsonWasSpecified)' == ''"
+ >false
+ <_UsePackageJsonVersionEnabled Condition="'$(_UsePackageJsonVersionEnabled)' == ''"
+ >false
+ <_UsePackageJsonVersionNormalized
+ Condition="'$(_UsePackageJsonVersionNormalized)' == ''"
+ >
+
+
+ true
+ true
+ false
+
+ <_VersionDetectionCacheDirectory
+ Condition="'$(_VersionDetectionCacheDirectory)' == ''"
+ >
+ <_VersionDetectionCacheText Condition="'$(_VersionDetectionCacheText)' == ''">
+ <_CachedRepoRoot Condition="'$(_CachedRepoRoot)' == ''">
+ <_CachedRootPackageJson Condition="'$(_CachedRootPackageJson)' == ''">
+ <_CachedPackageJsonVersion Condition="'$(_CachedPackageJsonVersion)' == ''">
+ <_CachedPackageJsonTicks Condition="'$(_CachedPackageJsonTicks)' == ''">
+ <_CurrentPackageJsonTicks Condition="'$(_CurrentPackageJsonTicks)' == ''">
+ <_VersionDetectionCacheKey Condition="'$(_VersionDetectionCacheKey)' == ''">
+ <_VersionDetectionCacheRoot Condition="'$(_VersionDetectionCacheRoot)' == ''">
+
+
diff --git a/src/src/DotNetProjectSdk/Sdk/Props/VersionDetection.props b/src/src/DotNetProjectSdk/Sdk/Props/VersionDetection.props
new file mode 100644
index 0000000..244e5cc
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/Props/VersionDetection.props
@@ -0,0 +1,85 @@
+
+
+ true
+ <_UsePackageJsonVersionNormalized>$([System.String]::Copy('$(UsePackageJsonVersion)').ToLowerInvariant())
+ <_UsePackageJsonVersionEnabled
+ Condition="'$(_UsePackageJsonVersionNormalized)' == 'true' OR '$(_UsePackageJsonVersionNormalized)' == 'strict'"
+ >true
+ <_RootPackageJsonWasSpecified Condition="'$(RootPackageJson)' != ''">true
+ <_RootPackageJsonWasSpecified Condition="'$(RootPackageJson)' == ''">false
+ <_VersionDetectionCacheRoot Condition="'$(EnableVersionDetectionCache)' == 'true'"
+ >$([System.IO.Path]::Combine('$([System.IO.Path]::GetTempPath())', 'Purview.DotNetProjectSdk', 'VersionDetection'))
+ <_VersionDetectionCacheKey Condition="'$(EnableVersionDetectionCache)' == 'true'"
+ >$([System.Text.RegularExpressions.Regex]::Replace('$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)'))', '[^A-Za-z0-9_.-]', '_'))
+ $([System.IO.Path]::Combine('$(_VersionDetectionCacheRoot)', '$(_VersionDetectionCacheKey).cache'))
+ <_VersionDetectionCacheDirectory Condition="'$(VersionDetectionCacheFile)' != ''"
+ >$([System.IO.Path]::GetDirectoryName('$(VersionDetectionCacheFile)'))
+ <_VersionDetectionCacheText
+ Condition="'$(_UsePackageJsonVersionEnabled)' == 'true' and '$(_RootPackageJsonWasSpecified)' != 'true' and '$(EnableVersionDetectionCache)' == 'true' and '$(VersionDetectionCacheFile)' != '' and Exists('$(VersionDetectionCacheFile)')"
+ >$([System.IO.File]::ReadAllText('$(VersionDetectionCacheFile)'))
+ <_CachedRepoRoot Condition="'$(_VersionDetectionCacheText)' != ''"
+ >$([System.String]::Copy($([System.Text.RegularExpressions.Regex]::Match('$(_VersionDetectionCacheText)', '(?m)^RepoRoot=(.*)$').Groups[1].Value)).Trim())
+ <_CachedRootPackageJson Condition="'$(_VersionDetectionCacheText)' != ''"
+ >$([System.String]::Copy($([System.Text.RegularExpressions.Regex]::Match('$(_VersionDetectionCacheText)', '(?m)^RootPackageJson=(.*)$').Groups[1].Value)).Trim())
+ <_CachedPackageJsonVersion Condition="'$(_VersionDetectionCacheText)' != ''"
+ >$([System.String]::Copy($([System.Text.RegularExpressions.Regex]::Match('$(_VersionDetectionCacheText)', '(?m)^PackageJsonVersion=(.*)$').Groups[1].Value)).Trim())
+ $(_CachedRepoRoot)
+ $(_CachedRootPackageJson)
+ <_PackageJsonVersion
+ Condition="'$(_UsePackageJsonVersionEnabled)' == 'true' and '$(_PackageJsonVersion)' == '' and '$(_CachedPackageJsonVersion)' != '' and '$(RootPackageJson)' != '' and Exists('$(RootPackageJson)')"
+ >$(_CachedPackageJsonVersion)
+
+
+
+ $([MSBuild]::NormalizePath('$(RootPackageJson)'))
+
+
+
+ $([MSBuild]::NormalizePath('$(GITHUB_WORKSPACE)'))
+ $([MSBuild]::NormalizePath('$(BUILD_SOURCESDIRECTORY)'))
+ $([MSBuild]::NormalizePath('$(BUILD_REPOSITORY_LOCALPATH)'))
+ $([MSBuild]::NormalizePath('$(CI_PROJECT_DIR)'))
+ $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', '.git'))
+ $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', '.git/HEAD'))
+ $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', '.git/config'))
+ $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'package.json'))
+
+
+
+ $([MSBuild]::NormalizePath('$(RepoRoot)', 'package.json'))
+
+
+
+ <_RootPackageJsonText>$([System.IO.File]::ReadAllText('$(RootPackageJson)'))
+ <_PackageJsonVersion>$([System.Text.RegularExpressions.Regex]::Match('$(_RootPackageJsonText)','"version"\s*:\s*"([^"]+)"').Groups[1].Value)
+
+
+
+ $(_PackageJsonVersion)
+ $(_PackageJsonVersion)
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/DotNetProjectSdk/Sdk/Sdk.props b/src/src/DotNetProjectSdk/Sdk/Sdk.props
new file mode 100644
index 0000000..9ca5df1
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/Sdk.props
@@ -0,0 +1,777 @@
+
+
+
+
+
+
+ <_SharedProjectNames>;Shared;SharedFramework;SharedInfrastructure;SharedInfra;SharedUtilities;SharedUtils;SharedLibrary;SharedLib;SharedHelpers;
+ <_SharedTestingProjectNames>;SharedTestingFramework;SharedTestingInfrastructure;SharedTestingInfra;SharedTestingUtilities;SharedTestingUtils;SharedTestingLibrary;SharedTestingLib;SharedTestingHelpers;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ false
+ false
+ false
+ false
+ false
+ true
+
+ false
+ false
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ true
+ true
+ <_PurviewProjectDeclaresTargetFramework
+ Condition="$([System.Text.RegularExpressions.Regex]::IsMatch($([System.IO.File]::ReadAllText(`$(MSBuildProjectFullPath)`)), `(?is)<TargetFrameworks?\s*>`))"
+ >true
+
+ $([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText(`$(MSBuildProjectFullPath)`)), `(?s-i)(?:^|\s|>)(?s-i)(?:^|\s|>)<\s*(?:Project|Import)\s(?:[^>]*?)\s?Sdk\s*="(?<sdkproj>.*?)"`).Groups['sdkproj'].Value)
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ $(NamespacePrefix)
+ $(PurviewNamespacePrefix)
+ $(MSBuildProjectName)
+ <_PurviewEscapedNamespacePrefix Condition="'$(PurviewNamespacePrefix)' != ''"
+ >$([System.Text.RegularExpressions.Regex]::Escape('$(PurviewNamespacePrefix)'))
+ <_PurviewNamespacePrefixTailSegment Condition="'$(PurviewNamespacePrefix)' != ''"
+ >$([System.Text.RegularExpressions.Regex]::Match('$(PurviewNamespacePrefix)', '([^\.]+)$').Groups[1].Value)
+ <_PurviewEscapedNamespacePrefixTailSegment Condition="'$(_PurviewNamespacePrefixTailSegment)' != ''"
+ >$([System.Text.RegularExpressions.Regex]::Escape('$(_PurviewNamespacePrefixTailSegment)'))
+ <_PurviewProjectNameWithoutRepeatedTail
+ Condition="'$(PurviewNamespacePrefix)' != '' AND '$(_PurviewEscapedNamespacePrefixTailSegment)' != ''"
+ >$([System.Text.RegularExpressions.Regex]::Replace('$(MSBuildProjectName)', '^$(_PurviewEscapedNamespacePrefixTailSegment)(?=\.|$)\.?', ''))
+ <_PurviewIsSegmentPrefixedProjectName
+ Condition="'$(PurviewNamespacePrefix)' != '' AND $([System.Text.RegularExpressions.Regex]::IsMatch('$(MSBuildProjectName)', '^$(_PurviewEscapedNamespacePrefix)($|\.)'))"
+ >true
+ $(MSBuildProjectName)
+ $(MSBuildProjectName)
+ $(PurviewNamespacePrefix)
+ $(PurviewNamespacePrefix).$(_PurviewProjectNameWithoutRepeatedTail)
+
+ $([System.Text.RegularExpressions.Regex]::Match($(MSBuildProjectName), `^.*\.([\w|_]\w*)Tests?$`).get_Groups().get_Item(1).ToString())
+ $(TestingType)Tests
+ true
+ true
+
+
+
+
+ <_TestingFrameworkNormalized>$([System.String]::Copy('$(TestingFramework)').Trim().ToLowerInvariant())
+
+
+
+ <_SubstituteFrameworkNormalized>$([System.String]::Copy('$(SubstituteFramework)').Trim().ToLowerInvariant())
+
+
+
+ <_TestDataFrameworkNormalized>$([System.String]::Copy('$(TestDataFramework)').Trim().ToLowerInvariant())
+
+
+
+
+
+
+ Absolute project directory path (with trailing separator) used by analyzers for project-relative file calculations.
+
+
+ Effective project root namespace used by analyzers that validate or customize namespace conventions.
+
+
+ Required namespace prefix used to derive RootNamespace for projects.
+
+
+ Disables the build error that requires NamespacePrefix to be set.
+
+
+ Selected testing framework (TUnit, Xunit, or None). Defaults to TUnit.
+
+
+ Selected mocking/substitute framework (TUnitMocks, NSubstitute, or None). Defaults to TUnitMocks.
+
+
+ Selected test data framework (Bogus or None). Defaults to Bogus.
+
+
+ NuGet package ID used for SourceLink integration.
+
+
+ Disables automatic SourceLink integration.
+
+
+ Opt-out flag that removes Purview telemetry source-generator integration.
+
+
+ Opt-out flag that removes Microsoft.Extensions.Telemetry.Abstractions.
+
+
+ When true, copies the bundled .agents folder from the SDK NuGet package into the consuming repository. Defaults to true.
+
+
+ Repo-relative destination folder that receives copied .agents content. Defaults to .agents.
+
+
+ When true, automatically packs the Sdk/ folder contents into the NuGet package with the correct root-level paths. Defaults to true.
+
+
+ Disables generation of the AssemblyInfo helper class in intermediate output.
+
+
+ When true, enables SDK generation of AssemblyName from the logical project name. When false (default), AssemblyName uses the standard .NET behaviour (project name).
+
+
+ Disables automatic InternalsVisibleTo generation for test and shared testing projects.
+
+
+ Controls SDK-added global usings for NamespacePrefix and RootNamespace.
+
+
+ True when the current project is a C# project (*.csproj).
+
+
+ True when the project name matches a supported *Test/*Tests suffix.
+
+
+ True for known shared testing helper projects (SharedTestingFramework, etc.).
+
+
+ Detected test category suffix from project name (Unit, Integration, E2E, etc.).
+
+
+ Inferred non-test project name that a test project targets.
+
+
+ True when a Dockerfile marker indicates container-project defaults should apply.
+
+
+ True when an SDK value is detected from the project/import declaration.
+
+
+ Detected SDK name from the project/import declaration (for example Microsoft.NET.Sdk.Web).
+
+
+ Marker used to distinguish API/web project behaviour in SDK logic.
+
+
+ True when SdkProjectName is Microsoft.NET.Sdk.Web.
+
+
+ True when SdkProjectName is Microsoft.NET.Sdk.Worker.
+
+
+ True when the project SDK starts with Aspire.Sdk.Host.
+
+
+ Path to the SDK-provided .editorconfig used for build-time style enforcement.
+
+
+ Destination path for bootstrapping a physical repo-level .editorconfig when missing.
+
+
+ When true, copies the SDK .editorconfig to RepositoryEditorConfigFilePath if that file does not exist.
+
+
+ Destination path for bootstrapping a physical repo-level global.json when missing.
+
+
+ When true, bootstraps global.json to RepositoryGlobalJsonFilePath if that file does not exist.
+
+
+ When true, disables SDK auto-copy bootstrapping for repository files such as .editorconfig and global.json.
+
+
+ Version written to the msbuild-sdks Purview.DotNetProjectSdk entry when bootstrapping global.json.
+
+
+ Current year value used by generated assembly metadata.
+
+
+ Relative path to the SDK-generated AssemblyInfo source file.
+
+
+ True when the project is a CLI project.
+
+
+ True when the project is a shared project.
+
+
+
+
+
+
+
+ netstandard2.0
+ true
+ true
+ false
+ false
+ symbols.nupkg
+ $(TargetsForTfmSpecificDebugSymbolsInPackage);PackSourceGeneratorSymbols
+ true
+ false
+
+
+
+ net10.0
+
+ true
+
+ <_PurviewPackagedEditorConfigFilePath>$(MSBuildThisFileDirectory).editorconfig
+ $(_PurviewPackagedEditorConfigFilePath)
+
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../../../.editorconfig'))
+ <_PurviewDiscoveredEditorConfigFilePath>$([MSBuild]::GetPathOfFileAbove('.editorconfig', '$(MSBuildProjectDirectory)'))
+ <_PurviewEffectiveEditorConfigFullPath>$([System.IO.Path]::GetFullPath('$(EditorConfigFilePath)'))
+ <_PurviewDiscoveredEditorConfigFullPath Condition="'$(_PurviewDiscoveredEditorConfigFilePath)' != ''"
+ >$([System.IO.Path]::GetFullPath('$(_PurviewDiscoveredEditorConfigFilePath)'))
+
+ false
+ true
+
+ true
+
+ <_PurviewSdkContainerDirectory>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..'))
+ <_PurviewSdkContainerDirectoryName>$([System.IO.Path]::GetFileName('$(_PurviewSdkContainerDirectory)'))
+ $(_PurviewSdkContainerDirectoryName)
+ 1.0.0
+
+ Microsoft.SourceLink.GitHub
+ false
+ false
+ false
+ false
+ false
+ AD0001;$(NoWarn)
+ IDE2001;$(NoWarn)
+ CA1014;CA1848;CA2007;CA2201;CA2225;CA2254;$(NoWarn)
+ RCS1090;RCS1108;$(NoWarn)
+ $([System.DateTime]::Now.Year)
+ 0.0.1
+ $(Version)
+
+ $(IntermediateOutputPath)/Properties/GeneratedAssemblyInfo.cs
+
+
+
+ Exe
+
+ $(NoWarn);CA1515;
+
+
+
+ Exe
+
+ $(NoWarn);CA1515;
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PurviewSharedProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_SharedProjectNames)', '^;|;$', '').Replace(';', '|'))
+ <_PurviewSharedTestingProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_SharedTestingProjectNames)', '^;|;$', '').Replace(';', '|'))
+ <_PurviewNamespaceRemovePattern>$(_PurviewSharedProjectRemovePattern)|$(_PurviewSharedTestingProjectRemovePattern)|Core|EF|Shared|ClientShared|ServiceDefaults
+ $(PurviewLogicalProjectName)
+ $([System.Text.RegularExpressions.Regex]::Replace('$(RootNamespace)', '\.(?:$(_PurviewNamespaceRemovePattern))(?=\.|$)', ''))
+ $([System.Text.RegularExpressions.Regex]::Replace($(RootNamespace), `[.]$(TestingType)Tests?$$`, ``))
+
+
+
+
+ $([System.Text.RegularExpressions.Regex]::Replace($(MSBuildProjectName), `[.]$(TestingType)Tests?$$`, ``))
+
+
+
+
+
+
+
+
+
+ <_Parameter1>"DynamicProxyGenAssembly2"
+
+
+
+
+
+
+
+
+
+
+
+
+ %(RecursiveDir)%(Filename)%(Extension)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ $(TargetsForTfmSpecificContentInPackage);IncludeLinkedSdkFiles;IncludeSdkDotAgentsGitIgnoreFiles
+
+
+
+
+ true
+ $(TargetsForTfmSpecificContentInPackage);IncludeProjectReferencedSourceGenerators
+
+
+
+
+ .agents/$([System.String]::Copy('%(RecursiveDir)').Replace("\", "/"))%(Filename)%(Extension)
+
+
+ .github/$([System.String]::Copy('%(RecursiveDir)').Replace("\", "/"))%(Filename)%(Extension)
+
+
+ build/$([System.String]::Copy('%(RecursiveDir)').Replace("\", "/"))%(Filename)%(Extension)
+
+
+ buildTransitive/$([System.String]::Copy('%(RecursiveDir)').Replace("\", "/"))%(Filename)%(Extension)
+
+
+ buildMultiTargeting/$([System.String]::Copy('%(RecursiveDir)').Replace("\", "/"))%(Filename)%(Extension)
+
+
+ %(Filename)%(Extension)
+
+
+ Sdk/$([System.String]::Copy('%(RecursiveDir)').Replace("\", "/"))%(Filename)%(Extension)
+
+
+
+
+
+ true
+ true
+ Linux
+ ..\..\
+
+
+
+
+
+
+
+
+ $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated
+
+
+
+
+ true
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.xml
+
+
+
+ $(NoWarn);CS1591;
+
+
+
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+ $(PackageTags);
+ true
+ true
+ true
+ snupkg
+
+
+
+
+
+ all
+ analyzers
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+ $(NoWarn);CA1062;CA1515;CA1707;CA1822;
+ $(NoWarn);CS1591;
+ false
+ false
+ false
+
+
+
+ Exe
+ 0
+ true
+ [NSubstitute*]*,[TUnit.*]*,[xunit.*]*,[Microsoft.Testing.*]*,[Microsoft.NET.Test*]*,[Bogus*]*
+ System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute
+ false
+ false
+
+ false
+ false
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ true
+
+
+
+
+ <_Parameter1>$(TestingType)
+
+
+ <_Parameter1>Skipping Shared Testing Project
+
+
+
+
+
+
+
+
+ <_Parameter1>Category
+ <_Parameter2>$(TestingType)
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/DotNetProjectSdk/Sdk/Sdk.targets b/src/src/DotNetProjectSdk/Sdk/Sdk.targets
new file mode 100644
index 0000000..5898c47
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/Sdk.targets
@@ -0,0 +1,824 @@
+
+
+
+
+ $([System.Text.RegularExpressions.Regex]::Match('$(TargetFrameworks)', '^[^;]+').Value)
+ $(TargetFramework)
+
+
+
+
+ $(IntermediateOutputPath)generated
+
+
+
+
+
+
+
+
+
+
+ GetSourceGeneratorAnalyzerFiles
+
+
+
+
+
+
+
+ <_PurviewSourceGeneratorCompilerProjectReference
+ Include="@(ProjectReference)"
+ Condition="'%(ProjectReference.OutputItemType)' == 'Analyzer' AND '%(ProjectReference.ReferenceOutputAssembly)' == 'false'"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PurviewSourceGeneratorRuntimeDependency Include="@(SourceGeneratorRuntimeDependency)">
+ $(TargetDir)%(SourceGeneratorRuntimeDependency.DestinationSubPath)
+ $(TargetDir)%(SourceGeneratorRuntimeDependency.Filename)%(SourceGeneratorRuntimeDependency.Extension)
+
+
+
+
+
+
+
+
+ <_PurviewSourceGeneratorAnalyzerFile Include="@(TargetPathWithTargetPlatformMoniker)" />
+
+ <_PurviewSourceGeneratorAnalyzerFile
+ Include="$(TargetDir)%(SourceGeneratorRuntimeDependency.DestinationSubPath)"
+ Condition="'@(SourceGeneratorRuntimeDependency)' != '' AND '%(SourceGeneratorRuntimeDependency.DestinationSubPath)' != ''"
+ />
+ <_PurviewSourceGeneratorAnalyzerFile
+ Include="$(TargetDir)%(SourceGeneratorRuntimeDependency.Filename)%(SourceGeneratorRuntimeDependency.Extension)"
+ Condition="'@(SourceGeneratorRuntimeDependency)' != '' AND '%(SourceGeneratorRuntimeDependency.DestinationSubPath)' == ''"
+ />
+
+
+
+
+
+
+
+ <_PurviewSourceGeneratorProjectReference
+ Include="@(ProjectReference)"
+ Condition="'%(ProjectReference.OutputItemType)' == 'Analyzer' AND '%(ProjectReference.ReferenceOutputAssembly)' == 'false' AND '%(ProjectReference.Pack)' != 'false'"
+ />
+
+
+
+
+
+
+
+
+ analyzers/dotnet/cs/
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ();
+ if (Directory.Exists(AgentPackRoot))
+ {
+ foreach (var firstLevelDir in Directory.GetDirectories(AgentPackRoot))
+ {
+ var firstLevelName = Path.GetFileName(firstLevelDir);
+ foreach (var secondLevelDir in Directory.GetDirectories(firstLevelDir))
+ {
+ var secondLevelName = Path.GetFileName(secondLevelDir);
+ var gitIgnorePath = Path.Combine(StagingRoot, firstLevelName, secondLevelName, ".gitignore");
+ Directory.CreateDirectory(Path.GetDirectoryName(gitIgnorePath));
+ File.WriteAllText(gitIgnorePath, GitIgnoreContent);
+ var item = new TaskItem(gitIgnorePath);
+ item.SetMetadata("PackagePath", ".agents/" + firstLevelName + "/" + secondLevelName + "/.gitignore");
+ files.Add(item);
+ }
+ }
+ }
+ GitIgnoreFiles = files.ToArray();
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ();
+ if (File.Exists(ProjectAssetsFile))
+ {
+ var roots = (NuGetPackageFolders ?? string.Empty)
+ .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(root => root.TrimEnd('\\', '/'))
+ .Where(root => root.Length > 0)
+ .ToArray();
+ var excludeFullPath = string.IsNullOrEmpty(ExcludeFolder) ? null : Path.GetFullPath(ExcludeFolder);
+
+ // project.assets.json "libraries" entries are consistently emitted by NuGet as
+ // "/": { "sha512": "...", "type": "package", "path": "/", "files": [...] }
+ var json = File.ReadAllText(ProjectAssetsFile);
+ var libraryPattern = new Regex(
+ "\"(?[^\"/]+)/[^\"]+\"\\s*:\\s*\\{[^{}]*?\"type\"\\s*:\\s*\"package\"[^{}]*?\"path\"\\s*:\\s*\"(?[^\"]+)\"",
+ RegexOptions.Singleline
+ );
+ foreach (Match match in libraryPattern.Matches(json))
+ {
+ var libraryPath = match.Groups["path"].Value;
+ if (string.IsNullOrEmpty(libraryPath))
+ continue;
+
+ var normalizedPath = libraryPath.Replace('/', Path.DirectorySeparatorChar);
+ foreach (var root in roots)
+ {
+ var candidate = Path.GetFullPath(Path.Combine(root, normalizedPath, ".agents"));
+ if (!Directory.Exists(candidate))
+ continue;
+
+ if (excludeFullPath != null && string.Equals(candidate, excludeFullPath, StringComparison.OrdinalIgnoreCase))
+ break;
+
+ var item = new TaskItem(candidate);
+ item.SetMetadata("PackageId", match.Groups["id"].Value);
+ results.Add(item);
+ break;
+ }
+ }
+ }
+ AgentFolders = results.ToArray();
+ ]]>
+
+
+
+
+
+
+ $(PurviewLogicalProjectName)
+ <_PurviewDefaultPackageId>$(AssemblyName)
+ <_PurviewDefaultPackageId Condition="'$(_PurviewNamespaceRemovePattern)' != ''"
+ >$([System.Text.RegularExpressions.Regex]::Replace('$(_PurviewDefaultPackageId)', '\.(?:$(_PurviewNamespaceRemovePattern))(?=\.|$)', ''))
+ $(_PurviewDefaultPackageId)
+
+
+
+
+
+ <_NsRemovePattern>@(NamespaceRemoveSuffix, '|')
+ <_FixedRootNamespace>$([System.Text.RegularExpressions.Regex]::Replace('$(RootNamespace)', '\.(?:$(_NsRemovePattern))(?=\.|$)', ''))
+
+
+
+
+
+
+ $(_FixedRootNamespace)
+
+
+
+
+
+
+ <_DirectoryBuildPropsDirectory>$([System.IO.Path]::GetDirectoryName('$(DirectoryBuildPropsPath)'))
+
+
+
+
+
+ <_GitRepositoryRoot>$([System.String]::Copy('$(_GitRepositoryRoot)').Trim())
+ <_RepositoryProbe0>$(_DirectoryBuildPropsDirectory)
+ <_RepositoryProbe1>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe0)\..'))
+ <_RepositoryProbe2>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe1)\..'))
+ <_RepositoryProbe3>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe2)\..'))
+ <_RepositoryProbe4>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe3)\..'))
+ <_DetectedRepositoryRoot Condition="'$(_GitRepositoryRoot)' != '' AND Exists('$(_GitRepositoryRoot)')"
+ >$(_GitRepositoryRoot)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe0)/.git/HEAD') OR Exists('$(_RepositoryProbe0)/.git'))"
+ >$(_RepositoryProbe0)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe1)/.git/HEAD') OR Exists('$(_RepositoryProbe1)/.git'))"
+ >$(_RepositoryProbe1)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe2)/.git/HEAD') OR Exists('$(_RepositoryProbe2)/.git'))"
+ >$(_RepositoryProbe2)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe3)/.git/HEAD') OR Exists('$(_RepositoryProbe3)/.git'))"
+ >$(_RepositoryProbe3)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe4)/.git/HEAD') OR Exists('$(_RepositoryProbe4)/.git'))"
+ >$(_RepositoryProbe4)
+ <_EffectiveRepositoryEditorConfigFilePath>$(RepositoryEditorConfigFilePath)
+ <_EffectiveRepositoryEditorConfigFilePath
+ Condition="'$(_EffectiveRepositoryEditorConfigFilePath)' == '' AND '$(_DetectedRepositoryRoot)' != ''"
+ >$([System.IO.Path]::Combine('$(_DetectedRepositoryRoot)', '.editorconfig'))
+ <_EffectiveRepositoryEditorConfigFilePath
+ Condition="'$(_EffectiveRepositoryEditorConfigFilePath)' == '' AND '$(_DirectoryBuildPropsDirectory)' != ''"
+ >$([System.IO.Path]::Combine('$(_DirectoryBuildPropsDirectory)', '.editorconfig'))
+ <_RepositoryEditorConfigDirectory>$([System.IO.Path]::GetDirectoryName('$(_EffectiveRepositoryEditorConfigFilePath)'))
+
+
+ $(_EffectiveRepositoryEditorConfigFilePath)
+
+
+
+
+
+
+
+ <_PurviewAgentSourceRoot Include="@(SourceRoot -> WithMetadataValue('SourceControl', 'git'))" />
+
+
+ <_PurviewAgentFolderRepoRoot Condition="'$(RepoRoot)' != ''">$(RepoRoot)
+ <_PurviewAgentFolderAgentsMd Condition="'$(_PurviewAgentFolderRepoRoot)' == ''"
+ >$([MSBuild]::GetPathOfFileAbove('AGENTS.md', '$(MSBuildProjectDirectory)'))
+ <_PurviewAgentFolderRepoRoot
+ Condition="'$(_PurviewAgentFolderRepoRoot)' == '' AND '$(_PurviewAgentFolderAgentsMd)' != ''"
+ >$([System.IO.Path]::GetDirectoryName('$(_PurviewAgentFolderAgentsMd)'))\
+ <_PurviewAgentFolderRepoRoot Condition="'$(_PurviewAgentFolderRepoRoot)' == ''"
+ >@(_PurviewAgentSourceRoot)
+ <_PurviewAgentFolderDestinationRoot Condition="'$(_PurviewAgentFolderRepoRoot)' != ''"
+ >$([MSBuild]::NormalizePath('$(_PurviewAgentFolderRepoRoot)', '$(AgentPackDestinationFolder)'))
+
+
+
+
+
+ <_PurviewAgentFolderFiles Include="$(MSBuildThisFileDirectory)..\.agents\**\*" />
+ <_PurviewAgentFolderDirectories Include="@(_PurviewAgentFolderFiles -> '$(_PurviewAgentFolderDestinationRoot)\%(RecursiveDir)')" />
+
+
+
+
+
+
+
+
+
+
+
+ <_PurviewPackagedAgentFolderFiles Include="%(_PurviewPackagedAgentFolder.Identity)\**\*" />
+
+
+ <_PurviewPackagedAgentFolderDirectories Include="@(_PurviewPackagedAgentFolderFiles -> '$(_PurviewAgentFolderDestinationRoot)\%(RecursiveDir)')" />
+
+
+
+
+
+
+
+ <_PurviewLinkedSdkFile
+ Include="@(None)"
+ Condition="$([System.String]::Copy('%(None.Link)').StartsWith('Sdk\')) OR $([System.String]::Copy('%(None.Link)').StartsWith('Sdk/'))"
+ >
+ $([System.String]::Copy('%(None.Link)').Substring(4).Replace('\', '/'))
+
+
+
+
+
+
+
+ <_PurviewSdkDotAgentsRoot>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', 'Sdk', '.agents'))
+ <_PurviewSdkDotAgentsGitIgnoreStagingRoot>$([MSBuild]::NormalizePath('$(IntermediateOutputPath)', '_PurviewSdkDotAgentsGitIgnores'))
+
+
+
+
+
+
+
+ %(PackagePath)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_DirectoryBuildPropsDirectory>$([System.IO.Path]::GetDirectoryName('$(DirectoryBuildPropsPath)'))
+
+
+
+
+
+ <_GitRepositoryRoot>$([System.String]::Copy('$(_GitRepositoryRoot)').Trim())
+ <_RepositoryProbe0>$(_DirectoryBuildPropsDirectory)
+ <_RepositoryProbe1>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe0)\..'))
+ <_RepositoryProbe2>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe1)\..'))
+ <_RepositoryProbe3>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe2)\..'))
+ <_RepositoryProbe4>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe3)\..'))
+ <_DetectedRepositoryRoot Condition="'$(_GitRepositoryRoot)' != '' AND Exists('$(_GitRepositoryRoot)')"
+ >$(_GitRepositoryRoot)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe0)/.git/HEAD') OR Exists('$(_RepositoryProbe0)/.git'))"
+ >$(_RepositoryProbe0)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe1)/.git/HEAD') OR Exists('$(_RepositoryProbe1)/.git'))"
+ >$(_RepositoryProbe1)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe2)/.git/HEAD') OR Exists('$(_RepositoryProbe2)/.git'))"
+ >$(_RepositoryProbe2)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe3)/.git/HEAD') OR Exists('$(_RepositoryProbe3)/.git'))"
+ >$(_RepositoryProbe3)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe4)/.git/HEAD') OR Exists('$(_RepositoryProbe4)/.git'))"
+ >$(_RepositoryProbe4)
+ <_EffectiveRepositoryGlobalJsonFilePath>$(RepositoryGlobalJsonFilePath)
+ <_EffectiveRepositoryGlobalJsonFilePath
+ Condition="'$(_EffectiveRepositoryGlobalJsonFilePath)' == '' AND '$(_DetectedRepositoryRoot)' != ''"
+ >$([System.IO.Path]::Combine('$(_DetectedRepositoryRoot)', 'global.json'))
+ <_EffectiveRepositoryGlobalJsonFilePath
+ Condition="'$(_EffectiveRepositoryGlobalJsonFilePath)' == '' AND '$(_DirectoryBuildPropsDirectory)' != ''"
+ >$([System.IO.Path]::Combine('$(_DirectoryBuildPropsDirectory)', 'global.json'))
+ <_RepositoryGlobalJsonDirectory>$([System.IO.Path]::GetDirectoryName('$(_EffectiveRepositoryGlobalJsonFilePath)'))
+
+
+ $(_EffectiveRepositoryGlobalJsonFilePath)
+
+
+
+ <_RepositoryGlobalJsonLines Include="{" />
+ <_RepositoryGlobalJsonLines Include=" "test": {" />
+ <_RepositoryGlobalJsonLines Include=" "runner": "Microsoft.Testing.Platform"" />
+ <_RepositoryGlobalJsonLines Include=" }," />
+ <_RepositoryGlobalJsonLines Include=" "msbuild-sdks": {" />
+ <_RepositoryGlobalJsonLines Include=" "Purview.DotNetProjectSdk": "$(PurviewDotNetProjectSdkVersionForGlobalJson)"" />
+ <_RepositoryGlobalJsonLines Include=" }" />
+ <_RepositoryGlobalJsonLines Include="}" />
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PurviewProjectDirectoryName>$([System.IO.Path]::GetFileName('$(MSBuildProjectDirectory)'))
+ <_PurviewProjectFileNameWithoutExtension>$([System.IO.Path]::GetFileNameWithoutExtension('$(MSBuildProjectFile)'))
+
+
+
+
+
+
+
+
+
+
+ <_Parameter1>$(AssemblyName).%(TestType.Identity)Tests
+
+
+
+ <_Parameter1>$(TargetProjectName).%(TestType.Identity)Tests
+
+
+
+ <_Parameter1>$(MSBuildProjectName).%(TestType.Identity)Tests
+
+
+
+ <_Parameter1
+ Condition="'$(EnableAssemblyNameGeneration)' == 'true' AND '$(PurviewNamespacePrefix)' != ''"
+ >$(PurviewNamespacePrefix).%(SharedTestingProjectName.Identity)
+ <_Parameter1
+ Condition="'$(EnableAssemblyNameGeneration)' != 'true' OR '$(PurviewNamespacePrefix)' == ''"
+ >%(SharedTestingProjectName.Identity)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/DotNetProjectSdk/Sdk/Targets/VersionDetection.targets b/src/src/DotNetProjectSdk/Sdk/Targets/VersionDetection.targets
new file mode 100644
index 0000000..2a641a4
--- /dev/null
+++ b/src/src/DotNetProjectSdk/Sdk/Targets/VersionDetection.targets
@@ -0,0 +1,67 @@
+
+
+ false
+
+
+
+
+ <_VersionDetectionCacheLine Include="RepoRoot=$(RepoRoot)" />
+ <_VersionDetectionCacheLine Include="RootPackageJson=$(RootPackageJson)" />
+ <_VersionDetectionCacheLine Include="PackageJsonVersion=$(_PackageJsonVersion)" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tests/Analyzers.IntegrationTests/AnalyzerTestInfrastructure.cs b/src/tests/Analyzers.IntegrationTests/AnalyzerTestInfrastructure.cs
new file mode 100644
index 0000000..430be14
--- /dev/null
+++ b/src/tests/Analyzers.IntegrationTests/AnalyzerTestInfrastructure.cs
@@ -0,0 +1,76 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.DotNetProjectSdk.Analyzers;
+
+///
+/// Shared Roslyn test infrastructure for analyzer/suppressor/code-fix integration tests.
+///
+static class AnalyzerTestInfrastructure
+{
+ static readonly string[] TrustedAssemblies = (
+ (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? ""
+ ).Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
+
+ public static ImmutableArray BuildBclReferences() =>
+ [.. TrustedAssemblies.Select(p => MetadataReference.CreateFromFile(p))];
+
+ public static CSharpCompilation CreateCompilation(string source, string filePath)
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(source, path: NormalizeFakePath(filePath));
+ return CSharpCompilation.Create(
+ "TestAssembly",
+ [syntaxTree],
+ BuildBclReferences(),
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ );
+ }
+
+ public static AnalyzerOptions CreateAnalyzerOptions(
+ string projectDir = @"C:\FakeProject\",
+ string rootNamespace = "An.Example.Project"
+ )
+ {
+ var values = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["build_property.ProjectDir"] = NormalizeFakePath(projectDir),
+ ["build_property.RootNamespace"] = rootNamespace,
+ };
+
+ InMemoryAnalyzerConfigOptions options = new(values);
+ InMemoryAnalyzerConfigOptionsProvider provider = new(options);
+ return new AnalyzerOptions([], provider);
+ }
+
+ ///
+ /// Converts a Windows-style fake path literal (e.g. C:\FakeProject\Extensions\System\Foo.cs)
+ /// into an absolute path using the current platform's directory separators. The
+ /// ExtensionsNamespaceHelper performs relative-path math with platform path APIs,
+ /// so tests must feed it native-format paths on every OS.
+ ///
+ public static string NormalizeFakePath(string windowsPath)
+ {
+ var slashed = windowsPath.Replace('\\', '/');
+
+ if (slashed.Length >= 2 && slashed[1] == ':')
+ slashed = slashed[2..];
+
+ return Path.Combine(Path.GetTempPath(), slashed.TrimStart('/'));
+ }
+
+ sealed class InMemoryAnalyzerConfigOptions(Dictionary values) : AnalyzerConfigOptions
+ {
+ public override bool TryGetValue(string key, out string value) => values.TryGetValue(key, out value!);
+ }
+
+ sealed class InMemoryAnalyzerConfigOptionsProvider(AnalyzerConfigOptions options) : AnalyzerConfigOptionsProvider
+ {
+ public override AnalyzerConfigOptions GlobalOptions => options;
+
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => options;
+
+ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => options;
+ }
+}
diff --git a/src/tests/Analyzers.IntegrationTests/Analyzers.IntegrationTests.csproj b/src/tests/Analyzers.IntegrationTests/Analyzers.IntegrationTests.csproj
new file mode 100644
index 0000000..42c902b
--- /dev/null
+++ b/src/tests/Analyzers.IntegrationTests/Analyzers.IntegrationTests.csproj
@@ -0,0 +1,14 @@
+
+
+ $(NoWarn);RS1038;RS1041;RS1036;RS2008
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceAnalyzerTests.cs b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceAnalyzerTests.cs
new file mode 100644
index 0000000..26de70d
--- /dev/null
+++ b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceAnalyzerTests.cs
@@ -0,0 +1,179 @@
+using System.Collections.Immutable;
+using System.Globalization;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+
+namespace Purview.DotNetProjectSdk.Analyzers.Extensions;
+
+///
+/// Integration tests for using Roslyn's analyzer
+/// testing harness with TUnit.
+///
+[Category("Integration")]
+public sealed class ExtensionsNamespaceAnalyzerTests
+{
+ static async Task> AnalyzeAsync(
+ string filePath,
+ string source,
+ CancellationToken cancellationToken
+ )
+ {
+ var compilation = AnalyzerTestInfrastructure.CreateCompilation(source, filePath);
+ var analyzers = ImmutableArray.Create(new ExtensionsNamespaceAnalyzer());
+ var options = AnalyzerTestInfrastructure.CreateAnalyzerOptions();
+ var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, options);
+ return await compilationWithAnalyzers.GetAllDiagnosticsAsync(cancellationToken);
+ }
+
+ [Test]
+ public async Task Analyzer_NoDiagnostic_WhenNamespaceMatchesExtensionsFolder(CancellationToken cancellationToken)
+ {
+ const string source = """
+ namespace System
+ {
+ public static class StringExtensions { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeAsync(
+ @"C:\FakeProject\Extensions\System\StringExtensions.cs",
+ source,
+ cancellationToken
+ );
+
+ Diagnostic[] matches = [.. diagnostics.Where(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId)];
+ await Assert.That(matches.Length).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task Analyzer_RaisesDiagnostic_WhenNamespaceDoesNotMatchExtensionsFolder(
+ CancellationToken cancellationToken
+ )
+ {
+ const string source = """
+ namespace An.Example.Project.Extensions.System
+ {
+ public static class StringExtensions { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeAsync(
+ @"C:\FakeProject\Extensions\System\StringExtensions.cs",
+ source,
+ cancellationToken
+ );
+
+ Diagnostic[] matches = [.. diagnostics.Where(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId)];
+ await Assert.That(matches).HasSingleItem();
+ await Assert
+ .That(matches[0].GetMessage(CultureInfo.InvariantCulture))
+ .Contains("An.Example.Project.Extensions.System");
+ await Assert.That(matches[0].GetMessage(CultureInfo.InvariantCulture)).Contains("System");
+ }
+
+ [Test]
+ [Arguments(
+ @"C:\FakeProject\Extensions\Microsoft\Extensions\Configuration\ConfigExt.cs",
+ "An.Example.Project.Microsoft.Extensions.Configuration",
+ "Microsoft.Extensions.Configuration",
+ DisplayName = "Deeply nested path → full path used as namespace"
+ )]
+ [Arguments(
+ @"C:\FakeProject\Extensions\System\StringExtensions.cs",
+ "An.Example.Project.Extensions.System",
+ "System",
+ DisplayName = "Single level → folder name only"
+ )]
+ public async Task Analyzer_RaisesDiagnostic_WithCorrectExpectedNamespace(
+ string fileName,
+ string wrongNamespace,
+ string expectedNamespace,
+ CancellationToken cancellationToken
+ )
+ {
+ var source = $$"""
+ namespace {{wrongNamespace}}
+ {
+ public static class Ext { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeAsync(fileName, source, cancellationToken);
+
+ Diagnostic[] matches = [.. diagnostics.Where(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId)];
+ await Assert.That(matches).HasSingleItem();
+ await Assert.That(matches[0].GetMessage(CultureInfo.InvariantCulture)).Contains(wrongNamespace);
+ await Assert.That(matches[0].GetMessage(CultureInfo.InvariantCulture)).Contains(expectedNamespace);
+ }
+
+ [Test]
+ public async Task Analyzer_NoDiagnostic_ForFileAtProjectRoot(CancellationToken cancellationToken)
+ {
+ const string source = """
+ namespace An.Example.Project
+ {
+ class Program { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeAsync(@"C:\FakeProject\Program.cs", source, cancellationToken);
+ Diagnostic[] matches = [.. diagnostics.Where(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId)];
+ await Assert.That(matches.Length).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task Analyzer_NoDiagnostic_ForNestedExtensionsFolder(CancellationToken cancellationToken)
+ {
+ const string source = """
+ namespace An.Example.Project.Services.Extensions
+ {
+ public static class ServiceExtensions { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeAsync(
+ @"C:\FakeProject\Services\Extensions\ServiceExtensions.cs",
+ source,
+ cancellationToken
+ );
+ Diagnostic[] matches = [.. diagnostics.Where(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId)];
+ await Assert.That(matches.Length).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task Analyzer_RaisesDiagnostic_ForFileScopedNamespace(CancellationToken cancellationToken)
+ {
+ const string source = """
+ namespace An.Example.Project.Extensions.System;
+
+ public static class StringExtensions { }
+ """;
+
+ var diagnostics = await AnalyzeAsync(
+ @"C:\FakeProject\Extensions\System\StringExtensions.cs",
+ source,
+ cancellationToken
+ );
+
+ Diagnostic[] matches = [.. diagnostics.Where(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId)];
+ await Assert.That(matches).HasSingleItem();
+ await Assert
+ .That(matches[0].GetMessage(CultureInfo.InvariantCulture))
+ .Contains("An.Example.Project.Extensions.System");
+ await Assert.That(matches[0].GetMessage(CultureInfo.InvariantCulture)).Contains("System");
+ }
+
+ [Test]
+ public async Task Analyzer_NoDiagnostic_ForTopLevelStatements(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using System;
+ Console.WriteLine("hello");
+ """;
+
+ var diagnostics = await AnalyzeAsync(@"C:\FakeProject\Extensions\System\Program.cs", source, cancellationToken);
+ Diagnostic[] matches = [.. diagnostics.Where(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId)];
+ await Assert.That(matches.Length).IsEqualTo(0);
+ }
+}
diff --git a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceCodeFixTests.cs b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceCodeFixTests.cs
new file mode 100644
index 0000000..c0ca69b
--- /dev/null
+++ b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceCodeFixTests.cs
@@ -0,0 +1,214 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+using Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+using Purview.DotNetProjectSdk.CodeFixers.ExtensionsNamespace;
+
+namespace Purview.DotNetProjectSdk.Analyzers.IntegrationTests.Extensions;
+
+///
+/// Integration tests for .
+///
+[Category("Integration")]
+public sealed class ExtensionsNamespaceCodeFixTests
+{
+ static async Task ApplyCodeFixAsync(string fileName, string source, CancellationToken cancellationToken)
+ {
+ var (workspace, document) = CreateWorkspaceAndDocument(fileName, source);
+ try
+ {
+ document = AddAnalyzerConfigToDocument(document);
+ var diagnostic = await RunAnalyzersAndGetDiagnosticAsync(document, cancellationToken);
+ var fixedText = await ApplyCodeFixAndGetTextAsync(document, diagnostic, cancellationToken);
+ return fixedText;
+ }
+ finally
+ {
+ workspace.Dispose();
+ }
+ }
+
+ static (AdhocWorkspace workspace, Document document) CreateWorkspaceAndDocument(string fileName, string source)
+ {
+ var workspace = new AdhocWorkspace();
+ var projectId = ProjectId.CreateNewId();
+ var documentId = DocumentId.CreateNewId(projectId);
+
+ var projectInfo = ProjectInfo
+ .Create(
+ projectId,
+ VersionStamp.Create(),
+ "TestProject",
+ "TestProject",
+ LanguageNames.CSharp,
+ parseOptions: CSharpParseOptions.Default,
+ compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ )
+ .WithMetadataReferences(AnalyzerTestInfrastructure.BuildBclReferences());
+
+ var nativeFilePath = AnalyzerTestInfrastructure.NormalizeFakePath(fileName);
+ var solution = workspace
+ .CurrentSolution.AddProject(projectInfo)
+ .AddDocument(
+ documentId,
+ Path.GetFileName(nativeFilePath),
+ SourceText.From(source),
+ filePath: nativeFilePath
+ );
+
+ var document = solution.GetDocument(documentId)!;
+ return (workspace, document);
+ }
+
+ static Document AddAnalyzerConfigToDocument(Document document)
+ {
+ var nativeProjectDir = AnalyzerTestInfrastructure.NormalizeFakePath(@"C:\FakeProject\");
+ var analyzerConfig = $$"""
+ is_root = true
+ [*.cs]
+ build_property.ProjectDir = {{nativeProjectDir}}
+ build_property.RootNamespace = An.Example.Project
+ """;
+
+ var solution = document.Project.Solution.AddAnalyzerConfigDocument(
+ DocumentId.CreateNewId(document.Project.Id),
+ ".editorconfig",
+ SourceText.From(analyzerConfig),
+ filePath: AnalyzerTestInfrastructure.NormalizeFakePath(@"C:\FakeProject\.editorconfig")
+ );
+
+ return solution.GetDocument(document.Id)!;
+ }
+
+ static async Task RunAnalyzersAndGetDiagnosticAsync(
+ Document document,
+ CancellationToken cancellationToken
+ )
+ {
+ var project = document.Project;
+ var compilation = (await project.GetCompilationAsync(cancellationToken))!;
+
+ var analyzers = ImmutableArray.Create(new ExtensionsNamespaceAnalyzer());
+ var diagnostics = await compilation
+ .WithAnalyzers(analyzers, project.AnalyzerOptions)
+ .GetAnalyzerDiagnosticsAsync(cancellationToken);
+
+ return diagnostics.Single(d => d.Id == ExtensionsNamespaceAnalyzer.DiagnosticId);
+ }
+
+ static async Task ApplyCodeFixAndGetTextAsync(
+ Document document,
+ Diagnostic diagnostic,
+ CancellationToken cancellationToken
+ )
+ {
+ var provider = new ExtensionsNamespaceCodeFixProvider();
+ var actions = new List();
+
+ var context = new CodeFixContext(
+ document,
+ diagnostic,
+ (action, _) => actions.Add(action),
+ CancellationToken.None
+ );
+
+ await provider.RegisterCodeFixesAsync(context);
+ var actionToApply = actions.Single();
+ var operations = await actionToApply.GetOperationsAsync(CancellationToken.None);
+
+ var applyChanges = operations.OfType().Single();
+ var fixedDocument = applyChanges.ChangedSolution.GetDocument(document.Id)!;
+ var fixedText = (await fixedDocument.GetTextAsync(cancellationToken))!;
+ return fixedText.ToString();
+ }
+
+ [Test]
+ public async Task CodeFixProvider_IsExported_ForVisualStudioDiscovery(CancellationToken cancellationToken)
+ {
+ _ = cancellationToken;
+
+ await Assert.That(typeof(ExtensionsNamespaceAnalyzer).IsPublic).IsTrue();
+ await Assert.That(typeof(ExtensionsNamespaceSuppressor).IsPublic).IsTrue();
+ await Assert.That(typeof(ExtensionsNamespaceCodeFixProvider).IsPublic).IsTrue();
+
+ var attributes = Attribute.GetCustomAttributes(typeof(ExtensionsNamespaceCodeFixProvider), inherit: false);
+ await Assert.That(attributes.OfType().Any()).IsTrue();
+ await Assert.That(attributes.OfType().Any()).IsTrue();
+ }
+
+ [Test]
+ public async Task FixableDiagnosticIds_ContainsExtensionsNamespaceDiagnosticId(CancellationToken cancellationToken)
+ {
+ _ = cancellationToken;
+
+ var provider = new ExtensionsNamespaceCodeFixProvider();
+
+ await Assert.That(provider.FixableDiagnosticIds).Contains(ExtensionsNamespaceAnalyzer.DiagnosticId);
+ }
+
+ [Test]
+ [Arguments(
+ @"C:\FakeProject\Extensions\System\StringExtensions.cs",
+ "An.Example.Project.Extensions.System",
+ "System",
+ DisplayName = "Block namespace → corrected to System"
+ )]
+ [Arguments(
+ @"C:\FakeProject\Extensions\Microsoft\Extensions\Configuration\ConfigExt.cs",
+ "An.Example.Project.Microsoft.Extensions.Configuration",
+ "Microsoft.Extensions.Configuration",
+ DisplayName = "Deeply nested → corrected to Microsoft.Extensions.Configuration"
+ )]
+ public async Task CodeFix_ReplacesNamespace_WithFolderDerivedNamespace(
+ string fileName,
+ string wrongNamespace,
+ string correctNamespace,
+ CancellationToken cancellationToken
+ )
+ {
+ var before = $$"""
+ namespace {{wrongNamespace}}
+ {
+ public static class Ext { }
+ }
+ """;
+
+ var after = $$"""
+ namespace {{correctNamespace}}
+ {
+ public static class Ext { }
+ }
+ """;
+
+ var fixedSource = await ApplyCodeFixAsync(fileName, before, cancellationToken);
+ await Assert.That(fixedSource).IsEqualTo(after);
+ }
+
+ [Test]
+ public async Task CodeFix_CorrectlyHandles_FileScopedNamespace(CancellationToken cancellationToken)
+ {
+ const string before = """
+ namespace An.Example.Project.Extensions.System;
+
+ public static class StringExtensions { }
+ """;
+
+ const string after = """
+ namespace System;
+
+ public static class StringExtensions { }
+ """;
+
+ var fixedSource = await ApplyCodeFixAsync(
+ @"C:\FakeProject\Extensions\System\StringExtensions.cs",
+ before,
+ cancellationToken
+ );
+ await Assert.That(fixedSource).IsEqualTo(after);
+ }
+}
diff --git a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceSuppressorTests.cs b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceSuppressorTests.cs
new file mode 100644
index 0000000..fdd395c
--- /dev/null
+++ b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceSuppressorTests.cs
@@ -0,0 +1,107 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+
+namespace Purview.DotNetProjectSdk.Analyzers.IntegrationTests.Extensions;
+
+///
+/// Integration tests for behavior.
+///
+[Category("Integration")]
+public sealed class ExtensionsNamespaceSuppressorTests
+{
+ static async Task> AnalyzeAsync(
+ string filePath,
+ string source,
+ CancellationToken cancellationToken
+ )
+ {
+ var compilation = AnalyzerTestInfrastructure.CreateCompilation(source, filePath);
+
+ var analyzers = ImmutableArray.Create(
+ new FakeIde0130Analyzer(),
+ new ExtensionsNamespaceSuppressor()
+ );
+ var analysisOptions = AnalyzerTestInfrastructure.CreateAnalyzerOptions();
+
+ var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, analysisOptions);
+ return await compilationWithAnalyzers.GetAllDiagnosticsAsync(cancellationToken);
+ }
+
+ [Test]
+ public async Task Suppressor_SuppressesIde0130_ForFileUnderExtensionsRoot(CancellationToken cancellationToken)
+ {
+ const string source = """
+ namespace System
+ {
+ public static class StringExtensions { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeAsync(
+ @"C:\FakeProject\Extensions\System\StringExtensions.cs",
+ source,
+ cancellationToken
+ );
+
+ Diagnostic[] ide0130Diagnostics = [.. diagnostics.Where(d => d.Id == "IDE0130")];
+ await Assert.That(ide0130Diagnostics.Length).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task Suppressor_DoesNotSuppressIde0130_ForFileOutsideExtensionsRoot(
+ CancellationToken cancellationToken
+ )
+ {
+ const string source = """
+ namespace Wrong.Namespace
+ {
+ public class MyService { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeAsync(@"C:\FakeProject\Services\MyService.cs", source, cancellationToken);
+
+ Diagnostic[] ide0130Diagnostics = [.. diagnostics.Where(d => d.Id == "IDE0130")];
+ await Assert.That(ide0130Diagnostics).HasSingleItem();
+ await Assert.That(ide0130Diagnostics[0].IsSuppressed).IsFalse();
+ }
+
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ sealed class FakeIde0130Analyzer : DiagnosticAnalyzer
+ {
+ static readonly DiagnosticDescriptor Rule = new(
+ "IDE0130",
+ "Namespace does not match folder structure",
+ "Namespace does not match folder structure",
+ "Style",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public override ImmutableArray SupportedDiagnostics => [Rule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(
+ AnalyzeNamespace,
+ Microsoft.CodeAnalysis.CSharp.SyntaxKind.NamespaceDeclaration
+ );
+ }
+
+ static void AnalyzeNamespace(SyntaxNodeAnalysisContext context)
+ {
+ if (
+ context.Node is not Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax namespaceDeclaration
+ )
+ {
+ return;
+ }
+
+ context.ReportDiagnostic(Diagnostic.Create(Rule, namespaceDeclaration.Name.GetLocation()));
+ }
+ }
+}
diff --git a/src/tests/Analyzers.IntegrationTests/TargetTypedObjectCreation/TargetTypedObjectCreationTests.cs b/src/tests/Analyzers.IntegrationTests/TargetTypedObjectCreation/TargetTypedObjectCreationTests.cs
new file mode 100644
index 0000000..bce42f0
--- /dev/null
+++ b/src/tests/Analyzers.IntegrationTests/TargetTypedObjectCreation/TargetTypedObjectCreationTests.cs
@@ -0,0 +1,110 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+using Purview.DotNetProjectSdk.CodeFixers.TargetTypedObjectCreation;
+
+namespace Purview.DotNetProjectSdk.Analyzers.TargetTypedObjectCreation;
+
+public sealed class TargetTypedObjectCreationTests
+{
+ [Test]
+ public async Task Analyzer_MethodCallResult_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = "var value = Factory.Create();";
+ var document = CreateDocument(source);
+
+ // Act
+ var diagnostics = await GetDiagnosticsAsync(document, cancellationToken);
+
+ // Assert
+ await Assert.That(diagnostics).IsEmpty();
+ }
+
+ [Test]
+ public async Task Analyzer_ObjectCreationWithVar_ReportsDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange
+ const string source = "var value = new Widget();";
+ var document = CreateDocument(source);
+
+ // Act
+ var diagnostics = await GetDiagnosticsAsync(document, cancellationToken);
+
+ // Assert
+ await Assert.That(diagnostics).Count().IsEqualTo(1);
+ await Assert.That(diagnostics[0].Id).IsEqualTo(TargetTypedObjectCreationAnalyzer.DiagnosticId);
+ }
+
+ [Test]
+ public async Task CodeFixProvider_Exposes_TargetTypedObjectCreationDiagnosticId(CancellationToken cancellationToken)
+ {
+ _ = cancellationToken;
+
+ var provider = new TargetTypedObjectCreationCodeFixProvider();
+
+ await Assert.That(provider.FixableDiagnosticIds).Contains(TargetTypedObjectCreationAnalyzer.DiagnosticId);
+ }
+
+ [Test]
+ public async Task CodeFix_ObjectCreationWithVar_UsesExplicitTypeAndTargetTypedNew(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange
+ const string source = "var value = new Widget();";
+ var document = CreateDocument(source);
+ var diagnostic = (await GetDiagnosticsAsync(document, cancellationToken)).Single();
+ var provider = new TargetTypedObjectCreationCodeFixProvider();
+ var actions = new List();
+ var context = new CodeFixContext(document, diagnostic, (action, _) => actions.Add(action), cancellationToken);
+
+ // Act
+ await provider.RegisterCodeFixesAsync(context);
+ var operations = await actions.Single().GetOperationsAsync(cancellationToken);
+ var changedDocument = ((ApplyChangesOperation)operations.Single()).ChangedSolution.GetDocument(document.Id)!;
+ var fixedSource = (await changedDocument.GetTextAsync(cancellationToken)).ToString();
+
+ // Assert
+ await Assert.That(fixedSource).IsEqualTo("Widget value = new();");
+ }
+
+ static Document CreateDocument(string source)
+ {
+ using var workspace = new AdhocWorkspace();
+ var projectId = ProjectId.CreateNewId();
+ var documentId = DocumentId.CreateNewId(projectId);
+ var project = ProjectInfo
+ .Create(
+ projectId,
+ VersionStamp.Create(),
+ "TestProject",
+ "TestProject",
+ LanguageNames.CSharp,
+ parseOptions: CSharpParseOptions.Default,
+ compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ )
+ .WithMetadataReferences(AnalyzerTestInfrastructure.BuildBclReferences());
+
+ return workspace
+ .CurrentSolution.AddProject(project)
+ .AddDocument(documentId, "Test.cs", SourceText.From(source), filePath: "Test.cs")
+ .GetDocument(documentId)!;
+ }
+
+ static async Task> GetDiagnosticsAsync(
+ Document document,
+ CancellationToken cancellationToken
+ )
+ {
+ var compilation = (await document.Project.GetCompilationAsync(cancellationToken))!;
+ var analyzers = ImmutableArray.Create(new TargetTypedObjectCreationAnalyzer());
+ return await compilation
+ .WithAnalyzers(analyzers, document.Project.AnalyzerOptions)
+ .GetAnalyzerDiagnosticsAsync(cancellationToken);
+ }
+}
diff --git a/src/tests/Analyzers.UnitTests/Analyzers.UnitTests.csproj b/src/tests/Analyzers.UnitTests/Analyzers.UnitTests.csproj
new file mode 100644
index 0000000..d637c1a
--- /dev/null
+++ b/src/tests/Analyzers.UnitTests/Analyzers.UnitTests.csproj
@@ -0,0 +1,9 @@
+
+
+ $(NoWarn);RS1038;RS1041;RS1036;RS2008
+
+
+
+
+
+
diff --git a/tests/DotNetProjectSdk.Analyzers.UnitTests/Tests/EditorBrowsableSuppressorTests.cs b/src/tests/Analyzers.UnitTests/EditorBrowsable/EditorBrowsableSuppressorTests.cs
similarity index 69%
rename from tests/DotNetProjectSdk.Analyzers.UnitTests/Tests/EditorBrowsableSuppressorTests.cs
rename to src/tests/Analyzers.UnitTests/EditorBrowsable/EditorBrowsableSuppressorTests.cs
index 00e1151..551c101 100644
--- a/tests/DotNetProjectSdk.Analyzers.UnitTests/Tests/EditorBrowsableSuppressorTests.cs
+++ b/src/tests/Analyzers.UnitTests/EditorBrowsable/EditorBrowsableSuppressorTests.cs
@@ -3,7 +3,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
-namespace Purview.DotNetProjectSdk.Analyzers.Tests;
+namespace Purview.DotNetProjectSdk.Analyzers.EditorBrowsable;
///
/// Unit-tests for using the Roslyn compilation API
@@ -20,6 +20,16 @@ public sealed class EditorBrowsableSuppressorTests
static ImmutableArray BuildBclReferences() =>
[.. TrustedAssemblies.Select(p => MetadataReference.CreateFromFile(p))];
+ static ImmutableArray BuildBclReferencesWithoutEditorBrowsable() =>
+ [
+ .. TrustedAssemblies
+ .Where(p =>
+ !Path.GetFileNameWithoutExtension(p)
+ .StartsWith("System.ComponentModel", StringComparison.OrdinalIgnoreCase)
+ )
+ .Select(p => MetadataReference.CreateFromFile(p)),
+ ];
+
static async Task> AnalyzeAsync(string source, CancellationToken cancellationToken)
{
var parseOptions = CSharpParseOptions.Default.WithDocumentationMode(DocumentationMode.Diagnose);
@@ -40,6 +50,29 @@ static async Task> AnalyzeAsync(string source, Cancel
return await compilationWithAnalyzers.GetAllDiagnosticsAsync(cancellationToken);
}
+ static async Task> AnalyzeWithoutEditorBrowsableReferenceAsync(
+ string source,
+ CancellationToken cancellationToken
+ )
+ {
+ var parseOptions = CSharpParseOptions.Default.WithDocumentationMode(DocumentationMode.Diagnose);
+
+ var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions, cancellationToken: cancellationToken);
+
+ var compilation = CSharpCompilation.Create(
+ "TestAssembly",
+ [syntaxTree],
+ BuildBclReferencesWithoutEditorBrowsable(),
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ );
+
+ var suppressors = ImmutableArray.Create(new EditorBrowsableSuppressor());
+
+ var compilationWithAnalyzers = compilation.WithAnalyzers(suppressors);
+
+ return await compilationWithAnalyzers.GetAllDiagnosticsAsync(cancellationToken);
+ }
+
///
/// A public method decorated with [EditorBrowsable(Never)] should have its CS1591
/// (missing XML documentation) suppressed.
@@ -119,6 +152,31 @@ public void VisibleMethod() { }
await Assert.That(d.IsSuppressed).IsFalse();
}
+ ///
+ /// When the compilation does not reference System.ComponentModel, the
+ /// EditorBrowsableAttribute type cannot be resolved, so the suppressor must be a
+ /// no-op and leave CS1591 unsuppressed.
+ ///
+ [Test]
+ public async Task DoesNotSuppress_WhenEditorBrowsableAttribute_IsNotReferenced(CancellationToken cancellationToken)
+ {
+ const string source = """
+ namespace MyLib;
+
+ public class MyClass
+ {
+ public void PublicMethod() { }
+ }
+ """;
+
+ var diagnostics = await AnalyzeWithoutEditorBrowsableReferenceAsync(source, cancellationToken);
+ var cs1591 = diagnostics.Where(d => d.Id == "CS1591").ToArray();
+
+ await Assert.That(cs1591).IsNotEmpty();
+ foreach (var d in cs1591)
+ await Assert.That(d.IsSuppressed).IsFalse();
+ }
+
///
/// Verifies the suppressor's SuppressionDescriptor ID and suppressed diagnostic ID are correct.
///
diff --git a/src/tests/Analyzers.UnitTests/ExtensionsNamespace/NamespaceCalculatorTests.cs b/src/tests/Analyzers.UnitTests/ExtensionsNamespace/NamespaceCalculatorTests.cs
new file mode 100644
index 0000000..b1fcc44
--- /dev/null
+++ b/src/tests/Analyzers.UnitTests/ExtensionsNamespace/NamespaceCalculatorTests.cs
@@ -0,0 +1,108 @@
+namespace Purview.DotNetProjectSdk.Analyzers.ExtensionsNamespace;
+
+///
+/// Unit tests for namespace derivation logic.
+///
+[Category("Unit")]
+public sealed class NamespaceCalculatorTests
+{
+ [Test]
+ [Arguments(
+ @"C:\repo\MyProject\",
+ @"C:\repo\MyProject\Extensions\System\StringExtensions.cs",
+ "System",
+ DisplayName = "Single level under Extensions → namespace matches folder name"
+ )]
+ [Arguments(
+ @"C:\repo\MyProject\",
+ @"C:\repo\MyProject\Extensions\Microsoft\Extensions\Configuration\ConfigExt.cs",
+ "Microsoft.Extensions.Configuration",
+ DisplayName = "Nested Extensions sub-folder → full path becomes namespace"
+ )]
+ [Arguments(
+ @"C:\repo\MyProject\",
+ @"C:\repo\MyProject\Extensions\MyLib\Foo\Bar.cs",
+ "MyLib.Foo",
+ DisplayName = "Multi-level folder → dot-separated namespace"
+ )]
+ [Arguments(
+ @"C:\repo\MyProject\",
+ @"C:\repo\MyProject\Extensions\TopLevel.cs",
+ "",
+ DisplayName = "File directly in Extensions → global namespace (empty string)"
+ )]
+ [Arguments(
+ @"C:\repo\MyProject\",
+ @"C:\repo\MyProject\Program.cs",
+ null,
+ DisplayName = "Root file → not in Extensions scope"
+ )]
+ [Arguments(
+ @"C:\repo\MyProject\",
+ @"C:\repo\MyProject\Services\Extensions\Foo.cs",
+ null,
+ DisplayName = "Nested Extensions folder → not in scope"
+ )]
+ public async Task ComputeExpectedNamespace_ReturnsCorrectResult(
+ string projectDir,
+ string filePath,
+ string? expectedNamespace
+ )
+ {
+ var result = ExtensionsNamespaceHelper.ComputeExpectedNamespace(
+ NormalizeFakePath(projectDir),
+ NormalizeFakePath(filePath)
+ );
+ await Assert.That(result).IsEqualTo(expectedNamespace);
+ }
+
+ [Test]
+ [Arguments(
+ @"C:\proj\",
+ @"C:\proj\Extensions\A\Foo.cs",
+ true,
+ DisplayName = "Nested file under root Extensions is in scope"
+ )]
+ [Arguments(
+ @"C:\proj\",
+ @"C:\proj\Extensions\Foo.cs",
+ true,
+ DisplayName = "Top-level file under root Extensions is in scope"
+ )]
+ [Arguments(@"C:\proj\", @"C:\proj\Program.cs", false, DisplayName = "Project root file is out of scope")]
+ [Arguments(
+ @"C:\proj\",
+ @"C:\proj\Services\Extensions\Foo.cs",
+ false,
+ DisplayName = "Nested Extensions folder is out of scope"
+ )]
+ [Arguments(
+ @"C:\proj\",
+ @"C:\proj\Extensionsfoo\A\Foo.cs",
+ false,
+ DisplayName = "Partial segment match is out of scope"
+ )]
+ public async Task IsInExtensionsRootScope_ReturnsCorrectResult(string projectDir, string filePath, bool expected)
+ {
+ var result = ExtensionsNamespaceHelper.IsInExtensionsRootScope(
+ NormalizeFakePath(projectDir),
+ NormalizeFakePath(filePath)
+ );
+ await Assert.That(result).IsEqualTo(expected);
+ }
+
+ ///
+ /// Converts a Windows-style fake path literal (e.g. C:\repo\MyProject\) into an
+ /// absolute path using the current platform's directory separators. The helper performs
+ /// relative-path math with platform path APIs, so tests must feed it native-format paths.
+ ///
+ static string NormalizeFakePath(string windowsPath)
+ {
+ var slashed = windowsPath.Replace('\\', '/');
+
+ if (slashed.Length >= 2 && slashed[1] == ':')
+ slashed = slashed[2..];
+
+ return Path.Combine(Path.GetTempPath(), slashed.TrimStart('/'));
+ }
+}
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/AgentPackFolderTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/AgentPackFolderTests.cs
new file mode 100644
index 0000000..53858ba
--- /dev/null
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/AgentPackFolderTests.cs
@@ -0,0 +1,426 @@
+using System.IO.Compression;
+using System.Text.Json;
+using Purview.DotNetProjectSdk.Harness;
+using Purview.DotNetProjectSdk.Infra;
+
+namespace Purview.DotNetProjectSdk;
+
+///
+/// Verifies the Sdk/.agents folder packaging workflow and PurviewAutoSdkPack behaviour.
+///
+public sealed class AgentPackFolderTests
+{
+ [Test]
+ public async Task PurviewAutoSdkPack_PacksPhysicalAndLinkedRootAssets(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "PackableProject",
+ extraProps: """
+
+ net8.0;net9.0
+ true
+ true
+ true
+ README.md
+ LICENSE.md
+ purview-logo.jpg
+ """,
+ extraItems: """
+
+
+ """,
+ cancellationToken: cancellationToken
+ );
+
+ Directory.CreateDirectory(Path.Combine(h.ProjectDirectory, "Sdk"));
+ await File.WriteAllTextAsync(
+ Path.Combine(h.ProjectDirectory, "Sdk", "README.md"),
+ "# Package",
+ cancellationToken
+ );
+ await File.WriteAllTextAsync(Path.Combine(h.SolutionDirectory, "LICENSE.md"), "License", cancellationToken);
+ await File.WriteAllBytesAsync(
+ Path.Combine(h.SolutionDirectory, "purview-logo.jpg"),
+ [0xFF, 0xD8, 0xFF, 0xD9],
+ cancellationToken
+ );
+
+ var feedDirectory = Path.Combine(h.SolutionDirectory, "feed");
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ var (exitCode, stdOut, stdErr) = await RunProcessAsync(
+ "dotnet",
+ $"pack \"{h.ProjectFilePath}\" -c Release -o \"{feedDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ h.SolutionDirectory,
+ cancellationToken
+ );
+
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+ await Assert.That(stdOut + stdErr).DoesNotContain("NU5118");
+ var packagePath = Directory.GetFiles(feedDirectory, "PackableProject.*.nupkg").Single();
+ using var package = await ZipFile.OpenReadAsync(packagePath, cancellationToken);
+ var entries = package.Entries.Select(entry => entry.FullName).ToList();
+ await Assert.That(entries).Contains("README.md");
+ await Assert.That(entries).Contains("LICENSE.md");
+ await Assert.That(entries).Contains("purview-logo.jpg");
+ }
+
+ [Test]
+ public async Task PurviewAutoSdkPack_ExposesSdkDotAgentsInProjectTree(CancellationToken cancellationToken)
+ {
+ var sdkProjectPath = Path.GetFullPath(Path.Combine(SdkPaths.SdkDirectory, "..", "DotNetProjectSdk.csproj"));
+
+ var (exitCode, stdOut, stdErr) = await RunProcessAsync(
+ "dotnet",
+ $"msbuild \"{sdkProjectPath}\" -nologo -noconlog -t:PrepareForBuild -getItem:None",
+ Path.GetDirectoryName(sdkProjectPath)!,
+ cancellationToken
+ );
+
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+
+ var jsonStart = stdOut.IndexOf('{', StringComparison.Ordinal);
+ await Assert.That(jsonStart >= 0).IsTrue();
+
+ using var doc = JsonDocument.Parse(stdOut[jsonStart..]);
+ var noneItems = doc.RootElement.GetProperty("Items").GetProperty("None").EnumerateArray();
+ var expectedPath = Path.GetFullPath(
+ Path.Combine(
+ Path.GetDirectoryName(sdkProjectPath)!,
+ "Sdk",
+ ".agents",
+ "skills",
+ "sdk-configuration-reference",
+ "SKILL.md"
+ )
+ );
+
+ var agentEntry = noneItems.FirstOrDefault(item =>
+ string.Equals(item.GetProperty("FullPath").GetString(), expectedPath, StringComparison.OrdinalIgnoreCase)
+ );
+
+ await Assert.That(agentEntry.ValueKind).IsEqualTo(JsonValueKind.Object);
+ await Assert
+ .That(agentEntry.GetProperty("PackagePath").GetString())
+ .IsEqualTo(".agents/skills/sdk-configuration-reference/SKILL.md");
+ }
+
+ [Test]
+ public async Task PurviewAutoSdkPack_PacksSdkDotAgentsSkillsIntoNuGetPackage(CancellationToken cancellationToken)
+ {
+ // Arrange
+ using var h = await ProjectHarness.CreateAsync(
+ "PackableProject",
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
+
+ await File.WriteAllTextAsync(Path.Combine(h.SolutionDirectory, ".git"), string.Empty, cancellationToken);
+ await File.WriteAllTextAsync(
+ Path.Combine(h.SolutionDirectory, "package.json"),
+ /*lang=json,strict*/
+ """{"name": "packable-project", "version": "1.0.0"}""",
+ cancellationToken
+ );
+ await File.WriteAllTextAsync(
+ Path.Combine(h.SolutionDirectory, "Directory.Packages.props"),
+ """
+
+
+ true
+
+
+
+
+
+
+
+ """,
+ cancellationToken
+ );
+
+ var agentPackSkillsDirectory = Path.Combine(h.ProjectDirectory, "Sdk", ".agents", "skills", "observability");
+ Directory.CreateDirectory(agentPackSkillsDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(agentPackSkillsDirectory, "SKILL.md"),
+ "# Observability\n",
+ cancellationToken
+ );
+
+ var feedDirectory = Path.Combine(h.SolutionDirectory, "feed");
+ Directory.CreateDirectory(feedDirectory);
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+
+ // Act
+ var (exitCode, stdOut, stdErr) = await RunProcessAsync(
+ "dotnet",
+ $"pack \"{h.ProjectFilePath}\" -c Release -o \"{feedDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ h.SolutionDirectory,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+
+ var packagePath = Directory
+ .GetFiles(feedDirectory, $"PackableProject.{packageVersion}.nupkg", SearchOption.TopDirectoryOnly)
+ .SingleOrDefault();
+
+ await Assert.That(packagePath).IsNotNull().Because("The packed project package was not created.");
+
+ using var zip = await ZipFile.OpenReadAsync(packagePath!, cancellationToken);
+ var entries = zip.Entries.Select(entry => entry.FullName).ToList();
+ await Assert.That(entries).Contains(".agents/skills/observability/SKILL.md");
+ await Assert
+ .That(entries)
+ .Contains(".agents/skills/observability/.gitignore")
+ .Because($"{stdOut}\n{stdErr}\n--- package entries ---\n{string.Join("\n", entries)}");
+
+ var gitIgnoreEntry = zip.Entries.Single(entry => entry.FullName == ".agents/skills/observability/.gitignore");
+ using var gitIgnoreStream = await gitIgnoreEntry.OpenAsync(cancellationToken);
+ using var reader = new StreamReader(gitIgnoreStream);
+ var gitIgnoreContent = (await reader.ReadToEndAsync(cancellationToken)).ReplaceLineEndings("\n");
+ await Assert
+ .That(gitIgnoreContent)
+ .IsEqualTo(
+ "# Ignore all files\n*\n\n# Don't ignore directories, so Git can traverse them\n!*/\n\n# Keep this file\n!.gitignore"
+ );
+ }
+
+ [Test]
+ public async Task PurviewAutoSdkPack_PacksSdkDotAgentsContentOutsideSkillsIntoNuGetPackage(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange
+ using var h = await ProjectHarness.CreateAsync(
+ "PackableProject",
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
+
+ await File.WriteAllTextAsync(Path.Combine(h.SolutionDirectory, ".git"), string.Empty, cancellationToken);
+ await File.WriteAllTextAsync(
+ Path.Combine(h.SolutionDirectory, "package.json"),
+ /*lang=json,strict*/
+ """{"name": "packable-project", "version": "1.0.0"}""",
+ cancellationToken
+ );
+ await File.WriteAllTextAsync(
+ Path.Combine(h.SolutionDirectory, "Directory.Packages.props"),
+ """
+
+
+ true
+
+
+
+
+
+
+
+ """,
+ cancellationToken
+ );
+
+ var agentPackPromptsDirectory = Path.Combine(h.ProjectDirectory, "Sdk", ".agents", "prompts", "example");
+ Directory.CreateDirectory(agentPackPromptsDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(agentPackPromptsDirectory, "PROMPT.md"),
+ "# Prompt\n",
+ cancellationToken
+ );
+
+ var feedDirectory = Path.Combine(h.SolutionDirectory, "feed");
+ Directory.CreateDirectory(feedDirectory);
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+
+ // Act
+ var (exitCode, stdOut, stdErr) = await RunProcessAsync(
+ "dotnet",
+ $"pack \"{h.ProjectFilePath}\" -c Release -o \"{feedDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ h.SolutionDirectory,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+
+ var packagePath = Directory
+ .GetFiles(feedDirectory, $"PackableProject.{packageVersion}.nupkg", SearchOption.TopDirectoryOnly)
+ .SingleOrDefault();
+
+ await Assert.That(packagePath).IsNotNull().Because("The packed project package was not created.");
+
+ using var zip = await ZipFile.OpenReadAsync(packagePath!, cancellationToken);
+ var entries = zip.Entries.Select(entry => entry.FullName).ToList();
+ await Assert.That(entries).Contains(".agents/prompts/example/PROMPT.md");
+ await Assert
+ .That(entries)
+ .Contains(".agents/prompts/example/.gitignore")
+ .Because($"{stdOut}\n{stdErr}\n--- package entries ---\n{string.Join("\n", entries)}");
+ }
+
+ [Test]
+ public async Task PurviewAutoSdkPack_PacksAllSdkRootFoldersIntoNuGetPackage(CancellationToken cancellationToken)
+ {
+ // Arrange
+ using var h = await ProjectHarness.CreateAsync(
+ "PackableProject",
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
+
+ await File.WriteAllTextAsync(Path.Combine(h.SolutionDirectory, ".git"), string.Empty, cancellationToken);
+ await File.WriteAllTextAsync(
+ Path.Combine(h.SolutionDirectory, "package.json"),
+ /*lang=json,strict*/
+ """{"name": "packable-project", "version": "1.0.0"}""",
+ cancellationToken
+ );
+ await File.WriteAllTextAsync(
+ Path.Combine(h.SolutionDirectory, "Directory.Packages.props"),
+ """
+
+
+ true
+
+
+
+
+
+
+
+ """,
+ cancellationToken
+ );
+
+ var sdkAgentSkillsDirectory = Path.Combine(h.ProjectDirectory, "Sdk", ".agents", "skills", "test");
+ Directory.CreateDirectory(sdkAgentSkillsDirectory);
+ await File.WriteAllTextAsync(Path.Combine(sdkAgentSkillsDirectory, "SKILL.md"), "# Test\n", cancellationToken);
+
+ var sdkAgentAgentsDirectory = Path.Combine(h.ProjectDirectory, "Sdk", ".agents", "agents");
+ Directory.CreateDirectory(sdkAgentAgentsDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(sdkAgentAgentsDirectory, "this-is-a-test-agent.md"),
+ "# Test\n",
+ cancellationToken
+ );
+
+ var sdkAgentPromptsDirectory = Path.Combine(h.ProjectDirectory, "Sdk", ".agents", "prompts");
+ Directory.CreateDirectory(sdkAgentPromptsDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(sdkAgentPromptsDirectory, "this-is-a-test-prompt.md"),
+ "# Test\n",
+ cancellationToken
+ );
+
+ var sdkGitHubDirectory = Path.Combine(h.ProjectDirectory, "Sdk", ".github", "workflows");
+ Directory.CreateDirectory(sdkGitHubDirectory);
+ await File.WriteAllTextAsync(Path.Combine(sdkGitHubDirectory, "ci.yml"), "name: CI\n", cancellationToken);
+
+ var sdkBuildDirectory = Path.Combine(h.ProjectDirectory, "Sdk", "build");
+ Directory.CreateDirectory(sdkBuildDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(sdkBuildDirectory, "Custom.targets"),
+ "\n",
+ cancellationToken
+ );
+
+ var sdkBuildTransitiveDirectory = Path.Combine(h.ProjectDirectory, "Sdk", "buildTransitive");
+ Directory.CreateDirectory(sdkBuildTransitiveDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(sdkBuildTransitiveDirectory, "Custom.props"),
+ "\n",
+ cancellationToken
+ );
+
+ var sdkBuildMultiTargetingDirectory = Path.Combine(h.ProjectDirectory, "Sdk", "buildMultiTargeting");
+ Directory.CreateDirectory(sdkBuildMultiTargetingDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(sdkBuildMultiTargetingDirectory, "Custom.props"),
+ "\n",
+ cancellationToken
+ );
+
+ await File.WriteAllTextAsync(
+ Path.Combine(h.ProjectDirectory, "Sdk", "README.md"),
+ "# README\n",
+ cancellationToken
+ );
+ await File.WriteAllTextAsync(
+ Path.Combine(h.ProjectDirectory, "Sdk", "logo.svg"),
+ "\n",
+ cancellationToken
+ );
+ await File.WriteAllTextAsync(
+ Path.Combine(h.ProjectDirectory, "Sdk", "Custom.props"),
+ "\n",
+ cancellationToken
+ );
+
+ var feedDirectory = Path.Combine(h.SolutionDirectory, "feed");
+ Directory.CreateDirectory(feedDirectory);
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+
+ // Act
+ var (exitCode, stdOut, stdErr) = await RunProcessAsync(
+ "dotnet",
+ $"pack \"{h.ProjectFilePath}\" -c Release -o \"{feedDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ h.SolutionDirectory,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+
+ var packagePath = Directory
+ .GetFiles(feedDirectory, $"PackableProject.{packageVersion}.nupkg", SearchOption.TopDirectoryOnly)
+ .SingleOrDefault();
+
+ await Assert.That(packagePath).IsNotNull().Because("The packed project package was not created.");
+
+ using var zip = await ZipFile.OpenReadAsync(packagePath!, cancellationToken);
+ var entries = zip.Entries.Select(entry => entry.FullName).ToList();
+
+ await Assert.That(entries).Contains(".agents/skills/test/SKILL.md");
+ await Assert.That(entries).Contains(".agents/skills/test/.gitignore");
+ await Assert.That(entries).Contains(".agents/prompts/this-is-a-test-prompt.md");
+ await Assert.That(entries).Contains(".agents/agents/this-is-a-test-agent.md");
+ await Assert.That(entries).Contains(".github/workflows/ci.yml");
+ await Assert.That(entries).Contains("build/Custom.targets");
+ await Assert.That(entries).Contains("buildTransitive/Custom.props");
+ await Assert.That(entries).Contains("buildMultiTargeting/Custom.props");
+ await Assert.That(entries).Contains("README.md");
+ await Assert.That(entries).Contains("logo.svg");
+ await Assert.That(entries).Contains("Sdk/Custom.props");
+ }
+
+ static async Task<(int Code, string StdOut, string StdErr)> RunProcessAsync(
+ string fileName,
+ string arguments,
+ string workingDirectory,
+ CancellationToken cancellationToken
+ )
+ {
+ using var process = new System.Diagnostics.Process
+ {
+ StartInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ },
+ };
+
+ process.Start();
+ var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
+ var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
+ await process.WaitForExitAsync(cancellationToken);
+
+ return (process.ExitCode, await stdoutTask, await stderrTask);
+ }
+}
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/AutoSharedProjectReferencingTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/AutoSharedProjectReferencingTests.cs
new file mode 100644
index 0000000..cfcb5e7
--- /dev/null
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/AutoSharedProjectReferencingTests.cs
@@ -0,0 +1,536 @@
+using Purview.DotNetProjectSdk.Harness;
+using Purview.DotNetProjectSdk.Infra;
+
+namespace Purview.DotNetProjectSdk;
+
+///
+/// Verifies that Shared*.csproj projects are automatically referenced by all non-test
+/// and non-shared projects. This tests the SDK's auto-discovery and auto-referencing
+/// feature that allows projects to transparently depend on shared libraries without
+/// explicit ProjectReference declarations.
+///
+public sealed class AutoSharedProjectReferencingTests
+{
+ [Test]
+ public async Task AspireHostProject_WithSharedProject_HasIsAspireProjectResourceFalse(
+ CancellationToken cancellationToken
+ )
+ {
+ using var sharedHarness = await ProjectHarness
+ .For("Shared")
+ .WithTargetFramework("net10.0")
+ .BuildAsync(cancellationToken);
+
+ using var appHostHarness = await ProjectHarness
+ .For("Acme.AppHost")
+ .WithProjectFileContent(
+ $"""
+
+
+
+ Acme
+ true
+ net10.0
+
+
+
+ """
+ )
+ .WithSolutionDirectory(sharedHarness.SolutionDirectory)
+ .BuildAsync(cancellationToken);
+
+ var projectReferences = await appHostHarness.GetProjectReferencesAsync(cancellationToken);
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+
+ await Assert.That(normalized).Contains("../Shared/Shared.csproj");
+
+ var aspireResourceFlags = await appHostHarness.GetItemMetadataValuesAsync(
+ "ProjectReference",
+ "IsAspireProjectResource",
+ cancellationToken
+ );
+
+ await Assert.That(aspireResourceFlags).Contains("false");
+ }
+
+ [Test]
+ public async Task NonTestProject_AutoReferences_SharedProject_InSiblingDirectory(
+ CancellationToken cancellationToken
+ )
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary",
+ async workDir =>
+ {
+ var sharedDir = Path.Combine(workDir, "Shared");
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, "Shared.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ await Assert.That(normalized).Contains("../Shared/Shared.csproj");
+ }
+ }
+
+ [Test]
+ public async Task NonTestProject_AutoReferences_SharedFramework_Project(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary",
+ async workDir =>
+ {
+ var sharedDir = Path.Combine(workDir, "SharedFramework");
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, "SharedFramework.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ await Assert.That(normalized).Contains("../SharedFramework/SharedFramework.csproj");
+ }
+ }
+
+ [Test]
+ public async Task NonTestProject_AutoReferences_Multiple_SharedProjects(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary",
+ async workDir =>
+ {
+ // Create Shared project
+ var sharedDir = Path.Combine(workDir, "Shared");
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, "Shared.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+
+ // Create SharedUtils project
+ var sharedUtilsDir = Path.Combine(workDir, "SharedUtils");
+ Directory.CreateDirectory(sharedUtilsDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedUtilsDir, "SharedUtils.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ await Assert.That(normalized).Contains("../Shared/Shared.csproj");
+ await Assert.That(normalized).Contains("../SharedUtils/SharedUtils.csproj");
+ }
+ }
+
+ [Test]
+ public async Task NonTestProject_AutoReferences_SharedInfrastructure_Project(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary",
+ async workDir =>
+ {
+ var sharedDir = Path.Combine(workDir, "SharedInfrastructure");
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, "SharedInfrastructure.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ await Assert.That(normalized).Contains("../SharedInfrastructure/SharedInfrastructure.csproj");
+ }
+ }
+
+ [Test]
+ public async Task NonTestProject_DoesNotAutoReference_SharedTestingProject(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary",
+ async workDir =>
+ {
+ var sharedDir = Path.Combine(workDir, "SharedTestingFramework");
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, "SharedTestingFramework.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ // SharedTesting* projects should be excluded from non-test projects
+ await Assert.That(normalized).DoesNotContain("../SharedTestingFramework/SharedTestingFramework.csproj");
+ }
+ }
+
+ [Test]
+ public async Task TestProject_DoesNotAutoReference_SharedProject(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary.UnitTests",
+ async workDir =>
+ {
+ var sharedDir = Path.Combine(workDir, "Shared");
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, "Shared.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ // Test projects should NOT auto-reference regular Shared*.csproj projects
+ await Assert.That(normalized).DoesNotContain("../Shared/Shared.csproj");
+ }
+ }
+
+ [Test]
+ public async Task TestProject_AutoReferences_SharedTestingProject(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary.UnitTests",
+ async workDir =>
+ {
+ var sharedDir = Path.Combine(workDir, "SharedTestingFramework");
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, "SharedTestingFramework.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ // Test projects SHOULD auto-reference SharedTesting* projects
+ await Assert.That(normalized).Contains("../SharedTestingFramework/SharedTestingFramework.csproj");
+ }
+ }
+
+ [Test]
+ public async Task TestProject_AutoReferences_TargetProject_InSiblingDirectory(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary.UnitTests",
+ async workDir =>
+ {
+ var targetDir = Path.Combine(workDir, "MyLibrary");
+ Directory.CreateDirectory(targetDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(targetDir, "MyLibrary.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ // Test projects should auto-reference their target project (MyLibrary in this case)
+ await Assert.That(normalized).Contains("../MyLibrary/MyLibrary.csproj");
+ }
+ }
+
+ [Test]
+ public async Task SharedProject_DoesNotAutoReference_SharedProjects(CancellationToken cancellationToken)
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "Shared",
+ async workDir =>
+ {
+ var anotherSharedDir = Path.Combine(workDir, "SharedUtils");
+ Directory.CreateDirectory(anotherSharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(anotherSharedDir, "SharedUtils.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ // Shared projects should NOT auto-reference other shared projects
+ await Assert.That(normalized).DoesNotContain("../SharedUtils/SharedUtils.csproj");
+ }
+ }
+
+ [Test]
+ [Arguments("SharedLibrary")]
+ [Arguments("SharedLib")]
+ [Arguments("SharedHelpers")]
+ [Arguments("SharedUtilities")]
+ [Arguments("SharedUtils")]
+ [Arguments("SharedInfra")]
+ [Arguments("SharedInfrastructure")]
+ public async Task NonTestProject_AutoReferences_AllWellKnownSharedProjectNames(
+ string sharedProjectName,
+ CancellationToken cancellationToken
+ )
+ {
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "MyLibrary",
+ async workDir =>
+ {
+ var sharedDir = Path.Combine(workDir, sharedProjectName);
+ Directory.CreateDirectory(sharedDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedDir, $"{sharedProjectName}.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ await Assert.That(normalized).Contains($"../{sharedProjectName}/{sharedProjectName}.csproj");
+ }
+ }
+
+ [Test]
+ public async Task SharedTestingProject_DoesNotAutoReference_SharedPrefixedTestProject(
+ CancellationToken cancellationToken
+ )
+ {
+ // Reproduces the cyclic ProjectReference bug (MSB4006): a SharedTestingFramework
+ // project placed next to a Shared.UnitTests project must NOT gain an automatic
+ // ProjectReference to Shared.UnitTests. Shared-testing projects manage their own
+ // explicit ProjectReferences and must never participate in the ../Shared*/Shared*.csproj
+ // library glob, otherwise SharedTestingFramework -> Shared.UnitTests and
+ // Shared.UnitTests -> SharedTestingFramework form a cycle.
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "SharedTestingFramework",
+ async workDir =>
+ {
+ var sharedUnitTestsDir = Path.Combine(workDir, "Shared.UnitTests");
+ Directory.CreateDirectory(sharedUnitTestsDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedUnitTestsDir, "Shared.UnitTests.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ // SharedTestingFramework must NOT auto-reference the Shared.UnitTests sibling
+ // (the ../Shared*/Shared*.csproj glob must not fire for shared-testing projects).
+ await Assert.That(normalized).DoesNotContain("../Shared.UnitTests/Shared.UnitTests.csproj");
+ }
+ }
+
+ [Test]
+ public async Task SharedTestingProject_WithForcedIsTestProjectFalse_DoesNotReferenceSharedTestProject(
+ CancellationToken cancellationToken
+ )
+ {
+ // Mirrors the restore-phase proof from the bug report. During NuGet restore's
+ // _GenerateRestoreProjectPathWalk, IsTestProject is false for SharedTestingFramework
+ // (dynamic test-package detection does not run), so the ../Shared*/Shared*.csproj
+ // glob would otherwise pull in the Shared.UnitTests sibling. Force that condition
+ // with -p:IsTestProject=false and confirm the glob still does not fire.
+ var (harness, _) = await TestHelpers.CreateProjectStructureAsync(
+ "SharedTestingFramework",
+ async workDir =>
+ {
+ var sharedUnitTestsDir = Path.Combine(workDir, "Shared.UnitTests");
+ Directory.CreateDirectory(sharedUnitTestsDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedUnitTestsDir, "Shared.UnitTests.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var references = await harness.GetItemIdentitiesAsync(
+ "ProjectReference",
+ "-p:IsTestProject=false",
+ cancellationToken
+ );
+ var normalized = references.Select(TestHelpers.NormalizePath).ToList();
+ await Assert.That(normalized).DoesNotContain("../Shared.UnitTests/Shared.UnitTests.csproj");
+ }
+ }
+
+ [Test]
+ public async Task SharedPrefixedTestProject_StillAutoReferences_SharedTestingProject(
+ CancellationToken cancellationToken
+ )
+ {
+ // Non-regression: a Shared-prefixed test project (Shared.UnitTests) must still
+ // auto-reference its sibling SharedTestingFramework via the test-project glob,
+ // so the fix removes only the reverse edge of the cycle, not the legitimate one.
+ var (harness, projectReferences) = await TestHelpers.CreateProjectStructureAsync(
+ "Shared.UnitTests",
+ async workDir =>
+ {
+ var sharedTestingDir = Path.Combine(workDir, "SharedTestingFramework");
+ Directory.CreateDirectory(sharedTestingDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(sharedTestingDir, "SharedTestingFramework.csproj"),
+ """
+
+
+ net10.0
+
+
+ """,
+ cancellationToken
+ );
+ },
+ null,
+ cancellationToken
+ );
+
+ using (harness)
+ {
+ var normalized = projectReferences.Select(TestHelpers.NormalizePath).ToList();
+ // Test projects SHOULD auto-reference SharedTesting* projects, even when the
+ // test project itself is Shared-prefixed (Shared.UnitTests).
+ await Assert.That(normalized).Contains("../SharedTestingFramework/SharedTestingFramework.csproj");
+ }
+ }
+}
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/CompilerVisiblePropertyTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/CompilerVisiblePropertyTests.cs
new file mode 100644
index 0000000..708e43e
--- /dev/null
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/CompilerVisiblePropertyTests.cs
@@ -0,0 +1,56 @@
+using Purview.DotNetProjectSdk.Harness;
+
+namespace Purview.DotNetProjectSdk;
+
+///
+/// Verifies SDK properties are exported as CompilerVisibleProperty items so Roslyn
+/// analyzers/source generators can consume them via build_property.*.
+///
+public sealed class CompilerVisiblePropertyTests
+{
+ [Test]
+ public async Task CompilerVisibleProperties_Include_AllSdkProperties(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
+ var compilerVisibleProperties = await h.GetItemIdentitiesAsync("CompilerVisibleProperty", cancellationToken);
+
+ var expected = new[]
+ {
+ "UsePackageJsonVersion",
+ "RootPackageJson",
+ "RepoRoot",
+ "Version",
+ "PackageVersion",
+ "NamespacePrefix",
+ "DisableNamespacePrefixCheck",
+ "TestingFramework",
+ "SubstituteFramework",
+ "TestDataFramework",
+ "SourceLinkPackageName",
+ "ExcludePurviewTelemetry",
+ "ExcludeMSTelemetryExtension",
+ "DisableGenerateAssemblyInfoClass",
+ "EnableAssemblyNameGeneration",
+ "DisableAutoInternalsVisibleTo",
+ "AutoIncludeUsings",
+ "IsCSharpProject",
+ "IsTestProject",
+ "IsSharedTestingProject",
+ "TestingType",
+ "TargetProjectName",
+ "IsContainerProject",
+ "IsSdkProject",
+ "SdkProjectName",
+ "IsWebProject",
+ "IsWebSdkProject",
+ "IsWorkerSdkProject",
+ "IsAspireHostProject",
+ "EditorConfigFilePath",
+ "CurrentYear",
+ "AutoGeneratedAssemblyInfoFile",
+ };
+
+ foreach (var propertyName in expected)
+ await Assert.That(compilerVisibleProperties).Contains(propertyName);
+ }
+}
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/CoreDefaultsTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/CoreDefaultsTests.cs
new file mode 100644
index 0000000..61675c8
--- /dev/null
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/CoreDefaultsTests.cs
@@ -0,0 +1,193 @@
+using Purview.DotNetProjectSdk.Harness;
+
+namespace Purview.DotNetProjectSdk;
+
+///
+/// Verifies the C# compiler defaults injected by Sdk.props — Nullable, ImplicitUsings,
+/// LangVersion, Deterministic, RootNamespace derivation, and CI flag passthrough.
+/// Default-property assertions are batched into a single MSBuild evaluation per test
+/// so the suite stays fast without losing any assertion.
+///
+public sealed class CoreDefaultsTests
+{
+ [Test]
+ public async Task DefaultProject_CSharpCompilerDefaults(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
+
+ var eval = await h.EvaluateAsync(
+ [
+ "Nullable",
+ "ImplicitUsings",
+ "LangVersion",
+ "Deterministic",
+ "ManagePackageVersionsCentrally",
+ "PublishRepositoryUrl",
+ "IncludeSymbols",
+ "SymbolPackageFormat",
+ "AnalysisLevel",
+ "AnalysisMode",
+ "EnableNETAnalyzers",
+ "EnforceCodeStyleInBuild",
+ ],
+ cancellationToken: cancellationToken
+ );
+
+ await Assert.That(eval.Properties["Nullable"]).IsEqualTo("enable").Because("Nullable should default to enable");
+ await Assert
+ .That(eval.Properties["ImplicitUsings"])
+ .IsEqualTo("enable")
+ .Because("ImplicitUsings should default to enable");
+ await Assert
+ .That(eval.Properties["LangVersion"])
+ .IsEqualTo("preview")
+ .Because("LangVersion should default to preview");
+ await Assert
+ .That(eval.Properties["Deterministic"])
+ .IsEqualTo("true")
+ .Because("Deterministic should default to true");
+ await Assert
+ .That(eval.Properties["ManagePackageVersionsCentrally"])
+ .IsEqualTo("true")
+ .Because("ManagePackageVersionsCentrally should default to true");
+ await Assert
+ .That(eval.Properties["PublishRepositoryUrl"])
+ .IsEqualTo("true")
+ .Because("PublishRepositoryUrl should default to true");
+ await Assert
+ .That(eval.Properties["IncludeSymbols"])
+ .IsEqualTo("true")
+ .Because("IncludeSymbols should default to true");
+ await Assert
+ .That(eval.Properties["SymbolPackageFormat"])
+ .IsEqualTo("snupkg")
+ .Because("SymbolPackageFormat should default to snupkg");
+ await Assert
+ .That(eval.Properties["AnalysisLevel"])
+ .IsEqualTo("latest")
+ .Because("AnalysisLevel should default to latest");
+ await Assert
+ .That(eval.Properties["AnalysisMode"])
+ .IsEqualTo("All")
+ .Because("AnalysisMode should default to All");
+ await Assert
+ .That(eval.Properties["EnableNETAnalyzers"])
+ .IsEqualTo("true")
+ .Because("EnableNETAnalyzers should default to true");
+ await Assert
+ .That(eval.Properties["EnforceCodeStyleInBuild"])
+ .IsEqualTo("true")
+ .Because("EnforceCodeStyleInBuild should default to true");
+ }
+
+ [Test]
+ public async Task DefaultProject_EditorConfig_ResolvesSdkEditorConfigAndListsIt(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
+
+ var eval = await h.EvaluateAsync(["EditorConfigFilePath"], ["EditorConfigFiles"], cancellationToken);
+
+ var editorConfigPath = eval.Properties["EditorConfigFilePath"];
+ await Assert.That(string.IsNullOrWhiteSpace(editorConfigPath)).IsFalse();
+ await Assert.That(File.Exists(editorConfigPath)).IsTrue();
+
+ var normalizedEditorConfigPath = Path.GetFullPath(editorConfigPath).TrimEnd('\\', '/');
+ var hasSdkEditorConfig = eval.Items["EditorConfigFiles"]
+ .Any(path => Path.GetFullPath(path).TrimEnd('\\', '/') == normalizedEditorConfigPath);
+
+ await Assert.That(hasSdkEditorConfig).IsTrue();
+ }
+
+ [Test]
+ public async Task WarnOnPackingNonPackableProject_WhenPackableIsFalse_DefaultsToFalse(
+ CancellationToken cancellationToken
+ )
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ extraProps: "false",
+ cancellationToken: cancellationToken
+ );
+ // The SDK sets LangVersion to "preview" unless explicitly overridden.
+ await Assert
+ .That(await h.GetPropertyAsync("WarnOnPackingNonPackableProject", cancellationToken))
+ .IsEqualTo("false");
+ }
+
+ [Test]
+ public async Task RootNamespace_DerivedFromNamespacePrefixAndProjectName(CancellationToken cancellationToken)
+ {
+ // NamespacePrefix=Test, ProjectName=MyLibrary → Test.MyLibrary
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ namespacePrefix: "Test",
+ cancellationToken: cancellationToken
+ );
+ await Assert.That(await h.GetPropertyAsync("RootNamespace", cancellationToken)).IsEqualTo("Test.MyLibrary");
+ }
+
+ [Test]
+ public async Task Ci_PropertySet_WhenEnvironmentVariablePresent(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ extraEnv: new Dictionary { ["CI"] = "true" },
+ cancellationToken: cancellationToken
+ );
+ await Assert.That(await h.GetPropertyAsync("ContinuousIntegrationBuild", cancellationToken)).IsEqualTo("true");
+ }
+
+ [Test]
+ public async Task Ci_PropertyNotSet_WhenEnvironmentVariableAbsent(CancellationToken cancellationToken)
+ {
+ // Ensure CI env var is not set for this test (it may be set in CI environments, so
+ // we override with empty string to simulate a local dev machine).
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ extraEnv: new Dictionary { ["CI"] = "" },
+ cancellationToken: cancellationToken
+ );
+ var value = await h.GetPropertyAsync("ContinuousIntegrationBuild", cancellationToken);
+ // ContinuousIntegrationBuild should be empty (not "true") when CI is not set.
+ await Assert.That(value).IsNotEqualTo("true");
+ }
+
+ [Test]
+ [Arguments("net9.0", "net9.0")]
+ [Arguments("net10.0", "net10.0")]
+ public async Task TargetFramework_Honoured_WhenExplicitlySet(
+ string tfm,
+ string expected,
+ CancellationToken cancellationToken
+ )
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: tfm,
+ cancellationToken: cancellationToken
+ );
+ await Assert.That(await h.GetPropertyAsync("TargetFramework", cancellationToken)).IsEqualTo(expected);
+ }
+
+ [Test]
+ public async Task Nullable_CanBeOverriddenInProjectFile(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ extraProps: "disable",
+ cancellationToken: cancellationToken
+ );
+ await Assert.That(await h.GetPropertyAsync("Nullable", cancellationToken)).IsEqualTo("disable");
+ }
+
+ [Test]
+ public async Task Nullable_CanBeOverriddenInDirectoryBuildProps(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ preImportProps: "disable",
+ cancellationToken: cancellationToken
+ );
+ await Assert.That(await h.GetPropertyAsync("Nullable", cancellationToken)).IsEqualTo("disable");
+ }
+}
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/DefaultsPropsTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/DefaultsPropsTests.cs
new file mode 100644
index 0000000..68f7665
--- /dev/null
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/DefaultsPropsTests.cs
@@ -0,0 +1,101 @@
+using System.Diagnostics;
+
+namespace Purview.DotNetProjectSdk;
+
+public sealed class DefaultsPropsTests
+{
+ [Test]
+ public async Task NonCsprojEvaluation_DoesNotFailOnBooleanConditions(CancellationToken cancellationToken)
+ {
+ var tempRoot = Path.Combine(Path.GetTempPath(), "PurviewSdkTests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempRoot);
+
+ try
+ {
+ var directoryBuildPropsPath = Path.Combine(tempRoot, "Directory.Build.props");
+ var directoryBuildTargetsPath = Path.Combine(tempRoot, "Directory.Build.targets");
+ var testProjectPath = Path.Combine(tempRoot, "restore.proj");
+
+ await File.WriteAllTextAsync(
+ directoryBuildPropsPath,
+ $$"""
+
+
+ Test
+
+
+
+ $(NoWarn);CA1031;CA2234
+
+
+ """,
+ cancellationToken
+ );
+
+ await File.WriteAllTextAsync(
+ directoryBuildTargetsPath,
+ $$"""
+
+
+
+ """,
+ cancellationToken
+ );
+
+ await File.WriteAllTextAsync(
+ testProjectPath,
+ """
+
+
+
+ """,
+ cancellationToken
+ );
+
+ var (exitCode, stdOut, stdErr) = await RunProcessAsync(
+ "dotnet",
+ $"msbuild \"{testProjectPath}\" -nologo -t:NoOp",
+ tempRoot,
+ cancellationToken
+ );
+
+ var output = stdOut + stdErr;
+ await Assert.That(exitCode).IsEqualTo(0);
+ await Assert.That(output).DoesNotContain("MSB4100");
+ }
+ finally
+ {
+ if (Directory.Exists(tempRoot))
+ Directory.Delete(tempRoot, recursive: true);
+ }
+ }
+
+ static async Task<(int Code, string StdOut, string StdErr)> RunProcessAsync(
+ string fileName,
+ string arguments,
+ string workingDirectory,
+ CancellationToken cancellationToken
+ )
+ {
+ using var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ },
+ };
+
+ process.Start();
+ var stdOutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
+ var stdErrTask = process.StandardError.ReadToEndAsync(cancellationToken);
+ await process.WaitForExitAsync(cancellationToken);
+
+ return (process.ExitCode, await stdOutTask, await stdErrTask);
+ }
+}
diff --git a/tests/DotNetProjectSdk.IntegrationTests/DotNetProjectSdk.IntegrationTests.csproj b/src/tests/DotNetProjectSdk.IntegrationTests/DotNetProjectSdk.IntegrationTests.csproj
similarity index 73%
rename from tests/DotNetProjectSdk.IntegrationTests/DotNetProjectSdk.IntegrationTests.csproj
rename to src/tests/DotNetProjectSdk.IntegrationTests/DotNetProjectSdk.IntegrationTests.csproj
index 775a9b2..1dd4300 100644
--- a/tests/DotNetProjectSdk.IntegrationTests/DotNetProjectSdk.IntegrationTests.csproj
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/DotNetProjectSdk.IntegrationTests.csproj
@@ -1,4 +1,4 @@
-
+
+
-
+
diff --git a/templates/global.json b/templates/global.json
index 1cacb02..f0d5f00 100644
--- a/templates/global.json
+++ b/templates/global.json
@@ -1,9 +1,4 @@
{
- "sdk": {
- "version": "10.0.202",
- "rollForward": "latestMinor",
- "allowPrerelease": false
- },
"test": {
"runner": "Microsoft.Testing.Platform"
},
diff --git a/tests/DotNetProjectSdk.Analyzers.UnitTests/DotNetProjectSdk.Analyzers.UnitTests.csproj b/tests/DotNetProjectSdk.Analyzers.UnitTests/DotNetProjectSdk.Analyzers.UnitTests.csproj
deleted file mode 100644
index fd3c087..0000000
--- a/tests/DotNetProjectSdk.Analyzers.UnitTests/DotNetProjectSdk.Analyzers.UnitTests.csproj
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs b/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs
deleted file mode 100644
index accc44a..0000000
--- a/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs
+++ /dev/null
@@ -1,293 +0,0 @@
-using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Text.Json;
-
-namespace Purview.DotNetProjectSdk.Harness;
-
-///
-/// Creates throwaway consumer projects on disk that import the SDK from source,
-/// allowing integration tests to invoke MSBuild and assert on observable build behaviour.
-///
-sealed class ProjectHarness : IAsyncDisposable
-{
- static readonly string TempBase = Path.Combine(Path.GetTempPath(), "PurviewSdkTests");
-
- readonly string _workDir;
-
- IDictionary? _extraEnv;
-
- public string ProjectName { get; }
-
- public string ProjectDirectory { get; }
-
- public string ProjectFilePath { get; }
-
- ProjectHarness(string workDir, string projectName)
- {
- _workDir = workDir;
- ProjectName = projectName;
- ProjectDirectory = Path.Combine(workDir, projectName);
- ProjectFilePath = Path.Combine(ProjectDirectory, $"{projectName}.csproj");
- }
-
- ///
- /// Creates a standard SDK-style consumer project.
- ///
- public static async Task CreateAsync(
- string projectName,
- string sdk = "Microsoft.NET.Sdk",
- string targetFramework = "net10.0",
- string namespacePrefix = "Test",
- bool withDockerfile = false,
- string? extraProps = null,
- string? extraItems = null,
- IDictionary? extraEnv = null,
- CancellationToken cancellationToken = default
- )
- {
- var workDir = Path.Combine(TempBase, Guid.NewGuid().ToString("N"));
- ProjectHarness harness = new(workDir, projectName);
- await harness.WriteBoilerplateAsync(namespacePrefix, cancellationToken);
-
- var propBlock = extraProps is null ? "" : $"\n\t\n\t\t{extraProps}\n\t";
- var itemBlock = extraItems is null ? "" : $"\n\t\n\t\t{extraItems}\n\t";
-
- await File.WriteAllTextAsync(
- harness.ProjectFilePath,
- $"""
-
-
- {targetFramework}
- {propBlock}{itemBlock}
-
- """,
- cancellationToken
- );
-
- if (withDockerfile)
- {
- await File.WriteAllTextAsync(
- Path.Combine(harness.ProjectDirectory, "Dockerfile"),
- "FROM mcr.microsoft.com/dotnet/runtime:10.0",
- cancellationToken
- );
- }
-
- harness._extraEnv = extraEnv;
- return harness;
- }
-
- ///
- /// Creates a consumer project with fully custom file content.
- /// The directory still gets the standard Directory.Build.props/targets bootstrapping.
- ///
- public static async Task CreateWithContentAsync(
- string projectName,
- string projectFileContent,
- string namespacePrefix = "Test",
- CancellationToken cancellationToken = default
- )
- {
- var workDir = Path.Combine(TempBase, Guid.NewGuid().ToString("N"));
- ProjectHarness harness = new(workDir, projectName);
- await harness.WriteBoilerplateAsync(namespacePrefix, cancellationToken);
- await File.WriteAllTextAsync(harness.ProjectFilePath, projectFileContent, cancellationToken);
-
- return harness;
- }
-
- async Task WriteBoilerplateAsync(string namespacePrefix, CancellationToken cancellationToken)
- {
- Directory.CreateDirectory(ProjectDirectory);
-
- await File.WriteAllTextAsync(
- Path.Combine(ProjectDirectory, "Directory.Build.props"),
- $"""
-
-
- {namespacePrefix}
-
-
-
- """,
- cancellationToken
- );
-
- await File.WriteAllTextAsync(
- Path.Combine(ProjectDirectory, "Directory.Build.targets"),
- $"""
-
-
-
- """,
- cancellationToken
- );
-
- await File.WriteAllTextAsync(
- Path.Combine(ProjectDirectory, "Directory.Packages.props"),
- """
-
-
- true
-
-
- """,
- cancellationToken
- );
- }
-
- ///
- /// Evaluates one or more MSBuild properties via dotnet msbuild -getProperty
- /// without triggering a build or package restore.
- ///
- public async Task> GetPropertiesAsync(
- CancellationToken cancellationToken,
- params string[] propertyNames
- )
- {
- if (propertyNames.Length == 0)
- return ImmutableDictionary.Empty;
-
- var propList = string.Join(",", propertyNames);
- var args = $"msbuild \"{ProjectFilePath}\" -nologo -noconlog -getProperty:{propList}";
-
- var (_, stdout, _) = await RunAsync("dotnet", args, cancellationToken);
-
- stdout = stdout.Trim();
-
- // -getProperty outputs plain text for a single property, JSON for multiple.
- var jsonStart = stdout.IndexOf('{', StringComparison.Ordinal);
- if (jsonStart < 0)
- {
- // Single property — plain text value.
- return propertyNames.Length == 1
- ? new Dictionary(StringComparer.OrdinalIgnoreCase) { [propertyNames[0]] = stdout }
- : propertyNames.ToDictionary(p => p, _ => "", StringComparer.OrdinalIgnoreCase);
- }
-
- var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
- try
- {
- using var doc = JsonDocument.Parse(stdout[jsonStart..]);
- if (doc.RootElement.TryGetProperty("Properties", out var propsEl))
- foreach (var prop in propsEl.EnumerateObject())
- result[prop.Name] = prop.Value.GetString() ?? string.Empty;
- }
- catch (JsonException)
- { /* return what we have */
- }
-
- return result;
- }
-
- /// Evaluates a single MSBuild property without building.
- public async Task GetPropertyAsync(string propertyName, CancellationToken cancellationToken)
- {
- var props = await GetPropertiesAsync(cancellationToken, propertyName);
- return props.TryGetValue(propertyName, out var v) ? v : string.Empty;
- }
-
- ///
- /// Evaluates one or more MSBuild items via dotnet msbuild -getItem
- /// without triggering a build or package restore.
- ///
- public async Task> GetItemIdentitiesAsync(
- string itemType,
- CancellationToken cancellationToken = default
- )
- {
- var args = $"msbuild \"{ProjectFilePath}\" -nologo -noconlog -getItem:{itemType}";
- var (_, stdout, _) = await RunAsync("dotnet", args, cancellationToken);
-
- var jsonStart = stdout.Trim().IndexOf('{', StringComparison.Ordinal);
- if (jsonStart < 0)
- return [];
-
- try
- {
- using var doc = JsonDocument.Parse(stdout[jsonStart..]);
- if (
- doc.RootElement.TryGetProperty("Items", out var itemsEl)
- && itemsEl.TryGetProperty(itemType, out var typeEl)
- )
- {
- var ids = new List();
- foreach (var item in typeEl.EnumerateArray())
- {
- if (item.TryGetProperty("Identity", out var id))
- ids.Add(id.GetString() ?? string.Empty);
- }
-
- return ids;
- }
- }
- catch (JsonException)
- { /* fall through */
- }
-
- return [];
- }
-
- ///
- /// Runs a full build of the consumer project.
- /// Pass =true when the project references NuGet packages.
- ///
- public async Task<(bool Success, string Output, string Errors)> BuildAsync(
- bool restore = false,
- CancellationToken cancellationToken = default
- )
- {
- var restoreFlag = restore ? "" : "--no-restore ";
- var args = $"build \"{ProjectFilePath}\" {restoreFlag}-nologo -v:quiet";
- var (code, stdout, stderr) = await RunAsync("dotnet", args, cancellationToken);
- return (code == 0, stdout, stderr);
- }
-
- async Task<(int Code, string StdOut, string StdErr)> RunAsync(
- string fileName,
- string arguments,
- CancellationToken cancellationToken
- )
- {
- using var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- FileName = fileName,
- Arguments = arguments,
- WorkingDirectory = ProjectDirectory,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false,
- CreateNoWindow = true,
- },
- };
-
- if (_extraEnv is not null)
- {
- foreach (var (key, value) in _extraEnv)
- process.StartInfo.Environment[key] = value;
- }
-
- process.Start();
- var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
- var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
-
- await process.WaitForExitAsync(cancellationToken);
-
- return (process.ExitCode, await stdoutTask, await stderrTask);
- }
-
- public async ValueTask DisposeAsync()
- {
- try
- {
- if (Directory.Exists(_workDir))
- Directory.Delete(_workDir, recursive: true);
- }
- catch (IOException)
- {
- // Best-effort cleanup; don't fail tests on leftover temp files.
- }
- }
-}
diff --git a/tests/DotNetProjectSdk.IntegrationTests/Tests/CoreDefaultsTests.cs b/tests/DotNetProjectSdk.IntegrationTests/Tests/CoreDefaultsTests.cs
deleted file mode 100644
index 5229aba..0000000
--- a/tests/DotNetProjectSdk.IntegrationTests/Tests/CoreDefaultsTests.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-using Purview.DotNetProjectSdk.Harness;
-
-namespace Purview.DotNetProjectSdk.Tests;
-
-///
-/// Verifies the C# compiler defaults injected by Sdk.props — Nullable, ImplicitUsings,
-/// LangVersion, Deterministic, RootNamespace derivation, and CI flag passthrough.
-///
-public sealed class CoreDefaultsTests
-{
- [Test]
- public async Task NullableEnabled_ByDefault(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
- await Assert.That(await h.GetPropertyAsync("Nullable", cancellationToken)).IsEqualTo("enable");
- }
-
- [Test]
- public async Task ImplicitUsings_Enabled_ByDefault(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
- await Assert.That(await h.GetPropertyAsync("ImplicitUsings", cancellationToken)).IsEqualTo("enable");
- }
-
- [Test]
- public async Task LangVersion_Preview_ByDefault(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
- // The SDK sets LangVersion to "preview" unless explicitly overridden.
- await Assert.That(await h.GetPropertyAsync("LangVersion", cancellationToken)).IsEqualTo("preview");
- }
-
- [Test]
- public async Task Deterministic_True_ByDefault(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
- await Assert.That(await h.GetPropertyAsync("Deterministic", cancellationToken)).IsEqualTo("true");
- }
-
- [Test]
- public async Task ManagePackageVersionsCentrally_True_ByDefault(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken);
- await Assert
- .That(await h.GetPropertyAsync("ManagePackageVersionsCentrally", cancellationToken))
- .IsEqualTo("true");
- }
-
- [Test]
- public async Task RootNamespace_DerivedFromNamespacePrefixAndProjectName(CancellationToken cancellationToken)
- {
- // NamespacePrefix=Test, ProjectName=MyLibrary → Test.MyLibrary
- await using var h = await ProjectHarness.CreateAsync(
- "MyLibrary",
- namespacePrefix: "Test",
- cancellationToken: cancellationToken
- );
- await Assert.That(await h.GetPropertyAsync("RootNamespace", cancellationToken)).IsEqualTo("Test.MyLibrary");
- }
-
- [Test]
- public async Task RootNamespace_TestSuffixStripped_ForTestProjects(CancellationToken cancellationToken)
- {
- // ProjectName=MyApp.UnitTests, NamespacePrefix=Test → Test.MyApp.UnitTests
- // After FixRootNamespaceTarget strips ".UnitTests" → Test.MyApp
- await using var h = await ProjectHarness.CreateAsync(
- "MyApp.UnitTests",
- namespacePrefix: "Test",
- cancellationToken: cancellationToken
- );
- await Assert.That(await h.GetPropertyAsync("RootNamespace", cancellationToken)).IsEqualTo("Test.MyApp");
- }
-
- [Test]
- public async Task RootNamespace_IntegrationTestsSuffixStripped(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync(
- "MyApp.IntegrationTests",
- namespacePrefix: "Test",
- cancellationToken: cancellationToken
- );
- await Assert.That(await h.GetPropertyAsync("RootNamespace", cancellationToken)).IsEqualTo("Test.MyApp");
- }
-
- [Test]
- public async Task Ci_PropertySet_WhenEnvironmentVariablePresent(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync(
- "MyLibrary",
- extraEnv: new Dictionary { ["CI"] = "true" },
- cancellationToken: cancellationToken
- );
- await Assert.That(await h.GetPropertyAsync("ContinuousIntegrationBuild", cancellationToken)).IsEqualTo("true");
- }
-
- [Test]
- public async Task Ci_PropertyNotSet_WhenEnvironmentVariableAbsent(CancellationToken cancellationToken)
- {
- // Ensure CI env var is not set for this test (it may be set in CI environments, so
- // we override with empty string to simulate a local dev machine).
- await using var h = await ProjectHarness.CreateAsync(
- "MyLibrary",
- extraEnv: new Dictionary { ["CI"] = "" },
- cancellationToken: cancellationToken
- );
- var value = await h.GetPropertyAsync("ContinuousIntegrationBuild", cancellationToken);
- // ContinuousIntegrationBuild should be empty (not "true") when CI is not set.
- await Assert.That(value).IsNotEqualTo("true");
- }
-
- [Test]
- [Arguments("net9.0", "net9.0")]
- [Arguments("net10.0", "net10.0")]
- public async Task TargetFramework_Honoured_WhenExplicitlySet(
- string tfm,
- string expected,
- CancellationToken cancellationToken
- )
- {
- await using var h = await ProjectHarness.CreateAsync(
- "MyLibrary",
- targetFramework: tfm,
- cancellationToken: cancellationToken
- );
- await Assert.That(await h.GetPropertyAsync("TargetFramework", cancellationToken)).IsEqualTo(expected);
- }
-}
diff --git a/tests/DotNetProjectSdk.IntegrationTests/Tests/TestWiringTests.cs b/tests/DotNetProjectSdk.IntegrationTests/Tests/TestWiringTests.cs
deleted file mode 100644
index df331c9..0000000
--- a/tests/DotNetProjectSdk.IntegrationTests/Tests/TestWiringTests.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using Purview.DotNetProjectSdk.Harness;
-
-namespace Purview.DotNetProjectSdk.Tests;
-
-///
-/// Verifies that the SDK wires the correct test framework packages and output type
-/// for projects that match the test-project naming convention.
-///
-public sealed class TestWiringTests
-{
- [Test]
- public async Task TestProject_OutputType_IsExe(CancellationToken cancellationToken)
- {
- // Test projects using Microsoft.Testing.Platform must be executables.
- await using var h = await ProjectHarness.CreateAsync("MyApp.UnitTests", cancellationToken: cancellationToken);
- await Assert.That(await h.GetPropertyAsync("OutputType", cancellationToken)).IsEqualTo("Exe");
- }
-
- [Test]
- public async Task TestProject_DefaultTestingFramework_IsTUnit(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync("MyApp.UnitTests", cancellationToken: cancellationToken);
- await Assert.That(await h.GetPropertyAsync("ProjectSdkTestFramework", cancellationToken)).IsEqualTo("TUnit");
- }
-
- [Test]
- public async Task TestProject_XUnit_WhenOptedIn(CancellationToken cancellationToken)
- {
- await using var h = await ProjectHarness.CreateAsync(
- "MyApp.UnitTests",
- extraProps: "XUnit",
- cancellationToken: cancellationToken
- );
- await Assert.That(await h.GetPropertyAsync("ProjectSdkTestFramework", cancellationToken)).IsEqualTo("XUnit");
- }
-
- [Test]
- public async Task SharedTestingProject_IsNotATestProject(CancellationToken cancellationToken)
- {
- // Shared testing projects provide helpers but are not runnable test projects.
- await using var h = await ProjectHarness.CreateAsync(
- "SharedTestingFramework",
- cancellationToken: cancellationToken
- );
- var props = await h.GetPropertiesAsync(cancellationToken, "IsTestProject", "IsSharedTestingProject");
- await Assert.That(props["IsSharedTestingProject"]).IsEqualTo("true");
- await Assert.That(props["IsTestProject"]).IsEqualTo("false");
- }
-}