В этой статье мы рассмотрим централизованное управление версиями пакетов, а также разберем настройку приватного 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 пакетов что позволит использовать код между проектами и декомпозировать гигантские монорепы.

Комментарии (9)


  1. timestrueroman
    11.12.2025 12:23

    Т.е. основное преимущество BaGet в том, что он сам докачивает зависимости? Звучит неплохо, учитывая, что у стандартного nuget репозитория возможностей не так много. Ему мы ещё веб-админку полноценную, с поиском по названию, версии, зависимостям и прочим - было бы замечательно.


    1. xtraroman Автор
      11.12.2025 12:23

      Да, Багет умеет скачивать зависимости сам. В конфиг прописываете откуда можно скачивать и все работает само. Это очень удобно. Один раз настроили и вас не дергают коллеги по пустякам. Админка есть. Простейшая. Поиск имеется.


      1. malstraem
        11.12.2025 12:23

        Моё мнение, при развернутом self-hosted GitLab'е (у вас наверняка он?) - не нужно. Гитлабовский Package Registry закрывает все потребности и позволяет нормально работать со своими внутренними пакетами.

        Один раз завели крупные группы, в которых делаете репы с системными либами и держите nuget.config в конечных проектах с адресами на Package Registry.


        1. xtraroman Автор
          11.12.2025 12:23

          Да, у нас развернут гитлаб.

          Вроде гитлабовский сервер пакетов не умеет ходить в интернет за недостающими зависимостями. Именно в этом основное преимущество Багета


          1. malstraem
            11.12.2025 12:23

            Так решается тем, что nuget.org держится в nuget.config наряду с адресами на гитлаб. Рабочие машины и раннеры чувствуют себя нормально.

            Заодно лента гитлаба и как нормальный источник отладочных символов выступает - VS и Rider дружат.

            Я может проблематику не понимаю.


            1. xtraroman Автор
              11.12.2025 12:23

              Если вы пользуетесь гитлабовским сервером пакетов в закрытом контуре, нужно постоянно заботиться о том, чтобы в вашей приватной галерее были все необходимые пакеты. А если в конфиге нугета прописать ещё и nuget.org, как вы предлагаете, получается что разработчики у вас не в закрытом контуре. Им нужно давать доступ в интернет.

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


  1. maksim_bronnikov
    11.12.2025 12:23

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

    По опыту, нежизнеспособно по одной причине - при обновлении пакетов через менеджер, перезатираются версии в Packages.props.

    А обновлять пакеты вручную, то есть, узнавать через менеджер актуальную версию, потом прописывать в Build.props, дико неудобно, поэтому не рекомендую.

    Так же, CPM хорош, когда у вас монорепа, если же каждый сервис лежит в своем репозитории, то "скрипач не нужен". (Ну или пробовать выносить props в git submodule)


    1. xtraroman Автор
      11.12.2025 12:23

      У нас прижилось. Авалония активно развивается и мы регулярно обновляемся на свежие версии. Обновление у нас делается изменением одной цифры в props файле.


    1. xtraroman Автор
      11.12.2025 12:23

      Насчёт CPM: даже если у вас разные репы он уменьшает число мест где указана версия, пакета. Такие проекты проще поддерживать поэтому в нём есть смысл.

      Про идею с субмодулями: думаю сработает. Но у нас это не прижилось. Сабмодули Гита не любим.