В чём разница между dotnet watch и dotnet run?
dotnet run один раз собирает ваш проект и запускает его. dotnet watch оборачивает dotnet run в наблюдатель за файлами: он перезапускает приложение или применяет hot reload каждый раз, когда вы сохраняете исходный файл. Вот что именно делает каждая команда, что задаёт dotnet watch, чего не задаёт dotnet run, и когда какую выбирать.
dotnet run один раз собирает ваш проект (по умолчанию Debug) и один раз запускает полученное приложение. Когда процесс завершается, вы возвращаетесь в командную строку. dotnet watch является наблюдателем за файлами, обёрнутым вокруг dotnet run: он запускает приложение, затем следит за исходными файлами, и каждый раз, когда вы сохраняете изменение, он либо применяет hot reload этого изменения к работающему процессу, либо перезапускает приложение. Коротко: dotnet run означает “собрать и запустить, один раз”; dotnet watch означает “держать приложение синхронным с моим кодом, пока я редактирую”. Они не конкуренты. dotnet watch буквально вызывает dotnet run под капотом, так что всё, что делает dotnet run, делает и dotnet watch, плюс наблюдение и hot reload сверху.
Всё, что ниже, использует SDK .NET 11 (11.0.100) с <TargetFramework>net11.0</TargetFramework> и C# 14. Основное поведение стабильно с SDK .NET 6, когда dotnet watch получил Hot Reload; изменения, зависящие от версии, отмечены там, где это важно.
Ментальная модель в одну строку
Выполните эти две команды подряд, и разница станет очевидна сразу:
# .NET 11 SDK 11.0.100, project targeting net11.0.
dotnet run # builds, launches once, returns to the prompt when the app exits
dotnet watch # builds, launches, then stays resident watching for file changes
dotnet run является завершающейся командой. Она делает свою работу и возвращает управление. dotnet watch является долгоживущим сеансом. Он не возвращается, пока вы не остановите его через Ctrl+C. Этот единственный факт поведения, резидентный против завершающегося, является корнем всех остальных различий.
Вот как две команды выстраиваются по возможностям:
| Поведение | dotnet run | dotnet watch |
|---|---|---|
| Собирает перед запуском | Да (через dotnet build) | Да (делегирует dotnet run) |
| Конфигурация по умолчанию | Debug | Debug |
| Запускает приложение | Один раз | Один раз, затем перезапуск при изменении |
| Следит за исходными файлами | Нет | Да |
| Hot Reload при редактировании | Нет | Да (с SDK .NET 6) |
| Перезапуск при неподдерживаемой правке | Н/Д | Да (запрос о rude edit или авто-перезапуск) |
| Авто-обновление браузера (веб) | Нет | Да |
| Возврат в командную строку по завершении | Когда приложение завершается | Только по Ctrl+C |
Читает launchSettings.json | Да | Да (через вызываемый dotnet run) |
dotnet run: собрать один раз, запустить один раз
Справка по dotnet run описывает её как команду, которая запускает ваше приложение “из исходного кода одной командой”. Она зависит от dotnet build, так что любое требование сборки применяется и к dotnet run. Последовательность такова: неявный restore, сборка в bin/<configuration>/<framework>/, затем запуск полученного бинарного файла. По умолчанию для большинства проектов используется Debug.
Поскольку dotnet run работает с проектом, а не со скомпилированной DLL, она также читает Properties/launchSettings.json и по умолчанию применяет первый профиль запуска. Оттуда берётся имя окружения вроде ASPNETCORE_ENVIRONMENT=Development и любые переменные окружения профиля. Вы можете отказаться от этого с помощью --no-launch-profile или явно выбрать профиль через -lp|--launch-profile.
Аргументы после литерального -- пересылаются вашему приложению, а не CLI:
# .NET 11 SDK 11.0.100. Everything after -- goes to your Main / top-level args.
dotnet run -- --input data.csv --verbose
-- важнее, чем кажется. dotnet run пересылает приложению любой токен, который не распознаёт, но сначала убирает собственные опции, что может переупорядочить остальное. Размещение аргументов приложения после -- помечает каждый следующий токен как аргумент приложения, так что CLI не переинтерпретирует его. Это также защищает ваши скрипты на будущее от новых опций dotnet run, которые позже могут столкнуться с токеном, предназначенным для приложения.
Некоторые вещи dotnet run намеренно не делает. Это не инструмент развёртывания: документация явно указывает, что она разрешает зависимости из кеша NuGet и “не рекомендуется использовать dotnet run для запуска приложений в продакшене”. Для этого вам нужен dotnet publish, что является отдельной темой, рассмотренной в разнице между dotnet build и dotnet publish. И она ни за чем не следит. Как только приложение запущено, dotnet run закончил работу; он понятия не имеет, что вы только что отредактировали файл.
dotnet watch: dotnet run плюс наблюдатель за файлами и Hot Reload
Справка по dotnet watch говорит прямо: dotnet watch “является наблюдателем за файлами. При обнаружении изменения он выполняет команду dotnet run или указанную команду dotnet”. Если изменение поддерживается для Hot Reload, он применяет его к работающему приложению без перезапуска. Если нет, он перезапускает приложение.
В этом и вся ценность. Вы редактируете файл .cs, нажимаете сохранить, и изменение вступает в силу за секунду-другую, не касаясь терминала. Без ручного цикла остановки, пересборки и перезапуска.
Подкоманда по умолчанию это run, так что эти две идентичны:
# .NET 11 SDK 11.0.100. 'dotnet watch' with no subcommand defaults to 'run'.
dotnet watch
dotnet watch run
С SDK .NET 8 dotnet watch принимает run, build или test в качестве дочерней команды. Так что dotnet watch test перезапускает ваш тестовый проект при каждом сохранении, что даёт плотный цикл red-green-refactor, который dotnet run не может предоставить вовсе.
Пересланные аргументы работают так же, делегируясь нижележащему dotnet run:
# .NET 11 SDK 11.0.100. Args after -- reach the app on every relaunch.
dotnet watch run -- --input data.csv
Пока сеанс активен, у вас есть два клавиатурных управления. Ctrl+R принудительно вызывает пересборку и перезапуск даже без изменения файла. Ctrl+C сворачивает всё целиком, и наблюдатель, и приложение. Ctrl+R делает что-либо только пока приложение действительно работает: если вы наблюдаете за консольным приложением, которое сразу завершается, нажатие Ctrl+R ничего не делает, хотя наблюдатель продолжает следить и перезапустит приложение при изменении файла.
Hot Reload и “rude edit”, вынуждающий перезапуск
Hot Reload это та часть, которая делает dotnet watch волшебным, и самое крупное, чего нет у dotnet run. С SDK .NET 6 dotnet watch применяет допустимые правки, тела методов, многие изменения инструкций, во многих случаях добавленные члены, к живому процессу, пока тот продолжает работать. Состояние вашего приложения в памяти переживает правку.
Не всякую правку можно применить к работающему процессу. Изменение сигнатуры метода, переименование типа, редактирование чего-то, что среда выполнения не может пропатчить на месте: это “rude edits”. Когда dotnet watch натыкается на такую, он не может выполнить Hot Reload и по умолчанию спрашивает, что делать:
dotnet watch ⌚ Unable to apply hot reload because of a rude edit.
❔ Do you want to restart your app - Yes (y) / No (n) / Always (a) / Never (v)?
Выберите Always, и он перестанет спрашивать и просто будет перезапускаться при каждой rude edit с этого момента. Вы также можете полностью пропустить запрос: задайте DOTNET_WATCH_RESTART_ON_RUDE_EDIT=1, чтобы всегда молча перезапускаться, или запускайте с --non-interactive (доступно с SDK .NET 7), чтобы он перезапускался при rude edits, не дожидаясь ввода в консоль, что вам и нужно в сценариях со скриптами или контейнерами. Если вы вообще не хотите иметь дело с Hot Reload и хотите простой цикл перезапуска-при-изменении, отключите его:
# .NET 11 SDK 11.0.100. Watch and restart, no Hot Reload.
dotnet watch --no-hot-reload
Движок Hot Reload здесь тот же, что использует Visual Studio, и набор поддерживаемых и неподдерживаемых правок следует тем же правилам; поведение авто-перезапуска Hot Reload при rude edits в Visual Studio 2026 отражает поведение CLI. Если вы понимаете rude edits в одном, вы понимаете их и в другом.
Что dotnet watch задаёт в окружении, чего не задаёт dotnet run
Когда dotnet watch запускает ваше приложение, он внедряет несколько переменных окружения, которые голый dotnet run никогда не задаёт. Их стоит знать, потому что они позволяют вашему приложению определить, что оно работает под наблюдателем, и иногда объясняют неожиданное поведение. Основные, согласно документации:
DOTNET_WATCHзадаётся равной1в каждом дочернем процессе, который запускает наблюдатель. Это чистый способ определить: “работаю ли я подdotnet watch?”.DOTNET_WATCH_ITERATIONначинается с1и увеличивается на единицу каждый раз, когда приложение перезапускается или получает hot reload. Вы можете записывать её в журнал, чтобы видеть, сколько циклов перезагрузки прошёл сеанс.DOTNET_HOTRELOAD_NAMEDPIPE_NAMEназывает именованный канал, который наблюдатель использует для передачи дельт Hot Reload работающему процессу.
Так что шаг сборки или диагностика запуска могут ветвиться в зависимости от наблюдателя:
// .NET 11, C# 14. Detect the watcher from inside the app.
if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1")
{
var iteration = Environment.GetEnvironmentVariable("DOTNET_WATCH_ITERATION");
Console.WriteLine($"Running under dotnet watch, reload iteration {iteration}");
}
Есть также несколько переключателей DOTNET_WATCH_SUPPRESS_* для отключения отдельных вариантов поведения (обновление браузера, запуск браузера, вывод эмодзи, инкрементальность MSBuild). Ни один из них не существует в мире dotnet run, потому что у dotnet run нет наблюдения, нет канала Hot Reload и нет внедрения обновления браузера, которое надо подавлять.
Какие файлы вызывают перезагрузку, а какие тихо нет
Частый источник путаницы: вы редактируете appsettings.json под dotnet watch, и ничего не перезапускается. Это сделано намеренно. dotnet watch следит за элементами в группе Watch проекта, которая по умолчанию включает всё из групп Compile и EmbeddedResource, по всему графу ссылок на проекты. На практике это означает:
**/*.cs*.csproj**/*.resx- Веб-контент:
wwwroot/**
Файлы конфигурации (.json, .config) намеренно не вызывают перезапуск, потому что система конфигурации имеет собственное обнаружение изменений через IOptionsMonitor и токены перезагрузки. Если вы действительно хотите, чтобы наблюдатель реагировал на другой тип файлов, расширьте группу Watch в .csproj:
<!-- .NET 11. Make dotnet watch react to JS files too. -->
<ItemGroup>
<Watch Include="**\*.js"
Exclude="node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" />
</ItemGroup>
С SDK .NET 10 вы также можете исключать целые папки, которые иначе вызывали бы шумные перезагрузки, с помощью DefaultItemExcludes, например каталог App_Data, в который приложение пишет во время выполнения. У dotnet run нет ничего из этой машинерии, потому что он никогда не перечитывает ничего после запуска.
Для веб-приложений dotnet watch также обновляет браузер
Есть ещё одно поведение, свойственное только dotnet watch, которого dotnet run не касается: для приложений ASP.NET Core и Blazor наблюдатель внедряет небольшой скрипт обновления браузера и открывает WebSocket обратно к инструменту. Когда вы сохраняете изменение Razor или CSS, браузер перезагружается (или обновляется вживую, для поддерживаемых правок Blazor), без переключения на него и нажатия F5. Именно поэтому dotnet watch задаёт DOTNET_WATCH_AUTO_RELOAD_WS_HOSTNAME, и промежуточное ПО обновления браузера появляется в веб-проектах. Под голым dotnet run вы не получаете ничего из этого: измените представление, и браузер показывает старую страницу, пока вы вручную не пересоберёте и не перезагрузите.
Одна оговорка, которую отмечает документация: если ваше приложение включает сжатие ответов, инструмент может не суметь внедрить скрипт обновления и предупреждает об этом. Решение состоит в том, чтобы самостоятельно обусловить скрипт обновления или отключить сжатие в разработке.
Версии, где изменилось поведение
Механика старая и стабильная, но несколько дат стоит закрепить:
- SDK .NET 6:
dotnet watchполучил Hot Reload. До этого был только перезапуск. - SDK .NET 7: появились
--non-interactiveи--disable-build-servers. - SDK .NET 8:
dotnet watchсузил дочерние команды доrun,buildиtest; раньше он мог оборачивать любую командуdotnet. - SDK .NET 9 (9.0.200): появился
dotnet run -e KEY=VALUE, так что теперь можно передать разовую переменную окружения прямо из CLI без профиля запуска. Посколькуdotnet watchпересылает вызовdotnet run, вы получаете это и под наблюдателем. См. dotnet run -e для переменных окружения без профилей запуска. - SDK .NET 10 (10.0.100):
dotnet run --file app.csдля файловых приложений и поддержкаDefaultItemExcludesвdotnet watch. - SDK .NET 11:
dotnet watchнаучился чисто интегрироваться с app host’ами Aspire и автоматически перезапускаться после сбоя, что подробно описано в dotnet watch в .NET 11: хосты Aspire и восстановление после сбоя.
Какую команду запускать
Обращайтесь к dotnet run, когда хотите запустить приложение один раз: проверить изменение, запустить консольный инструмент, прописать одиночный вызов в CI или в любой момент, когда вы не планируете редактировать код во время его работы. Она собирает, запускает и завершается вместе с приложением. Это простейшее, что запускает ваш код.
Обращайтесь к dotnet watch, когда вы в цикле редактировать-сохранить-увидеть: итерируете над API, оформляете страницу Blazor, крутите red-green с dotnet watch test или отлаживаете что-то, что требует нескольких правок, чтобы прижать. Это стоит вам резидентного терминала, и Hot Reload иногда откатывается к перезапуску при rude edit, но взамен вы перестаёте платить ручной налог пересборки-перезапуска при каждом изменении.
Тест в одно предложение: если вы собираетесь запустить приложение и затем продолжать печатать в редакторе, используйте dotnet watch; если вы просто хотите, чтобы оно работало, используйте dotnet run. Ни одна из них не заменяет dotnet publish, который по-прежнему остаётся единственным поддерживаемым способом создать то, что вы действительно развернёте.
Связанные материалы
- В чём разница между dotnet build и dotnet publish?
- dotnet watch в .NET 11 Preview 3: хосты Aspire, восстановление после сбоя и более разумный Ctrl+C
- .NET 11 Preview 3: dotnet run -e задаёт переменные окружения без профилей запуска
- Visual Studio 2026: авто-перезапуск Hot Reload при rude edits
- .NET 11 Preview 5: файловые приложения получают директиву #:ref
Источники
- Справка по команде dotnet watch (Microsoft Learn)
- Справка по команде dotnet run (Microsoft Learn)
- Справка по команде dotnet build (Microsoft Learn)
- Поддерживаемые фреймворки и сценарии .NET для Hot Reload (Microsoft Learn)
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.