Posted in:

I recently needed to migrate NuGet packages from one feed to another. I decided that I would download the entire contents of the feed first which was on an on-premises server, allowing me to inspect what was in it and delete any obsolete stuff before pushing to the new location in Azure DevOps as an Azure Artifacts feed.

This can be automated quite easily in C# with the NuGet.Protocol library. We can use the PackageSearchResource to list all NuGet packages in the feed, by searching with an empty string. Then for each package, we can use IPackageSearchMetadata. to get all the versions. Then the FindPackageByIdResource can be used with the CopyNupkgToStreamAsync to save the contents of the feed to disk.

Obviously take care not to fill up your local hard disk with this!

Downloading packages

First, here's my code to download all packages from a NuGet feed.

ILogger logger = NullLogger.Instance;
CancellationToken cancellationToken = CancellationToken.None;
var nugetFeed = $"http://mynugetserver/repository/myfeed/";
var downloadPath = $"C:\\NuGetDump\\";
if (!Directory.Exists(downloadPath))
    Directory.CreateDirectory(downloadPath);
    
SourceRepository repository = Repository.Factory.GetCoreV3(nugetFeed);
SourceCacheContext cache = new SourceCacheContext();
PackageSearchResource packageSearchResource = await repository.GetResourceAsync<PackageSearchResource>();
FindPackageByIdResource findPackageByIdResource = await repository.GetResourceAsync<FindPackageByIdResource>();
SearchFilter searchFilter = new SearchFilter(includePrerelease: true);
int skip = 0;
long bytesDownloaded = 0;
while(true)
{
    var results = (await packageSearchResource.SearchAsync(
        "", // search string
        searchFilter,
        skip: skip,
        take: 20,
        logger,
        cancellationToken)).ToList();
    if (results.Count == 0) break;
    skip += results.Count;
    Console.WriteLine("--");    
    foreach (IPackageSearchMetadata result in results)
    {
        Console.WriteLine($"package {result.Identity.Id} {result.Identity.Version}");
        var versions = await result.GetVersionsAsync();
        foreach(var v in versions)
        {
            using var packageStream = File.OpenWrite($"{downloadPath}{result.Identity.Id}.{v.Version}.nupkg");
            await findPackageByIdResource.CopyNupkgToStreamAsync(
                result.Identity.Id, // package id
                v.Version,
                packageStream,
                cache,
                logger,
                cancellationToken);
            Console.WriteLine($"   downloaded version {v.Version} {packageStream.Length} bytes");
            bytesDownloaded += packageStream.Length;
        }
    }
}
Console.WriteLine($"Downloaded {bytesDownloaded}");

Pushing to Azure DevOps

The next step after clearing out unwanted packages was upload each one into Azure Artifacts. This was also relatively trivial with the NuGet.Protocol library. The most complicated thing is that you need to create a personal access token with permissions to push to the feed. You then use that in a PackageSourceCredential that you configure in your PackageSource.

Once you've connected, you then just need to call Push on the PackageUpdateResource. In my example I'm looping through every .nupkg file in a folder and pushing it. This will fail if the feed already contains package with the same name and version.

Note that my sample code has a username and an API Key - both can be set to dummy values as the PAT is the only thing needed to authorize pushing to the feed.

ILogger logger = NullLogger.Instance;
CancellationToken cancellationToken = CancellationToken.None;
var targetFeed = "https://pkgs.dev.azure.com/MyOrg/_packaging/MyFeed/nuget/v3/index.json";
var nugetPath = @"C:\NuGetDump\";
var patToken = "your-token-here";

var packageSource = new PackageSource(targetFeed)
{
    Credentials = new PackageSourceCredential(
        source: targetFeed,
        username: "myUsername", // not important
        passwordText: patToken,
        isPasswordClearText: true,
        validAuthenticationTypesText: null)
};
SourceRepository repository = Repository.Factory.GetCoreV3(packageSource);
PackageUpdateResource resource = await repository.GetResourceAsync<PackageUpdateResource>();

string apiKey = "my-api-key"; // apparently not needed

foreach(var file in System.IO.Directory.EnumerateFiles(nugetPath, "*.nupkg"))
{
    Console.WriteLine($"pushing {file}");
    try
    {
        await resource.Push(
            file,
            symbolSource: null,
            timeoutInSecond: 5 * 60,
            disableBuffering: false,
            getApiKey: packageSource => apiKey,
            getSymbolApiKey: packageSource => null,
            noServiceEndpoint: false,
            skipDuplicate: true,
            symbolPackageUpdateResource: null,
            logger);

    }
    catch (Exception e)
    {
        Console.WriteLine("ERROR " + e.ToString());
    }
}

Summary

The NuGet.Protocol library makes it very easy to automate tasks involving downloading and pushing NuGet packages in C#. Of course you could easily do the same thing with PowerShell and the nuget command line tools, but I still generally prefer to automate things like this with C#, usually using LINQPad which is excellent as a lightweight dev environment.

Comments

Comment by 陳奐廷

Thank you for saving my life

陳奐廷