Categories
c# Uncategorized

Detecting Init-Only Properties with Reflection in C# 9

This is a super-quick post to hopefully save someone the trouble of figuring out how to detect init-only properties (as I had to this morning).

Init-only properties are a new feature in C# 9, and let you define properties that can only be set in the constructor or in an object initialisation block:

public class MyObject
{
    public string InitOnlyProperty { get; init; }
}

static void Main(string[] args)
{
    var newInstance = new MyObject
    {
        InitOnlyProperty = "value"
    };

    // Compiler error!!
    newInstance.InitOnlyProperty = "something";
}

The init; used instead of set; marks the property as init-only.

If you want to check if any given property is init-only at runtime, it’s a little more complicated than just checking a single flag on PropertyInfo.

Looking at the generated IL for our code in dotPeek, we can see that the generated set method’s return value has an additional modreq option for System.Runtime.CompilerServices.IsExternalInit.

// .property instance string InitOnlyProperty()
public string InitOnlyProperty
{
    // .method public hidebysig specialname instance string
        // get_InitOnlyProperty() cil managed
    [CompilerGenerated] get
    // .maxstack 8
    // 
    // IL_0000: ldarg.0      // this
    // IL_0001: ldfld        string InitOnlyDetection.Program/MyObject::'<InitOnlyProperty>k__BackingField'
    // IL_0006: ret
    // 
    {
        return this.\u003CInitOnlyProperty\u003Ek__BackingField;
    }

    // .method public hidebysig specialname instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)
        // set_InitOnlyProperty(
        // string 'value'
        // ) cil managed
    [CompilerGenerated] set
    // .maxstack 8
    // 
    // IL_0000: ldarg.0      // this
    // IL_0001: ldarg.1      // 'value'
    // IL_0002: stfld        string InitOnlyDetection.Program/MyObject::'<InitOnlyProperty>k__BackingField'
    // IL_0007: ret
    // 
    {
        this.\u003CInitOnlyProperty\u003Ek__BackingField = value;
    }
}

So, we can detect init-only properties with the following extension method:

using System.Linq;
using System.Reflection;

public static class PropertyExtensions
{
    /// <summary>
    /// Determines if this property is marked as init-only.
    /// </summary>
    /// <param name="property">The property.</param>
    /// <returns>True if the property is init-only, false otherwise.</returns>
    public static bool IsInitOnly(this PropertyInfo property)
    {
        if (!property.CanWrite)
        {
            return false;
        }

        var setMethod = property.SetMethod;

        // Get the modifiers applied to the return parameter.
        var setMethodReturnParameterModifiers = setMethod.ReturnParameter.GetRequiredCustomModifiers();

        // Init-only properties are marked with the IsExternalInit type.
        return setMethodReturnParameterModifiers.Contains(typeof(System.Runtime.CompilerServices.IsExternalInit));
    }
}

There you go, told you it would be super quick!

Categories
c# Uncategorized

Making Users Re-Enter their Password: ASP.NET Core & IdentityServer4

It’s often good security behaviour to require users to re-enter their password when they want to change some secure property of their account, like generate personal access tokens, or change their Multi-factor Authentication (MFA) settings.

You may have seen the Github ‘sudo’ mode, which asks you to re-enter your password when you try to change something sensitive.

Sudo Mode Dialog
The Github sudo mode prompt.

Most of the time a user’s session is long-lived, so when they want to do something sensitive, it’s best to check they still are who they say.

I’ve been working on the implementation of IdentityServer4 at Enclave for the past week or so, and had this requirement to require password confirmation before users can modify their MFA settings.

I thought I’d write up how I did this for posterity, because it took a little figuring out.

The Layout

In our application, we have two components, both running on ASP.NET Core 3.1

  • The Accounts app that holds all the user data; this is where Identity Server runs; we use ASP.NET Core Identity to do the actual user management.
  • The Portal app that holds the UI. This is a straightforward MVC app right now, no JS or SPA to worry about.

To make changes to a user’s account settings, the Profile Controller in the Portal app makes API calls to the Accounts app.

The Portal calls APIs in the Accounts app

All the API calls to the Accounts app are already secured using the Access Token from when the user logged in; we have an ASP.NET Core Policy in place for our additional API (as per the IdentityServer docs) to protect it.

The Goal

The desired outcome here is that specific sensitive API endpoints within the Accounts app require the calling user to have undergone a second verification, where they must have re-entered their password recently in order to use the API.

What we want to do is:

  • Allow the Portal app to request a ‘step-up’ access token from the Accounts app.
  • Limit the step-up access token to a short lifetime (say 15 minutes), with no refresh tokens.
  • Call a sensitive API on the Accounts App, and have the Accounts App validate the step-up token.

Issuing the Step-Up Token

First up, we need to generate a suitable access token when asked. I’m going to add a new controller, StepUpApiController, in the Accounts app.

This controller is going to have a single endpoint, which requires a regular access token before you can call it.

We’re going to use the provided IdentityServerTools class, that we can inject into our controller, to do the actual token generation.

Without further ado, let’s look at the code for the controller:

[Route("api/stepup")]
[ApiController]
[Authorize(ApiScopePolicy.WriteUser)]
public class StepUpApiController : ControllerBase
{
    private static readonly TimeSpan ValidPeriod = TimeSpan.FromMinutes(15);

    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IdentityServerTools _idTools;

    public StepUpApiController(UserManager<ApplicationUser> userManager,
                               IdentityServerTools idTools)
    {
        _userManager = userManager;
        _idTools = idTools;
    }

    [HttpPost]
    public async Task<StepUpApiResponse> StepUp(StepUpApiModel model)
    {
        var user = await _userManager.GetUserAsync(User);

        // Verify the provided password.
        if (await _userManager.CheckPasswordAsync(user, model.Password))
        {
            var clientId = User.FindFirstValue(JwtClaimTypes.ClientId);

            var claims = new Claim[]
            {
                new Claim(JwtClaimTypes.Subject, User.FindFirstValue(JwtClaimTypes.Subject)),
            };

            // Create a token that:
            //  - Is associated to the User's client.
            //  - Is only valid for our configured period (15 minutes)
            //  - Has a single scope, indicating that the token can only be used for stepping up.
            //  - Has the same subject as the user.
            var token = await _idTools.IssueClientJwtAsync(
                clientId,
                (int)ValidPeriod.TotalSeconds,
                new[] { "account-stepup" },
                additionalClaims: claims);

            return new StepUpApiResponse { Token = token, ValidUntil = DateTime.UtcNow.Add(ValidPeriod) };
        }

        Response.StatusCode = StatusCodes.Status401Unauthorized;

        return null;
    }
}

A couple of important points here:

  • In order to even access this API, the normal access token being passed in the requested must conform to our own WriteUser scope policy, which requires a particular scope be in the access token to get to this API.
  • This generated access token is really basic; it has a single scope, “account-stepup”, and only a single additional claim containing the subject.
  • We associate the step-up token to the same client ID as the normal access token, so only the requesting client can use that token.
  • We explicitly state a relatively short lifetime on the token (15 minutes here).

Sending the Token

This is the easy bit; once you have the token, you can store it somewhere in the client, and send it in a subsequent request.

Before sending the step-up token, you’ll want to check the expiry on it, and if you need a new one, then prompt the user for their credentials and start the process again.

For any request to the sensitive API, we need to include both the normal access token from the user’s session, plus the new step-up token.

I set this up when I create the HttpClient:

private async Task<HttpClient> GetClient(string? stepUpToken = null)
{
    var client = new HttpClient();

    // Set the base address to the URL of our Accounts app.
    client.BaseAddress = _accountUrl;

    // Get the regular user access token in the session and add that as the normal
    // Authorization Bearer token.
    // _contextAccessor is an instance of IHttpContextAccessor.
    var accessToken = await _contextAccessor.HttpContext.GetUserAccessTokenAsync();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    if (stepUpToken is object)
    {
        // We have a step-up token; include it as an additional header (without the Bearer indicator).
        client.DefaultRequestHeaders.Add("X-Authorization-StepUp", stepUpToken);
    }

    return client;
}

That X-Authorization-StepUp header is where we’re going to look when checking for the token in the Accounts app.

Validating the Step-Up Token

To validate a provided step-up token in the Accounts app, I’m going to define a custom ASP.NET Core Policy that requires the API call to provide a step-up token.

If there are terms in here that don’t seem immediately obvious, check out the docs on Policy-based authorization in ASP.NET Core. It’s a complex topic, but the docs do a pretty good job of breaking it down.

Let’s take a look at an API call endpoint that requires step-up:

[ApiController]
[Route("api/user")]
[Authorize(ApiScopePolicy.WriteUser)]
public class UserApiController : Controller
{
    [HttpPost("totp-enable")]
    [Authorize("require-stepup")]
    public async Task<IActionResult> EnableTotp(TotpApiEnableModel model)
    {
        // ... do stuff ...
    }
}

That Authorize attribute I placed on the action method specifies that we want to enforce a require-stepup policy on this action. Authorize attributes are additive, so a request to EnableTotp requires both our normal WriteUser policy and our step-up policy.

Defining our Policy

To define our require-stepup policy, lets jump over to our Startup class; specifically, in ConfigureServices, where we set up Authorization using the AddAuthorization method:

services.AddAuthorization(options =>
{
    // Other policies omitted...

    options.AddPolicy("require-stepup", policy =>
    { 
        policy.AddAuthenticationSchemes("local-api-scheme");
        policy.RequireAuthenticatedUser();
        
        // Add a new requirement to the policy (for step-up).
        policy.AddRequirements(new StepUpRequirement());
    });
});

The ‘local-api-scheme’ is the built-in scheme provided by IdentityServer for protecting local API calls.

That requirement class, StepUpRequirement is just a simple marker class for indicating to the policy that we need step-up. It’s also how we wire up a handler to check that requirement:

public class StepUpRequirement : IAuthorizationRequirement
{
}

Defining our Authorization Handler

We now need an Authorization Handler that lets us check incoming requests meet our new step-up requirement.

So, let’s create one:

public class StepUpAuthorisationHandler : AuthorizationHandler<StepUpRequirement>
{
    private const string StepUpTokenHeader = "X-Authorization-StepUp";

    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ITokenValidator _tokenValidator;

    public StepUpAuthorisationHandler(
        IHttpContextAccessor httpContextAccessor,
        ITokenValidator tokenValidator)
    {
        _httpContextAccessor = httpContextAccessor;
        _tokenValidator = tokenValidator;
    }

    /// <summary>
    /// Called by the framework when we need to check a request.
    /// </summary>
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        StepUpRequirement requirement)
    {
        // Only interested in authenticated users.
        if (!context.User.IsAuthenticated())
        {
            return;
        }

        var httpContext = _httpContextAccessor.HttpContext;

        // Look for our special request header.
        if (httpContext.Request.Headers.TryGetValue(StepUpTokenHeader, out var stepUpHeader))
        {
            var headerValue = stepUpHeader.FirstOrDefault();

            if (!string.IsNullOrEmpty(headerValue))
            {
                // Call our method to check the token.
                var validated = await ValidateStepUp(context.User, headerValue);

                // Token was valid, so succeed.
                // We don't explicitly have to fail, because that is the default.
                if (validated)
                {
                    context.Succeed(requirement);
                }
            }
        }
    }

    private async Task<bool> ValidateStepUp(ClaimsPrincipal user, string header)
    {
        // Use the normal token validator to check the access token is valid, and contains our
        // special expected scope.
        var validated = await _tokenValidator.ValidateAccessTokenAsync(header, "account-stepup");

        if (validated.IsError)
        {
            // Bad token.
            return false;
        }

        // Validate that the step-up token is for the same client as the access token.
        var clientIdClaim = validated.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.ClientId);

        if (clientIdClaim is null || clientIdClaim.Value != user.FindFirstValue(JwtClaimTypes.ClientId))
        {
            return false;
        }

        // Confirm a subject is supplied.
        var subjectClaim = validated.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);

        if (subjectClaim is null)
        {
            return false;
        }

        // Confirm that the subject of the stepup and the current user are the same.
        return subjectClaim.Value == user.FindFirstValue(JwtClaimTypes.Subject);
    }
}

Again, let’s take a look at the important bits of the class:

  • The handler derives from AuthorizationHandler<StepUpRequirement>, indicating to ASP.NET that we are a handler for our custom requirement.
  • We stop early if there is no authenticated user; that’s because the step-up token is only valid for a user who is already logged in.
  • We inject and use IdentityServer’s ITokenValidator interface to let us validate the token using ValidateAccessTokenAsync; we specify the scope we require.
  • We check that the client ID of the step-up token is the same as the regular access token used to authenticate with the API.
  • We check that the subjects match (i.e. this step-up token is for the same user).

The final hurdle is to register our authorization handler in our Startup class:

services.AddSingleton<IAuthorizationHandler, StepUpAuthorisationHandler>();

Wrapping Up

There you go, we’ve now got a secondary access token being issued to indicate step-up has been done, and we’ve got a custom authorization handler to check our new token.

Categories
Uncategorized

Loading Plugins/Extensions at Run Time from NuGet in .NET Core : Part 1 – NuGet

This post is the first in a short series, writing up my efforts creating an plugin/extension system working in .NET Core that:

  • Loads extension packages from NuGet, with all their dependencies (this post).
  • Loads the extensions into my .NET Core Process.
  • Allows the loaded extensions to be unloaded.

Background

As a bit of context, I’m currently building an open-source BDD testing platform that goes beyond Gherkin, AutoStep, which is built entirely in C#, on top of .NET Core 3.1.

In AutoStep, I need to be able to load in extensions that provide additional functionality for AutoStep. For example, extensions might provide:

  • Bindings for some UI platform or library
  • Custom Report Formats
  • Integration with some external Test Management System

In terms of what’s in them, AutoStep extensions are going to consist of things like:

  • .NET DLLs
  • AutoStep Test Files
  • Dependencies on various NuGet packages (Selenium.WebDriver anyone?).

All of the above items fit pretty well within the scope of NuGet packages, and I don’t want to build my own extension packaging, hosting, versioning and so on, so I’m going to say that each extension can be represented by a NuGet package.

AutoStep does not require the .NET Core SDK to build or run any tests, so I can’t just create a csproj, chuck PackageReferences in and be done with it.

I need to bake the idea of extensions into the platform itself.


If you want to jump ahead, you can check out the GitHub repository for AutoStep.Extensions, which provides the NuGet package used to load extensions into our VS Code Language Server and our commmand-line runner.

Loading Extensions from NuGet

Microsoft supplies the NuGet Client SDK, to work with both NuGet packages and source repositories; specifically the NuGet.Protocol and NuGet.Resolver packages.

The documentation on how to actually use the NuGet Client libraries is a bit sparse, so I’m permanently indebted to Martin Bjorkstrom for writing a blog post on it that I used as a pretty detailed guide to get me started.

Loading our extension packages from NuGet involves three phases:

  1. Determine the best version of an extension package to install, given a version range (and normal NuGet rules).
    For example, if the version of the extension requested is 1.4.0, and there is a 1.4.5 version available, we want that one.
  2. Get the list of all NuGet package dependencies (recursively) for each extension.
  3. Download and Extract your packages.

Choosing the Extension Version

This is (relatively) the easy bit. First up, we’ll create some of the context objects we need to get started:

/// <summary>
/// Represents the configuration for a single extension to install.
/// </summary>
public class ExtensionConfiguration
{
    public string Package { get; set; }
    public string Version { get; set; }
    public bool PreRelease { get; set; }
}

public async Task LoadExtensions()
{
    // Define a source provider, with the main NuGet feed, plus my own feed.
    var sourceProvider = new PackageSourceProvider(NullSettings.Instance, new[]
    {
        new PackageSource("https://api.nuget.org/v3/index.json"),
        new PackageSource("https://f.feedz.io/autostep/ci/nuget/index.json")
    });

    // Establish the source repository provider; the available providers come from our custom settings.
    var sourceRepositoryProvider = new SourceRepositoryProvider(sourceProvider, Repository.Provider.GetCoreV3());

    // Get the list of repositories.
    var repositories = sourceRepositoryProvider.GetRepositories();

    // Disposable source cache.
    using var sourceCacheContext = new SourceCacheContext();

    // You should use an actual logger here, this is a NuGet ILogger instance.
    var logger = new NullLogger();

    // My extension configuration:
    var extensions = new[]
    { 
        new ExtensionConfiguration
        {
            Package = "AutoStep.Web",
            PreRelease = true // Allow pre-release versions.
        }
    };
}

Next, let’s write a method to actually get the desired package identity to install. The GetPackageIdentity method goes through each repository, and either:

  • Picks the latest available version if no version range has been configured or,
  • If a version range has been specified, uses the provided NuGet VersionRange class to find the best match given the set of all versions.
private async Task<PackageIdentity> GetPackageIdentity(
          ExtensionConfiguration extConfig, SourceCacheContext cache, ILogger nugetLogger,
          IEnumerable<SourceRepository> repositories, CancellationToken cancelToken)
{
    // Go through each repository.
    // If a repository contains only pre-release packages (e.g. AutoStep CI), and 
    // the configuration doesn't permit pre-release versions,
    // the search will look at other ones (e.g. NuGet).
    foreach (var sourceRepository in repositories)
    {
        // Get a 'resource' from the repository.
        var findPackageResource = await sourceRepository.GetResourceAsync<FindPackageByIdResource>();

        // Get the list of all available versions of the package in the repository.
        var allVersions = await findPackageResource.GetAllVersionsAsync(extConfig.Package, cache, nugetLogger, cancelToken);

        NuGetVersion selected;

        // Have we specified a version range?
        if (extConfig.Version != null)
        {
            if (!VersionRange.TryParse(extConfig.Version, out var range))
            {
                throw new InvalidOperationException("Invalid version range provided.");
            }

            // Find the best package version match for the range.
            // Consider pre-release versions, but only if the extension is configured to use them.
            var bestVersion = range.FindBestMatch(allVersions.Where(v => extConfig.PreRelease || !v.IsPrerelease));

            selected = bestVersion;
        }
        else
        {
            // No version; choose the latest, allow pre-release if configured.
            selected = allVersions.LastOrDefault(v => v.IsPrerelease == extConfig.PreRelease);
        }

        if (selected is object)
        {
            return new PackageIdentity(extConfig.Package, selected);
        }
    }

    return null;
}

Let’s plug that code into our previous code, so we’re now getting the identity:

// ...

// My extension configuration:
var extensions = new[] { new ExtensionConfiguration
{
    Package = "AutoStep.Web",
    PreRelease = true // Allow pre-release versions.
}};

foreach (var ext in extensions)
{
    var packageIdentity = await GetPackageIdentity(ext, sourceCacheContext, logger, repositories, CancellationToken.None);

    if (packageIdentity is null)
    {
        throw new InvalidOperationException($"Cannot find package {ext.Package}.");
    }
}

With this we get a package identity of AutoStep.Web.1.0.0-develop.20 (the latest pre-release version at the time).

Get the List of Package Dependencies

This is where things get interesting. We need to get the complete set of all dependencies, across all the extensions, that we need to install in order to use the extension package.

First off, let’s look at an initial, very naive solution, which just does a straight-forward recurse through the entire dependency graph.

private async Task GetPackageDependencies(PackageIdentity package, SourceCacheContext cacheContext, 
                                          NuGetFramework framework, ILogger logger, 
                                          IEnumerable<SourceRepository> repositories,
                                          ISet<SourcePackageDependencyInfo> availablePackages, 
                                          CancellationToken cancelToken)
{
    // Don't recurse over a package we've already seen.
    if (availablePackages.Contains(package))
    {
        return;
    }

    foreach (var sourceRepository in repositories)
    {
        // Get the dependency info for the package.
        var dependencyInfoResource = await sourceRepository.GetResourceAsync<DependencyInfoResource>();
        var dependencyInfo = await dependencyInfoResource.ResolvePackage(
            package,
            framework,
            cacheContext,
            logger,
            cancelToken);

        // No info for the package in this repository.
        if (dependencyInfo == null)
        {
            continue;
        }

        // Add to the list of all packages.
        availablePackages.Add(dependencyInfo);

        // Recurse through each package.
        foreach (var dependency in dependencyInfo.Dependencies)
        {
            await GetPackageDependencies(
                new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion),
                cacheContext,
                framework,
                logger,
                repositories,
                availablePackages,
                cancelToken);
        }

        break;
    }
}

That does indeed create the complete graph of all libraries required by that extension, the problem is that it has 104 packages in it!

Long package list

I’ve got the AutoStep.Web package at the top there, but I’ve also got
System.Runtime, which I definitely don’t want.

All the extensions are going to reference the AutoStep.Extensions.Abstractions package (because that’s where we define our interfaces for extensions), but we don’t want to download it ourselves!

Besides the fact that we don’t need to download these shared packages, if we load in the AutoStep.Extensions.Abstractions assembly from the extension’s dependencies, it will not be compatible with the version referenced by the host process.

The actual requirement for our behaviour here is:

All packages provided by the host process should be excluded from the set of dependencies to install.

Filtering the Dependencies

At runtime, how do we know what the set of installed packages are for a .NET Core Application? Luckily, there happens to be an existing file containing this information, the {AssemblyName}.deps.json file that gets copied to your output directory.

You probably haven’t had to worry about it much, but if you look in your application’s output directory, you’ll find it.

It contains the complete package reference graph for your application, and looks a little something like this:

{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v3.1",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v3.1": {
      "NugetConsole/1.0.0": {
        "dependencies": {
          "Microsoft.Extensions.DependencyModel": "3.1.3",
          "NuGet.Protocol": "5.5.1",
          "NuGet.Resolver": "5.5.1"
        },
        "runtime": {
          "NugetConsole.dll": {}
        }
      },
      "Microsoft.CSharp/4.0.1": {
        "dependencies": {
          "System.Collections": "4.3.0",
          "System.Diagnostics.Debug": "4.3.0",
          "System.Dynamic.Runtime": "4.3.0",
          "System.Globalization": "4.3.0",
          "System.Linq": "4.3.0",
          "System.Linq.Expressions": "4.3.0",
          "System.ObjectModel": "4.3.0",

// ...a lot more content...

Handily, we don’t have to parse this ourselves. If you add the Microsoft.Extensions.DependencyModel package to your project, you can directly access this content using DependencyContext.Default, which gives you a DependencyContext you can interrogate.

Let’s define a method that takes this DependencyContext and a PackageDependency, and checks whether it is provided by the host:

private bool DependencySuppliedByHost(DependencyContext hostDependencies, PackageDependency dep)
{
    // See if a runtime library with the same ID as the package is available in the host's runtime libraries.
    var runtimeLib = hostDependencies.RuntimeLibraries.FirstOrDefault(r => r.Name == dep.Id);

    if (runtimeLib is object)
    {
        // What version of the library is the host using?
        var parsedLibVersion = NuGetVersion.Parse(runtimeLib.Version);

        if (parsedLibVersion.IsPrerelease)
        {
            // Always use pre-release versions from the host, otherwise it becomes
            // a nightmare to develop across multiple active versions.
            return true;
        }
        else
        {
            // Does the host version satisfy the version range of the requested package?
            // If so, we can provide it; otherwise, we cannot.
            return dep.VersionRange.Satisfies(parsedLibVersion);
        }
    }

    return false;
}

Then, let’s plug that in to our existing GetPackageDependencies method:

private async Task GetPackageDependencies(PackageIdentity package, SourceCacheContext cacheContext, NuGetFramework framework, 
                                          ILogger logger, IEnumerable<SourceRepository> repositories, DependencyContext hostDependencies,
                                          ISet<SourcePackageDependencyInfo> availablePackages, CancellationToken cancelToken)
{
    // Don't recurse over a package we've already seen.
    if (availablePackages.Contains(package))
    {
        return;
    }

    foreach (var sourceRepository in repositories)
    {
        // Get the dependency info for the package.
        var dependencyInfoResource = await sourceRepository.GetResourceAsync<DependencyInfoResource>();
        var dependencyInfo = await dependencyInfoResource.ResolvePackage(
            package,
            framework,
            cacheContext,
            logger,
            cancelToken);

        // No info for the package in this repository.
        if (dependencyInfo == null)
        {
            continue;
        }


        // Filter the dependency info.
        // Don't bring in any dependencies that are provided by the host.
        var actualSourceDep = new SourcePackageDependencyInfo(
            dependencyInfo.Id,
            dependencyInfo.Version,
            dependencyInfo.Dependencies.Where(dep => !DependencySuppliedByHost(hostDependencies, dep)),
            dependencyInfo.Listed,
            dependencyInfo.Source);

        availablePackages.Add(actualSourceDep);

        // Recurse through each package.
        foreach (var dependency in actualSourceDep.Dependencies)
        {
            await GetPackageDependencies(
                new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion),
                cacheContext,
                framework,
                logger,
                repositories,
                hostDependencies,
                availablePackages,
                cancelToken);
        }

        break;
    }
}

This cuts down on the set of packages significantly, but it’s still pulling down some runtime-provided packages I don’t want:

AutoStep.Web : 1.0.0-develop.20       // correct
Selenium.Chrome.WebDriver : 79.0.0    // correct
Selenium.WebDriver : 3.141.0          // correct
Newtonsoft.Json : 10.0.3              // correct
Microsoft.CSharp : 4.3.0              // Ah. This is a runtime package...
System.ComponentModel.TypeConverter : 4.3.0 
System.Collections.NonGeneric : 4.3.0       
System.Collections.Specialized : 4.3.0
System.ComponentModel : 4.3.0
System.ComponentModel.Primitives : 4.3.0
System.Runtime.Serialization.Primitives : 4.3.0
System.Runtime.Serialization.Formatters : 4.3.0
System.Xml.XmlDocument : 4.3.0

So, something is still not right. What’s causing these packages to be present?

Well, simply put, my program doesn’t use System.ComponentModel, so it isn’t in the list of my dependencies. But it is provided by the host, because it’s part of the distributed .NET Runtime.

Ignoring Runtime-Provided Packages

We want to filter out runtime-provided packages completely, but how do we know which ones to exclude? We can’t just filter out any System.* packages, because there are a number of System.* packages that aren’t shipped with the runtime (e.g. System.Text.Json).

As far as I can tell, it’s more or less impossible to determine the full set at run time dynamically.

After some considerable searching however, I found a complete listing of all runtime-provided packages in an MSBuild task in the dotnet SDK, called PackageConflictOverrides, which tells the build system which packages don’t need to be restored! Yay!

This allowed me to define the following static lookup class (excerpt only). You can find a full version here.

/// <summary>
/// Contains a pre-determined list of NuGet packages that are provided by the run-time, and
/// therefore should not be restored from an extensions dependency list.
/// </summary>
internal static class RuntimeProvidedPackages
{
    /// <summary>
    /// Checks whether the set of known runtime packages contains the given package ID.
    /// </summary>
    /// <param name="packageId">The package ID.</param>
    /// <returns>True if the package is provided by the framework, otherwise false.</returns>
    public static bool IsPackageProvidedByRuntime(string packageId)
    {
        return ProvidedPackages.Contains(packageId);
    }

    /// <summary>
    /// This list comes from the package overrides for the .NET SDK,
    /// at https://github.com/dotnet/sdk/blob/v3.1.201/src/Tasks/Common/targets/Microsoft.NET.DefaultPackageConflictOverrides.targets.
    /// If the executing binaries ever change to a newer version, this project must update as well, and refresh this list.
    /// </summary>
    private static readonly ISet<string> ProvidedPackages = new HashSet<string>
    {
        "Microsoft.CSharp",
        "Microsoft.Win32.Primitives",
        "Microsoft.Win32.Registry",
        "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple",
        "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl",
        "System.AppContext",
        "System.Buffers",
        "System.Collections",
        "System.Collections.Concurrent",
        // Removed a load for brevity....
        "System.Xml.ReaderWriter",
        "System.Xml.XDocument",
        "System.Xml.XmlDocument",
        "System.Xml.XmlSerializer",
        "System.Xml.XPath",
        "System.Xml.XPath.XDocument",
    };
}

Ok, so let’s update our DependencySuppliedByHost method to use this look-up:

private bool DependencySuppliedByHost(DependencyContext hostDependencies, PackageDependency dep)
{
    // Check our look-up list.
    if(RuntimeProvidedPackages.IsPackageProvidedByRuntime(dep.Id))
    {
        return true;
    }

    // See if a runtime library with the same ID as the package is available in the host's runtime libraries.
    var runtimeLib = hostDependencies.RuntimeLibraries.FirstOrDefault(r => r.Name == dep.Id);

    if (runtimeLib is object)
    {
        // What version of the library is the host using?
        var parsedLibVersion = NuGetVersion.Parse(runtimeLib.Version);

        if (parsedLibVersion.IsPrerelease)
        {
            // Always use pre-release versions from the host, otherwise it becomes
            // a nightmare to develop across multiple active versions.
            return true;
        }
        else
        {
            // Does the host version satisfy the version range of the requested package?
            // If so, we can provide it; otherwise, we cannot.
            return dep.VersionRange.Satisfies(parsedLibVersion);
        }
    }

    return false;
}

Now, when we run our code, we get precisely the set of packages we want!

AutoStep.Web : 1.0.0-develop.20
Selenium.Chrome.WebDriver : 79.0.0
Selenium.WebDriver : 3.141.0
Newtonsoft.Json : 10.0.3

Downloading and Extracting

At the moment, our list of dependencies ‘might’ contain duplicates. For example,
two different extensions might reference two different versions of NewtonSoft.Json.
We need to pick one to install that will be compatible with both.

To do this, we need to use the supplied PackageResolver class to constrain the set of packages
to only the ones we want to actually download and install, in a new GetPackagesToInstall method:

private IEnumerable<SourcePackageDependencyInfo> GetPackagesToInstall(SourceRepositoryProvider sourceRepositoryProvider, 
                                                                      ILogger logger, IEnumerable<ExtensionConfiguration> extensions, 
                                                                      HashSet<SourcePackageDependencyInfo> allPackages)
{
    // Create a package resolver context.
    var resolverContext = new PackageResolverContext(
            DependencyBehavior.Lowest,
            extensions.Select(x => x.Package),
            Enumerable.Empty<string>(),
            Enumerable.Empty<PackageReference>(),
            Enumerable.Empty<PackageIdentity>(),
            allPackages,
            sourceRepositoryProvider.GetRepositories().Select(s => s.PackageSource),
            logger);

    var resolver = new PackageResolver();

    // Work out the actual set of packages to install.
    var packagesToInstall = resolver.Resolve(resolverContext, CancellationToken.None)
                                    .Select(p => allPackages.Single(x => PackageIdentityComparer.Default.Equals(x, p)));
    return packagesToInstall;
}

Once we have that list, we can pass it to another new method that actually downloads and extracts the packages for us, InstallPackages.

private async Task InstallPackages(SourceCacheContext sourceCacheContext, ILogger logger, 
                                    IEnumerable<SourcePackageDependencyInfo> packagesToInstall, string rootPackagesDirectory, 
                                    ISettings nugetSettings, CancellationToken cancellationToken)
{
    var packagePathResolver = new PackagePathResolver(rootPackagesDirectory, true);
    var packageExtractionContext = new PackageExtractionContext(
        PackageSaveMode.Defaultv3,
        XmlDocFileSaveMode.Skip,
        ClientPolicyContext.GetClientPolicy(nugetSettings, logger),
        logger);

    foreach (var package in packagesToInstall)
    {
        var downloadResource = await package.Source.GetResourceAsync<DownloadResource>(cancellationToken);

        // Download the package (might come from the shared package cache).
        var downloadResult = await downloadResource.GetDownloadResourceResultAsync(
            package,
            new PackageDownloadContext(sourceCacheContext),
            SettingsUtility.GetGlobalPackagesFolder(nugetSettings),
            logger,
            cancellationToken);

        // Extract the package into the target directory.
        await PackageExtractor.ExtractPackageAsync(
            downloadResult.PackageSource,
            downloadResult.PackageStream,
            packagePathResolver,
            packageExtractionContext,
            cancellationToken);
    }
}

Let’s go ahead and plug those extra methods into our main calling method:

public async Task LoadExtensions()
{
    // Define a source provider, with nuget, plus my own feed.
    var sourceProvider = new PackageSourceProvider(NullSettings.Instance, new[]
    {
        new PackageSource("https://api.nuget.org/v3/index.json"),
        new PackageSource("https://f.feedz.io/autostep/ci/nuget/index.json")
    });

    // Establish the source repository provider; the available providers come from our custom settings.
    var sourceRepositoryProvider = new SourceRepositoryProvider(sourceProvider, Repository.Provider.GetCoreV3());

    // Get the list of repositories.
    var repositories = sourceRepositoryProvider.GetRepositories();

    // Disposable source cache.
    using var sourceCacheContext = new SourceCacheContext();

    // You should use an actual logger here, this is a NuGet ILogger instance.
    var logger = new NullLogger();

    // My extension configuration:
    var extensions = new[]
    {
        new ExtensionConfiguration
        {
            Package = "AutoStep.Web",
            PreRelease = true // Allow pre-release versions.
        }
    };

    // Replace this with a proper cancellation token.
    var cancellationToken = CancellationToken.None;

    // The framework we're using.
    var targetFramework = NuGetFramework.ParseFolder("netcoreapp3.1");
    var allPackages = new HashSet<SourcePackageDependencyInfo>();

    var dependencyContext = DependencyContext.Default;

    foreach (var ext in extensions)
    {
        var packageIdentity = await GetPackageIdentity(ext, sourceCacheContext, logger, repositories, cancellationToken);

        if (packageIdentity is null)
        {
            throw new InvalidOperationException($"Cannot find package {ext.Package}.");
        }

        await GetPackageDependencies(packageIdentity, sourceCacheContext, targetFramework, logger, repositories, dependencyContext, allPackages, cancellationToken);
    }

    var packagesToInstall = GetPackagesToInstall(sourceRepositoryProvider, logger, extensions, allPackages);

    // Where do we want to install our packages?
    // For now we'll pop them in the .extensions folder.
    var packageDirectory = Path.Combine(Environment.CurrentDirectory, ".extensions");
    var nugetSettings = Settings.LoadDefaultSettings(packageDirectory);

    await InstallPackages(sourceCacheContext, logger, packagesToInstall, packageDirectory, nugetSettings, cancellationToken);
}

With all these changes, here’s what the ./extensions folder looks like when we run this:

> ls ./extensions
AutoStep.Web.1.0.0-develop.20
Newtonsoft.Json.10.0.3
Selenium.Chrome.WebDriver.79.0.0
Selenium.WebDriver.3.141.0

All the packages we need are now on disk!


Wrapping Up

At the end of this post, we now have a mechanism for loading packages and a filtered set of dependencies from NuGet.

In the next post, we will load those packages into a custom AssemblyLoadContext and use them in our application.

You can find the complete set of code from this post in this gist.

The ‘production’ code this fed into is in the GitHub repository for AutoStep.Extensions, which provides the NuGet package used to load extensions into our VS Code Language Server and our commmand-line runner.