Visitar URL original
Add `#:sdk` support for .NET file-based apps by devlead · Pull Request #755 · dotnet-outdated/dotnet-outdated · GitHub
Skip to content

Add #:sdk support for .NET file-based apps - #755

Merged
slang25 merged 10 commits into
dotnet-outdated:masterfrom
devlead:feature/fileSDK
Jun 3, 2026
Merged

slang25 merged 10 commits into
dotnet-outdated:masterfrom
devlead:feature/fileSDK

Conversation

@devlead

@devlead devlead commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add support for MSBuild SDK packages declared with #:sdk in file-based apps (for example #:sdk Cake.Sdk@6.0.0)
  • Report SDK directives from the .cs source instead of transient SDK-expanded dependencies from the restore graph
  • Update #:sdk directives, including variable-backed ids and versions through #:property (for example #:sdk $(SdkId)@$(SdkVersion))
  • Rewrite literal #:sdk lines in-place; fail upgrades when no matching directive was changed
  • Omit -f / --framework when running dotnet add package on .cs paths (newer SDKs i.e. .NET 11 will fail and reject it)
  • Add test-projects/file-based-app-sdk fixture, unit/integration/e2e tests, and a CI smoke test on the existing file-based-app job (SDK 10.0.300)

Builds on file-based app support from #752 (#:package and #:property).

Changes

  • New FileBasedAppReference model with directive expressions and UsesPropertyReferences guard
  • VariableTrackingService: parse/upgrade #:sdk, compile-time regex sources, prefer variable-backed entries when deduplicating discoveries
  • ProjectAnalysisService: merge #:package / #:sdk directives into authoritative dependencies for file-based apps
  • DotNetPackageService: route SDK upgrades through variable or direct file rewrite paths; return failure when nothing changes
  • README: document #:sdk; FAQ points to dotnet/sdk (dotnet/cli is archived)

Validation

  • dotnet build --configuration Release
  • dotnet test --configuration Release --framework net9.0
  • dotnet test --configuration Release --framework net9.0 --filter "FullyQualifiedName~FileBasedApp|FullyQualifiedName~File_Based_App"
  • dotnet run --configuration Release --framework net9.0 --project src/DotNetOutdated -- test-projects/file-based-app-sdk/app.cs --fail-on-updates (expects exit code 2)
  • Manual: dotnet outdated <path-to-cake.cs> -u updates #:sdk Cake.Sdk@… in place

Extend file-based app handling beyond #:package so MSBuild SDK
packages declared with #:sdk (for example Cake.Sdk@6.0.0) are
discovered, reported, and upgraded from the .cs source file.

Analysis
- Parse #:package and #:sdk directives from file-based app sources
- Prefer directive-based dependencies over restore-graph expansion
  (report SDK over transient SDK dependencies)
- Support variable-backed directives via #:property, including
  $(SdkId)@$(SdkVersion) and $(SdkId)@literal-version forms
- Dedupe discovered references by preferring variable-backed entries

Upgrade
- Update #:property for variable-backed #:package and #:sdk
- Rewrite literal #:sdk lines in-place; block literal direct rewrites
  when expressions contain $(...)
- Return failure when no directive or property file was changed
- Omit -f/--framework for dotnet add on .cs paths (newer SDKs reject it)

Implementation
- Add FileBasedAppReference model and compile-time regex sources
- Extend VariableTrackingService and ProjectAnalysisService
- Harden DotNetPackageService upgrade routing and error reporting

Tests and CI
- Add test-projects/file-based-app-sdk fixture
- Add unit, integration, and end-to-end coverage for SDK directives
- Add file-based-app-sdk smoke test to the SDK 10.0.300 CI job

Docs
- Document #:sdk usage in README and FAQ
- Point file-based app upgrade FAQ to dotnet/sdk (dotnet/cli is archived)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for MSBuild SDK packages declared via #:sdk in .NET file-based apps, making analysis and upgrades reflect the authoritative directives in the .cs file rather than transient SDK-expanded restore-graph dependencies.

Changes:

  • Introduces FileBasedAppReference (+ helper) and extends VariableTrackingService to discover and update #:sdk directives (including #:property-backed expressions).
  • Updates analysis/upgrade flows to treat file-based app directives as authoritative dependencies and to update SDK directives in-place (and avoid -f/--framework for .cs paths).
  • Adds fixture + unit/integration/e2e coverage and a CI smoke test for file-based app SDK directives.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/DotNetOutdated.Tests/VariableTrackingServiceTests.cs Adds unit tests for discovering/updating #:sdk directives, including variable-backed scenarios.
test/DotNetOutdated.Tests/EndToEndTests.cs Adds e2e coverage for analyzing and upgrading file-based apps that use #:sdk.
test/DotNetOutdated.Tests/DotNetPackageServiceTests.cs Extends service tests for SDK directive upgrades and verifies no -f for .cs paths.
test-projects/file-based-app-sdk/app.cs New test fixture file-based app that declares an SDK via #:sdk.
src/DotNetOutdated.Core/Services/VariableTrackingService.Regexes.cs Centralizes directive/property regexes using [GeneratedRegex].
src/DotNetOutdated.Core/Services/VariableTrackingService.cs Implements discovery/update for #:sdk and returns success/failure for variable/direct updates.
src/DotNetOutdated.Core/Services/ProjectAnalysisService.cs Merges file-based directive discoveries into the authoritative dependency set for analysis output.
src/DotNetOutdated.Core/Services/FileBasedAppReference.cs New model representing file-based app #:package/#:sdk references and expression metadata.
src/DotNetOutdated.Core/Services/DotNetPackageService.cs Routes SDK upgrades through directive/variable rewrite paths and omits -f for .cs paths.
README.md Documents #:sdk support and updates FAQ guidance to the correct upstream repo.
.github/workflows/test.yml Adds a CI smoke test for file-based app SDK directive analysis behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

UpdatePackageVariable Step 2 previously always wrote the package
reference file and set msbuildUpdated even when no matching elements
were found or regex replacements made no changes. That could return
true while nothing was modified, conflicting with the bool contract
and masking no-op upgrades.

Compare packageRefContent before and after replacements; write and
set msbuildUpdated only when the file content actually differs.
@devlead
devlead requested a review from Copilot June 3, 2026 12:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Address review feedback for #:sdk / #:package support by improving
update safety, reducing duplicate I/O, and restoring a backward-
compatible ProjectAnalysisService API.

VariableTrackingService:
- Scan file-based apps in a single pass via GetOrScanFileBasedApp,
  sharing results between DiscoverFileBasedAppReferences and
  DiscoverPackageVariables (with a dedicated scan cache)
- Still collect parent *.props property definitions during the scan
- Replace only the version token in UpdateFileBasedAppDirectReference
  so trailing inline comments and other suffix text are preserved
- Broaden update failure warnings to include ElementType
- Reuse already-read file content when resolving #:property values
  during directive updates (CollectFileBasedAppPropertyDefinitionsForUpdate)
- Invalidate both variable and file-based scan caches on changes

ProjectAnalysisService:
- Reintroduce constructors without IVariableTrackingService,
  delegating to VariableTrackingService(fileSystem) for compatibility

Tests:
- Add Update_FileBasedAppDirectSdk_PreservesTrailingContentOnDirectiveLine
@devlead
devlead requested a review from Copilot June 3, 2026 13:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Address PR feedback for #:sdk / #:package handling: pinned versions
should resolve as exact ranges, #:property forward references must
work when directives appear before definitions, and failed upgrades
should explain what form of directive was expected.

VariableTrackingService:
- Scan file-based apps in two passes: collect all #:property values
  first, then parse #:package/#:sdk directives (fixes forward refs)
- Use exact inclusive VersionRange (min=max) for directive references
  instead of minimum-only ranges
- Avoid nullable annotations outside a nullable context on out params

FileBasedAppReference:
- Enable nullable reference types for this file
- Mark ResolvedVersion/VersionRange as required; VariableInfo as nullable
- Add CreateExactVersionRange helper for pinned directive versions

DotNetPackageService:
- Include actionable hints in file-based app update failure messages
  (literal #:sdk form, #:property path, variable update failures)

Tests:
- Assert exact version ranges on discovered references
- Add Discover_FileBasedApp_SdkBeforePropertyDefinitions_ResolvesForwardReferences
- Assert improved failure output for property-reference SDK directives
@devlead
devlead requested a review from Copilot June 3, 2026 14:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Address PR feedback for file-based app upgrade behavior, reference
discovery, and xUnit test attribute visibility.

DotNetPackageService:
- When UpdatePackageVariable or UpdateFileBasedAppDirectReference
  returns false, verify the #:property or directive already matches
  the requested version before failing
- Treat unchanged file-based app content as a successful upgrade

VariableTrackingService:
- Deduplicate DiscoverFileBasedAppReferences by (Kind, Name) so the
  same id in both #:package and #:sdk is not collapsed

EndToEndTests:
- Move RequiresFileBasedAppSdkAttribute to namespace scope with
  explicit FactAttribute documentation for test discovery
- Hoist DotNetSdkDetector alongside the attribute

Tests:
- Add no-op success coverage for variable-backed and literal #:sdk paths
- Add Discover_FileBasedAppReferences_PreservesDistinctPackageAndSdkWithSameName
- Assert file-based app failure hints on RunStatus.Errors, not Output
@devlead
devlead requested a review from Copilot June 3, 2026 15:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Comment thread .github/workflows/test.yml Resolved
devlead added 2 commits June 3, 2026 17:13
When a file-based app declares the same id in both #:package and #:sdk,
ProjectAnalysisService previously keyed TargetFramework.Dependencies
only by reference.Name, so one directive silently overwrote the other.

FileBasedAppReferenceHelper:
- Add GetDependencyDictionaryKey to produce kind-specific keys
  ({name}#package vs {name}#sdk) while keeping Dependency.Name as the
  NuGet package id

ProjectAnalysisService:
- Store file-based app references under kind-specific dictionary keys
- Remove graph-only dependencies by Dependency.Name rather than key,
  so stripping still works with the new key scheme

Tests:
- Add GetDependencyDictionaryKey_DistinguishesPackageAndSdkWithSameName
…mantics

Address PR feedback on IVariableTrackingService breaking changes and
file-based app property resolution order.

IVariableTrackingService:
- Restore void UpdatePackageVariable for backward-compatible callers
- Add TryUpdatePackageVariable returning whether any file was updated
- VariableTrackingService implements void by delegating to TryUpdate

VariableTrackingService:
- Let later #:property lines in the same .cs file override earlier
  definitions (last definition wins), matching MSBuild-like expectations
- Keep first-wins behavior for imported .props files unchanged

DotNetPackageService:
- Use TryUpdatePackageVariable when file-based app upgrades need to
  distinguish updated vs no-op/failure

Tests:
- Add Discover_FileBasedApp_LaterPropertyDefinitionWinsWithinFile
- Assert update success via TryUpdatePackageVariable where a bool is required
- Update mocks to stub TryUpdatePackageVariable
@devlead
devlead requested a review from Copilot June 3, 2026 15:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment on lines +654 to +656
if (!TryParsePackageDirective(directive, out var packageExpression, out var versionExpression) ||
!TryResolveProperties(packageExpression, propertyDefinitions, out var packageName) ||
result.ContainsKey(packageName))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not planning to change this, by design.

The realistic case — literal #:package SharedId@1.0.0 + #:sdk SharedId@2.0.0 — is already handled: literal references go through a List and are de-duplicated by (Kind, Name), so both survive (covered by Discover_FileBasedAppReferences_PreservesDistinctPackageAndSdkWithSameName).

The remaining gap is only the variable-backed same-id collision in the PackageVariableInfo map. Re-keying that map by (kind, name) doesn't stand alone: the update path (DotNetPackageService.AddPackage) looks the entry up by package name only and has no Kind in scope, because the upgrade pipeline identifies dependencies by name throughout. Distinguishing kinds there would require threading Kind through IDotNetPackageService.AddPackage — a broader public API change — for a scenario that doesn't occur in practice (a NuGet id used simultaneously as a package dependency and an MSBuild SDK). The cost/risk isn't justified, so I'm leaving the variable map name-keyed and documenting it as a known limitation.

slang25 and others added 2 commits June 3, 2026 20:39
Directive references (#:package / #:sdk) were stored with an exact
[X, X] version range, so ResolvePackageVersions could never resolve a
newer version and outdated SDKs were never reported. Use a
minimum-inclusive, open-upper-bound range instead.

ApplyFileBasedAppDirectives kept the dependency-graph entry for any
package also declared as a directive and then added a second entry under
the name#package key, duplicating the dependency. Remove all
non-transitive graph entries before re-adding the authoritative directive
references. The two issues previously masked each other: the exact range
suppressed the duplicate package entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses Copilot review feedback: after killing the dotnet --list-sdks
process on timeout, wait for it to actually exit before returning so the
terminating child process is reaped and resources are released.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

…s, path normalization

- TryParsePackageDirective now strips trailing content (e.g. inline
  '// pinned' comments) so directives like '#:sdk Cake.Sdk@6.0.0 // pinned'
  are discovered/analyzed, matching UpdateFileBasedAppDirectReference which
  already preserves trailing content on upgrade.
- File-based '#:property' update regexes now use RegexOptions.IgnoreCase so a
  '#:property sdkversion=...' referenced as $(SdkVersion) updates correctly,
  consistent with the case-insensitive propertyDefinitions lookup.
- ApplyFileBasedAppDirectives now uses the normalized analyzedProjectPath so
  directive discovery and its cache key off the same path as restore/asset
  loading even when the caller passes a relative path.
- Adds tests for trailing-comment discovery and case-insensitive property update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@slang25 slang25 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work 🙂

@slang25
slang25 merged commit 938d4e2 into dotnet-outdated:master Jun 3, 2026
16 checks passed
@slang25 slang25 mentioned this pull request Jun 11, 2026
slang25 added a commit that referenced this pull request Jun 11, 2026
Patch release: adds `#:sdk` support for .NET file-based apps (#755).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
github-actions Bot pushed a commit to EelcoLos/nx-tinkering that referenced this pull request Jun 16, 2026
Updated
[dotnet-outdated-tool](https://github.com/dotnet-outdated/dotnet-outdated)
from 4.8.0 to 4.8.1.

<details>
<summary>Release notes</summary>

_Sourced from [dotnet-outdated-tool's
releases](https://github.com/dotnet-outdated/dotnet-outdated/releases)._

## 4.8.1

## What's Changed
* Add `#:sdk` support for .NET file-based apps by @​devlead in
dotnet-outdated/dotnet-outdated#755

## New Contributors
* @​devlead made their first contribution in
dotnet-outdated/dotnet-outdated#755

**Full Changelog**:
dotnet-outdated/dotnet-outdated@v4.8.0...v4.8.1

Commits viewable in [compare
view](dotnet-outdated/dotnet-outdated@v4.8.0...v4.8.1).
</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
hwinther pushed a commit to hwinther/test that referenced this pull request Jun 25, 2026
Updated
[dotnet-outdated-tool](https://github.com/dotnet-outdated/dotnet-outdated)
from 4.8.0 to 4.8.1.

<details>
<summary>Release notes</summary>

_Sourced from [dotnet-outdated-tool's
releases](https://github.com/dotnet-outdated/dotnet-outdated/releases)._

## 4.8.1

## What's Changed
* Add `#:sdk` support for .NET file-based apps by @​devlead in
dotnet-outdated/dotnet-outdated#755

## New Contributors
* @​devlead made their first contribution in
dotnet-outdated/dotnet-outdated#755

**Full Changelog**:
dotnet-outdated/dotnet-outdated@v4.8.0...v4.8.1

Commits viewable in [compare
view](dotnet-outdated/dotnet-outdated@v4.8.0...v4.8.1).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dotnet-outdated-tool&package-manager=nuget&previous-version=4.8.0&new-version=4.8.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
slang25 added a commit to GREsau/dotnet-outdated that referenced this pull request Nov 8, 2026
Resolves conflicts with variable tracking (dotnet-outdated#714), the CPM --no-restore
direct write (dotnet-outdated#713), file-based apps (dotnet-outdated#752/dotnet-outdated#755) and short TFM names (dotnet-outdated#750).
AddPackage now takes the VersionRange through all of master's new paths;
single-version paths use the range's lower bound for now (refined in
follow-up commits). Restores the System.Text.Json using needed by master's
tests and adapts DotNetPackageServiceTests to the new signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants