deprecate-integration
Sunsets (soft-deprecates) a shipped Aspire hosting integration: marks its public API [Obsolete], adds a README warning banner, hides the package from `aspire…
npx skills add https://github.com/microsoft/aspire --skill deprecate-integrationYou are a specialized integration-deprecation agent for the microsoft/aspire repository. Your job is to perform a complete, consistent soft sunset of a shipped hosting integration (for example Aspire.Hosting.GitHub.Models) so that:
- every public and exported API surfaces a clear
[Obsolete]warning that points at a tracking issue, - the package stops showing up in
aspire add, - integration-specific automation that only exists to maintain it is removed,
- the solution still builds clean and the package still publishes one final obsolete version,
- and a later release can delete the integration entirely with minimal extra work.
Do not delete the project, its tests, its playground, or its polyglot fixtures during a soft sunset. Deletion is a separate, later phase (see "Phase 2: full removal" at the end).
Background: the two-phase lifecycle
Aspire retires an integration in two releases:
- Phase 1 — soft sunset (this skill). Mark the API
[Obsolete], warn in the README, hide it fromaspire add, drop its bespoke tooling/workflows, and keep it packable so the next release ships a final, clearly-deprecated version. Existing apps keep working. - Phase 2 — full removal (a later release). Delete the project, tests, playground, polyglot fixtures, and solution entry. Keep the package id in the CLI
DeprecatedPackageslist so previously-published versions stay hidden fromaspire add.
Precedent in the repo:
Aspire.Hosting.DaprandAspire.Hosting.NodeJsare already in Phase 2 — theirsrc/projects are gone, but their ids remain inDeprecatedPackages.s_all.
Inputs to collect
Before starting, determine:
- Integration project — e.g.
src/Aspire.Hosting.<Name>/and its package idAspire.Hosting.<Name>. - Tracking issue URL — the GitHub issue explaining why the integration is being sunset. Every obsolete message, README banner, and explanatory comment links to it.
- A short reason — one sentence a user will read (e.g. "GitHub Models is no longer available to new customers").
If the tracking issue is unknown, ask for it before editing — the message text and comments depend on it.
Step 1 — Add a shared deprecation message constant
Create src/Aspire.Hosting.<Name>/<Name>Deprecation.cs with a single internal constant reused by every [Obsolete] attribute. Centralizing the text keeps all the warnings identical and makes the follow-up edit trivial.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Aspire.Hosting.<Name>;
internal static class <Name>Deprecation
{
// <Reason>, so the integration is being sunset.
// See <tracking-issue-url> for details.
public const string Message = "<Reason>, so the Aspire <Name> integration is deprecated and will be removed in a future release. See <tracking-issue-url> for details.";
}
Match the integration's existing root namespace. If a type lives in a different namespace than the constant, add a using for the deprecation namespace to that file.
Step 2 — Mark the entire public + exported API [Obsolete]
Add [Obsolete(<Name>Deprecation.Message)] to each public type and each extension method the integration exposes:
- the resource class(es) (
public class <Name>Resource : Resource, ...), - any public descriptor/option/enum-wrapper types the integration exposes (the type, not each of its entries — see scoping below),
- all extension methods in
*Extensions.cs, includingpublicandinternalones.
Scope it correctly — mark each public type and extension method once, and do not over-apply:
- Marking a type
[Obsolete]already covers all of its members. Any access through an obsolete type (including its nested types, fields, constants, andstatic readonlydescriptor instances) raises CS0618 for consumers. So when you mark the enclosing type, do not also decorate its nested types, members, or per-item constants — that is redundant and produces a large, noisy diff for no behavioral gain. - Do not edit generated files (
*.Generated.cs, or any file produced by tooling) to add[Obsolete]. Mark the hand-authored partial that declares the public type instead; the obsolete attribute on the enclosing type carries over to the generated partial's members automatically. - Never overwrite a pre-existing
[Obsolete(...)]attribute that carries a more specific message. Some members may already be obsolete for a different, narrower reason (for example, individual catalog entries marked[Obsolete("This item has been removed from the service.")]). Leave those messages intact — only add[Obsolete(<Name>Deprecation.Message)]to members that are not already obsolete.
Other important details:
- Keep
[AspireExport]/[AspireExportIgnore]attributes in place.[Obsolete]does not remove a member from polyglot code generation. The ATS scanner reads[Obsolete](AtsCapabilityScannersetsIsObsolete) and the generators still emit the member, just annotated@deprecated(e.g.AtsTypeScriptCodeGeneratoremits a@deprecatedJSDoc tag). Removing the export attributes would change the generated SDK surface and break polyglot apphosts — do not do it. - Place
[Obsolete(...)]above the existing attributes on each member, then the other attributes, then the signature. - Do not add
error: trueto[Obsolete]. It must stay a warning so the final release still compiles for consumers and so the package builds.
Example:
[Obsolete(<Name>Deprecation.Message)]
[AspireExport]
public static IResourceBuilder<<Name>Resource> WithApiKey(this IResourceBuilder<<Name>Resource> builder, ...)
You do not normally need <NoWarn>CS0618</NoWarn> in the integration's own product .csproj: Roslyn does not raise CS0618 when an obsolete member references another obsolete member within the same obsolete context. Rely on the build (Step 6) to confirm rather than pre-emptively suppressing.
Step 3 — Add a README warning banner
At the very top of src/Aspire.Hosting.<Name>/README.md (right under the # <Title> heading), add a GitHub alert banner so the deprecation is unmissable on NuGet and GitHub:
> [!WARNING]
> **This integration is deprecated and no longer supported.**
> <Reason>, so the
> `Aspire.Hosting.<Name>` integration has been sunset. It will not receive
> further updates and will be removed in a future release. Existing applications
> continue to function, but new use is discouraged.
> See [microsoft/aspire#<issue-number>](<tracking-issue-url>)
> for details.
Leave the rest of the README intact so existing users can still read the usage docs.
Step 4 — Remove integration-specific automation and tooling
Delete anything that exists only to maintain this integration, because it should stop running once the integration is frozen:
- Scheduled GitHub Actions workflows that refresh data for it (e.g.
.github/workflows/update-<name>.yml). - Bespoke code generators / data-refresh tools under
src/Aspire.Hosting.<Name>/tools/(e.g.GenModel.cs,tools/Directory.Build.props,tools/Directory.Build.targets). - Any
<Compile Remove="tools\**\*.cs" />(or similar) lines in the integration.csprojthat only existed to exclude that tooling — remove them when you delete the tooling so the project file stays clean.
Search for other references to the removed workflow/tooling (CI trigger maps, docs) and clean up dangling mentions. Do not remove shared infrastructure used by other integrations.
Step 5 — Hide the package from aspire add
Add the package id to the CLI deny-list in src/Aspire.Cli/NuGet/NuGetPackageCache.cs:
internal static class DeprecatedPackages
{
private static readonly FrozenSet<string> s_all = new[]
{
"Aspire.Hosting.Dapr",
"Aspire.Hosting.<Name>", // keep the list alphabetically sorted
"Aspire.Hosting.NodeJs"
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
...
}
This filters the package out of aspire add search/listing and aspire update suggestions by default. It does not affect aspire restore, which resolves already-declared packages by exact id directly through the bundled NuGet service — that is why existing apps and polyglot fixtures keep restoring fine.
Users who still need it can opt back in with the feature flag (aspire config set features.showDeprecatedPackages true), so this is a soft hide, not a hard block.
Step 6 — Suppress CS0618 in first-party consumers (build to find them)
Marking the API [Obsolete] makes every first-party project that uses it emit CS0618, which is an error under TreatWarningsAsErrors. Build the affected projects and fix each warning site by adding a scoped NoWarn with an explanatory comment that links the issue. Typical consumers:
- the integration's own test project (
tests/Aspire.Hosting.<Name>.Tests/...csproj), - any playground apphosts that demonstrate the integration (
playground/<Name>EndToEnd/.../*.AppHost.csproj).
<PropertyGroup>
<!-- This project intentionally exercises the deprecated <Name> integration (<tracking-issue-url>). -->
<NoWarn>$(NoWarn);CS0618</NoWarn>
</PropertyGroup>
Prefer per-project NoWarn over editing shared props. Do not suppress CS0618 globally. Let the build tell you exactly which projects need it rather than guessing — add the suppression only where a real CS0618 appears.
Step 7 — Update the CLI deprecated-filtering tests
The CLI tests that assert the deny-list behavior must include the newly deprecated id so they keep covering it. Update both:
tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs— add the package to the fake search results in bothDeprecatedPackagesAreFilteredByDefault(asserting it is filtered out) andDeprecatedPackagesAreIncludedWhenShowDeprecatedPackagesEnabled(asserting it returns when the flag is on).tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs— add a dropped.nupkgfor the package to every test that exercisesDeprecatedPackages(there are typically several: the pinned-local-source test plus the local-folder-source filter/include pair). Search the file for the existing deprecated ids (for exampleAspire.Hosting.Dapr) and mirror each occurrence so coverage stays complete.
Follow the existing assertion style in those files even if it uses Assert.DoesNotContain; match the surrounding test for consistency rather than introducing a different pattern.
Step 8 — Keep the package publishable (ship one final obsolete release)
Do not set <IsPackable>false</IsPackable> or <SuppressFinalPackageVersion> on the integration .csproj. The whole point of Phase 1 is that the next release publishes a final, clearly-deprecated version that existing users restore. Confirm the project is still packable and that nothing you removed (Step 4) accidentally dropped packaging metadata.
Step 9 — Leave polyglot fixtures and ATS baselines alone
No changes are needed for tests/PolyglotAppHosts/Aspire.Hosting.<Name>/... during a soft sunset:
aspire restoreresolves the declared package by exact id (bypasses theDeprecatedPackagesfilter), so the per-language SDK still regenerates.- Code generation still exports the obsolete members (now annotated
@deprecated), so the committedapphost.mts/apphost.py/apphost.go/AppHost.javacallers still type-check and the Polyglot SDK Validation jobs stay green.
Per repo policy, do not add unit tests that assert the shape of generated code. If an ATS export surface genuinely changes, update the tests/PolyglotAppHosts apps for all languages instead — but a pure [Obsolete] addition does not change the exported surface, so usually nothing here changes. (Integrations without a committed *.ats.txt baseline have no snapshot to update.)
Step 10 — Build and verify
Run a restore + build, then the targeted test projects:
./build.sh
dotnet test --project tests/Aspire.Hosting.<Name>.Tests/Aspire.Hosting.<Name>.Tests.csproj --no-launch-profile -- --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"
dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"
Verify:
- the solution builds clean (no leftover CS0618 errors anywhere),
- the integration tests and the CLI deprecated-filtering tests pass,
- a quick scan shows no dangling references to the removed workflow/tooling.
Optionally smoke-test the user-facing behavior with the CLI: aspire add Aspire.Hosting.<Name> should report no match by default, and building an apphost that calls the API should emit CS0618 with your message.
Files-to-touch checklist
| Area | File(s) | Action |
|---|---|---|
| Message constant | src/Aspire.Hosting.<Name>/<Name>Deprecation.cs | Add internal Message const linking the issue |
| API surface | src/Aspire.Hosting.<Name>/*.cs (resource, descriptors, *Extensions.cs) | [Obsolete(...Message)] on every public/exported member; keep [AspireExport] |
| README | src/Aspire.Hosting.<Name>/README.md | Add [!WARNING] banner at top |
| Bespoke tooling | src/Aspire.Hosting.<Name>/tools/**, .github/workflows/update-<name>.yml, related .csproj <Compile Remove> | Delete integration-only automation and its csproj plumbing |
| CLI hide | src/Aspire.Cli/NuGet/NuGetPackageCache.cs | Add id to DeprecatedPackages.s_all (alphabetical) |
| Consumer warnings | integration test .csproj, playground apphost .csproj | Add scoped <NoWarn>$(NoWarn);CS0618</NoWarn> with comment |
| CLI tests | tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs, tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs | Add the new id to the deprecated-filtering cases |
| Packaging | integration .csproj | Confirm still packable — do not disable packing |
Phase 2: full removal (a later release — not this skill's default)
Only when explicitly asked to fully remove an already-sunset integration:
- Delete
src/Aspire.Hosting.<Name>/,tests/Aspire.Hosting.<Name>.Tests/, the playground, andtests/PolyglotAppHosts/Aspire.Hosting.<Name>/. - Remove the project from
Aspire.slnxand any solution/build references. - Keep the id in
DeprecatedPackages.s_allso previously-published versions remain hidden fromaspire add(this is why Dapr/NodeJs ids persist there after their projects were deleted). - Remove the now-unused per-consumer
NoWarn>CS0618suppressions that referenced the deleted integration. - Build and run the CLI tests again.