When Azure DevOps builds a Visual Studio solution, only the startup or web project publishes by default because MSBuild targets specific entry points. To publish all projects, you must explicitly configure the build task to use /p:DeployOnBuild=true combined with /p:PublishProfile=YourProfile for each target, or switch to a dedicated Publish step that iterates through the solution items.
Understanding the Default Build Behavior
In a typical .NET solution containing multiple projects, such as a Web API and several class libraries, the build process is designed to compile code rather than deploy artifacts. When you add a standard "Visual Studio Build" task in an Azure DevOps pipeline, it invokesmsbuild.exe on your .sln file. By default, MSBuild compiles every project referenced in the solution to produce assembly files (DLLs) in the bin directory. However, compilation is not the same as publishing. Publishing involves creating a self-contained folder structure or a zip package ready for deployment to a server, App Service, or container registry.
Most developers encounter this issue when they expect the output artifact to contain all compiled binaries, but only find the files from one project, usually the main web application. This happens because the build task does not automatically trigger the publish workflow for non-web projects unless explicitly instructed. The pipeline succeeds because the compilation succeeded, but the deployment preparation fails silently for the other projects. Understanding this distinction is crucial before applying fixes.
Configuring MSBuild Arguments Correctly
The most common approach to fixing this is modifying the arguments passed to the MSBuild task. Many guides suggest using/p:DeployOnBuild=true, which is correct but incomplete. If you only use this flag, MSBuild will attempt to publish the project that has a default publish profile or is marked as the startup project. It will ignore libraries and worker services. To force publication of specific projects, you need to combine parameters carefully.
You should update your pipeline YAML or classic editor configuration to include the following arguments in the MSBuild task:
/p:DeployOnBuild=true
/p:PublishProfile=FolderProfile
/p:PackageLocation=$(build.artifactStagingDirectory)
However, note that PublishProfile expects a single profile name. If your solution has different publish profiles for different projects, this single argument will fail or apply only to the first match. A more robust method involves ensuring that each project you wish to publish has a corresponding publish profile defined in Visual Studio. You can create these by right-clicking the project in Solution Explorer, selecting "Publish," and choosing "Folder." Once created, these profiles appear in the Properties/PublishProfiles folder within the project.
If you have multiple projects, you may need to run separate MSBuild commands for each, or use a loop in your pipeline to iterate through the projects. For example, if you have WebApp.csproj and WorkerService.csproj, you might need distinct steps or complex arguments that specify the exact project file path along with the deploy flags.
Using the Dotnet Command Alternative
For modern .NET Core and .NET 5+ solutions, using thedotnet CLI often provides clearer control than the legacy MSBuild task. The dotnet publish command allows you to target specific projects explicitly. In your pipeline, replace the generic Build task with a Dotnet task set to the "publish" command.
dotnet publish src/WebApp/WebApp.csproj -c Release --output $(build.artifactStagingDirectory)/WebApp
dotnet publish src/Worker/Worker.csproj -c Release --output $(build.artifactStagingDirectory)/Worker
This approach ensures that both projects are published independently into their respective folders. It avoids the ambiguity of MSBuild trying to guess which project to publish. This method is particularly effective when dealing with mixed project types, such as a web front-end and a background service, as it gives you precise control over the output paths and configurations for each component.
Verifying Publish Profiles and Dependencies
Even with correct arguments, publication can fail if the publish profiles are misconfigured or missing dependencies. Ensure that each project intended for publication has a valid.pubxml file in its properties folder. These files define how the project is packaged, including whether it includes dependent assemblies. If a library project relies on other internal projects, those references must be correctly resolved during the publish phase.
Check the "Copy Local" property of project references. If set to false, the DLLs might not be copied to the output directory during publish, leading to runtime errors even if the build succeeds. For internal dependencies, ensure they are either published alongside the main project or available in the global assembly cache. Additionally, verify that the pipeline agent has access to any required SDKs or tools needed to restore packages and publish successfully.
It is also worth noting that some older frameworks or custom build targets might override default behaviors. If you are using legacy .NET Framework projects, the behavior differs slightly from .NET Core. In those cases, relying on the MSBuild task with explicit _PublishedWebsites targets might be necessary. Always test your pipeline changes in a feature branch before merging to production to avoid deployment failures.
Troubleshooting Common Pitfalls
If your pipeline still publishes only one project after applying these fixes, check the build logs for warnings related to missing profiles or failed targets. Look for messages indicating that MSBuild could not find the specifiedPublishProfile. Another common issue is caching. Sometimes, old artifacts or cached NuGet packages interfere with the build process. Clearing the cache by adding a clean step before the build can resolve strange inconsistencies.
Furthermore, ensure that your solution file itself is not corrupted or pointing to incorrect project paths. Running devenv /clean locally can help verify that the solution builds cleanly outside of the pipeline environment. If you continue to face issues, consider breaking down your monolithic solution into smaller, independent pipelines that handle each project separately. This modular approach reduces complexity and makes troubleshooting significantly easier.
For teams managing complex microservices architectures, integrating with container registries via Docker tasks might be a better alternative than traditional file-based publishing. This shifts the focus from individual project outputs to container images, which encapsulate all dependencies. You can explore related techniques for optimizing code analysis or removing unnecessary CI/CD triggers to streamline your workflow further.
Frequently Asked Questions
Why does my build succeed but no files appear in the artifact?
A successful build only produces compiled DLLs in thebin folder; it does not automatically create deployment-ready packages. You must explicitly invoke the publish step or use MSBuild arguments like DeployOnBuild=true to generate the final output files that get uploaded as artifacts.