使用 nuget server 的 API 來實作搜索安裝 nuget 包
Intro
nuget 現在幾乎是 dotnet 開發不可缺少的一部分了,還沒有用過 nuget 的就有點落后時代了,還不快用起來
nuget 是 dotnet 里的包管理機制,類似于前端的 npm ,php 的 composer,java 里的 maven ...
nuget 定義了一套關于 nuget server 的規范,使得用戶可以自己實作一個 nuget server
也正是這些規范,使得我們可以根據這些規范來實作 nuget server 的包管理的功能,今天主要介紹一下,根據 nuget server 的 api 規范使用原始的 HTTP 請求來實作 nuget 包的搜索和使用 nuget 提供的客戶端 SDK 來實作 nuget 包的搜索和下載
Nuget Server Api
Nuget 協議介紹
nuget 的協議有好幾個版本,目前主要用的是 v3,開源的 nuget server Baget 也實作了基于 nuget protocal v3 的規范
我們添加 nuget 源的時候會指定一個 source url,類似 https://api.nuget.org/v3/index.json 這樣的,著通常被稱為 Service Index,是一個 nuget 源的入口,有點類似于 Identity Server 里的發現檔案,通過這個地址可以獲取到一系列的資源的地址
有一些資源是協議規范里定義的必須要實作的,有一些是可選的,具體參考官方檔案,以后隨著版本變化,可能會有差異,目前 nuget.org 提供的資源如下:

Nuget.org 提供了兩種搜索的方式,
一個是 SearchQuery,會根據包名稱、 tag、description 等資訊去匹配關鍵詞,
一個是 SearchAutocomplete 根據包名稱的前綴去匹配包的名稱
獲取某個 nuget 包的版本資訊,可以使用 PackageBaseAddress 來獲取
ServiceIndex 回傳的資訊示例如下:

回傳的資訊會有一個 resources 的陣列,會包含各種不同型別的資源,對應的 @id 就是呼叫這種型別的API要用到的地址,下面來看一個搜索的示例

在每個 API 的檔案頁面可以看到會使用的 @type,呼叫這個 API 的時候應該使用這些 @type 對應的資源

這里的 @id 就是上面的 resource 對應的 @id,
引數說明:
q 搜索時所用的關鍵詞,
skip/take 用來分頁顯示查詢結果,
prelease 用來指定是否限制預覽版的 package,true 包含預覽版的 nuget 包,false 只包含已經正式發布的 nuget 包
semVerLevel 是用來指定包的語意版本
The
semVerLevelquery parameter is used to opt-in to SemVer 2.0.0 packages. If this query parameter is excluded, only packages with SemVer 1.0.0 compatible versions will be returned (with the standard NuGet versioning caveats, such as version strings with 4 integer pieces). IfsemVerLevel=2.0.0is provided, both SemVer 1.0.0 and SemVer 2.0.0 compatible packages will be returned. See the SemVer 2.0.0 support for nuget.org for more information
packageType 用來指定 nuget 包的型別,目前支持的型別包括 Dependency(默認)專案依賴項,DotnetTool(dotnetcore 2.1 引入的 dotnet cli tool),Template (dotnet new 用) 自定義的專案模板
其他的 API 可以自行參考官方檔案:https://docs.microsoft.com/en-us/nuget/api/service-index
Packages
SearchQuery 回傳的資訊比較多而且可能并不準確,適用于不清楚包的名稱的時候使用,如果知道 nuget 包的名稱(PackageId) ,可以使用 SearchAutocomplete 來搜索,這樣更精準,回傳的資訊也更簡單,只有匹配的 package 名稱
通過原始 api 呼叫的方式實作 nuget 包的搜索
using var httpClient = new HttpClient(new NoProxyHttpClientHandler());
// loadServiceIndex
var serviceIndexResponse = await httpClient.GetStringAsync(NugetServiceIndex);
var serviceIndexObject = JObject.Parse(serviceIndexResponse);
var keyword = "weihanli";
//https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
var queryEndpoint = serviceIndexObject["resources"]
.First(x => x["@type"].Value<string>() == "SearchQueryService")["@id"]
.Value<string>();
var queryUrl = $"{queryEndpoint}?q={keyword}&skip=0&take=5&prerelease=false&semVerLevel=2.0.0";
var queryResponse = await httpClient.GetStringAsync(queryUrl);
Console.WriteLine($"formatted queryResponse:");
Console.WriteLine($"{JObject.Parse(queryResponse).ToString(Formatting.Indented)}");
// https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource
var autoCompleteQueryEndpoint = serviceIndexObject["resources"]
.First(x => x["@type"].Value<string>() == "SearchAutocompleteService")["@id"]
.Value<string>();
var autoCompleteQueryUrl = $"{autoCompleteQueryEndpoint}?q={keyword}&skip=0&take=5&prerelease=false&semVerLevel=2.0.0";
var autoCompleteQueryResponse = await httpClient.GetStringAsync(autoCompleteQueryUrl);
Console.WriteLine($"formatted autoCompleteQueryResponse:");
Console.WriteLine($"{JObject.Parse(autoCompleteQueryResponse).ToString(Formatting.Indented)}");
output 示例:
Query 回傳示例

Autocomplete 回傳結果

從上面我們可以看到 Query 介面回傳了很多的資訊,Autocomplete 介面只回傳了 package 的名稱,回傳的資訊更為簡潔,所以如果可以使用 Autocomplete 的方式就盡可能使用 Autocomplete 的方式
Package Versions
前面我們提到了可以使用 PackageBaseAddress 來查詢某個 nuget 包的版本資訊,檔案地址:https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource,來看一下示例:
using (var httpClient = new HttpClient(new NoProxyHttpClientHandler()))
{
// loadServiceIndex
var serviceIndexResponse = await httpClient.GetStringAsync(NugetServiceIndex);
var serviceIndexObject = JObject.Parse(serviceIndexResponse);
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
var packageVersionsEndpoint = serviceIndexObject["resources"]
.First(x => x["@type"].Value<string>() == "PackageBaseAddress/3.0.0")["@id"]
.Value<string>();
var packageVersionsQueryUrl = $"{packageVersionsEndpoint}/dbtool.core/index.json";
var packageVersionsQueryResponse = await httpClient.GetStringAsync(packageVersionsQueryUrl);
Console.WriteLine("DbTool.Core versions:");
Console.WriteLine(JObject.Parse(packageVersionsQueryResponse)
.ToString(Formatting.Indented));
}
output 示例:

注:api 地址中的 packageId 要轉小寫
Nuget Client SDK
除了上面的根據 api 自己呼叫,我們還可以使用 nuget 提供的客戶端 sdk 實作上述功能,這里就不詳細介紹了,有需要可能查閱官方檔案:https://docs.microsoft.com/en-us/nuget/reference/nuget-client-sdk
下面給出一個使用示例:
var packageId = "WeihanLi.Common";
var packageVersion = new NuGetVersion("1.0.38");
var logger = NullLogger.Instance;
var cache = new SourceCacheContext();
// 在 SDK 的概念里,每一個 nuget 源是一個 repository
var repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json");
// SearchQuery
{
var resource = await repository.GetResourceAsync<PackageSearchResource>();
var searchFilter = new SearchFilter(includePrerelease: false);
var results = await resource.SearchAsync(
"weihanli",
searchFilter,
skip: 0,
take: 20,
logger,
CancellationToken.None);
foreach (var result in results)
{
Console.WriteLine($"Found package {result.Identity.Id} {result.Identity.Version}");
}
}
// SearchAutoComplete
{
var autoCompleteResource = await repository.GetResourceAsync<AutoCompleteResource>();
var packages =
await autoCompleteResource.IdStartsWith("WeihanLi", false, logger, CancellationToken.None);
foreach (var package in packages)
{
Console.WriteLine($"Found Package {package}");
}
}
//
{
// get package versions
var findPackageByIdResource = await repository.GetResourceAsync<FindPackageByIdResource>();
var versions = await findPackageByIdResource.GetAllVersionsAsync(
packageId,
cache,
logger,
CancellationToken.None);
foreach (var version in versions)
{
Console.WriteLine($"Found version {version}");
}
}
More
你可以使用 nuget sdk 方便的實作 nuget 包的下載安裝,內部實作了簽名校驗等,這樣就可以把本地不存在的 nuget 包下載到本地了,
實作示例:
{
var pkgDownloadContext = new PackageDownloadContext(cache);
var downloadRes = await repository.GetResourceAsync<DownloadResource>();
var downloadResult = await RetryHelper.TryInvokeAsync(async () =>
await downloadRes.GetDownloadResourceResultAsync(
new PackageIdentity(packageId, packageVersion),
pkgDownloadContext,
@"C:\Users\liweihan\.nuget\packages", // nuget globalPackagesFolder
logger,
CancellationToken.None), r => true);
Console.WriteLine(downloadResult.Status.ToString());
}
最后提供一個決議 nuget globalPackagesFolder 的兩種思路:
一個是前面有篇文章介紹的,有個默認的組態檔,然后就是默認的配置,寫了一個決議的方法示例,支持 Windows/Linux/Mac:
{
var packagesFolder = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
if (string.IsNullOrEmpty(packagesFolder))
{
// Nuget globalPackagesFolder resolve
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var defaultConfigFilePath =
$@"{Environment.GetEnvironmentVariable("APPDATA")}\NuGet\NuGet.Config";
if (File.Exists(defaultConfigFilePath))
{
var doc = new XmlDocument();
doc.Load(defaultConfigFilePath);
var node = doc.SelectSingleNode("/configuration/config/add[@key='globalPackagesFolder']");
if (node != null)
{
packagesFolder = node.Attributes["value"]?.Value;
}
}
if (string.IsNullOrEmpty(packagesFolder))
{
packagesFolder = $@"{Environment.GetEnvironmentVariable("USERPROFILE")}\.nuget\packages";
}
}
else
{
var defaultConfigFilePath =
$@"{Environment.GetEnvironmentVariable("HOME")}/.config/NuGet/NuGet.Config";
if (File.Exists(defaultConfigFilePath))
{
var doc = new XmlDocument();
doc.Load(defaultConfigFilePath);
var node = doc.SelectSingleNode("/configuration/config/add[@key='globalPackagesFolder']");
if (node != null)
{
packagesFolder = node.Attributes["value"]?.Value;
}
}
if (string.IsNullOrEmpty(packagesFolder))
{
defaultConfigFilePath = $@"{Environment.GetEnvironmentVariable("HOME")}/.nuget/NuGet/NuGet.Config";
if (File.Exists(defaultConfigFilePath))
{
var doc = new XmlDocument();
doc.Load(defaultConfigFilePath);
var node = doc.SelectSingleNode("/configuration/config/add[@key='globalPackagesFolder']");
if (node != null)
{
packagesFolder = node.Value;
}
}
}
if (string.IsNullOrEmpty(packagesFolder))
{
packagesFolder = $@"{Environment.GetEnvironmentVariable("HOME")}/.nuget/packages";
}
}
}
Console.WriteLine($"globalPackagesFolder: {packagesFolder}");
}
另一個是可以根據 nuget 提供的一個命令查詢 nuget locals global-packages -l,通過命令輸出獲取

Reference
- https://github.com/WeihanLi/SamplesInPractice/blob/master/NugetSample/RawApiSample.cs
- https://github.com/WeihanLi/SamplesInPractice/blob/master/NugetSample/NugetClientSdkSample.cs
- https://docs.microsoft.com/en-us/nuget/reference/nuget-client-sdk
- https://docs.microsoft.com/en-us/nuget/create-packages/set-package-type
- https://docs.microsoft.com/en-us/nuget/api/overview
- https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/6434.html
標籤:.NET Core
