Перейти к основному содержимому

NuGet - система управления пакетами

Разработчику Архитектору

NuGet и политика платформы

Контекст и значение

NuGet — это целая платформа поставки и распространения программных компонентов в экосистеме .NET. Её появление в 2010 году стало ответом на системную потребность в унифицированном, декларативном и автоматизированном способе совместного использования кода между разработчиками и командами. До NuGet распространение библиотек происходило через ручное копирование DLL, обмен архивами, или — в лучшем случае — через внутренние системы сборки. Такой подход не масштабировался, не обеспечивал версионного контроля и был подвержен множеству ошибок: несоответствие версий, дублирование сборок, отсутствие метаданных, невозможность отслеживания источника и лицензий.

NuGet изменил парадигму: он ввёл понятие пакета как атомарной, самодостаточной, версионированной единицы распространения кода и сопутствующих ресурсов. Это позволило стандартизировать процесс поставки библиотек, и заложить основы для централизованной, прозрачной и управляемой экосистемы совместного использования программного обеспечения.

Сегодня NuGet — обязательный элемент жизненного цикла любой .NET-разработки: от локального прототипирования до CI/CD-конвейеров в облаке. Его значение выходит далеко за рамки утилитарной функции «установить библиотеку»: NuGet влияет на архитектуру приложений, на методы интеграции, на практики внутреннего реюзинга и даже на безопасность поставок.


Что такое NuGet?

NuGet — это платформа управления пакетами для экосистемы .NET, включающая в себя:

  • Формат пакета — стандартизированный способ упаковки кода, метаданных и зависимостей;
  • Инструменты клиентского уровня — командные интерфейсы (CLI), интеграции в IDE (Visual Studio, Rider) и системные утилиты для создания, установки, обновления и публикации пакетов;
  • Инфраструктура узлов (feeds) — серверные компоненты, через которые пакеты распространяются (публичные, частные, гибридные);
  • Политики содержания и распространения — правила, определяющие допустимый контент, лицензирование, идентификацию авторов, а также механизмы модерации и санкционирования.

Важно понимать: NuGet — не репозиторий. Это распространённая ошибка. NuGet — это технология и экосистема. Репозиторий (точнее, узел, или feed) — это лишь один из элементов инфраструктуры, совместимой с этой технологией. Центральный публичный узел — nuget.org, но NuGet-совместимые узлы можно развернуть самостоятельно, используя, например, Azure Artifacts, MyGet, ProGet, или даже простой статический веб-сервер с поддержкой протокола OData (для старых версий) или V3 API (для современных).


Пакет NuGet

Пакет NuGet — это артефакт распространения. Технически он представляет собой обычный ZIP-архив с расширением .nupkg. Этот факт важен: .nupkgтранспортный контейнер. Внутри него — строго определённая структура каталогов и файлов, регулируемая спецификацией NuGet.

Основные компоненты пакета

  1. Манифест (*.nuspec)
    Это XML-файл, содержащий метаданные пакета:

    • уникальный идентификатор (<id>), обычно по соглашению — CompanyName.LibraryName;
    • версия (<version>), строго в формате major.minor.patch[-prerelease] (например, 2.1.0, 3.0.0-alpha.2);
    • описание, авторы, лицензия, ссылки на проект и репозиторий;
    • зависимости — перечень других NuGet-пакетов, необходимых для корректной работы текущего.

    Манифест может встраиваться напрямую в файл проекта (в .csproj при использовании SDK-стиля), но при сборке он всё равно извлекается и помещается в корень .nupkg.

  2. Сборки (lib/, ref/, runtimes/)
    Это бинарные файлы — DLL, которые будут использоваться потребителями пакета. Расположение сборок внутри архива строго регламентировано: оно указывает, для какой целевой платформы (Target Framework Moniker, TFM) предназначена данная сборка. Например:

    • lib/net6.0/MyLib.dll — сборка, скомпилированная для .NET 6.0;
    • lib/netstandard2.0/MyLib.dll — сборка, совместимая со всеми платформами, реализующими .NET Standard 2.0;
    • runtimes/win-x64/native/native.dll — нативная библиотека, специфичная для Windows на архитектуре x64.

    Такая структура позволяет одному пакету содержать множество сборок, предназначенных для разных платформ, и делает возможным многоплатформенное нацеливание.

  3. Ресурсы (content/, contentFiles/, build/, tools/)

    • content/ — устаревший способ внедрения файлов (например, конфигурационных) напрямую в проект потребителя (использовался в packages.config);
    • contentFiles/ — современный, декларативный подход: указываются файлы, которые попадут в проект как ссылки (content), или будут скопированы как ресурсы (resources), с возможностью указания языка (cs, vb) и действия (compile, none, embed);
    • build/ — файлы .props и .targets, подключаемые в процесс сборки MSBuild (позволяют влиять на этапы компиляции, генерации кода и т.п.);
    • tools/ — исполняемые файлы или скрипты, доступные во время разработки (например, генераторы кода, утилиты командной строки), но не включаемые в итоговую сборку.
  4. Документация (docs/) и другие произвольные файлы
    Хотя не регламентированы строго, допустимы дополнительные структуры — например, docs/ с Markdown-руководствами. Важно: NuGet не интерпретирует их автоматически — это задача клиента.

Принцип «только то, что нужно»

Ключевой дизайн-принцип NuGet — минимизация избыточности. При установке пакета NuGet анализирует целевую платформу проекта и извлекает только те сборки и ресурсы, которые совместимы с ней. Если пакет содержит сборки для net48, net6.0, net7.0 и netstandard2.1, а проект целится на net8.0, будет выбрана сборка с наивысшей совместимостью (в данном случае, скорее всего, netstandard2.1 или net6.0, если она есть — выбор определяется алгоритмом разрешения совместимости). Это снижает размер итогового приложения, исключает конфликты и упрощает поддержку.


Как работает NuGet

NuGet реализует классическую клиент-серверную модель с элементами peer-to-peer-оптимизации (через кэширование). Взаимодействие можно описать через три основные роли:

  1. Создатель пакета (publisher)
    Разработчик или автоматизированная система, которая:

    • компилирует код;
    • формирует манифест (вручную или на основе .csproj);
    • упаковывает артефакты в .nupkg (через dotnet pack, nuget pack или MSBuild);
    • публикует пакет в узле (через dotnet nuget push, nuget push, или API узла).
  2. Узел (feed)
    Сервер, хранящий пакеты и предоставляющий к ним доступ по протоколам:

    • V2 (OData-based, устаревший, но ещё поддерживаемый);
    • V3 (основан на статических JSON-файлах, оптимизирован для CDN и высокой доступности — используется nuget.org с 2015 года);
    • Local Feed — просто каталог файловой системы (например, \\server\packages или C:\local-nuget), доступный по UNC или HTTP.

    Узел не обязан быть централизованным. Он может быть:

    • публичным (nuget.org);
    • частным (Azure Artifacts, Nexus Repository, ProGet);
    • гибридным (с зеркалированием nuget.org и дополнением внутренними пакетами);
    • временным (для CI/CD — например, пакет, собранный в одном задании и использованный в другом).
  3. Потребитель пакета (consumer)
    Это проект (и разработчик, его поддерживающий), который:

    • объявляет зависимости (в .csproj или packages.config);
    • восстанавливает пакеты (самостоятельно или автоматически);
    • интегрирует содержимое пакета в свою сборку.

Процесс восстановления (restore) — центральный механизм NuGet. Он состоит из нескольких этапов:

  • чтение списка зависимостей (PackageReference, packages.config, или project.assets.json);
  • разрешение графа зависимостей (dependency graph resolution): определение, какие версии каких пакетов фактически будут использованы с учётом транзитивных зависимостей и стратегии выбора версий (например, «ближайшая совместимая»);
  • загрузка недостающих пакетов из узлов (с учётом кэша);
  • извлечение нужных файлов в локальное хранилище (глобальный пакетный кэш);
  • обновление метаданных проекта (если требуется).

Важно: восстановление — это чисто декларативная операция. Результат полностью предопределён содержанием файла проекта и состоянием узлов. Это делает сборки детерминированными и воспроизводимыми — фундаментальное требование для CI/CD.


Политика платформы

NuGet как платформа строится на балансе между открытостью экосистемы и управляемостью рисков. Эта политика проявляется в нескольких слоях.

1. Открытая публикация и модерация

На nuget.org любой зарегистрированный пользователь может опубликовать пакет — без предварительной модерации. Это демократизирует экосистему, позволяет быстро распространять инновации, но создаёт риски:

  • поддельные пакеты (typosquatting — Newtonsoft.Json vs NewtonsoftJson);
  • вредоносный код;
  • нарушение лицензий;
  • устаревшие, неподдерживаемые библиотеки.

В ответ на эти вызовы NuGet внёс ряд мер:

  • автоматическая проверка и сканирование (на вирусы, известные уязвимости через интеграцию с OSS Index, Sonatype и др.);
  • рекомендации по именованию (префиксы организаций, подтверждение прав на домены);
  • возможность отзыва пакетов (deprecation и unlist);
  • метки «verified» и «trusted» для официальных пакетов (например, от Microsoft);
  • поддержка подписи пакетов (package signing) — криптографическая гарантия подлинности и целостности.

Однако окончательная ответственность за выбор пакетов лежит на потребителе. NuGet не является гарантом качества — он лишь транспортом и каталогом.

2. Частные узлы и гибкость распространения

Ключевое преимущество NuGet — отсутствие привязки к единственному репозиторию. Организации могут:

  • размещать проприетарные библиотеки внутри сети (без риска утечки);
  • зеркалировать публичные пакеты для повышения отказоустойчивости и скорости сборки;
  • применять собственные политики:
    • автоматическое сканирование на уязвимости (например, через Snyk или Whitesource);
    • белые/чёрные списки пакетов;
    • требование подписи;
    • квоты и лимиты.

Таким образом, политика платформы NuGet — это фреймворк для реализации политик на уровне организации.

3. Управление версиями и жизненным циклом

NuGet строго следует принципам семантического версионирования (SemVer 2.0). Это не просто рекомендация — это часть контракта между создателем и потребителем:

  • MAJOR-изменения — нарушают обратную совместимость;
  • MINOR — добавляют функционал, сохраняя совместимость;
  • PATCH — исправления без изменений API.

NuGet использует эту информацию при разрешении зависимостей:

  • при указании [1.0.0, 2.0.0) (включительно 1.0.0, исключая 2.0.0) будет выбрана самая новая минорная/патч-версия в диапазоне;
  • при 1.0.* — самая новая версия с мажорной 1 и минорной 0;
  • при 1.0.0 — строго эта версия.

Такой подход позволяет создателям пакетов безопасно эволюционировать API, а потребителям — контролировать степень риска при обновлениях.


Средства NuGet

NuGet — это экосистема инструментов, которые дополняют друг друга, частично дублируют функциональность и постепенно эволюционируют в сторону унификации. Эта множественность обусловлена историей развития .NET: от классического .NET Framework через переходный период к .NET Core и, наконец, к единой платформе .NET 5+. Ниже рассмотрены все основные средства, их назначение, сфера применения и взаимосвязи.

1. CLI dotnet

dotnet — это кросс-платформенная утилита командной строки, поставляемая вместе с .NET SDK. Начиная с .NET Core 1.0 (2016 г.), она постепенно стала основным и рекомендуемым способом взаимодействия со всей экосистемой .NET, включая NuGet.

Хотя dotnet не является частью NuGet как проекта, он включает встроенные подкоманды NuGet, реализованные поверх NuGet.Core — библиотеки, лежащей в основе всей логики управления пакетами. Это означает, что dotnet restore, dotnet add package, dotnet pack, dotnet nuget push — это нативные вызовы NuGet-движка, интегрированные в единый CLI-интерфейс.

Ключевые команды и их роль

  • dotnet restore
    Восстанавливает зависимости проекта: читает PackageReference, разрешает граф, загружает пакеты в глобальный кэш (~/.nuget/packages на Unix/macOS, %userprofile%\.nuget\packages на Windows), генерирует obj/project.assets.json.
    Выполняется автоматически при dotnet build, но может вызываться явно — например, в CI/CD-конвейерах до сборки, чтобы отделить этап загрузки зависимостей от компиляции.

  • dotnet add package <ID>
    Добавляет ссылку на пакет в файл проекта (.csproj). По умолчанию выбирает последнюю стабильную версию, совместимую с целевой платформой проекта. Поддерживает флаги:

    • --version — явное указание версии (включая предварительные релизы: --version 5.0.0-rc.1);
    • --prerelease — разрешить предрелизные версии при автоматическом выборе;
    • --source — использовать конкретный узел (например, внутренний feed Azure Artifacts).
  • dotnet list package
    Инспектирует зависимости: показывает прямые (top-level), транзитивные и устаревшие пакеты. Особенно полезен для аудита:

    dotnet list package --outdated

    выведет все пакеты, для которых доступна более новая версия.

  • dotnet pack
    Упаковывает проект в .nupkg. Использует метаданные из .csproj (в SDK-стиле) для формирования манифеста. Поддерживает кастомизацию через свойства MSBuild:

    dotnet pack -p:PackageVersion=2.1.0 -p:RepositoryUrl=https://github.com/org/repo
  • dotnet nuget push
    Публикует .nupkg в указанный узел. Требует API-ключа (обычно через -k или окружение NUGET_API_KEY).
    Пример:

    dotnet nuget push MyLib.2.1.0.nupkg --source https://api.nuget.org/v3/index.json --api-key <ключ>

Преимущества dotnet CLI

  • Единство среды: не нужно устанавливать дополнительные утилиты — всё идёт в комплекте с SDK.
  • Кросс-платформенность: один и тот же интерфейс на Windows, Linux, macOS.
  • Глубокая интеграция с проектной моделью SDK-стиля: понимает TargetFramework, PackageReference, IsPackable, условную компиляцию и т.д.
  • Автоматическое восстановление: при build, publish, run зависимости восстанавливаются «на лету», если отсутствуют.
  • Прозрачность для CI/CD: команды идемпотентны, логируют в стандартные потоки, легко интегрируются в скрипты.

Ограничения

  • Не поддерживает проекты в старом формате (.csproj без <Project Sdk="...">), если только они не переведены на PackageReference. Для packages.config требуется nuget.exe.
  • Нет прямой поддержки некоторых legacy-операций (например, nuget spec для генерации .nuspec вручную).

2. CLI nuget.exe

nuget.exe — это оригинальный консольный клиент NuGet, разработанный для Windows и ориентированный на .NET Framework и Visual Studio. Он существует с 2010 года и до сих пор поддерживается, особенно в сценариях, где нужна максимальная совместимость со старыми проектами или специфическими операциями, не перенесёнными в dotnet.

Когда используется nuget.exe

  • Работа с проектами, использующими packages.config (например, старые ASP.NET Web Forms, WPF на .NET Framework);
  • Генерация .nuspec вручную через nuget spec;
  • Операции с локальными feed’ами на основе файловой системы (хотя dotnet тоже поддерживает это);
  • Интеграция в среды, где .NET SDK недоступен (например, чистый .NET Framework runtime без SDK);
  • Использование устаревших команд, таких как nuget init (копирование пакетов в локальный feed).

Особенности поведения

  • Для nuget restore требуется, чтобы в проекте был packages.config или файл решения (.sln) — иначе он не знает, что восстанавливать.
  • При восстановлении в режиме packages.config пакеты распаковываются в папку packages/ рядом с решением — не в глобальный кэш. Это приводит к дублированию на диске, но даёт полную изоляцию проектов.
  • Не понимает SDK-стиль проектов «из коробки» — для них предпочтителен dotnet restore.

Совместимость и развёртывание

nuget.exe — это автономный исполняемый файл. Его можно скачать с nuget.org/downloads или получить через Chocolatey (choco install nuget.commandline). Версии привязаны к поколениям NuGet:

  • 2.x — поддержка .NET Framework, V2-узлов;
  • 3.x — переход на V3, начало поддержки .NET Core;
  • 4.x+ — полная поддержка .NET Standard и .NET Core, но без интеграции в dotnet CLI.

Современные рекомендации: если проект использует .NET SDK (а это все проекты, созданные после 2017 г.), предпочтителен dotnet CLI. nuget.exe — инструмент для legacy-поддержки и специфических задач.


3. Консоль диспетчера пакетов (Package Manager Console, PMC)

Это компонент Visual Studio — встроенный интерпретатор PowerShell, расширенный командлетами NuGet. Доступен через Tools → NuGet Package Manager → Package Manager Console.

Уникальные возможности

  • PowerShell-интеграция: команды возвращают объекты, а не строки — их можно фильтровать, преобразовывать, сохранять в переменные.
    Пример:
    Get-Package -ProjectName MyWebApp | Where-Object { $_.Id -like "*Entity*" }
  • Доступ к метаданным решения и проектов: можно писать скрипты, зависящие от структуры решения (например, «установить пакет во все проекты, кроме тестовых»).
  • Поддержка Install-Package, Update-Package, Uninstall-Package как для PackageReference, так и для packages.config — в отличие от dotnet, который работает только с PackageReference.

Типичные сценарии

  • Массовое обновление пакетов во всех проектах решения:
    Get-Project -All | ForEach-Object { Update-Package -Id Newtonsoft.Json -ProjectName $_.Name }
  • Удаление пакета с очисткой ненужных зависимостей:
    Uninstall-Package -Id SomeLib -RemoveDependencies

Ограничения

  • Работает только внутри Visual Studio (Windows-версия; в VS for Mac и VS Code отсутствует).
  • Требует, чтобы решение было загружено.
  • Не подходит для CI/CD — это инструмент разработчика, а не сборочной системы.

4. Пользовательский интерфейс диспетчера пакетов (Package Manager UI)

Графический интерфейс в Visual Studio: Manage NuGet Packages for Solution. Позволяет визуально:

  • искать пакеты на подключённых узлах;
  • просматривать версии, описания, зависимости;
  • устанавливать/обновлять/удалять пакеты по проектам.

Плюсы

  • Интуитивно понятен для новичков.
  • Показывает «что изменилось» при обновлении (список изменённых зависимостей).
  • Интегрирует предпросмотр изменений в .csproj до подтверждения.

Минусы

  • Не позволяет автоматизировать.
  • Скрытые операции: например, при установке пакета в решение UI может не показать конфликты зависимостей, пока не начнётся сборка.
  • Медленнее CLI при работе с большим числом пакетов.

Этот интерфейс — хороший компаньон для разведки и разовых действий, но не замена декларативному управлению через файлы проекта.


5. MSBuild

NuGet тесно интегрирован в MSBuild — движок сборки .NET. В SDK-стиле (начиная с .NET Core) эта интеграция реализована через импорт целей (*.targets) и свойств (*.props) в процесс сборки.

Как это работает

Когда проект использует <PackageReference>, MSBuild (через Microsoft.NET.Sdk) автоматически:

  1. Выполняет Restore как часть предварительных целей (BeforeBuild), если включено свойство RestorePackagesWithLockFile или RestoreLockedMode;
  2. Добавляет ссылки на DLL из пакетов в ReferencePath;
  3. Копирует файлы содержимого (из contentFiles, build, runtimes) в выходной каталог, если это необходимо;
  4. Обрабатывает build/*.targets — позволяет, например, запустить генератор кода перед компиляцией.

Это означает: управление зависимостями — часть сборки, а не отдельный этап. Для разработчика это проявляется в том, что dotnet build «просто работает», даже если пакеты не установлены локально.

Важные свойства MSBuild, связанные с NuGet

  • RestoreSources — список узлов (разделённых ;), используется при восстановлении;
  • RestoreFallbackFolders — дополнительные каталоги для поиска пакетов (например, локальный кэш);
  • PackageOutputPath — куда dotnet pack помещает .nupkg;
  • IncludeSymbols и SymbolPackageFormat — управление генерацией символьных пакетов (.snupkg).

Используя эти свойства, можно полностью настроить поведение NuGet без изменения кода — через командную строку или конфигурационные файлы (Directory.Build.props).


6. Конфигурация: nuget.config и иерархия настроек

Все клиенты NuGet (CLI, Visual Studio, MSBuild) используют общую систему конфигурации — файл nuget.config в формате XML. Он может находиться в нескольких местах, образуя иерархию, где нижестоящие переопределяют вышестоящие:

  1. Машинный уровень (%appdata%\NuGet\nuget.config на Windows, ~/.config/NuGet/NuGet.Config на Unix) — настройки по умолчанию для пользователя;
  2. Решение/проект (nuget.config в корне репозитория) — настройки для команды;
  3. Явное указание (-ConfigFile в CLI).

Типичное содержание

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="internal-feed" value="https://pkgs.dev.azure.com/org/_packaging/feed/nuget/v3/index.json" />
</packageSources>

<packageSourceCredentials>
<internal-feed>
<add key="Username" value="user" />
<add key="ClearTextPassword" value="token" />
</internal-feed>
</packageSourceCredentials>

<config>
<add key="globalPackagesFolder" value="C:\custom-nuget-cache" />
<add key="repositoryPath" value=".\packages" /> <!-- для packages.config -->
</config>
</configuration>

Политические аспекты конфигурации

  • <clear /> перед <add> — важная практика для явного контроля источников. Без него системные/машинные узлы (например, Microsoft Visual Studio Offline Packages) могут вмешаться в разрешение.
  • Хранение учётных данных — чувствительная операция. В CI/CD предпочтительно использовать переменные окружения (NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED=1) или встроенные провайдеры (например, Azure Artifacts Credential Provider).
  • globalPackagesFolder — ключ к оптимизации дискового пространства и скорости восстановления на серверах сборки.

Конфигурация — это политический документ: он определяет, откуда разрешено брать код, как проверять подлинность и где хранить артефакты. Её следует версионировать вместе с исходным кодом (за исключением учётных данных).


Управление зависимостями: PackageReference и packages.config в сравнении

Введение: две модели — два мира

Разница между PackageReference и packages.config — это не просто формат файла. Это две фундаментально разные модели интеграции пакетов в проект, отражающие эволюцию мышления в .NET-сообществе:

  • packages.config — модель проект-локального копирования: зависимости извлекаются внутрь дерева решения и обрабатываются до сборки. Это внешний по отношению к MSBuild процесс.
  • PackageReference — модель декларативной интеграции: зависимости остаются вне решения, а их обработка встроена непосредственно в конвейер MSBuild. Это делает управление зависимостями частью сборки.

Эта разница определяет всё: от структуры файлов до поведения при обновлении, от размера репозитория до уязвимостей при сборке.


1. packages.config — историческая модель

Формат и расположение

  • Файл packages.config — XML-документ в корне каждого проекта (не решения).
  • Пример:
    <?xml version="1.0" encoding="utf-8"?>
    <packages>
    <package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
    <package id="NLog" version="5.0.4" targetFramework="net48" />
    </packages>

Как работает восстановление

  1. При nuget restore или открытии решения в Visual Studio:

    • клиент читает packages.config;
    • загружает каждый пакет в папку packages/ на уровне решения (например, MySolution/packages/Newtonsoft.Json.13.0.3/);
    • извлекает содержимое:
      • DLL копируются в lib/, затем — в References проекта (как явные ссылки на файлы в папке packages/);
      • скрипты из tools/install.ps1 выполняются (да, PowerShell-скрипты во время установки — серьёзный вектор атаки);
      • файлы из content/ копируются напрямую в проект (например, web.config.transform).
  2. При сборке:

    • MSBuild видит ссылки на DLL в packages/, но не знает, откуда они взялись;
    • никаких метаданных о транзитивных зависимостях — только то, что явно указано в packages.config.

Преимущества (в историческом контексте)

  • Полнейшая изоляция: проект содержит все файлы, необходимые для сборки (если packages/ в репозитории).
  • Простота для инструментов вне экосистемы .NET (например, сборка в Ant или Maven — достаточно скопировать packages/).
  • Поддержка в Visual Studio с 2010 года.

Критические недостатки

ПроблемаПоследствия
Плоский список зависимостейНевозможно корректно разрешить транзитивные конфликты. Если пакет A требует LibX 1.0, а пакет B — LibX 2.0, в packages.config обе версии просто перечисляются. При сборке MSBuild возьмёт последнюю добавленную — неопределённое поведение.
DLL-спагетти в ReferencesВ обозревателе решений — десятки явных ссылок на файлы вида ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll. Затрудняет навигацию, усложняет анализ.
Избыточное копированиеКаждый проект дублирует одни и те же DLL в bin/, даже если они идентичны. Итоговый размер приложения — сумма всех lib/ пакетов, без дедупликации.
Выполнение скриптов при установкеinstall.ps1, uninstall.ps1 — мощный, но небезопасный механизм. Исторически использовался для регистрации COM-компонентов, копирования native-библиотек, модификации web.config. Сегодня считается уязвимым и отключён по умолчанию в новых версиях NuGet.
Невозможность условной компиляцииНельзя сказать: «использовать LibX только если TargetFramework == net48». Всё устанавливается всегда.
Отсутствие поддержки dotnet CLIДля проектов с packages.config dotnet restore не работает. Требуется nuget.exe.

Когда остаётся актуальным

  • Поддержка унаследованных решений на .NET Framework (особенно ASP.NET Web Forms, WCF-сервисы);
  • Интеграция с инструментами, которые не понимают SDK-стиля (например, старые версии InstallShield);
  • Сценарии, где абсолютная изоляция важнее производительности (например, автономная сборка в закрытой сети без узлов).

Но даже в этих случаях рекомендуется плановая миграция.


2. PackageReference — современная декларативная модель

Формат и расположение

  • Зависимости объявляются непосредственно в файле проекта (.csproj, .vbproj и др.) с помощью элемента <PackageReference>.
  • Пример:
    <Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
    </ItemGroup>
    </Project>

Как работает восстановление

  1. При dotnet restore (или автоматически при сборке):

    • MSBuild считывает PackageReference;
    • вызывает NuGet-движок для разрешения полного графа зависимостей, включая транзитивные;
    • результат фиксируется в obj/project.assets.json — JSON-файл с детальным описанием:
      • какие пакеты использованы;
      • какие сборки выбраны для каждой платформы;
      • зависимости каждого пакета;
      • конфликты и их разрешения.
  2. При сборке:

    • MSBuild импортирует project.assets.json как часть конвейера;
    • добавляет ссылки на DLL из глобального кэша (~/.nuget/packages/);
    • копирует только нужные файлы (например, runtimes/win-x64/native/*.dll) в выходной каталог;
    • подключает build/*.targets для этапов препроцессинга.

Ключевые технические преимущества

ВозможностьКак это работаетЗначение
Разрешение транзитивных зависимостейNuGet строит граф всех зависимостей, затем применяет алгоритм ближайшей совместимой версии (nearest compatible version). Если A → X 2.0, B → X 1.5, а проект → X 2.0, будет выбрана 2.0 для всех.Устраняет конфликты, гарантирует использование одной версии библиотеки в приложении.
Глобальный кэш пакетовПакеты хранятся один раз на машине (%userprofile%\.nuget\packages). Несколько проектов используют одни и те же файлы.Экономия дискового пространства (до 70% при большом числе проектов), ускорение восстановления.
Условные ссылкиПоддержка Condition и TargetFramework:
<PackageReference Include="LibA" Version="1.0.0" Condition="'$(TargetFramework)' == 'net48'" />, <PackageReference Include="LibB" Version="2.0.0" Condition="'$(TargetFramework)' == 'net8.0'" />Возможность тонкой настройки зависимостей под платформу.
Отсутствие скриптов установкиinstall.ps1 игнорируется. Вся логика должна быть реализована через MSBuild-цели (build/*.targets) — прозрачно и безопасно.Повышение безопасности: нет выполнения произвольного кода при добавлении пакета.
Поддержка блокировки зависимостейФайл packages.lock.json (включается через <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>) фиксирует точные версии всех пакетов, включая транзитивные. Аналог package-lock.json в npm.100% воспроизводимость сборок — критично для production и аудита.
Интеграция с современными практикамиРаботает с .NET SDK, Source Link, EmbedAllSources, Deterministic Builds, SBOM.Соответствие DevSecOps-стандартам.

Влияние на разработку и эксплуатацию

  • Репозиторий «чистый»: в Git попадает только .csproj и packages.lock.json (если используется), но не бинарники.
  • Сборка «с нуля» мгновенна: git clone → dotnet restore → dotnet build — всё восстанавливается из узлов.
  • CI/CD-конвейеры проще: не нужно кэшировать папку packages/, достаточно кэшировать глобальный кэш (~/.nuget/packages).
  • Безопасность выше: отсутствие скриптов, поддержка подписи пакетов, интеграция с анализаторами уязвимостей (например, через dotnet list package --vulnerable).

3. Сравнительная таблица: ключевые параметры

Критерийpackages.configPackageReference
ФорматОтдельный XML-файл на проектВстроенные элементы в .csproj
Хранение пакетовЛокальная папка packages/ (в решении)Глобальный кэш пользователя
Транзитивные зависимостиВручную (плоский список)Автоматически (граф зависимостей)
Разрешение конфликтовНет (неопределённое поведение)Да (алгоритм ближайшей версии)
Поддержка .NET SDKНетДа (обязательно)
Работа с dotnet CLIНет (требуется nuget.exe)Полная
Условная компиляцияНетДа (Condition, TargetFramework)
Блокировка версийНетДа (packages.lock.json)
БезопасностьНизкая (скрипты при установке)Высокая (только MSBuild-цели)
Размер репозиторияБольшой (если packages/ включён)Минимальный
Скорость восстановленияМедленнее (копирование в packages/)Быстрее (использование кэша)
Поддержка в новых проектахУстаревшаяРекомендуемая (по умолчанию с 2017 г.)

4. Миграция с packages.config на PackageReference

Процесс не всегда тривиален, но Visual Studio (начиная с 2017 15.7) предоставляет встроенный конвертер:
Правой кнопкой по packages.config → Migrate packages.config to PackageReference.

Что делает конвертер

  1. Анализирует зависимости и транзитивные ссылки;
  2. Переносит <package> в <PackageReference>;
  3. Удаляет packages.config и папку packages/;
  4. Генерирует project.assets.json.

На что обратить внимание

  • Скрипты install.ps1 теряются — их логику нужно перенести в build/*.targets или заменить на альтернативные решения (например, Microsoft.Web.LibraryManager для клиентских библиотек).
  • Content-файлы не копируются автоматически — если пакет полагался на content/web.config.transform, потребуется ручная настройка (например, через contentFiles или MSBuild-копирование).
  • Некоторые пакеты несовместимы — особенно устаревшие, созданные до 2016 г. В этом случае может потребоваться поиск альтернатив или обновление пакета.

Для массовой миграции в решении можно использовать инструмент NuGet PackageReference Updater или написать скрипт на основе dotnet CLI.


5. Политические и архитектурные выводы

Выбор модели управления зависимостями — это архитектурное решение, отражающее ценности проекта:

  • packages.config подразумевает изоляцию и контроль «здесь и сейчас» — подходит для закрытых, статичных систем, где обновления редки.
  • PackageReference подразумевает гибкость, воспроизводимость и интеграцию в поток — подходит для живых продуктов, развиваемых в CI/CD-парадигме.

Microsoft официально объявила packages.config устаревшим в 2018 году. Новые типы проектов (библиотеки, консольные приложения, ASP.NET Core) используют PackageReference по умолчанию. Для .NET Framework-проектов миграция возможна и рекомендуется — за исключением случаев, где используются legacy-пакеты без поддержки SDK-стиля (например, некоторые COM-обёртки или устаревшие ORM).


Механизмы разрешения зависимостей и конфликты

Когда разработчик пишет в .csproj:

<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

— он выражает намерение: «мне нужна эта библиотека, и она должна быть совместима с моим проектом». Но между намерением и итоговой сборкой лежит сложный процесс, состоящий из четырёх ключевых этапов:

  1. Сборка графа зависимостей — определение всех пакетов, от которых напрямую или косвенно зависит проект.
  2. Разрешение версий — выбор конкретной версии для каждого пакета, учитывающий совместимость, диапазоны и политики.
  3. Разрешение целевых платформ — выбор конкретной сборки внутри пакета (например, net6.0, netstandard2.0, net48).
  4. Генерация ассетов — фиксация результата в project.assets.json и подготовка ссылок для MSBuild.

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


1. Сборка графа зависимостей

Граф зависимостей — это ориентированный ациклический граф (DAG), где:

  • узлы — пакеты (с указанием идентификатора и версии);
  • рёбра — зависимости (с диапазонами версий).

Как строится граф

  1. NuGet читает все <PackageReference> в проекте — это корневые узлы.
  2. Для каждого корневого узла загружается его манифест (из .nupkg или из кэша).
  3. Из манифеста извлекаются <dependency> — и они добавляются как дочерние узлы.
  4. Процесс повторяется рекурсивно, пока не будут обработаны все транзитивные зависимости.

Важно: диапазоны версий сохраняются на этапе построения. Например, если Newtonsoft.Json 13.0.3 требует Microsoft.CSharp [4.3.0, ), это фиксируется как ограничение, но конкретная версия Microsoft.CSharp ещё не выбрана.

Пример графа (упрощённо)

MyApp
├─ Newtonsoft.Json 13.0.3
│ ├─ Microsoft.CSharp [4.3.0, )
│ └─ System.ComponentModel.TypeConverter [4.3.0, )
└─ Serilog 3.0.1
├─ Microsoft.CSharp [4.0.1, )
└─ System.Diagnostics.DiagnosticSource [5.0.0, )

Здесь уже виден потенциальный конфликт: Newtonsoft.Json требует Microsoft.CSharp ≥ 4.3.0, а Serilog≥ 4.0.1. NuGet должен выбрать одну версию, удовлетворяющую обоим условиям.


2. Разрешение версий

NuGet применяет алгоритм, названный Nearest Compatible Version (ближайшая совместимая версия). Его суть — жадный выбор наиболее свежей версии, совместимой со всеми требованиями. Правила:

  1. Для каждого пакета собираются все ограничения из графа.
  2. Выбирается наибольшая версия, удовлетворяющая всем ограничениям одновременно.
  3. Если такая версия существует — она используется.
  4. Если нет — ошибка сборки: «Version conflict detected».

Пример разрешения

ПакетОграничения из графаВыбранная версия
Microsoft.CSharp≥ 4.3.0 (от Newtonsoft.Json), ≥ 4.0.1 (от Serilog)4.7.0 (последняя стабильная ≥ 4.3.0)
System.Diagnostics.DiagnosticSource≥ 5.0.0 (от Serilog)8.0.0 (если доступна и совместима с целевой платформой)

Особые случаи

  • Точное совпадение (1.0.0) — требует именно этой версии. Если другой пакет требует 1.0.1, конфликт неизбежен.
  • Диапазоны с верхней границей ([1.0.0, 2.0.0)) — если доступна только 2.0.0, версия не подходит.
  • Предрелизные версии — по умолчанию игнорируются, если не указан IncludePrerelease="true" или флаг --prerelease в CLI.

Почему «ближайшая», а не «старейшая»?

Выбор наиболее свежей версии, а не минимально допустимой, — осознанное решение в пользу:

  • безопасности — новые версии чаще содержат исправления уязвимостей;
  • стабильности API — в .NET экосистеме major-версии редко ломают совместимость без веской причины;
  • поддержки платформ — новые версии чаще добавляют поддержку актуальных TFMs (например, net8.0).

Это отличает NuGet от некоторых других менеджеров (например, pip в Python по умолчанию выбирает наименьшую совместимую версию).


3. Разрешение целевых платформ (Target Framework Resolution)

Даже если версия пакета выбрана, нужно определить, какую именно сборку из него использовать — ведь пакет может содержать реализации для net48, net6.0, netstandard2.0 и т.д.

Как определяется совместимость

NuGet использует таблицу совместимости, встроенную в .NET SDK. Упрощённо:

Целевая платформа проектаСовместимые TFM в пакете (в порядке приоритета)
net8.0net8.0net7.0net6.0netstandard2.1netstandard2.0
net6.0net6.0netstandard2.1netstandard2.0
net48net48net472 → … → net45netstandard2.0netstandard1.6

Если в пакете есть lib/net8.0/MyLib.dll и проект целится на net8.0 — будет выбрана именно эта сборка. Если её нет, но есть lib/netstandard2.0/MyLib.dll — будет выбрана она, если netstandard2.0 совместим с net8.0 (а он совместим).

Многоплатформенное нацеливание (Multi-targeting)

Если разработчик пакета хочет поддержать максимальное число платформ, он компилирует сборки для нескольких TFMs и помещает их в один .nupkg:

lib/
├─ net48/MyLib.dll
├─ net6.0/MyLib.dll
├─ netstandard2.0/MyLib.dll
└─ net8.0/MyLib.dll

При установке NuGet извлекает только одну из этих сборок — ту, что максимально близка к целевой платформе проекта. Это гарантирует:

  • использование нативных API (например, Span<T> в netstandard2.1+);
  • минимизацию размера итоговой сборки.

«Понижение» платформы

Если пакет содержит только net48 и netstandard1.3, а проект — net8.0, будет выбрана netstandard1.3. Это может привести к:

  • потере производительности (отсутствие новых API);
  • runtime-ошибкам (если netstandard1.3 не реализует нужный интерфейс на целевой платформе).

Поэтому рекомендуется:

  • для библиотек — целиться на наивысшую возможную версию .NET Standard (например, 2.1), если не требуется специфичный API;
  • для приложений — явно указывать TargetFramework, а не полагаться на netstandard.

4. Конфликты и их разрешение

Конфликты неизбежны в сложных системах. NuGet различает два типа:

4.1. Конфликты версий (Version Conflicts)

Симптом:
Ошибка при восстановлении:
NU1107: Version conflict detected for Microsoft.CSharp.

Причина:
Два или более пакета требуют несовместимые версии одного и того же пакета.
Пример:

  • A требует X [1.0.0, 2.0.0)
  • B требует X [2.0.0, 3.0.0)
    → Нет версии, удовлетворяющей обоим.

Возможные решения:

  • Явное указание версии в корне:
    <PackageReference Include="X" Version="2.0.0" />
    Это переопределяет требования дочерних пакетов (если 2.0.0 совместима с их нижней границей).
  • Использование Update вместо Version:
    <PackageReference Include="X" Version="1.5.0" />
    <PackageReference Update="X" Version="2.0.0" />
    (работает только при наличии исходной ссылки).
  • Центральное управление через Directory.Packages.props (см. ниже).

4.2. Конфликты сборок (Assembly Conflicts)

Симптом:
Предупреждение при сборке:
MSB3277: Found conflicts between different versions of "System.Memory" that could not be resolved.

Причина:
Разные пакеты привносят разные версии одной и той же сборки (например, System.Memory 4.5.3 и 4.5.4), и MSBuild не может автоматически выбрать, какую использовать.

Как работает разрешение:
MSBuild применяет политику AutoUnify по умолчанию:

  • если версии сборок совместимы по major.minor (т.е. 4.5.x), выбирается наиболее свежая;
  • если нет — ошибка.

Управление вручную:

  • Отключить AutoUnify:
    <PropertyGroup>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
    </PropertyGroup>
    → MSBuild сгенерирует app.config с <bindingRedirect>.
  • Использовать PrivateAssets="All" для «скрытия» зависимости от транзитивного наследования (см. ниже).

5. project.assets.json — технический контракт сборки

Этот файл — ключевой артефакт PackageReference. Он генерируется в obj/ при каждом restore и содержит:

  • полный граф зависимостей с выбранными версиями;
  • маппинг «пакет → конкретные файлы (DLL, native libs)»;
  • зависимости для каждой целевой платформы (если проект multi-targeting);
  • метаданные для MSBuild: пути, свойства, условия.

Зачем это нужно?

  • Детерминированность: два разработчика, сделавшие dotnet restore, получат одинаковый project.assets.json, если узлы не менялись.
  • Интеграция с MSBuild: цели импортируют этот файл как ItemGroup, поэтому сборка «знает», откуда брать ссылки.
  • Диагностика: при ошибках можно открыть project.assets.json и увидеть, почему была выбрана та или иная версия.

Пример фрагмента

{
"targets": {
"net8.0": {
"Newtonsoft.Json/13.0.3": {
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"System.ComponentModel.TypeConverter": "4.3.0"
},
"compile": {
"lib/netstandard2.0/Newtonsoft.Json.dll": {}
},
"runtime": {
"lib/netstandard2.0/Newtonsoft.Json.dll": {}
}
}
}
}
}

Файл не предназначен для редактирования вручную — это промежуточный артефакт. Но его анализ — мощный инструмент отладки.


6. Продвинутые механизмы управления зависимостями

6.1. PrivateAssets, ExcludeAssets, IncludeAssets

Эти атрибуты управляют, как зависимость распространяется на потребителей:

АтрибутЗначение (по умолчанию)Эффект
PrivateAssetscontentfiles;analyzers;buildЗависимость не транзитивна: потребитель пакета не унаследует её. Полезно для тестовых утилит (xunit.runner.visualstudio).
ExcludeAssetsЯвное исключение типов: <PackageReference Include="A" ExcludeAssets="runtime" /> — DLL не попадёт в bin/.
IncludeAssetsallЯвное включение: <PackageReference Include="B" IncludeAssets="compile" /> — только для компиляции, не для runtime.

Пример:

<PackageReference Include="Microsoft.SourceLink.GitHub" 
PrivateAssets="All"
IncludeAssets="runtime;build;native;contentfiles;analyzers;buildtransitive" />

— делает SourceLink доступным только при сборке библиотеки, но не при её использовании.

6.2. Централизованное управление зависимостями (Directory.Packages.props)

Начиная с NuGet 6.2+ и .NET SDK 8.0+, появилась поддержка central package management (CPM). Создаётся файл Directory.Packages.props на уровне репозитория:

<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Serilog" Version="3.0.1" />
</ItemGroup>
</Project>

В проектах остаётся только:

<PackageReference Include="Newtonsoft.Json" />

Преимущества:

  • единые версии для всего решения (устранение «версионных дрейфов»);
  • упрощение обновления (меняется один файл);
  • принудительное соблюдение политик (например, запрет устаревших версий).

Этот подход особенно ценен в монорепозиториях и крупных организациях.


Кэширование, глобальный пакетный кэш и оптимизации

Ранние менеджеры пакетов (включая первую версию NuGet с packages.config) полагались на локальное копирование: каждый проект или решение имело собственную папку packages/, в которую извлекались все зависимости. Это давало полную изоляцию, но влекло серьёзные издержки:

  • Дублирование на диске: 10 проектов, использующих Newtonsoft.Json, хранили 10 копий одной и той же DLL.
  • Медленное восстановление: каждый restore означал полную загрузку и распаковку пакетов, даже если они уже были на машине.
  • Загромождение репозиториев: при включении packages/ в Git объём репозитория рос экспоненциально.

С переходом на PackageReference NuGet внёс фундаментальное изменение: пакеты стали разделяемыми ресурсами. Вместо копирования — ссылка. Вместо изоляции — централизованное управление. Это потребовало новой модели хранения — глобального пакетного кэша.


1. Глобальный пакетный кэш (global-packages-folder)

Это — основное хранилище распакованных пакетов на машине. По умолчанию расположен в:

  • Windows: %userprofile%\.nuget\packages\
  • Unix/macOS: ~/.nuget/packages/

Структура каталогов

Кэш организован по принципу ключ-значение, где ключ — комбинация идентификатора пакета и его версии:

~/.nuget/packages/
├─ newtonsoft.json/
│ ├─ 13.0.1/
│ │ ├─ lib/
│ │ │ └─ netstandard2.0/
│ │ │ └─ Newtonsoft.Json.dll
│ │ ├─ build/
│ │ │ └─ Newtonsoft.Json.targets
│ │ └─ newtonsoft.json.13.0.1.nupkg
│ └─ 13.0.3/
│ └─ ... (аналогично)
├─ microsoft.extensions.logging/
│ └─ 8.0.0/
│ └─ ...
└─ ...

Обратите внимание:

  • каждый пакет и каждая его версия хранятся отдельно — это гарантирует, что проекты, использующие 13.0.1 и 13.0.3, не конфликтуют;
  • внутри — полная распаковка .nupkg: все файлы, как в архиве, но в иерархии каталогов;
  • сам .nupkg также сохраняется — для целостности и возможности пересборки метаданных.

Почему именно так?

  • Параллелизм: несколько процессов (dotnet restore, Visual Studio, Rider) могут читать из кэша одновременно без блокировок.
  • Целостность: если кэш повреждён (например, не хватило места), достаточно удалить одну папку — остальные останутся работоспособными.
  • Очистка: nuget locals all --clear удаляет весь кэш, не затрагивая настройки.

Управление расположением

Расположение можно изменить через:

  • nuget.config:
    <config>
    <add key="globalPackagesFolder" value="D:\nuget-cache" />
    </config>
  • переменную окружения: NUGET_PACKAGES=D:\nuget-cache
  • флаг CLI: dotnet restore --packages D:\nuget-cache

Это полезно в сценариях:

  • сборка на машине с малым SSD (перенос на HDD);
  • shared-сборка в CI/CD (см. ниже);
  • аудит безопасности (изоляция кэша в отдельный том).

2. Fallback-папки (fallbackFolders) — многоуровневая память зависимостей

Глобальный кэш — не единственное место, откуда NuGet берёт пакеты. Существует иерархия fallback-папок — локальных каталогов, проверяемых до обращения к узлам. Это реализует принцип кэш-иерархии, аналогичный CPU cache (L1 → L2 → RAM → disk).

Уровни fallback-иерархии

  1. Глобальный кэш (globalPackagesFolder)
    → Уровень L1: самый быстрый, пользовательский.

  2. Системные fallback-папки
    Например:

    • %programfiles%\dotnet\sdk\NuGetFallbackFolder\ (в поставке .NET SDK до 3.1)
    • %programfiles%\Microsoft Visual Studio\...\Common7\IDE\CommonExtensions\Microsoft\NuGet\
      → Уровень L2: только для чтения, содержит базовые пакеты (например, Microsoft.NETCore.App.Ref), поставляемые вместе с SDK/VS. Гарантирует работу без интернета при создании нового проекта.
  3. Проектные fallback-папки
    Указываются в nuget.config:

    <config>
    <add key="repositoryPath" value=".\local-packages" />
    </config>

    → Уровень L3: для автономных сценариев (например, сборка в закрытой сети).

Как работает поиск

При restore:

  1. NuGet проверяет глобальный кэш — есть ли требуемая версия пакета.
  2. Если нет — проверяет fallback-папки (в порядке объявления).
  3. Только если нигде нет — обращается к узлам (nuget.org, внутренние feeds).

Это означает: если пакет есть в fallback-папке, он будет использован, даже если на nuget.org доступна более новая версия. Это критично для стабильности в enterprise-средах.

Практическое применение fallback-папок

  • Зеркалирование nuget.org: администратор разворачивает локальный feed (например, Azure Artifacts), затем копирует часто используемые пакеты в fallback-папку. Сборка идёт мгновенно, даже при падении основного узла.
  • Автономная разработка: на выставке или в командировке — скопировать ~/.nuget/packages на флешку, указать как fallback.
  • Изоляция сред: dev, test, prod — могут использовать разные fallback-наборы, гарантируя идентичность зависимостей.

3. Оптимизации для CI/CD-сред

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

3.1. Кэширование глобального кэша

В Azure Pipelines, GitHub Actions, GitLab CI можно кэшировать ~/.nuget/packages между заданиями:

GitHub Actions:

- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-

Azure Pipelines:

- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
restoreKeys: 'nuget | "$(Agent.OS)"'
path: $(NUGET_PACKAGES)

Это сокращает время restore с минут до секунд — особенно при неизменных зависимостях.

3.2. Использование packages.lock.json

Как упоминалось ранее, этот файл фиксирует точные версии всех пакетов, включая транзитивные. Включается в .csproj:

<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

В CI/CD он позволяет:

  • гарантировать, что сборка на сервере использует те же версии, что и на машине разработчика;
  • избежать «случайных обновлений» из-за выхода новой версии пакета между коммитами.

Если packages.lock.json включён в репозиторий, dotnet restore будет использовать его в строгом режиме (--locked-mode), и любое несоответствие вызовет ошибку.

3.3. Отключение восстановления при локальной разработке

В CI/CD можно отделить этапы:

dotnet restore --disable-parallel --verbosity quiet  # только restore
dotnet build --no-restore # сборка БЕЗ восстановления

Флаг --no-restore гарантирует, что сборка использует уже восстановленные зависимости, исключая race conditions.


4. Управление кэшами вручную: nuget locals

Иногда требуется очистка или диагностика. CLI nuget предоставляет команду locals:

nuget locals all --list        # показать пути всех кэшей
nuget locals http-cache --clear # очистить HTTP-кэш (запросы к узлам)
nuget locals global-packages --clear # очистить глобальный кэш
nuget locals temp --clear # очистить временные файлы

Аналогично в dotnet:

dotnet nuget locals all --clear

Когда это нужно

  • Диагностика: если «странный» пакет ведёт себя иначе — возможно, в кэше старая версия.
  • Освобождение места: кэш может занимать десятки гигабайт при активной разработке.
  • Изоляция тестов: CI-задание может стартовать с чистого кэша для гарантии «свежести».

Важно: очистка кэша не влияет на project.assets.json или .csproj. При следующем restore всё будет восстановлено — просто с сетью.


5. Отличие globalPackagesFolder от repositoryPath

Это — частая путаница, особенно при миграции с packages.config.

ПараметрglobalPackagesFolderrepositoryPath
Для какой моделиPackageReferencepackages.config
Расположение по умолчанию~/.nuget/packages./packages (рядом с решением)
СодержимоеРаспакованные пакеты (каталоги)Распакованные пакеты + .nupkg
РазделяемостьДа (между проектами)Нет (только внутри решения)
Используется в dotnet CLIДаНет

Если в nuget.config указаны оба параметра, repositoryPath игнорируется для проектов с PackageReference.


6. Как NuGet экономит место и время: итоговая модель

NuGet выстраивает иерархическую систему хранения, где каждый уровень оптимизирован под свою задачу:

  1. L1: Глобальный кэш — разделяемый, быстрый доступ, полная воспроизводимость.
  2. L2: Системные fallback-папки — стабильность «из коробки», работа без сети.
  3. L3: Локальные fallback-папки — контроль для enterprise, автономность.
  4. L4: Узлы (feeds) — источник истины, обновляемый в фоне.

Эта модель позволяет:

  • Разработчику — мгновенно переключаться между проектами без дублирования;
  • CI/CD — минимизировать сетевой трафик и время сборки;
  • Администратору — контролировать, какие пакеты и версии доступны в организации.

Экономия измеряется в людяхо-часах: отсутствие «странного поведения из-за кэша» — это снижение когнитивной нагрузки и повышение доверия к системе.


Как собрать и опубликовать библиотеку с поддержкой нескольких платформ

Цель — создать один пакет NuGet, который может быть использован в максимально широком спектре проектов. Это достигается за счёт многоплатформенного нацеливания (multi-targeting): библиотека компилируется отдельно для каждой целевой платформы, и результаты помещаются в один .nupkg.

Пример охвата:

  • net48 — для унаследованных приложений на .NET Framework;
  • net6.0, net8.0 — для современных приложений на .NET;
  • netstandard2.0, netstandard2.1 — для максимальной совместимости (если API позволяет).

Важно: поддержка нескольких платформ — это не просто «скомпилировать под netstandard». Это осознанный выбор, основанный на анализе:

  • каких API требует ваша библиотека;
  • какие платформы используют ваши потребители;
  • какие trade-offs вы готовы принять (сложность сборки vs охват).

1. Подготовка проекта: SDK-стиль и multi-targeting

1.1. Создание проекта

Начните с проекта библиотеки классов в SDK-стиле (создаётся по умолчанию в Visual Studio 2017+ или через dotnet new classlib):

dotnet new classlib -n MyAwesomeLibrary

1.2. Настройка целевых платформ

В файле .csproj замените <TargetFramework> на <TargetFrameworks> (обратите внимание на множественное число):

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net6.0;net8.0;netstandard2.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

Обратите внимание:

  • платформы перечисляются через ;;
  • порядок не важен для сборки, но влияет на порядок в project.assets.json;
  • netstandard2.0 включён для совместимости с net461+ и netcoreapp2.0+.

1.3. Условная компиляция

Если для разных платформ требуется разный код (например, использование Span<T> в netstandard2.1+, но не в net48), используйте preprocessor directives и многоверсионные TFMs:

#if NET48
// Legacy-реализация без Span<T>
var buffer = new byte[size];
#else
// Современная реализация
Span<byte> buffer = stackalloc byte[size];
#endif

Доступные символы:

  • NET48, NET6_0, NET8_0, NETSTANDARD2_0 и т.д.
  • NETFRAMEWORK — для всех .NET Framework;
  • NETCOREAPP — для всех .NET Core/.NET 5+.

Можно также разделять код по файлах с помощью соглашения об именовании:

  • MyService.net48.cs
  • MyService.netstandard2.0.cs
    NuGet и MSBuild автоматически включат нужный файл при сборке под соответствующую платформу.

2. Расширение метаданных пакета

Минимальный пакет можно собрать и без дополнительных настроек, но для production-библиотеки требуется богатый манифест. В SDK-стиле метаданные задаются в .csproj:

<PropertyGroup>
<!-- Обязательные -->
<PackageId>MyCompany.MyAwesomeLibrary</PackageId>
<Version>1.0.0</Version>
<Authors>Timur Tagirov</Authors>
<Company>MyCompany</Company>
<Description>A high-performance utility library for data processing.</Description>

<!-- Рекомендуемые -->
<PackageProjectUrl>https://github.com/mycompany/myawesomelibrary</PackageProjectUrl>
<RepositoryUrl>https://github.com/mycompany/myawesomelibrary</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>data;utility;performance</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>

<!-- Опциональные, но важные -->
<PackageReleaseNotes>Initial release.</PackageReleaseNotes>
<Copyright>Copyright © 2025 MyCompany</Copyright>
<NeutralLanguage>en</NeutralLanguage>
</PropertyGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

Ключевые рекомендации

  • PackageId — используйте обратный DNS (например, CompanyName.LibraryName). Это минимизирует риск коллизий и typosquatting.
  • PackageLicenseExpression — указывайте SPDX-идентификатор (например, MIT, Apache-2.0). Избегайте licenseUrl — он устарел и не проходит модерацию на nuget.org.
  • PackageReadmeFile — README попадёт на главную страницу пакета на nuget.org. Обязательно используйте Markdown с примерами кода.
  • PackageIcon — иконка 64×64 или 128×128 PNG. Улучшает узнаваемость.

3. Расширенные сценарии сборки

3.1. Поддержка native-зависимостей

Если библиотека использует нативные библиотеки (.dll, .so, .dylib), их нужно включить в runtimes/:

runtimes/
├─ win-x64/
│ └─ native/
│ └─ mylib.dll
├─ linux-x64/
│ └─ native/
│ └─ libmylib.so
└─ osx-x64/
└─ native/
└─ libmylib.dylib

В .csproj:

<ItemGroup>
<None Update="runtimes\**" Pack="true" PackagePath="runtimes" />
</ItemGroup>

NuGet автоматически скопирует нужную native-библиотеку в bin/ при сборке потребителя.

Чтобы потребители могли отлаживать ваш код (а не только смотреть декомпилированный), включите Source Link:

<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>

Это позволит:

  • прыгать в исходники библиотеки из Visual Studio;
  • видеть точные строки кода в стек-трейсах;
  • использовать dotnet-symbol для загрузки PDB в отладчике.

3.3. Оптимизация для AOT и trimming

Если библиотека может использоваться в приложениях с PublishAot или TrimMode=link, добавьте аннотации:

<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>

И используйте атрибуты в коде:

[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(MyType))]
public void DoSomething() { ... }

Это предотвратит удаление критических типов при trimming.


4. Сборка и упаковка

4.1. Локальная сборка

dotnet build -c Release
dotnet pack -c Release

Результат:

  • bin/Release/MyAwesomeLibrary.1.0.0.nupkg — основной пакет;
  • bin/Release/MyAwesomeLibrary.1.0.0.snupkg — символьный пакет (если включены символы).

4.2. Проверка содержимого пакета

Вскройте .nupkg как ZIP-архив и проверьте структуру:

lib/
├─ net48/MyAwesomeLibrary.dll
├─ net6.0/MyAwesomeLibrary.dll
├─ net8.0/MyAwesomeLibrary.dll
└─ netstandard2.0/MyAwesomeLibrary.dll
runtimes/ # если есть
contentFiles/ # если есть
README.md
icon.png
MyAwesomeLibrary.nuspec

Используйте nuget verify для валидации:

nuget verify MyAwesomeLibrary.1.0.0.nupkg

Он проверит:

  • целостность архива;
  • наличие обязательных метаданных;
  • соответствие лицензии.

4.3. Тестирование потребления

Создайте тестовый проект и добавьте пакет локально:

dotnet new console -n TestApp
cd TestApp
dotnet add package MyAwesomeLibrary --source ../MyAwesomeLibrary/bin/Release
dotnet run

Убедитесь, что:

  • нет ошибок сборки;
  • runtime-поведение корректно;
  • native-библиотеки копируются (если есть);
  • отладка с Source Link работает.

5. Публикация

5.1. Получение API-ключа

  • Для nuget.org: зайдите в аккаунт → API KeysCreate.
  • Для Azure Artifacts: Artifacts → Connect to feed → NuGet → View instructions.

Ключ лучше хранить в переменной окружения:

export NUGET_API_KEY=ваш_ключ

5.2. Публикация

dotnet nuget push MyAwesomeLibrary.1.0.0.nupkg \
--source https://api.nuget.org/v3/index.json \
--api-key $NUGET_API_KEY

Для Azure Artifacts:

dotnet nuget push MyAwesomeLibrary.1.0.0.nupkg \
--source "https://pkgs.dev.azure.com/org/_packaging/feed/nuget/v3/index.json" \
--api-key AzureDevOps

(ключ AzureDevOps — зарезервированное значение; аутентификация идёт через Azure CLI или Personal Access Token).

5.3. Что происходит при публикации

  1. NuGet проверяет:
    • уникальность PackageId + Version;
    • корректность метаданных (особенно лицензии);
    • отсутствие вредоносного кода (автоматический сканер).
  2. Пакет сохраняется в хранилище узла.
  3. Индекс обновляется (для nuget.org — в течение 5–10 минут).
  4. Потребители могут найти пакет через поиск.

5.4. Предварительные релизы (prerelease)

Используйте суффиксы: 1.0.0-alpha, 1.0.0-beta.2, 1.0.0-rc.1.

В .csproj:

<Version>1.0.0-beta.1</Version>

Потребители смогут установить только с флагом:

dotnet add package MyAwesomeLibrary --prerelease

Это позволяет тестировать изменения перед стабильным релизом.


6. Сопровождение и обновление

6.1. Депрекация (deprecation)

Если пакет устарел, но не должен исчезать (чтобы не сломать существующие проекты):

  • На nuget.org: Manage Package → Deprecate → укажите замену (например, MyCompany.MyAwesomeLibrary.New).
  • В новой версии добавьте атрибут:
    [Obsolete("Use MyCompany.MyAwesomeLibrary.New instead.")]
    public class OldService { ... }

6.2. Отзыв (unlist)

Если пакет содержит критическую уязвимость:

  • Unlist скроет пакет из поиска, но оставит его доступным по прямой ссылке (чтобы не сломать сборки).
  • Не используйте Delete — это нарушает контракт с потребителями.

6.3. Стратегия версионирования

Следуйте SemVer:

  • MAJOR — только при нарушении обратной совместимости (удаление API, изменение поведения);
  • MINOR — добавление API, новых платформ, улучшения;
  • PATCH — исправления багов, обновления зависимостей.

Избегайте «псевдо-major»-версий (например, 2.0.0 без изменений API) — это снижает доверие.


7. Рекомендации для enterprise-библиотек

Если библиотека внутренняя:

  1. Подпись пакетов (Package Signing)
    Настройте CI на подпись .nupkg с помощью сертификата:

    nuget sign MyLibrary.1.0.0.nupkg -CertificateFingerprint ABC... -Timestamper http://...

    Это гарантирует подлинность и целостность.

  2. SBOM (Software Bill of Materials)
    Генерируйте sbom.json через dotnet msbuild /t:GenerateSBOM. Это требуется для compliance в госсекторе и финансах.

  3. Сканирование уязвимостей
    Включите в CI:

    dotnet list package --vulnerable

    или интеграцию с OWASP Dependency-Track.


Освоение главы0%