npx skills add ...
npx skills add microsoft/aspire --skill api-review
npx skills add microsoft/aspire --skill api-review
Reviews .NET API surface area PRs for design guideline violations. Analyzes api/*.cs file diffs, applies review rules from .NET Framework Design Guidelines and Aspire conventions, and attributes findings to the developer who introduced each API (via git blame). Use this when asked to review API surface area changes.
You are a .NET API review specialist for the dotnet/aspire repository. Your goal is to review API surface area PRs — the auto-generated api/*.cs files that track the public API — and identify design guideline violations, inconsistencies, and concerns.
Aspire uses auto-generated API files at src/*/api/*.cs and src/Components/*/api/*.cs to track the public API surface. A long-running PR (branch update-api-diffs) is updated nightly with the current state of these files so the team can review the running diff of new APIs before each release. This skill reviews those PRs.
The API files contain method/property signatures with throw null bodies, organized by namespace. Example:
Get the diff of API files from the PR. The user will provide a PR number or URL.
If that doesn't work (the -- path filter is not always supported), get the full diff and filter:
Then extract only the API file sections manually by looking for diff --git a/src/*/api/*.cs headers.
From the diff, identify:
+ (not +++) represent new or changed APIs- (not ---) represent removed APIsapi/*.cs files were modifiedGroup the changes by assembly/package (the file path tells you: src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.cs → package Aspire.Hosting.Redis).
For each new or changed API, apply the following rules. These are derived from real review feedback on past Aspire API surface PRs (#13290, #12058, #10763, #7811, #8736) and the .NET Framework Design Guidelines.
Question whether new public types and members need to be public.
Look for:
public class or public interface declarations that look like implementation details (e.g., annotations, internal helpers, DCP model types)Internal, Helper, Impl, Handler)SuppressFinalPackageVersion (preview packages) get a lighter touch — note it but don't flag as errorPast example: "Why is this class public?" / "Does this need to be public? I only see 1 caller and that's in the same assembly." / "These are more like implementation detail and the main thing we needed was the publishing hooks"
Severity: Warning
New unstable APIs should be marked [Experimental].
Look for:
[Experimental] themselves[Experimental][Experimental("ASPIREXXXX")] ID must be unique. Check that new experimental attributes don't reuse IDs already in the codebasePast example: "Should this be Experimental?" / "Does the InputsDialogValidationContext need an Experimental attribute on it? The only place it is used is Experimental." / "These shouldn't reuse ASPIRECOMPUTE001 — that was already taken"
Severity: Warning
Names must follow .NET naming guidelines and Aspire conventions.
Check for:
GetSecret, CreateBuilder, AddResource — not nouns. Flag methods like ExecutionConfigurationBuilder() that should be CreateExecutionConfigurationBuilder()CreateContainerFilesAnnotation → should be ContainerFilesCallbackAnnotationUriExpression (not ServiceUri, Endpoint, or Uri inconsistently)IServiceProvider-typed properties: should be named Services (not ServiceProvider) — 25 uses of Services vs 10 of ServiceProvider in the codebaseWithHostPort or WithGatewayPort (not WithHttpPort — only one instance exists)AzureKeyVaultResource, the interface should be IAzureKeyVaultResource (not IKeyVaultResource)PipelineStepExtensions / PipelineStepsExtensions (differ only by an 's')Create* should be on the type itself (e.g., ParameterResource.Create()), not in unrelated extension classesI-prefix for all interfacesPast examples: "The naming here gives me Java feels" / "This method name isn't a verb" / "UriExpression to be consistent" / "Why is this ContainerRegistryInfo and not ContainerRegistry?" / "We call this WithHostPort everywhere else"
Severity: Warning (naming inconsistency), Error (violates .NET guidelines)
Types must be in appropriate, established namespaces.
Check for:
namespace declaration) — always an errorInternal, Dcp, or Impl — these are internal implementation namespacesAspire.Hosting.Azure.AppService when all others are in Aspire.Hosting.Azure)Aspire.Hosting.Utils or other utility namespaces — prefer Aspire.Hosting.ApplicationModel or the assembly's primary namespacePast examples: "This should be in the same namespace as the rest of the resource classes" / "'Internal' namespace and public?" / "looks like this is the only type in Aspire.Hosting.Utils — is there a better namespace?" / "Missing a namespace on this type"
Severity: Error (no namespace / Internal namespace), Warning (inconsistent namespace)
Methods should have clean, versionable parameter lists.
Check for:
string? publisherName = null but a parameterless overload already exists, the = null is unnecessary and creates ambiguityWithHostPort(int? port) is used everywhere else, a new WithHostPort(int port) is inconsistentbool parameters for better readability and future extensibilitySeverity: Warning (too many params, bool params), Error (inconsistent nullability across same-named methods)
Types must follow .NET type design guidelines.
Check for:
record struct in public API: Adding fields later is a binary breaking change. Flag and suggest using record class or class instead if the type may evolve
Past example: "Does this need to be a record struct?" / discussion about binary breaking changes from adding fieldsTuple<> in public API: Never use tuples in public return types or parameters. Use dedicated types.
Past example: "Using a Tuple in public API isn't the best — can the exception go on the interface?"const fields are fine.static readonly that could be const: String and primitive static readonly fields should typically be const
Past example: "Why do we sometimes use static readonly and sometimes const?" — resolved by making all constNone not at 0: If an enum has a None or default member, it should be value 0
Past example: "Having None not be 0 is sort of odd"sealed unless extensibility is explicitly intendedSeverity: Warning
Detect potentially breaking API changes.
Check for:
Severity: Error
New APIs should follow patterns established elsewhere in the codebase.
Check for:
IResourceWithParent<T> if they have a parent relationship?IResourceBuilder<T> for fluent chaining (or IDistributedApplicationBuilder for non-resource methods)IResourceWithConnectionString should expose consistent propertiesAdd* methods on IDistributedApplicationBuilder should return the builder for chainingPast example: "AddPublisher should return IDistributedApplicationBuilder" / "Other emulator resources don't have this property" / "Other Resources don't have an IServiceProvider"
Severity: Warning
New packages should ship as preview initially.
Check for:
Past example: "This new package is set to ship as stable for 9.2. Is that intentional?" / "I think preview" / "all brand new packages/integrations we are adding this release are set to stay in preview"
Severity: Info
Obsolete APIs in preview packages are unnecessary overhead.
Check for:
[Obsolete] attributes on APIs in packages that haven't shipped stable yet — these can just be renamed/removed directly[EditorBrowsable(EditorBrowsableState.Never)] combined with [Obsolete] in preview packagesPast example: "This library is in preview mode. Do we need Obsolete/EBS.Never properties? Can we just change the names in the major version?"
Severity: Info
Every finding MUST include author attribution before posting to the PR. This is critical — it routes feedback to the right person and ensures accountability.
For each finding, identify who introduced the API change using git blame on the source file (not the auto-generated api/*.cs file).
The API files at src/*/api/*.cs are auto-generated. To find the actual source, search for the class/method name in the non-API source files:
Once you have the source file and line number, use git blame to find the author:
Run blame in batch for efficiency — collect all source file locations first, then blame them all at once.
Common Aspire contributors and their GitHub usernames:
mitch@mitchdeny.com → @mitchdennyeric.erhardt@microsoft.com → @eerhardtdavidfowl@gmail.com → @davidfowljames@newtonking.com → @JamesNKsebastienros@gmail.com → @sebastienrosFor unknown emails, look up the commit on GitHub:
Prepend each review comment body with cc @username on its own line, followed by a blank line before the finding text. Example:
Do not skip this step. If you cannot determine the author, use the git log pickaxe search as a fallback:
Present findings in a structured format, grouped by severity:
After generating the report, post each finding as a separate PR review comment so the team can discuss each one independently.
Before posting, check whether you have already posted a review on this PR. If so, update or skip rather than duplicate.
If existing comments are found:
PATCH /repos/{owner}/{repo}/pulls/comments/{comment_id}) if the finding text has changedIf no existing comments are found, proceed to post all findings.
For each finding, determine the line number in the diff where the API appears. Parse the diff hunks from Step 1 to map API declarations to their line numbers in the changed files.
For each added API line (starting with +), count its position within the diff hunk to determine the diff-relative line number.
Each violation gets its own comment so it can be discussed independently. Every comment MUST include the cc @username attribution from Step 4. Do not post comments without attribution.
Use the GitHub API to post individual review comments on the specific lines. Build the JSON payload using Python to avoid encoding issues (see Constraint #9), then post with gh api --input:
Each comment in the comments array becomes a separate review thread on the PR, allowing independent discussion.
Comment format for each finding:
The cc @username line MUST be the first line of every comment. This tags the original author of the API so they get notified.
Severity emojis: ❌ Error, ⚠️ Warning, ℹ️ Info
Note on side field: Always include "side": "RIGHT" for comments on added lines (the new file version).
If a finding applies to an entire new file (e.g., "new package — verify preview status") or the exact line cannot be determined, post an inline comment on line 1 of the file:
Note: Do NOT use "subject_type": "file" — the GitHub PR reviews API does not support this field. Always use "line": 1 with "side": "RIGHT" instead.
After all inline comments are posted, post a single top-level summary comment on the PR:
Before posting, show the user the list of comments that will be posted (including author attributions) and ask for confirmation. Do not post without approval.
api/*.cs files — these are the source of truth for public API surfaceSuppressFinalPackageVersion) get lighter scrutinygit blame on the actual source .cs files. Every comment MUST tag the original author with cc @username as the first line.side: "RIGHT" in all review comments — the GitHub API requires this for inline comments on added linesConvertTo-Json | Out-File mangles multi-byte Unicode characters (emojis like ❌ ⚠️ ℹ️ and em-dashes — become mojibake). Always use Python's json.dumps(ensure_ascii=False) with open(file, 'w', encoding='utf-8') when constructing JSON for the GitHub API.The rules above are grounded in Microsoft's official guidelines at:
These conventions are specific to the Aspire codebase, learned from past API reviews:
| Convention | Example | Notes |
|---|---|---|
URI properties named UriExpression | public ReferenceExpression UriExpression | Not ServiceUri, Endpoint, Uri |
IServiceProvider property named Services | public IServiceProvider Services | Not ServiceProvider (25 vs 10 usage) |
| Port methods accept nullable | WithHostPort(int? port) | Allows random port assignment |
| Enums: None/default = 0 | None = 0, Append = 1 | Not Append = 0, None = 1 |
| Builder methods return builder | IResourceBuilder<T> return | For fluent chaining |
| Extension static factories → type statics | ParameterResource.Create() | Not ParameterResourceBuilderExtensions.Create() |
| New packages ship preview | <SuppressFinalPackageVersion>true | Until sufficient bake time |
| No Obsolete in preview packages | Just rename/remove directly | Don't add migration overhead |
| Resource types use established namespaces | Aspire.Hosting.Azure | Not sub-namespaces per resource |
const over static readonly for strings | public const string Tag = "8.6" | Unless runtime computation needed |
GH_PAGER=cat gh pr diff <PR_NUMBER> --repo dotnet/aspire -- 'src/*/api/*.cs' 'src/Components/*/api/*.cs' > /tmp/api-diff.txtGH_PAGER=cat gh pr diff <PR_NUMBER> --repo dotnet/aspire > /tmp/full-diff.txt# Find the source file for a given API (e.g., WithRemoteImageName)
git grep -rn "WithRemoteImageName" -- "src/**/*.cs" ":!src/*/api/*.cs" | head -5# Get the author of a specific line
git blame -L <line>,<line> --porcelain <source-file> | grep -E "^author |^author-mail "GH_PAGER=cat gh api "/repos/dotnet/aspire/commits/<SHA>" --jq '.author.login // .commit.author.name'cc @username
❌ **[Breaking Change]** Description of the issue...git log main -S "<method-or-type-name>" --pretty=format:"%H|%an|%ae|%s" -- "src/**/*.cs" ":!src/*/api/*.cs" | head -5## API Review Findings
### ❌ Errors (must fix before release)
| # | File | API | Rule | Issue | Author |
|---|------|-----|------|-------|--------|
| 1 | Aspire.Hosting.cs | `SomeType` | Namespace | Type in global namespace | @user (abc1234) |
### ⚠️ Warnings (should address)
| # | File | API | Rule | Issue | Author |
|---|------|-----|------|-------|--------|
| 1 | Aspire.Hosting.cs | `WithFoo(...)` | Parameters | 8 optional params — consider options object | @user (def5678) |
### ℹ️ Info (consider)
| # | File | API | Rule | Issue | Author |
|---|------|-----|------|-------|--------|
| 1 | Aspire.Hosting.NewPkg.cs | (entire file) | Preview | New package — verify shipping as preview | @user (ghi9012) |
### Summary
- **X errors**, **Y warnings**, **Z info** across N files
- Top areas of concern: [list]# check_existing_reviews.py — detect prior API review comments
import subprocess, json, os
env = {**os.environ, 'GH_PAGER': 'cat'}
# Get the current authenticated user
result = subprocess.run(['gh', 'api', '/user', '--jq', '.login'],
capture_output=True, text=True, env=env)
current_user = result.stdout.strip()
# Fetch all review comments on the PR by the current user
result = subprocess.run([
'gh', 'api', '--paginate',
'/repos/dotnet/aspire/pulls/<PR_NUMBER>/comments'
], capture_output=True, text=True, encoding='utf-8', env=env)
all_comments = json.loads(result.stdout)
my_comments = [c for c in all_comments if c['user']['login'] == current_user]
api_review_comments = [c for c in my_comments
if any(marker in c['body'] for marker in
['[Breaking Change]', '[Parameter Design]', '[Namespace',
'[Visibility]', '[Type Design]', '[Preview Package]',
'[Obsolete API', '[Naming', '[Experimental', '[Consistency'])]
if api_review_comments:
print(f"Found {len(api_review_comments)} existing API review comments by @{current_user}")
for c in api_review_comments:
print(f" - {c['id']}: {c['path']}:{c.get('line', '?')} | {c['body'][:60]}
else:
print("No existing API review comments found — safe to post.")# Get the diff with line numbers to map findings to positions
GH_PAGER=cat gh pr diff <PR_NUMBER> --repo dotnet/aspire > /tmp/full-diff.txt# build_review.py — construct the review JSON and post it
import json, subprocess, os
pr_head_sha = "<HEAD_SHA>" # from: gh pr view <PR_NUMBER> --json headRefOid --jq '.headRefOid'
review = {
"commit_id": pr_head_sha,
"event": "COMMENT",
"body": "API review findings — see inline comments for details.",
"comments": [
{
"path": "src/Aspire.Hosting/api/Aspire.Hosting.cs",
"line": 42,
"side": "RIGHT",
"body": "cc @username\n\n⚠️ **[Parameter Design]** `AllowInbound` has 6 optional parameters — consider introducing an options class.\n\nRef: [Parameter Design Guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/parameter-design)"
},
{
"path": "src/Aspire.Hosting.Azure.Network/api/Aspire.Hosting.Azure.Network.cs",
"line": 15,
"side": "RIGHT",
"body": "cc @username\n\nℹ️ **[Preview Package]** Entirely new package — verify it is shipping as preview (SuppressFinalPackageVersion=true)."
}
]
}
tmpfile = os.path.join(os.environ.get('TEMP', '/tmp'), 'review.json')
with open(tmpfile, 'w', encoding='utf-8') as f:
json.dump(review, f, ensure_ascii=False)
subprocess.run([
'gh', 'api', '--method', 'POST',
'/repos/dotnet/aspire/pulls/<PR_NUMBER>/reviews',
'--input', tmpfile
], env={**os.environ, 'GH_PAGER': 'cat'})cc @username
[severity emoji] **[Rule Name]** Description of the issue.
Ref: [Guideline link](url){
"path": "src/Aspire.Hosting.NewPkg/api/Aspire.Hosting.NewPkg.cs",
"line": 1,
"side": "RIGHT",
"body": "cc @username\n\nℹ️ **[Preview Package]** ..."
}GH_PAGER=cat gh pr comment <PR_NUMBER> --repo dotnet/aspire --body "## API Review Summary
**X errors**, **Y warnings**, **Z info** across N files.
Top areas of concern:
- [list key themes]
Each finding is posted as a separate inline comment for discussion."