Tuesday, May 5, 2020

Post-Build Events and .NET Core

I have used post-build events in Visual Studio for a long time, primarily to copy files from one location to another. But due to changes in how we work with .NET Core, I have changed the way I write build events.

Short version:
The '$(SolutionDir)' macro doesn't work well from the command line. Replace references to the Solution with references relative to the current Project.
If you'd like an overview on using build events (and the macros that are available), take a look at this article: Using Build Events in Visual Studio to Make Life Easier.

Using a Post-Build Event
Let's look at an example of how I use post-build events. Here's the folder structure for a solution:

This solution has 4 projects: (1) "Common" is a shared library with cross-cutting classes defined; (2) "PeopleViewer" is a desktop application, (3) "PeopleViewer.Presentation" is a view-model class library, and (4) "PersonReader.CSV" is a data reader.

The top folder, "AdditionalFiles", contains a text file with the data. The "PeopleViewer" needs access to this file in order for the application to work. So, in this scenario, I want to copy the contents of the "AdditionalFiles" folder into the output folder for the "PeopleViewer" project.

Here is a post-build event that does this:

    xcopy "$(SolutionDir)AdditionalFiles\*.*" "$(TargetDir)" /Y

The "$(SolutionDir)" macro expands into the fully qualified path for the solution. This includes the trailing "\". The "$(TargetDir)" macro expands to the fully-qualified location where the .exe file ends up -- something like "[fullpath]\PeopleViewer\bin\Debug\" for .NET Framework and "[fullpath]\PeopleViewer\bin\Debug\netcoreapp3.1" for .NET Core.

This works fine when we build the application from Visual Studio:

    4>1 File(s) copied

The build succeeds and the file is copied.

The problem arises when we leave Visual Studio.

.NET Core and the Lack of Solutions
In the .NET Core world, the concept of the "Solution" is less important. We often open folders (not solutions) in Visual Studio Code. And when we use the command-line tools, we build projects (not solutions). The ".sln" file is not important in this scenario, and it is generally not used at all.

This works because the projects have all of the information that they need to build, including dependencies on other projects and on NuGet packages.

So to build the "PeopleViewer" project, I can open the command line to the project location and type "dotnet build".

But, the post-build event fails:

  0 File(s) copied
  File not found - *.*
C:\Development\Sessions\DI\Scratch\PeopleViewer\PeopleViewer.csproj(15,5): error MSB3073: The command "xcopy "*Undefined*AdditionalFiles\*.*" "C:\Development\Sessions\DI\Scratch\PeopleViewer\bin\Debug\netcoreapp3.0\" /Y" exited with code 4.

The message tells us that the copy step failed. And if we look closer at the message, we can see why:

    "xcopy "*Undefined*AdditionalFiles\*.*"

When trying to expand the macros, the '$(SolutionDir)' is undefined. There is no concept of a solution at this level, just the project. So the build fails.

Relative to the Project
The fix for this is to not use any of the "Solution" macros in the post-build events. Instead, set things relative to the current project.

Here's an updated post-build event:

    xcopy "$(ProjectDir)..\AdditionalFiles\*.*" "$(TargetDir)" /Y

To get to the "AdditionalFiles" folder, we start with the current project folder, then go up a level (with the "..").

When we build this from Visual Studio, it still works. But more importantly, it works when we build from the command line:

  1 File(s) copied


Post-build events are really useful, and I'm often using them to copy data files or late-bound assemblies to the output folder of my projects. But I've changed how I use them.
Instead of using post-build events relative to the Solution, I use events relative to the Project.
It's always exciting to come up with a fix to an issue. But this isn't completely solved.

Cross-Platform Concerns
One of the really awesome things about .NET Core is that it is cross platform. I can create applications that will run on Windows, macOS, and Linux.

But "xcopy" is a Windows command. How do we copy files for other OSes?

For that we need to dig into MSBuild. That will wait for the next article, but here's a preview:

This block in the project file will copy the files from the "AdditionalFiles" folder to the output folder. And it works on macOS and Linux.

We'll explore that in the next article.

Update: That article is now available: Cross-Platform Build Events in .NET Core using MSBuild.

Happy Coding!


  1. Hi Jeremy!

    Thanks for sharing, it was really cool. But the ending!!!

    It ends on the most interesting part. While I was reading through the article I was asking myself multiple times - "why he is using Windows util in cross platform project file?? This's .NET core, man". And I am glad I read it till the end :)

    I suppose, the best way to do post build (or any other) events and don't worry about cross platform utils - via MsBuild, so I am waiting for your next article!


    1. Thanks, Vlad.

      I decided to split the topic this way because lots of .NET devs (probably most .NET devs if we think about all the corporate devs) are in the Windows-only world. The example project in this article is WPF, and so it's not cross-platform. This article addresses the tool chain expansion that lets us work on .NET outside of Visual Studio.

      As I moved projects to be cross platform, I came across different issues like how to xplat copy and how to do things conditionally based on the OS. I'm working on the next article, currently deciding how deep to go into MSBuild tasks. I'll probably focus on copying files to start with just to get folks started in that direction.

      Here's a sneak peek at the MSBuild code: https://github.com/jeremybytes/understanding-interfaces-core31/blob/master/web/completed/04-DynamicLoading-Plugin/PersonReader.SQL/PersonReader.SQL.csproj

      More coming soon!

  2. This is wonderful article. I'm new in the Build Events. Thanks a lot!

  3. Hi Jeremy,
    I have strange behavior from my post-build event. Actually I don't have post-build event (>Properties>Build Events) but some Zipping activity exist. Some files are collected and put in new directory. Is there some file where this event is stored and applied every time when I build my project?

    Best regards,