Skip to content

Commit 17c4faa

Browse files
authored
Add automatic migration for deprecated packages (#514)
1 parent ac4b8d2 commit 17c4faa

6 files changed

Lines changed: 746 additions & 4 deletions

File tree

claude.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,59 @@ PackageUpdate is a .NET global tool that updates NuGet packages for all solution
1212
- Uses C# preview language features (`LangVersion>preview`)
1313
- Central Package Management (CPM) is required for all target solutions
1414

15+
## Coding Conventions
16+
17+
### Lambda Expressions
18+
19+
Always use underscore `_` for single-parameter lambda expressions instead of named parameters:
20+
21+
```csharp
22+
// ✅ Correct
23+
packages.Where(_ => _.Id == "MyPackage")
24+
packages.FirstOrDefault(_ => _.Version == "1.0.0")
25+
packages.OrderByDescending(_ => _)
26+
elements.Any(_ => _.IsEnabled)
27+
28+
// ❌ Incorrect - don't use named parameters
29+
packages.Where(p => p.Id == "MyPackage")
30+
packages.FirstOrDefault(pkg => pkg.Version == "1.0.0")
31+
elements.Any(e => e.IsEnabled)
32+
```
33+
34+
This applies even when the parameter is used multiple times in the expression:
35+
36+
```csharp
37+
// ✅ Correct
38+
xml.Descendants("PackageVersion")
39+
.FirstOrDefault(_ =>
40+
string.Equals(
41+
_.Attribute("Include")?.Value,
42+
packageName,
43+
StringComparison.OrdinalIgnoreCase))
44+
45+
// ❌ Incorrect
46+
xml.Descendants("PackageVersion")
47+
.FirstOrDefault(e =>
48+
string.Equals(
49+
e.Attribute("Include")?.Value,
50+
packageName,
51+
StringComparison.OrdinalIgnoreCase))
52+
```
53+
54+
**Exception:** Use descriptive parameter names when creating complex anonymous types or when the lambda body is long and clarity would benefit from a meaningful name:
55+
56+
```csharp
57+
// Named parameter acceptable for complex Select projections
58+
var packageVersions = xml.Descendants("PackageVersion")
59+
.Select(element => new
60+
{
61+
Element = element,
62+
Package = element.Attribute("Include")?.Value,
63+
CurrentVersion = element.Attribute("Version")?.Value,
64+
Pinned = element.Attribute("Pinned")?.Value == "true"
65+
})
66+
```
67+
1568
## Build and Test Commands
1669

1770
```bash
@@ -46,10 +99,11 @@ packageupdate --build
4699

47100
### Key Components
48101

49-
- **Updater.cs**: Core update logic
102+
- **Updater.cs**: Core update and migration logic
50103
- Parses `Directory.Packages.props` XML
51104
- Respects `Pinned="true"` attribute to skip packages
52105
- Queries NuGet sources for latest versions via NuGet.Protocol API
106+
- Detects deprecated packages and auto-migrates to alternatives when current version is deprecated
53107
- Preserves file formatting (newlines, indentation, trailing newlines)
54108
- Only considers stable versions when current version is stable
55109
- Only considers pre-release versions when current version is pre-release
@@ -93,6 +147,50 @@ The tool only works with CPM. Each solution must have a `Directory.Packages.prop
93147

94148
Packages with `Pinned="true"` attribute are never updated, even when explicitly targeted via `--package` flag.
95149

150+
### Package Migration
151+
152+
The tool automatically detects and migrates deprecated packages when an alternative is available.
153+
154+
#### How It Works
155+
156+
When updating packages, PackageUpdate checks if the **current version** of a package is marked as deprecated in NuGet. If the package has an alternative specified and that alternative is available in configured NuGet sources, the tool will automatically migrate:
157+
158+
1. Replaces the `Include` attribute with the alternative package name
159+
2. Sets the `Version` to the latest version of the alternative (or the minimum version from the range if specified)
160+
3. Logs the migration with deprecation reason
161+
162+
#### Migration Examples
163+
164+
```xml
165+
<!-- Before -->
166+
<PackageVersion Include="WindowsAzure.Storage" Version="9.3.3" />
167+
168+
<!-- After (migrated) -->
169+
<PackageVersion Include="Azure.Storage.Common" Version="12.26.0" />
170+
```
171+
172+
#### Migration Behavior
173+
174+
- **Pinned packages**: Never migrated (Pinned="true" is respected)
175+
- **No alternative available**: Package version updated normally, warning logged
176+
- **Alternative not found**: Package version updated normally, warning logged
177+
- **Alternative already exists**: Migration skipped, warning logged, both packages remain
178+
- **--package flag**: Migrations still occur for specified deprecated packages
179+
- **Current version check**: Only migrates if the **current** version is deprecated, not if only newer versions are deprecated
180+
181+
#### Migration Logging
182+
183+
Successful migration:
184+
```
185+
Migrated WindowsAzure.Storage -> Azure.Storage.Common (Version: 12.26.0) [Deprecated: Legacy]
186+
```
187+
188+
Deprecation warnings (when no migration possible):
189+
```
190+
Package WindowsAzure.Storage is deprecated but has no alternative. Reasons: Legacy
191+
Package WindowsAzure.Storage is deprecated with alternative Azure.Storage.Common, but alternative already exists
192+
```
193+
96194
### Version Selection Logic
97195

98196
- Uses `FindPackageByIdResource` to query all versions efficiently

readme.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,199 @@ The next time you run the updater, it will update to the latest version.
317317
- Comments and formatting around pinned packages are preserved during updates
318318

319319

320+
## Automatic Package Migration
321+
322+
323+
### Overview
324+
325+
PackageUpdate automatically detects and migrates deprecated NuGet packages to their recommended alternatives. When a package is marked as deprecated on NuGet.org with an alternative package specified, the tool will automatically replace it during updates.
326+
327+
328+
### How It Works
329+
330+
When updating packages, PackageUpdate:
331+
332+
1. Checks if the **current version** of each package is marked as deprecated
333+
2. If an alternative package is specified in the deprecation metadata:
334+
- Verifies the alternative package exists in configured NuGet sources
335+
- Checks that the alternative doesn't already exist in `Directory.Packages.props`
336+
- Replaces the package reference with the alternative
337+
- Sets the version to the latest available version of the alternative
338+
3. Logs the migration with the deprecation reason
339+
340+
341+
### Example Migration
342+
343+
**Before:**
344+
345+
```xml
346+
<Project>
347+
<ItemGroup>
348+
<PackageVersion Include="WindowsAzure.Storage" Version="9.3.3" />
349+
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
350+
</ItemGroup>
351+
</Project>
352+
```
353+
354+
**After running `packageupdate`:**
355+
356+
```xml
357+
<Project>
358+
<ItemGroup>
359+
<PackageVersion Include="Azure.Storage.Common" Version="12.26.0" />
360+
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
361+
</ItemGroup>
362+
</Project>
363+
```
364+
365+
Console output:
366+
367+
```
368+
Migrated WindowsAzure.Storage -> Azure.Storage.Common (Version: 12.26.0) [Deprecated: Legacy]
369+
Updated Newtonsoft.Json: 13.0.1 -> 13.0.3
370+
```
371+
372+
373+
### Migration Behavior
374+
375+
376+
#### Automatic by Default
377+
378+
Migrations happen automatically without requiring any flags or configuration. The tool detects deprecated packages and migrates them seamlessly.
379+
380+
381+
#### Pinned Packages Are Never Migrated
382+
383+
If a package is pinned, it will not be migrated even if it's deprecated:
384+
385+
```xml
386+
<PackageVersion Include="WindowsAzure.Storage" Version="9.3.3" Pinned="true" />
387+
```
388+
389+
This package will remain unchanged.
390+
391+
392+
#### When Alternative Already Exists
393+
394+
If the alternative package already exists in `Directory.Packages.props`, the migration is skipped:
395+
396+
```xml
397+
<Project>
398+
<ItemGroup>
399+
<PackageVersion Include="WindowsAzure.Storage" Version="9.3.3" />
400+
<PackageVersion Include="Azure.Storage.Common" Version="12.0.0" />
401+
</ItemGroup>
402+
</Project>
403+
```
404+
405+
Output:
406+
407+
```
408+
Package WindowsAzure.Storage is deprecated with alternative Azure.Storage.Common, but alternative already exists
409+
```
410+
411+
Both packages remain in the file, and only `Azure.Storage.Common` gets updated to the latest version.
412+
413+
414+
#### When No Alternative is Available
415+
416+
If a package is deprecated but has no alternative specified, the tool logs a warning and continues with normal version update:
417+
418+
```
419+
Package SomeDeprecatedPackage is deprecated but has no alternative. Reasons: Legacy
420+
```
421+
422+
423+
#### Current Version Check
424+
425+
The tool only migrates if the **current** version you're using is deprecated. If you're on an older, non-deprecated version, and only newer versions are deprecated, no migration occurs. This prevents unnecessary migrations when you're deliberately staying on an older version.
426+
427+
428+
#### Specific Package Flag
429+
430+
The migration feature works with the `--package` flag:
431+
432+
```bash
433+
packageupdate --package WindowsAzure.Storage
434+
```
435+
436+
If `WindowsAzure.Storage` is deprecated with an alternative, it will be migrated automatically.
437+
438+
439+
### Common Scenarios
440+
441+
442+
#### Scenario 1: Microsoft Azure SDK Packages
443+
444+
Many older Azure SDK packages have been deprecated in favor of the new Azure SDK:
445+
446+
- `WindowsAzure.Storage``Azure.Storage.Common` or `Azure.Storage.Blobs`
447+
- `Microsoft.Azure.Storage.Blob``Azure.Storage.Blobs`
448+
- `Microsoft.Azure.DocumentDB``Microsoft.Azure.Cosmos`
449+
450+
These migrations happen automatically when you run `packageupdate`.
451+
452+
453+
#### Scenario 2: Preventing Migration
454+
455+
If you want to prevent migration of a deprecated package (e.g., you're not ready to migrate yet), pin the package:
456+
457+
```xml
458+
<!-- Pinned: Not ready to migrate to Azure.Storage.Blobs yet -->
459+
<PackageVersion Include="WindowsAzure.Storage" Version="9.3.3" Pinned="true" />
460+
```
461+
462+
463+
#### Scenario 3: Manual Review After Migration
464+
465+
After automatic migration, you may want to:
466+
467+
1. Review the changes in `Directory.Packages.props`
468+
2. Update your code to use the new package's API (if breaking changes exist)
469+
3. Test thoroughly before committing
470+
471+
The migration updates the package reference but doesn't modify your source code.
472+
473+
474+
### Logging
475+
476+
477+
#### Successful Migration
478+
479+
```
480+
Migrated WindowsAzure.Storage -> Azure.Storage.Common (Version: 12.26.0) [Deprecated: Legacy]
481+
```
482+
483+
484+
#### Migration Skipped (Alternative Exists)
485+
486+
```
487+
Package WindowsAzure.Storage is deprecated with alternative Azure.Storage.Common, but alternative already exists
488+
```
489+
490+
491+
#### Migration Skipped (No Alternative)
492+
493+
```
494+
Package MyOldPackage is deprecated but has no alternative. Reasons: Legacy, Critical Bugs
495+
```
496+
497+
498+
#### Migration Skipped (Alternative Not Found)
499+
500+
```
501+
Package OldPackage is deprecated with alternative NewPackage, but alternative not found in sources
502+
```
503+
504+
505+
### Technical Details
506+
507+
- Migration uses NuGet's official deprecation metadata API (`PackageDeprecationMetadata`)
508+
- The `AlternatePackage` information comes directly from package authors via NuGet.org
509+
- File formatting, comments, and XML structure are preserved during migration
510+
- Migrations are logged distinctly from version updates for clarity
511+
512+
320513
## Icon
321514

322515
[Update](https://thenounproject.com/search/?q=update&i=2060555) by [Andy Miranda](https://thenounproject.com/andylontuan88) from [The Noun Project](https://thenounproject.com/).

0 commit comments

Comments
 (0)