NuGet 3 tool, as it is expected from a package manager, by itself built using packages. These packages are published on the NuGet.org gallery and can be used by any applications required NuGet-like features. Usage scenarios include plugins, packaged as .nupkg, application content, package-based installers and others. Several projects, like Chocolatey or Wyam, already integrate NuGet for the different proposes, however for the really wide adoption of the NuGet libraries, a better API documentation is required.

This post demonstrates one of the ways of incorporating the NuGet libraries in an application. Dave Glick, an author of Wyam’s, has a great introduction to NuGet v3 APIs and I recommend to read his posts before continuing, however it is not required. The NuGet usage approach described in this post is different from the approach reviewed in the mentioned articles. When applied, it allows to create .NET Standard compatible libraries and incorporate the NuGet tooling not only in .NET Framework applications, but also in .NET Core solutions.

NuGet 3 uses a zillion of libraries. Unlike NuGet 2, composed from just a few libraries, NuGet 3 design is based on multiple small libraries. For example, the post’s sample code uses nine libraries. Another note about the API – it is still in development. Post is based on version 3.5.0-rc1-final of NuGet and before the release some APIs may change.

Top level NuGet libraries used by the solutions are NuGet.DependencyResolver and NuGet.Protocol.Core.v3.

Workflow

The logical workflow is similar to NuGet restore command and from the developer perspective it includes the following phases:

Main concepts

Prepare package sources

The following code adds the official NuGet feed as the package source and registers the sources in the RemoteDependencyWalker’s context.

var resourceProviders = new List>();
resourceProviders.AddRange(Repository.Provider.GetCoreV3());
 
var repositories = new List
{
    new SourceRepository(new PackageSource("https://api.nuget.org/v3/index.json"), resourceProviders)
};
 
var cache = new SourceCacheContext();
var walkerContext = new RemoteWalkContext();
 
foreach (var sourceRepository in repositories)
{
    var provider = new SourceRepositoryDependencyProvider(sourceRepository, _logger, cache, true);
    walkerContext.RemoteLibraryProviders.Add(provider);
}

Identify a list of packages to install

RemoteDependencyWalker accepts only one root library to calculate the dependencies. In case of the multiple root target libraries, they should be wrapped inside of a fake library and IProjectDependencyProvider allows to include the fake library in the dependency resolution process.

IProjectDependencyProvider defines SupportsType method, which allows to control library types handled by the class and GetLibrary method which is expected to return the library object.

The trick is to define the fake library as a LibraryDependencyTarget.Project and only accept this type of libraries to be resolved by ProjectDependencyProvider. So, when RemoteDependencyWalker will ask for the instance of the fake library, it can be constructed with the list of targeted libraries as dependencies. For example, the following code assumes that two NuGet libs are the targeted libraries to install.

public Library GetLibrary(LibraryRange libraryRange, NuGetFramework targetFramework, string rootPath)
{
    var dependencies = new List();
 
    dependencies.AddRange( new []
    {
        new LibraryDependency
        {
            LibraryRange = new LibraryRange("NuGet.Protocol.Core.v3", VersionRange.Parse("3.0.0"), LibraryDependencyTarget.Package)
        },
        new LibraryDependency
        {
            LibraryRange = new LibraryRange("NuGet.DependencyResolver", VersionRange.Parse("3.0.0"), LibraryDependencyTarget.Package)
        },
    });
 
    return new Library
    {
        LibraryRange = libraryRange,
        Identity = new LibraryIdentity
        {
            Name = libraryRange.Name,
            Version = NuGetVersion.Parse("1.0.0"),
            Type = LibraryType.Project,
        },
        Dependencies = dependencies,
        Resolved = true
    };
}

Dependency discovery

When all preparations are done, RemoteDependencyWalker can start to discover the dependencies

walkerContext.ProjectLibraryProviders.Add(new ProjectLibraryProvider());

var fakeLib = new LibraryRange("FakeLib", VersionRange.Parse("1.0.0"), LibraryDependencyTarget.Project);
var frameworkVersion = FrameworkConstants.CommonFrameworks.Net461;
var walker = new RemoteDependencyWalker(walkerContext);
 
GraphNode result = await walker.WalkAsync(
    fakeLib,
    frameworkVersion,
    frameworkVersion.GetShortFolderName(), RuntimeGraph.Empty, true);
 
foreach (var node in result.InnerNodes)
{
    await InstallPackageDependencies(node);
}

The provided code does more than the dependencies discovery. It defines the supported .NET framework version and it iterates through the result to install the packages.

Installing the packages

And now application is ready to install the discovered packages

HashSet _installedPackages = new HashSet();
 
private async Task InstallPackageDependencies(GraphNode node)
{
    foreach (var innerNode in node.InnerNodes)
    {
        if (!_installedPackages.Contains(innerNode.Key))
        {
            _installedPackages.Add(innerNode.Key);
            await InstallPackage(innerNode.Item.Data.Match);
        }
 
        await InstallPackageDependencies(innerNode);
    }
}
 
private async Task InstallPackage(RemoteMatch match)
{
    var packageIdentity = new PackageIdentity(match.Library.Name, match.Library.Version);
 
    var versionFolderPathContext = new VersionFolderPathContext(
        packageIdentity,
        @"D:\Temp\MyApp\",
        _logger,
        PackageSaveMode.Defaultv3,
        XmlDocFileSaveMode.None);
 
    await PackageExtractor.InstallFromSourceAsync(
        stream => match.Provider.CopyToAsync(
            match.Library,
            stream,
            CancellationToken.None),
        versionFolderPathContext,
        CancellationToken.None);
}

As the result of execution, all resolved packages will be de-duplicated and installed in D:\Temp\MyApp[package-name] subfolder. Each package subfolder includes .nupkg, .nuspec and libraries for all supported frameworks.

And that’s it. The provided code demonstrates the whole workflow. There are tons of small details hidden behind this simple demo, but it should be enough for staring your own experiments. Fill free to comment if you have any questions.

blog comments powered by Disqus