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.
Основные компоненты пакета
-
Манифест (
*.nuspec)
Это XML-файл, содержащий метаданные пакета:- уникальный идентификатор (
<id>), обычно по соглашению —CompanyName.LibraryName; - версия (
<version>), строго в формате major.minor.patch[-prerelease] (например,2.1.0,3.0.0-alpha.2); - описание, авторы, лицензия, ссылки на проект и репозиторий;
- зависимости — перечень других NuGet-пакетов, необходимых для корректной работы текущего.
Манифест может встраиваться напрямую в файл проекта (в
.csprojпри использовании SDK-стиля), но при сборке он всё равно извлекается и помещается в корень.nupkg. - уникальный идентификатор (
-
Сборки (
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.
Такая структура позволяет одному пакету содержать множество сборок, предназначенных для разных платформ, и делает возможным многоплатформенное нацеливание.
-
Ресурсы (
content/,contentFiles/,build/,tools/)content/— устаревший способ внедрения файлов (например, конфигурационных) напрямую в проект потребителя (использовался вpackages.config);contentFiles/— современный, декларативный подход: указываются файлы, которые попадут в проект как ссылки (content), или будут скопированы как ресурсы (resources), с возможностью указания языка (cs,vb) и действия (compile,none,embed);build/— файлы.propsи.targets, подключаемые в процесс сборки MSBuild (позволяют влиять на этапы компиляции, генерации кода и т.п.);tools/— исполняемые файлы или скрипты, доступные во время разработки (например, генераторы кода, утилиты командной строки), но не включаемые в итоговую сборку.
-
Документация (
docs/) и другие произвольные файлы
Хотя не регламентированы строго, допустимы дополнительные структуры — например,docs/с Markdown-руководствами. Важно: NuGet не интерпретирует их автоматически — это задача клиента.
Принцип «только то, что нужно»
Ключевой дизайн-принцип NuGet — минимизация избыточности. При установке пакета NuGet анализирует целевую платформу проекта и извлекает только те сборки и ресурсы, которые совместимы с ней. Если пакет содержит сборки для net48, net6.0, net7.0 и netstandard2.1, а проект целится на net8.0, будет выбрана сборка с наивысшей совместимостью (в данном случае, скорее всего, netstandard2.1 или net6.0, если она есть — выбор определяется алгоритмом разрешения совместимости). Это снижает размер итогового приложения, исключает конфликты и упрощает поддержку.
Как работает NuGet
NuGet реализует классическую клиент-серверную модель с элементами peer-to-peer-оптимизации (через кэширование). Взаимодействие можно описать через три основные роли:
-
Создатель пакета (publisher)
Разработчик или автоматизированная система, которая:- компилирует код;
- формирует манифест (вручную или на основе
.csproj); - упаковывает артефакты в
.nupkg(черезdotnet pack,nuget packили MSBuild); - публикует пакет в узле (через
dotnet nuget push,nuget push, или API узла).
-
Узел (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 — например, пакет, собранный в одном задании и использованный в другом).
-
Потребитель пакета (consumer)
Это проект (и разработчик, его поддерживающий), который:- объявляет зависимости (в
.csprojилиpackages.config); - восстанавливает пакеты (самостоятельно или автоматически);
- интегрирует содержимое пакета в свою сборку.
- объявляет зависимости (в
Процесс восстановления (restore) — центральный механизм NuGet. Он состоит из нескольких этапов:
- чтение списка зависимостей (
PackageReference,packages.config, илиproject.assets.json); - разрешение графа зависимостей (dependency graph resolution): определение, какие версии каких пакетов фактически будут использованы с учётом транзитивных зависимостей и стратегии выбора версий (например, «ближайшая совместимая»);
- загрузка недостающих пакетов из узлов (с учётом кэша);
- извлечение нужных файлов в локальное хранилище (глобальный пакетный кэш);
- обновление метаданных проекта (если требуется).
Важно: восстановление — это чисто декларативная операция. Результат полностью предопределён содержанием файла проекта и состоянием узлов. Это делает сборки детерминированными и воспроизводимыми — фундаментальное требование для CI/CD.
Политика платформы
NuGet как платформа строится на балансе между открытостью экосистемы и управляемостью рисков. Эта политика проявляется в нескольких слоях.
1. Открытая публикация и модерация
На nuget.org любой зарегистрированный пользователь может опубликовать пакет — без предварительной модерации. Это демократизирует экосистему, позволяет быстро распространять инновации, но создаёт риски:
- поддельные пакеты (typosquatting —
Newtonsoft.JsonvsNewtonsoftJson); - вредоносный код;
- нарушение лицензий;
- устаревшие, неподдерживаемые библиотеки.
В ответ на эти вызовы 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, но без интеграции в
dotnetCLI.
Современные рекомендации: если проект использует .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) автоматически:
- Выполняет
Restoreкак часть предварительных целей (BeforeBuild), если включено свойствоRestorePackagesWithLockFileилиRestoreLockedMode; - Добавляет ссылки на DLL из пакетов в
ReferencePath; - Копирует файлы содержимого (из
contentFiles,build,runtimes) в выходной каталог, если это необходимо; - Обрабатывает
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. Он может находиться в нескольких местах, образуя иерархию, где нижестоящие переопределяют вышестоящие:
- Машинный уровень (
%appdata%\NuGet\nuget.configна Windows,~/.config/NuGet/NuGet.Configна Unix) — настройки по умолчанию для пользователя; - Решение/проект (
nuget.configв корне репозитория) — настройки для команды; - Явное указание (
-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>
Как работает восстановление
-
При
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).
- DLL копируются в
- клиент читает
-
При сборке:
- MSBuild видит ссылки на DLL в
packages/, но не знает, откуда они взялись; - никаких метаданных о транзитивных зависимостях — только то, что явно указано в
packages.config.
- MSBuild видит ссылки на DLL в
Преимущества (в историческом контексте)
- Полнейшая изоляция: проект содержит все файлы, необходимые для сборки (если
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>
Как работает восстановление
-
При
dotnet restore(или автоматически при сборке):- MSBuild считывает
PackageReference; - вызывает NuGet-движок для разрешения полного графа зависимостей, включая транзитивные;
- результат фиксируется в
obj/project.assets.json— JSON-файл с детальным описанием:- какие пакеты использованы;
- какие сборки выбраны для каждой платформы;
- зависимости каждого пакета;
- конфликты и их разрешения.
- MSBuild считывает
-
При сборке:
- MSBuild импортирует
project.assets.jsonкак часть конвейера; - добавляет ссылки на DLL из глобального кэша (
~/.nuget/packages/); - копирует только нужные файлы (например,
runtimes/win-x64/native/*.dll) в выходной каталог; - подключает
build/*.targetsдля этапов препроцессинга.
- MSBuild импортирует
Ключевые технические преимущества
| Возможность | Как это работает | Значение |
|---|---|---|
| Разрешение транзитивных зависимостей | 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.config | PackageReference |
|---|---|---|
| Формат | Отдельный 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.
Что делает конвертер
- Анализирует зависимости и транзитивные ссылки;
- Переносит
<package>в<PackageReference>; - Удаляет
packages.configи папкуpackages/; - Генерирует
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" />
— он выражает намерение: «мне нужна эта библиотека, и она должна быть совместима с моим проектом». Но между намерением и итоговой сборкой лежит сложный процесс, состоящий из четырёх ключевых этапов:
- Сборка графа зависимостей — определение всех пакетов, от которых напрямую или косвенно зависит проект.
- Разрешение версий — выбор конкретной версии для каждого пакета, учитывающий совместимость, диапазоны и политики.
- Разрешение целевых платформ — выбор конкретной сборки внутри пакета (например,
net6.0,netstandard2.0,net48). - Генерация ассетов — фиксация результата в
project.assets.jsonи подготовка ссылок для MSBuild.
Любой из этих этапов может привести к конфликту — и тогда NuGet должен либо автоматически его разрешить, либо остановить сборку с диагностикой. Рассмотрим каждый этап подробно.
1. Сборка графа зависимостей
Граф зависимостей — это ориентированный ациклический граф (DAG), где:
- узлы — пакеты (с указанием идентификатора и версии);
- рёбра — зависимости (с диапазонами версий).
Как строится граф
- NuGet читает все
<PackageReference>в проекте — это корневые узлы. - Для каждого корневого узла загружается его манифест (из
.nupkgили из кэша). - Из манифеста извлекаются
<dependency>— и они добавляются как дочерние узлы. - Процесс повторяется рекурсивно, пока не будут обработаны все транзитивные зависимости.
Важно: диапазоны версий сохраняются на этапе построения. Например, если 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 (ближайшая совместимая версия). Его суть — жадный выбор наиболее свежей версии, совместимой со всеми требованиями. Правила:
- Для каждого пакета собираются все ограничения из графа.
- Выбирается наибольшая версия, удовлетворяющая всем ограничениям одновременно.
- Если такая версия существует — она используется.
- Если нет — ошибка сборки: «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.0 | net8.0 → net7.0 → net6.0 → netstandard2.1 → netstandard2.0 |
net6.0 | net6.0 → netstandard2.1 → netstandard2.0 |
net48 | net48 → net472 → … → net45 → netstandard2.0 → netstandard1.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:→ MSBuild сгенерирует<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>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
Эти атрибуты управляют, как зависимость распространяется на потребителей:
| Атрибут | Значение (по умолчанию) | Эффект |
|---|---|---|
PrivateAssets | contentfiles;analyzers;build | Зависимость не транзитивна: потребитель пакета не унаследует её. Полезно для тестовых утилит (xunit.runner.visualstudio). |
ExcludeAssets | — | Явное исключение типов: <PackageReference Include="A" ExcludeAssets="runtime" /> — DLL не попадёт в bin/. |
IncludeAssets | all | Явное включение: <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-иерархии
-
Глобальный кэш (
globalPackagesFolder)
→ Уровень L1: самый быстрый, пользовательский. -
Системные 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. Гарантирует работу без интернета при создании нового проекта.
-
Проектные fallback-папки
Указываются вnuget.config:<config>
<add key="repositoryPath" value=".\local-packages" />
</config>→ Уровень L3: для автономных сценариев (например, сборка в закрытой сети).
Как работает поиск
При restore:
- NuGet проверяет глобальный кэш — есть ли требуемая версия пакета.
- Если нет — проверяет fallback-папки (в порядке объявления).
- Только если нигде нет — обращается к узлам (
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.
| Параметр | globalPackagesFolder | repositoryPath |
|---|---|---|
| Для какой модели | PackageReference | packages.config |
| Расположение по умолчанию | ~/.nuget/packages | ./packages (рядом с решением) |
| Содержимое | Распакованные пакеты (каталоги) | Распакованные пакеты + .nupkg |
| Разделяемость | Да (между проектами) | Нет (только внутри решения) |
Используется в dotnet CLI | Да | Нет |
Если в nuget.config указаны оба параметра, repositoryPath игнорируется для проектов с PackageReference.
6. Как NuGet экономит место и время: итоговая модель
NuGet выстраивает иерархическую систему хранения, где каждый уровень оптимизирован под свою задачу:
- L1: Глобальный кэш — разделяемый, быстрый доступ, полная воспроизводимость.
- L2: Системные fallback-папки — стабильность «из коробки», работа без сети.
- L3: Локальные fallback-папки — контроль для enterprise, автономность.
- 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.csMyService.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/ при сборке потребителя.
3.2. Source Link и отладка
Чтобы потребители могли отлаживать ваш код (а не только смотреть декомпилированный), включите 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 Keys → Create. - Для 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. Что происходит при публикации
- NuGet проверяет:
- уникальность
PackageId + Version; - корректность метаданных (особенно лицензии);
- отсутствие вредоносного кода (автоматический сканер).
- уникальность
- Пакет сохраняется в хранилище узла.
- Индекс обновляется (для nuget.org — в течение 5–10 минут).
- Потребители могут найти пакет через поиск.
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-библиотек
Если библиотека внутренняя:
-
Подпись пакетов (Package Signing)
Настройте CI на подпись.nupkgс помощью сертификата:nuget sign MyLibrary.1.0.0.nupkg -CertificateFingerprint ABC... -Timestamper http://...Это гарантирует подлинность и целостность.
-
SBOM (Software Bill of Materials)
Генерируйтеsbom.jsonчерезdotnet msbuild /t:GenerateSBOM. Это требуется для compliance в госсекторе и финансах. -
Сканирование уязвимостей
Включите в CI:dotnet list package --vulnerableили интеграцию с OWASP Dependency-Track.