dotnet-new-lib-slnx
.NET Library Solution Setup (Codebelt Conventions)
Scaffold new .NET NuGet library solutions following the codebeltnet engineering conventions — the same pattern used across codebeltnet. Produces a fully wired solution with multi-target framework support, strong-name signing, NuGet packaging, DocFX documentation, CI pipeline, centralized build config, semantic versioning, and code quality tooling.
Generate the scaffold in the user's current working directory. Do not create an extra top-level {REPO_SLUG} or {SOLUTION_NAME} folder unless the user explicitly asks for a nested output folder.
Step 1: Collect Parameters
Read FORMS.md and collect all parameters by presenting each field to the user one at a time using the agent's native input mechanism when the host supports it. If the host does not render native form controls, follow the deterministic plain-text fallback defined in FORMS.md instead of improvising your own questioning style. Do not proceed to Step 2 until all required fields are collected and the user confirms the summary.
For fields that already present a recommended default or computed_default, treat a blank response as accepting that shown value. Do not get stuck in a clarification loop for root_namespace, repository_url, package_project_url, or other defaultable fields just because the user did not type over the recommended choice.
Consistency matters more than creativity during parameter collection. Do not paraphrase field prompts, merge questions, or switch interaction styles mid-flow.
Assume the default shape is a single packable library project whose project name matches solution_name. Do not ask for separate library project names unless the user explicitly asks for a multi-project solution or names additional packages/modules.
Collect repository_url before package_project_url so the package website field can present the repository URL as the recommended default and let the user either accept it or replace it with a dedicated site/docs URL.
Default target_frameworks to the newest generally supported .NET LTS for new libraries by reading https://raw.githubusercontent.com/dotnet/core/refs/heads/main/release-notes/releases-index.json. Filter to .NET entries whose support-phase is active or maintenance, then choose the highest LTS channel and format it as net{major}.0. Exclude preview channels. Also surface every other generally supported non-preview LTS and STS channel so the user can deliberately choose any actively supported track. Only suggest multiple TFMs when the user explicitly asks for compatibility across older runtimes or there is a clear support requirement.
When presenting target_frameworks, compute these quick-picks from that same releases index before free text:
- Recommended: newest generally supported LTS only
- Additional single-target choices: every other supported LTS or STS channel, sorted newest to oldest and labeled with its support track
- Expanded scope: all generally supported
.NETchannels, newest to oldest, excluding preview channels
Default benchmark_runner_project_name to benchmark-runner. Treat it as a solution-level tooling project name, not a per-library package name.
Step 2: Load the Variant Guide
Read references/library.md for the library-specific project structure, template file mapping, .slnx format, multi-project guidance, and project reference conventions.
Step 3: Resolve Dynamic Dependency Versions
Before writing Directory.Packages.props, resolve every *_VERSION placeholder in that file to the latest stable listed version for its matching package ID on NuGet.org.
- Use the NuGet V3 service index at
https://api.nuget.org/v3/index.jsonto discover the package metadata endpoints - Prefer registration metadata so you can ignore unlisted versions and prerelease builds
- If registration metadata is unavailable, fall back to the package base address versions list from the same service index and still exclude prerelease versions
- Resolve each package independently by package ID; never reuse one generic "latest" value across multiple packages
- Never hardcode version numbers from stale examples, screenshots, or prior scaffolds
This includes the benchmark-related packages:
BenchmarkDotNetBenchmarkDotNet.Diagnostics.WindowsCodebelt.Extensions.BenchmarkDotNet.Console
Step 4: Apply the Substitution Map
When copying template files, replace these placeholders in file contents:
| Placeholder | Value |
|---|---|
{SOLUTION_NAME} |
Solution name (e.g. MyLibrary) |
{ROOT_NAMESPACE} |
Root namespace prefix (e.g. Acme) |
{PROJECT_NAME} |
Packable project/package name. Default to {SOLUTION_NAME} for the single-project case |
{AUTHOR} |
Author name |
{AUTHOR_EMAIL} |
Author email |
{COMPANY_OR_PERSON} |
Company name or individual publisher name for copyright, NuGet metadata, and DocFX branding |
{COPYRIGHT_YEAR} |
Copyright year (e.g. 2026) |
{PACKAGE_PROJECT_URL} |
Public package website or docs URL shown as Project website on NuGet |
{REPOSITORY_URL} |
Source repository URL shown as Source repository on NuGet |
{REPO_OWNER} |
GitHub org/user (from URL) |
{REPO_SLUG} |
Repo name (last URL segment, lowercased) |
{TARGET_FRAMEWORKS} |
Computed from the official .NET releases index; offer the newest generally supported LTS, every other supported LTS or STS single-target choice, or all generally supported non-preview channels for broader scope |
{DOCFX_TARGET_FRAMEWORK} |
Highest selected generally supported non-preview TFM used for DocFX metadata generation |
{BENCHMARK_RUNNER_PROJECT_NAME} |
Tooling project name for the benchmark host (default benchmark-runner) |
{BENCHMARK_RUNNER_NAMESPACE} |
Benchmark runner namespace derived from the tooling project name, replacing invalid identifier characters such as - with _ |
{BENCHMARK_RUNNER_TARGET_FRAMEWORK} |
Highest selected generally supported non-preview executable TFM from target_frameworks |
{BENCHMARK_RUNTIME_JOBS} |
One .AddJob(...) line per selected executable benchmark runtime, derived from target_frameworks |
{SNK_FILE} |
e.g. {repo-slug}.snk |
{SONARCLOUD_ORG} |
SonarCloud org slug (or omit job if skipped) |
{SONARCLOUD_KEY} |
SonarCloud project key |
Directory.Packages.props also contains package-specific placeholders such as {BENCHMARKDOTNET_VERSION}, {BENCHMARKDOTNET_DIAGNOSTICS_WINDOWS_VERSION}, {CODEBELT_EXTENSIONS_BENCHMARKDOTNET_CONSOLE_VERSION}, and {MICROSOFT_NET_TEST_SDK_VERSION}. Resolve each of them from NuGet.org in Step 3 before writing the final file.
Step 5: Generate All Files
Generate files in this order:
1. Copy shared templates
Copy every file from assets/shared/ to the project root, preserving directory structure. Treat the current working directory as that project root. Apply placeholder substitution (Step 4) to all file contents during the copy.
Do this as a recursive, dotfile-aware copy. Hidden folders and files under assets/shared/ are part of the scaffold and must not be skipped. In particular, copy assets/shared/.bot/README.md as a real file in the generated repo; do not replace it with a synthetic .gitkeep or placeholder note.
Preserve UTF-8 when reading, copying, and writing text files. Do not transcode templates to ANSI, OEM, Windows-1252, or any system-default code page during generation. The shared .editorconfig in the scaffold declares charset = utf-8, and generated text files should match it from the start.
When a text file does not need substitutions, prefer a byte-preserving file copy instead of read/transform/write.
When a text file does need substitutions, use explicit UTF-8 APIs end-to-end. In PowerShell, prefer .NET file APIs with an explicit UTF8Encoding instance rather than locale-dependent text cmdlets. For example:
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
$content = [System.IO.File]::ReadAllText($src, $utf8NoBom)
$updated = Apply-Replacements -Content $content -Map $replaceMap
[System.IO.File]::WriteAllText($dest, $updated, $utf8NoBom)
Avoid Get-Content / Set-Content or other default-encoding text paths unless you have explicitly verified they preserve UTF-8 correctly for BOM-less templates.
Preserve the template's BOM policy by default. If the source template is UTF-8 without BOM, write UTF-8 without BOM unless the target file format or tool explicitly requires a BOM.
Exception: generate testenvironments.json instead of copying it verbatim. Always include the WSL-Ubuntu entry, then add one Docker-Ubuntu entry per selected target framework using the Docker image tag codebeltnet/ubuntu-testrunner:{major} where {major} comes from the TFM.
Exception: do not leave Directory.Packages.props with unresolved placeholder tokens. Resolve each package version placeholder to the latest stable listed NuGet.org version for that exact package ID before writing the file.
Before finalizing the Docker entries, validate that each generated tag exists in the Docker Hub tags feed for codebeltnet/ubuntu-testrunner. Prefer the machine-readable tags API over manual inspection:
https://hub.docker.com/v2/repositories/codebeltnet/ubuntu-testrunner/tags?page_size=100
Examples:
net10.0→codebeltnet/ubuntu-testrunner:10net10.0;net9.0;net8.0→ three Docker entries with tags10,9, and8
2. Copy library Directory.Build.props
Copy assets/library/Directory.Build.props to the project root, applying placeholder substitution.
3. Copy DocFX templates
Copy assets/library/.docfx/ to the project root, applying placeholder substitution. DocFX generates API reference documentation for NuGet packages.
Use {PROJECT_NAME} for the DocFX source project glob and {DOCFX_TARGET_FRAMEWORK} for metadata generation so the generated docs track the actual scaffolded project name and runtime instead of a hardcoded example.
4. Generate library-specific files
Follow the variant guide (Step 2) for the remaining files: project structure, .csproj files, tuning benchmark projects under tuning/, the solution-level benchmark runner under tooling/, .nuget/{ProjectName}/ metadata folders (per packable project), and the .slnx solution file.
For the default single-project case, use solution_name as {PROJECT_NAME} everywhere. Only branch into multiple {PROJECT_NAME} values when the user explicitly wants multiple library packages in the same solution.
Generate benchmarking in two layers:
- Benchmark project:
tuning/{PROJECT_NAME}.Benchmarks/{PROJECT_NAME}.Benchmarks.csproj - Benchmark runner host:
tooling/{BENCHMARK_RUNNER_PROJECT_NAME}/{BENCHMARK_RUNNER_PROJECT_NAME}.csproj - Runner entry point:
tooling/{BENCHMARK_RUNNER_PROJECT_NAME}/Program.cs - Reports/output folder:
reports/
The tuning benchmark project holds the actual benchmark types and references the source project. The tooling benchmark runner invokes those tuning projects.
Program.cs in the benchmark runner must call BenchmarkProgram.Run(...) and add one benchmark job per selected executable TFM from target_frameworks. Examples:
net48→.AddJob(slimJob.WithRuntime(ClrRuntime.Net48))net9.0→.AddJob(slimJob.WithRuntime(CoreRuntime.Core90))net10.0→.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0))
Skip non-executable TFMs such as netstandard* when building the benchmark job list. If no executable runtime remains, note that the benchmark runner needs a manual runtime decision before it can be used.
Set the benchmark runner project TargetFramework to the highest selected generally supported non-preview executable TFM from target_frameworks. This keeps the runner aligned with the newest runtime the user chose, whether that supported track is LTS or STS.
Step 6: Post-Generation Checklist
After generating, verify:
-
.slnxreferences all generated src/, test/, tuning/, and tooling/ projects -
Directory.Build.propsreferences the correct.snkfilename - Each packable project has a
.nuget/{ProjectName}/folder withPackageReleaseNotes.txt,icon.png(placeholder), andREADME.md -
Directory.Packages.propslists all<PackageReference>packages used in the solution -
Directory.Packages.propscontains concrete version numbers with no unresolved*_VERSIONplaceholders - Every
Directory.Packages.propsversion was resolved from the latest stable listed NuGet.org package version at generation time -
tuning/{PROJECT_NAME}.Benchmarks/{PROJECT_NAME}.Benchmarks.csprojreferences the main source project and relies on central package management -
tooling/{BENCHMARK_RUNNER_PROJECT_NAME}/Program.cscontains one runtime job per selected executable TFM -
tooling/{BENCHMARK_RUNNER_PROJECT_NAME}/{BENCHMARK_RUNNER_PROJECT_NAME}.csprojreferences the default tuning benchmark project and relies on central package management -
ci-pipeline.ymlhas the correct SNK and SonarCloud settings - Root governance docs exist:
README.md,CHANGELOG.md,LICENSE,.github/CODE_OF_CONDUCT.md,.github/CONTRIBUTING.md -
.docfx/docfx.jsonlists all source projects and has correct metadata -
.editorconfigis present, setscharset = utf-8, and keeps file-scoped namespace enforcement - Generated text files do not contain common mojibake markers such as
—,–,â€, or� -
AGENTS.mdreferences.bot/and coding guidelines -
.github/copilot-instructions.mdhas project-specific patterns -
.bot/folder exists and is listed in.gitignore -
.bot/README.mdexists in the generated repo and came from the shared asset template, not from a synthetic.gitkeepfallback -
.github/dependabot.ymlwatches the repo root so central NuGet package management stays current after scaffolding
Summarize what was generated and note any manual steps (e.g. registering with SonarCloud, populating .docfx/images/ with logo/favicon).
Step 7: Generate Strong Name Key
After scaffolding is complete, invoke the dotnet-strong-name-signing skill to generate the .snk file. The skill will default the key name to the repository folder name and place it at the repo root — which is exactly where Directory.Build.props expects it via {SNK_FILE}.