Skip to content

Instantly share code, notes, and snippets.

@tintoy
Last active September 26, 2021 02:54
Show Gist options
  • Save tintoy/3d9467d59b2547ad227ba5d1560ee838 to your computer and use it in GitHub Desktop.
Save tintoy/3d9467d59b2547ad227ba5d1560ee838 to your computer and use it in GitHub Desktop.
NuGet feed lister
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NuGet.Client" Version="4.2.0" />
<PackageReference Include="NuGet.Configuration" Version="5.8.1" />
<PackageReference Include="NuGet.Credentials" Version="5.8.1" />
<PackageReference Include="NuGet.PackageManagement" Version="5.8.1" NoWarn="NU1701" />
<PackageReference Include="NuGet.Packaging" Version="5.8.1" />
<PackageReference Include="NuGet.Versioning" Version="5.8.1" />
<PackageReference Include="Serilog" Version="2.5.0" />
<PackageReference Include="Serilog.Enrichers.Demystify" Version="0.1.0-dev-00016" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.0.0" />
</ItemGroup>
</Project>
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using System.Threading;
namespace NuGetFeedLister
{
/// <summary>
/// Helper methods for interacting with the NuGet API.
/// </summary>
public static class NuGetHelper
{
/// <summary>
/// Get all package sources configured for the specified workspace.
/// </summary>
/// <param name="workspaceRootDirectory">
/// The workspace's root directory.
/// </param>
/// <returns>
/// A list of configured package sources.
/// </returns>
public static List<PackageSource> GetWorkspacePackageSources(string workspaceRootDirectory)
{
if (String.IsNullOrWhiteSpace(workspaceRootDirectory))
throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'workspaceRootDirectory'.", nameof(workspaceRootDirectory));
return new List<PackageSource>(
new PackageSourceProvider(
Settings.LoadDefaultSettings(workspaceRootDirectory)
)
.LoadPackageSources()
);
}
/// <summary>
/// Get NuGet AutoComplete APIs for the specified package source URLs.
/// </summary>
/// <param name="packageSourceUrls">
/// The package source URLs.
/// </param>
/// <returns>
/// A task that resolves to a list of <see cref="AutoCompleteResource"/>s.
/// </returns>
public static Task<List<AutoCompleteResource>> GetAutoCompleteResources(params string[] packageSourceUrls)
{
return GetAutoCompleteResources(
packageSourceUrls.Select(packageSourceUrl => new PackageSource(packageSourceUrl))
);
}
/// <summary>
/// Get NuGet AutoComplete APIs for the specified package sources.
/// </summary>
/// <param name="packageSources">
/// The package sources.
/// </param>
/// <returns>
/// A task that resolves to a list of <see cref="AutoCompleteResource"/>s.
/// </returns>
public static Task<List<AutoCompleteResource>> GetAutoCompleteResources(params PackageSource[] packageSources)
{
return GetAutoCompleteResources(
(IEnumerable<PackageSource>)packageSources
);
}
/// <summary>
/// Get NuGet AutoComplete APIs for the specified package sources.
/// </summary>
/// <param name="packageSources">
/// The package sources.
/// </param>
/// <param name="cancellationToken">
/// An optional <see cref="CancellationToken"/> that can be used to cancel the operation.
/// </param>
/// <returns>
/// A task that resolves to a list of <see cref="AutoCompleteResource"/>s.
/// </returns>
public static async Task<List<AutoCompleteResource>> GetAutoCompleteResources(IEnumerable<PackageSource> packageSources, CancellationToken cancellationToken = default(CancellationToken))
{
if (packageSources == null)
throw new ArgumentNullException(nameof(packageSources));
List<AutoCompleteResource> autoCompleteResources = new List<AutoCompleteResource>();
var providers = new List<Lazy<INuGetResourceProvider>>();
// Add v3 API support
providers.AddRange(Repository.Provider.GetCoreV3());
foreach (PackageSource packageSource in packageSources)
{
SourceRepository sourceRepository = new SourceRepository(packageSource, providers);
AutoCompleteResource autoCompleteResource = await sourceRepository.GetResourceAsync<AutoCompleteResource>(cancellationToken);
if (autoCompleteResource != null)
autoCompleteResources.Add(autoCompleteResource);
}
return autoCompleteResources;
}
/// <summary>
/// Suggest package Ids based on a prefix.
/// </summary>
/// <param name="autoCompleteResources">
/// The <see cref="AutoCompleteResource"/>s used to retrieve suggestions.
/// </param>
/// <param name="packageIdPrefix">
/// The package Id prefix to match.
/// </param>
/// <param name="includePrerelease">
/// Include packages with only pre-release versions available?
/// </param>
/// <param name="logger">
/// An optional NuGet logger to be used for reporting errors / progress (etc).
/// </param>
/// <param name="cancellationToken">
/// An optional cancellation token that can be used to cancel the request.
/// </param>
/// <returns>
/// A sorted set of suggested package Ids.
/// </returns>
public static async Task<SortedSet<string>> SuggestPackageIds(this IEnumerable<AutoCompleteResource> autoCompleteResources, string packageIdPrefix, bool includePrerelease = false, ILogger logger = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (autoCompleteResources == null)
throw new ArgumentNullException(nameof(autoCompleteResources));
if (packageIdPrefix == null)
throw new ArgumentNullException(nameof(packageIdPrefix));
IEnumerable<string>[] results = await Task.WhenAll(
autoCompleteResources.Select(
autoCompleteResource => autoCompleteResource.IdStartsWith(packageIdPrefix, includePrerelease, logger ?? NullLogger.Instance, cancellationToken)
)
);
return new SortedSet<string>(
results.Flatten()
);
}
/// <summary>
/// Suggest versions for the specified package.
/// </summary>
/// <param name="autoCompleteResources">
/// The <see cref="AutoCompleteResource"/>s used to retrieve suggestions.
/// </param>
/// <param name="versionPrefix">
/// An optional version prefix to match.
/// </param>
/// <param name="packageId">
/// The package Id to match.
/// </param>
/// <param name="includePrerelease">
/// Include pre-release versions?
/// </param>
/// <param name="logger">
/// An optional NuGet logger to be used for reporting progress (etc).
/// </param>
/// <param name="cancellationToken">
/// An optional cancellation token that can be used to cancel the request.
/// </param>
/// <returns>
/// A sorted set of suggested package versions.
/// </returns>
public static async Task<SortedSet<NuGetVersion>> SuggestPackageVersions(this IEnumerable<AutoCompleteResource> autoCompleteResources, string packageId, bool includePrerelease = false, string versionPrefix = "", ILogger logger = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (autoCompleteResources == null)
throw new ArgumentNullException(nameof(autoCompleteResources));
if (packageId == null)
throw new ArgumentNullException(nameof(packageId));
IEnumerable<NuGetVersion>[] results = await Task.WhenAll(
autoCompleteResources.Select(
autoCompleteResource => autoCompleteResource.VersionStartsWith(packageId, versionPrefix, includePrerelease, null, logger ?? NullLogger.Instance, cancellationToken)
)
);
return new SortedSet<NuGetVersion>(
results.Flatten(),
VersionComparer.VersionReleaseMetadata
);
}
/// <summary>
/// Flatten the sequence, enumerating nested sequences.
/// </summary>
/// <typeparam name="TSource">
/// The source element type.
/// </typeparam>
/// <param name="source">
/// The source sequence of sequences.
/// </param>
/// <returns>
/// The flattened sequence.
/// </returns>
static IEnumerable<TSource> Flatten<TSource>(this IEnumerable<IEnumerable<TSource>> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
return source.SelectMany(items => items);
}
}
}
using Serilog;
using Serilog.Events;
using System;
using System.Threading.Tasks;
using NC = NuGet.Common;
namespace NuGetFeedLister
{
/// <summary>
/// Serilog logging adapter for NuGet.
/// </summary>
class NuGetSerilogAdapter
: NC.ILogger
{
public NuGetSerilogAdapter(ILogger parentLogger)
{
if (parentLogger == null)
throw new ArgumentNullException(nameof(parentLogger));
Logger = parentLogger.ForContext(propertyName: "SourceContext", value: "NuGet");
}
ILogger Logger { get; }
LogEventLevel ConvertLogLevel(NC.LogLevel level)
{
switch (level)
{
case NC.LogLevel.Error:
{
return LogEventLevel.Error;
}
case NC.LogLevel.Warning:
{
return LogEventLevel.Warning;
}
case NC.LogLevel.Information:
{
return LogEventLevel.Information;
}
case NC.LogLevel.Debug:
{
return LogEventLevel.Verbose; // NuGet vs Serilog - Debug and Verbose are swapped
}
case NC.LogLevel.Verbose:
{
return LogEventLevel.Debug; // NuGet vs Serilog - Debug and Verbose are swapped
}
default:
{
return LogEventLevel.Information;
}
}
}
void NC.ILogger.Log(NC.LogLevel level, string data)
{
LogEventLevel logEventLevel = ConvertLogLevel(level);
Logger.Write(logEventLevel, data);
}
void NC.ILogger.Log(NC.ILogMessage message)
{
LogEventLevel logEventLevel = ConvertLogLevel(message.Level);
Logger.Write(logEventLevel, "{NuGetLogCode:l}: {NuGetLogMessage}",
message.Code,
message.Message
);
}
Task NC.ILogger.LogAsync(NC.LogLevel level, string data)
{
LogEventLevel logEventLevel = ConvertLogLevel(level);
Logger.Write(logEventLevel, data);
return Task.CompletedTask;
}
Task NC.ILogger.LogAsync(NC.ILogMessage message)
{
LogEventLevel logEventLevel = ConvertLogLevel(message.Level);
Logger.Write(logEventLevel, "{NuGetLogCode:l}: {NuGetLogMessage}",
message.Code,
message.Message
);
return Task.CompletedTask;
}
void NC.ILogger.LogError(string data)
{
Logger.Error(data);
}
void NC.ILogger.LogWarning(string data)
{
Logger.Warning(data);
}
void NC.ILogger.LogInformation(string data)
{
Logger.Information(data);
}
void NC.ILogger.LogInformationSummary(string data)
{
Logger.Information(data);
}
void NC.ILogger.LogMinimal(string data)
{
Logger.Information(data);
}
void NC.ILogger.LogDebug(string data)
{
Logger.Verbose(data);
}
void NC.ILogger.LogVerbose(string data)
{
Logger.Debug(data);
}
}
}
using NuGet.Configuration;
using NuGet.Credentials;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using Serilog;
using Serilog.Sinks.SystemConsole.Themes;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace NuGetFeedLister
{
/// <summary>
/// A quick-and-dirty test for listing packages from all configured package sources.
/// </summary>
static class Program
{
/// <summary>
/// The package Id prefix to search for.
/// </summary>
static readonly string SearchForPackagePrefix = "Microsoft.AspNetCore";
/// <summary>
/// The logger used by NuGet (forwards log entries to Serilog).
/// </summary>
static NuGetSerilogAdapter NuGetLogger { get; set; }
/// <summary>
/// The main program entry-point.
/// </summary>
static async Task Main()
{
ConfigureLogging();
CancellationTokenSource cancellationSource = new CancellationTokenSource();
cancellationSource.CancelAfter(
TimeSpan.FromSeconds(10)
);
try
{
ConfigureNuGetCredentialProviders();
List<PackageSource> packageSources = NuGetHelper.GetWorkspacePackageSources(
workspaceRootDirectory: Directory.GetCurrentDirectory()
);
Log.Information("Found {PackageSourceCount} configured package sources.", packageSources.Count);
List<AutoCompleteResource> autoCompleteResources = await NuGetHelper.GetAutoCompleteResources(packageSources, cancellationSource.Token);
for (int packageSourceIndex = 0; packageSourceIndex < packageSources.Count; packageSourceIndex++)
{
Log.Information("Service index for packagesource {PackageSourceName} ({PackageSourceUri}):",
packageSources[packageSourceIndex].Name ?? "<no name>",
packageSources[packageSourceIndex].SourceUri
);
ServiceIndexResourceV3 serviceIndex = (ServiceIndexResourceV3)
autoCompleteResources[packageSourceIndex]
.GetType()
.GetField("_serviceIndex", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(autoCompleteResources[packageSourceIndex]);
foreach (ServiceIndexEntry indexEntry in serviceIndex.Entries)
{
Log.Information(" {ServiceType} (v{ClientVersion}) => {ServiceUri}",
indexEntry.Type,
indexEntry.ClientVersion,
indexEntry.Uri
);
}
Log.Information("Requesting suggestions for package prefix {PackagePrefix} from package source {PackageSourceName} ({PackageSourceUri})...",
SearchForPackagePrefix,
packageSources[packageSourceIndex].Name ?? "<no name>",
packageSources[packageSourceIndex].SourceUri
);
IEnumerable<string> suggestions = await autoCompleteResources[packageSourceIndex].IdStartsWith(
packageIdPrefix: SearchForPackagePrefix,
includePrerelease: true,
log: NuGetLogger,
cancellationSource.Token
);
Log.Information("Received {SuggestionCount} suggestions for package prefix {PackagePrefix} from package source {PackageSourceName} ({PackageSourceUri}).",
suggestions.Count(),
SearchForPackagePrefix,
packageSources[packageSourceIndex].Name ?? "<no name>",
packageSources[packageSourceIndex].SourceUri
);
foreach (string suggestion in suggestions)
Log.Information("\tSuggested package: {PackageId}", suggestion);
}
}
catch (Exception unexpectedError)
{
Log.Error(unexpectedError, "Unexpected error.");
}
}
/// <summary>
/// Configure Serilog and NuGet logging.
/// </summary>
static void ConfigureLogging()
{
ILogger rootLogger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.WithDemystifiedStackTraces()
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "[{SourceContext}/{Level:u3}] {Message:lj}{NewLine}{Exception}",
theme: SystemConsoleTheme.Literate
)
.CreateLogger();
Log.Logger = rootLogger.ForContext(propertyName: "SourceContext", value: nameof(Program));
NuGetLogger = new NuGetSerilogAdapter(rootLogger);
// Hackily override NuGet's default logger.
typeof(NuGet.Common.NullLogger)
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)
.SetValue(obj: null /* static */, value: NuGetLogger);
}
/// <summary>
/// Configure NuGet's credential providers (i.e. support for authenticated package feeds).
/// </summary>
static void ConfigureNuGetCredentialProviders()
{
DefaultCredentialServiceUtility.SetupDefaultCredentialService(
logger: NuGetLogger,
nonInteractive: true
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment