В этой статье мы рассмотрим централизованное управление версиями пакетов, а также разберем настройку приватного NuGet-сервера BaGet для эффективной работы в изолированных средах. Мы уверены, что статья будет полезна многим российским разработчикам, поскольку NET разработка в изолированных средах в нашей стране действительно широко распространена.

Nuget.exe vs dotnet nuget

Бывалые разработчики помнят времена когда NuGet еще не был частью платформы. Если вы все еще разрабатываете под .NET Framework, вам будет нужен отдельный nuget.exe.

Сейчас нет никакого смысла использовать бинарные файлы NuGet. Менеджер пакетов теперь входит в состав NET SDK и доступен через команды dotnet nuget.

Централизованное управление версиями пакетов

Допустим, вы создали проект приложения, используя шаблоны:

dotnet new install Avalonia.Templates
dotnet new avalonia.mvvm

У вас получится что-то вроде:

<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Avalonia" Version="11.3.9" />
    <PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
    <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
    <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.9" />
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
  </ItemGroup>
</Project>

Если вы делаете простой одноразовый проект, этот подход достаточно хорош.

Но представьте, что будет, если у вас таких проектов несколько. Версии пакетов, прописанные прямо в .csproj, будут доставлять вам боль при обновлениях.

Для решения этой проблемы придумали централизованное управление версиями. Создадим файл Directory.Packages.props рядом с проектом.

<Project>
  <ItemGroup>
    <PackageVersion Include="Avalonia" Version="11.3.9" />
    <PackageVersion Include="Avalonia.Desktop" Version="11.3.9" />
    <PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.9" />
    <PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.9" />
    <PackageVersion Include="CommunityToolkit.Mvvm" Version="11.3.9" />
  </ItemGroup>
</Project>

А сам проект можно переписать так:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Avalonia"/>
    <PackageReference Include="Avalonia.Desktop"/>
    <PackageReference Include="Avalonia.Themes.Fluent"/>
    <PackageReference Include="Avalonia.Fonts.Inter"/>
    <PackageReference Include="CommunityToolkit.Mvvm"/>
  </ItemGroup>
</Project>

Обратите внимание на опцию ManagePackageVersionsCentrally и на отсутствие версий у зависимостей в файле проекта. Версии зависимостей теперь хранятся в Directory.Packages.props.

В файле Directory.Packages.props несколько раз повторяе��ся номер версии Avalonia. При обновлении придётся в нескольких местах менять его. Можно случайно забыть. Давайте это улучшим.

Создадим ещё один файл Directory.Build.props:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    <AvaloniaVersion>11.3.9</AvaloniaVersion>
  </PropertyGroup>
</Project>

В файле Directory.Build.props можно хранить настройки, общие для всех проектов. Поэтому опция ManagePackageVersionsCentrally из проекта переезжает в этот файл. Мы задали переменную MSBuild под названием AvaloniaVersion и теперь можем переписать Directory.Packages.props:

<Project>
  <ItemGroup>
    <PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)" />
    <PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
    <PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
    <PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
    <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.2.1" />
  </ItemGroup>
</Project>

Более подробро про централизованное управление версиями можно почитать в этой статье
https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management

В итоге должна получиться структура файлов как на картинке

структура файлов
структура файлов

Следует заметить, что некоторые шаблоны Avalonia уже настроены на централизованное управление пакетами. Например, шаблон avalonia.xplat. Возможно, после этой статьи вы станете применять его чаще.

dotnet new avalonia.xplat

На этом можно было бы и завершить часть статьи, посвящённую эффективному применению NuGet-пакетов в .NET-разработке. Но, поскольку в России популярна разработка в закрытых контурах предприятий, расскажем ещё про кеши NuGet и про приватные галереи.

Локальные кеши Nuget

При работе с NuGet на машине разработчика есть два типа кешей.

http-cache — он хранит информацию о доступных в галерее пакетах и их версиях. Некоторые про него не знают. И, например, после загрузки нового пакета в галерею негодуют, почему dotnet restore не видит новый пакет. К счастью, у http-cache короткое время жизни, и спустя несколько минут dotnet restore начинает магически работать как ожидается. Чтобы не ждать и не надеяться на магию, достаточно просто почистить кеш:

dotnet nuget locals http-cache --clear

Второй кеш — более известный. Называется global-packages. Кеш global-packages тоже можно почистить командой:

dotnet nuget locals global-packages --clear

Это бывает нужно, например, когда необходимо освободить место на диске или если у вас есть повреждённые пакеты.

Кеш global-packages любят использовать неправильно. При разработке в закрытом контуре часто притаскивают во внутреннюю сеть содержимое папки .nuget\packages\ вместо того, чтобы настроить галерею пакетов.

Настройка приватной галереи пакетов с использованием BaGet.

Есть много разных NuGet-серверов. Мы остановились на BaGet из-за того, что он умеет сам пополнять галерею пакетов из интернета. Когда вы собираете проект, который содержит зависимости, отсутствующие в приватной галерее, BaGet умеет ходить на на nuget.org, и автоматом пополнять вашу локальную галлерею пакетов.

BaGet vs BaGetter: разработка BaGet сейчас не ведётся, но у него есть форк, который называется BaGetter. Он активно развивается сейчас. Мы будем рассматривать в этой статье именно BaGet. У BaGet есть старая проблема
https://github.com/loic-sharma/BaGet/issues/513#issuecomment-1535715536
Она проявилась у нас при попытке загрузки пакета
https://www.nuget.org/packages/Magick.NET-Q8-AnyCPU
в приватную галлерею.
Мы решили эту проблему в своем локальном форке BaGet. На переход BaGetter который уже содержит этот фикс мы пока не решились.

Настройка BaGet. Наверное, нет смысла пересказывать инструкции. Мы рекомендуем установку через Docker: https://loic-sharma.github.io/BaGet/installation/docker/. После настройки сервера у вас будет API-ключ, с помощью которого можно публиковать пакеты.

dotnet nuget push --source $NUGET_SERVER --api-key $NUGET_SERVER_KEY my.package.1.0.0.nupkg

Если вы работаете в закрытом контуре, скорее всего, после настройки BaGet вы захотите загрузить в него пакеты из вашего локального кеша пакетов.

set CACHE_PATH=%USERPROFILE%\.nuget\packages

for /r "%CACHE_PATH%" %%f in (*.nupkg) do (
    dotnet nuget push "%%f" --source %SERVER% --api-key %API_KEY%
)

Чтобы использовать приватную галерею в командных проектах, мы рекомендуем в корневой папке создать nuget.config и добавить этот файл в систему контроля версий. Таким образом, настройки приватной галереи получат все разработчики, когда заберут себе последнюю версию из системы контроля версий.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <packageSources>
        <clear />
        <add key="nugetfeed" value="$NUGET_SERVER" allowInsecureConnections="true" />
    </packageSources>
</configuration>

Поскольку мы не настраивали HTTPS для приватной галереи пакетов, кроме собственно адреса галереи, конфиг NuGet содержит настройки, которые разрешают HTTP: allowInsecureConnections. Без этого dotnet не будет принимать пакеты из галереи.

Заключение

Теперь вы знаете как правильно потреблять NuGet пакеты в ваших проектах. В следующей статье мы рассмотрим более продвинутые сценарии применения которые включают создание собственных NuGet пакетов что позволит использовать код между проектами и декомпозировать гигантские монорепы.