How to Compile a .NET Project from a Batch Script (MSBuild)
Automating .NET project compilation through Batch Script is fundamental to build pipelines, continuous integration, and repeatable release processes. Whether you are building a legacy .NET Framework project with MSBuild or a modern .NET 6+ application with the dotnet CLI, a Batch Script can orchestrate the entire build workflow including restoration, compilation, testing, and packaging.
In this guide, we will explore how to compile .NET projects from a Batch Script using both MSBuild and the dotnet CLI, covering build configurations, output management, and error handling.
Understanding the Two Build Systems
| Tool | Projects | Usage |
|---|---|---|
MSBuild (msbuild.exe) | .NET Framework (.csproj, .sln) | Older projects, WPF, WinForms, ASP.NET |
dotnet CLI (dotnet build) | .NET Core / .NET 5+ (.csproj, .sln) | Modern cross-platform projects |
Both tools ultimately use MSBuild under the hood, but the dotnet CLI provides a simpler, more modern interface.
Method 1: Building with MSBuild (.NET Framework)
@echo off
setlocal
set "solution=MyApp.sln"
set "config=Release"
set "logfile=build.log"
:: Verify solution file exists
if not exist "%solution%" (
echo [ERROR] Solution file not found: %solution%
pause
exit /b 1
)
:: Find MSBuild via vswhere
set "msbuild="
set "vswhere=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if exist "%vswhere%" (
for /f "usebackq delims=" %%M in (`"%vswhere%" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe 2^>nul`) do set "msbuild=%%M"
)
if not defined msbuild (
echo [ERROR] MSBuild not found. Install Visual Studio or Build Tools.
pause
exit /b 1
)
echo =============================================
echo .NET BUILD
echo Solution: %solution%
echo Config: %config%
echo MSBuild: %msbuild%
echo =============================================
echo.
:: Clean
echo [1/2] Cleaning...
"%msbuild%" "%solution%" /t:Clean /p:Configuration=%config% /v:minimal /nologo >nul
echo Done.
:: Build
echo [2/2] Building...
"%msbuild%" "%solution%" /t:Build /p:Configuration=%config% /v:minimal /nologo /fl /flp:logfile=%logfile%
if %errorlevel%==0 (
echo.
echo [SUCCESS] Build succeeded.
) else (
echo.
echo [ERROR] Build failed. See %logfile% for details.
exit /b 1
)
pause
Finding MSBuild with vswhere
The vswhere.exe tool (included with Visual Studio) locates the correct MSBuild version. This is the recommended approach because MSBuild's path changes with each Visual Studio version.
Method 2: Building with dotnet CLI (.NET Core / .NET 5+)
@echo off
setlocal
set "project=MyWebApp"
set "config=Release"
set "publish_dir=publish"
:: Verify dotnet CLI is available
where dotnet >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] dotnet CLI not found. Install the .NET SDK.
pause
exit /b 1
)
echo =============================================
echo DOTNET BUILD
echo Project: %project%
echo Config: %config%
echo =============================================
echo.
:: Restore NuGet packages
echo [1/3] Restoring packages...
dotnet restore "%project%" --verbosity quiet
if %errorlevel% neq 0 (
echo [ERROR] Package restore failed.
pause
exit /b 1
)
echo Packages restored.
:: Build
echo [2/3] Building...
dotnet build "%project%" -c %config% --no-restore --verbosity minimal
if %errorlevel% neq 0 (
echo [ERROR] Build failed.
pause
exit /b 1
)
echo Build succeeded.
:: Publish (create deployable output)
echo [3/3] Publishing...
dotnet publish "%project%" -c %config% --no-build -o "%publish_dir%"
if %errorlevel% neq 0 (
echo [ERROR] Publish failed.
pause
exit /b 1
)
echo.
echo [SUCCESS] Output: %publish_dir%\
pause
Method 3: Complete Build Pipeline
A full build script with versioning, build numbering, and artifact packaging:
@echo off
setlocal enabledelayedexpansion
:: Configuration
set "solution=MyApp.sln"
set "config=Release"
set "publish_dir=artifacts\publish"
set "package_dir=artifacts\packages"
:: Verify solution exists
if not exist "%solution%" (
echo [ERROR] Solution file not found: %solution%
exit /b 1
)
echo =============================================
echo BUILD PIPELINE
echo =============================================
echo.
:: Version
set "version=1.0.0"
if exist VERSION (
set /p "version=" < VERSION
)
set "build=0"
if exist BUILD_NUMBER (
set /p "build=" < BUILD_NUMBER
)
set /a build+=1
>BUILD_NUMBER echo !build!
set "full_version=!version!.!build!"
echo Version: !full_version!
echo.
:: Stage 1: Clean
echo [1/5] Cleaning...
if exist "%publish_dir%" rmdir /s /q "%publish_dir%"
if exist "%package_dir%" rmdir /s /q "%package_dir%"
mkdir "%publish_dir%"
mkdir "%package_dir%"
dotnet clean "%solution%" -c %config% --verbosity quiet >nul 2>&1
echo Done.
:: Stage 2: Restore
echo [2/5] Restoring dependencies...
dotnet restore "%solution%" --verbosity quiet
if !errorlevel! neq 0 (
echo [FAIL] Restore failed.
exit /b 1
)
echo Done.
:: Stage 3: Build
echo [3/5] Building...
dotnet build "%solution%" -c %config% --no-restore /p:Version=!full_version! --verbosity minimal
if !errorlevel! neq 0 (
echo [FAIL] Build failed.
exit /b 1
)
echo Done.
:: Stage 4: Test
echo [4/5] Running tests...
dotnet test "%solution%" -c %config% --no-build --verbosity minimal --logger "trx;LogFileName=results.trx"
if !errorlevel! neq 0 (
echo [FAIL] Tests failed.
exit /b 1
)
echo Done.
:: Stage 5: Publish and Package
echo [5/5] Publishing and packaging...
dotnet publish "%solution%" -c %config% --no-build -o "%publish_dir%"
if !errorlevel! neq 0 (
echo [FAIL] Publish failed.
exit /b 1
)
set "zip_file=%package_dir%\MyApp_v!full_version!.zip"
powershell -NoProfile -Command ^
"try { Compress-Archive -Path '%publish_dir%\*' -DestinationPath '!zip_file!' -Force -ErrorAction Stop; exit 0 }" ^
"catch { Write-Host ('[ERROR] ' + $_.Exception.Message); exit 1 }"
if !errorlevel! neq 0 (
echo [FAIL] Packaging failed.
exit /b 1
)
echo Done.
echo.
echo =============================================
echo BUILD COMPLETE: v!full_version!
echo Package: !zip_file!
echo =============================================
pause
Method 4: Multi-Project Solution Build
Build specific projects in a solution with different configurations:
@echo off
setlocal enabledelayedexpansion
set "config=Release"
:: Define projects as numbered entries
set "proj_count=3"
set "proj[0]=src\API\API.csproj|API"
set "proj[1]=src\Web\Web.csproj|Web Frontend"
set "proj[2]=src\Worker\Worker.csproj|Worker Service"
echo Building solution projects (%config%^)...
echo.
set "failed=0"
set /a last=proj_count - 1
for /L %%i in (0,1,!last!) do (
for /f "tokens=1,2 delims=|" %%A in ("!proj[%%i]!") do (
set /a step=%%i + 1
echo [!step!/%proj_count%] %%B...
if not exist "%%A" (
echo FAIL: project file not found
set /a failed+=1
) else (
dotnet build "%%A" -c %config% --verbosity quiet
if !errorlevel! neq 0 (
echo FAIL
set /a failed+=1
) else (
echo OK
)
)
)
)
echo.
if !failed! gtr 0 (
echo [ERROR] !failed! project(s^) failed to build.
exit /b 1
) else (
echo [SUCCESS] All projects built.
)
pause
Method 5: Using Visual Studio Developer Command Prompt
For projects that require the full Visual Studio build environment:
@echo off
setlocal enabledelayedexpansion
set "solution=MyApp.sln"
set "config=Release"
:: Try to find VsDevCmd.bat across common editions
set "vs_year=2022"
set "vs_found="
for %%E in (Community Professional Enterprise BuildTools) do (
set "try_path=%ProgramFiles%\Microsoft Visual Studio\%vs_year%\%%E\Common7\Tools\VsDevCmd.bat"
if exist "!try_path!" (
set "vs_found=!try_path!"
)
)
if not defined vs_found (
echo [ERROR] Visual Studio %vs_year% not found (checked Community, Professional, Enterprise, BuildTools^).
pause
exit /b 1
)
:: Initialize VS Developer environment
echo Initializing Visual Studio environment...
call "!vs_found!" >nul 2>&1
echo Using: !vs_found!
echo.
:: Now MSBuild and other tools are on PATH
msbuild "%solution%" /t:Rebuild /p:Configuration=%config% /v:minimal /nologo
if %errorlevel%==0 (
echo.
echo [SUCCESS] Build complete.
) else (
echo.
echo [ERROR] Build failed.
exit /b 1
)
pause
Common Mistakes
The Wrong Way: Hardcoding MSBuild Path
:: WRONG - Path changes with every VS version
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe" MyApp.sln
Output Concern:
MSBuild's installation path changes with each Visual Studio edition and version. Hardcoding the path makes the script break on other machines. Use vswhere.exe to dynamically locate MSBuild, or use the dotnet CLI for .NET Core+ projects.
The Wrong Way: Not Restoring Packages First
:: WRONG - Build may fail due to missing NuGet packages
dotnet build MyApp.sln -c Release
:: error NU1... Package 'X' was not found
While dotnet build performs an implicit restore by default, explicit restore with dotnet restore before building ensures dependencies are resolved, especially in CI environments with separate restore and build steps. Use --no-restore on the build step only after a confirmed successful restore.
Best Practices
- Use
vswhereto find MSBuild: Never hardcode MSBuild paths. - Separate restore/build/test/publish steps: Each stage should be independently verifiable.
- Pass version numbers to the build: Use
/p:Version=x.y.zto embed version info in assemblies. - Log build output: Use
/fl(file logger) to capture detailed build logs. - Fail fast: Check
%errorlevel%after each step and abort the pipeline on first failure.
Conclusion
Compiling .NET projects from a Batch Script is accomplished through either msbuild.exe for .NET Framework projects or dotnet build for modern .NET applications. By structuring the build as a multi-stage pipeline (clean, restore, build, test, publish, package), each step is independently verified and the pipeline fails fast on the first error. Dynamic MSBuild discovery via vswhere, version embedding through build properties, and artifact packaging complete the automation, producing consistent, reproducible builds suitable for any CI/CD environment.