5 Ways To Improve Your .NET Builds
Transcript
Hi, my name is Stu and in this video we're going to take a look at five ways to hack your MSBuild. If you're new here, this channel is all about me breaking down different technologies into easily digestible chunks.
We're going to be taking a look at multiple aspects of the MSBuild system today in regards to a C# .NET project. First of all, we're going to take a look at a binary log.
Every time that you build your project, you should be able to see the messages from MSBuild saying what's happening with your project. By default, not much information is given to you in the console, but more information is available through the extended logging such as verbose logging. Sometimes though, the verbose logging is not enough for what we need to do.
This is where the binary log file comes in. A binary log is the most complete set of information you can get about your build. Because it's not written to the console or to your disk, you can speed up your build, have smaller disk size, and the structure of the file preserves the exact build event arguments that could later be replayed to reconstruct the exact events and information as if a real build was running. Traditional file logs erase structure and are harder to parse, especially for multi-core builds.
Before I show you how to use binary logs, let's take a quick look around the project that we're going to be using today. We have one solution file that references two projects: our sample project which is currently set to version 1, and our NuGet targets project which we're going to cover a bit later on. In order to generate a binary log, I'm going to press Ctrl and apostrophe to bring up the console and I'm going to do my.
dotnet build on the solution as I would normally, and then I'm going to include a /bl at the end and then hit enter. This is going to build my project as normal, but if I go to disk you'll be able to see that there is now this msbuild.binlog. In order to view this binlog file, we need the MSBuild Log Viewer tool installed. I'll leave a link in the description below for this tool.
I'm going to double click and open the build log. This tells us everything that we need to know about the build that's just happened. In the top left hand corner is a really easy way of searching through the binlog. For example, if I wanted to have a look for anything that contained the word "target", you type in target, hit enter, and you'll see everything that lists the target. The UI on the right hand side of the screen gives you a tree-like view that enables you to see the different properties at any point in time during the build, as well as any targets that are run including target dependencies, all in a nice hierarchical view.
For tip number two, we're going to be taking a look at conditions on properties. When you specify a condition on a property, what you're telling MSBuild to do is set this property when this condition is met. There's quite a lot of things that you can do with these conditions, so this is going to be outside of the scope of this video, but I'm going to show you at a basic level how to implement conditions on a property.
Let's take this GeneratePackageOnBuild property. Here I've defaulted it to true, so it was going to constantly build a package on the build. However, I may only want to generate the package on a build when it is referencing .NET Core App 3.
- A lot of abstract for this example, but bear with me. So to do this, I'm going to press space here and type
Conditionequals, and then to reference a different MSBuild property I need to use the dollar symbol and the parentheses and then type the property name in between it, so TargetFramework. Then to do equality we need the double equal sign, and then in single quotes I'm going to put in the target framework that I wish for it to be built on, so this is going to be netcoreapp3.1.
I'm going to save this and just clear up my NuGet folder quickly, and I'm going to run the build again. As you can see, in the NuGet folder only a single project has been built. Now let's take a look what happens when I change this to net5.0 and I'm going to build the project again. Now that the condition has been met, our package is being built properly.
This leads us on nicely to tip number three, which is the use of custom properties. For this I'm just going to reset my folder quickly and I'm going to change net5 to true. The property that we're going to look at is going to be called HelloPackage. Now this property is not defined anywhere in MSBuild as far as I'm aware. We can call dotnet build and when our NuGet folder appears you'll see that we only have a single project listed again.
To set this property, I can do this on the command line. So I'm going to write dotnet build and then press space, /p: which stands for parameter, and I'm going to say HelloPackage=false. So I'm explicitly setting it to false, and again nothing should happen. As we expected, nothing has happened.
This time however, if I change this to true and run the build again, you can now see that our target package has been created. One thing that we can do with these custom properties is assign them a default value if they haven't been set anywhere, like on a command line or in the targets file. To do this, I'm going to create a new line in my property group and put in the property name that I desire with a conditional statement and put in the property name itself again and see where it equals a blank string and then default it to true. This means now I clear out my NuGet folder again and delete that parameter on the command line, I can run.
dotnet build and the package will be generated, as you can see here.
Now let's go on to tip number four: using custom targets. Targets in MSBuild are a little bit like build tasks that happen in a specific order. There are three main ways that you can have your target executed. The first is by declaring it before another target. The second is after another target. Or the third is direct invocation from the command line.
So to create a custom target, we need to stay in the csproj file and create a new section called Target. Now we need to supply this a name. I'm going to call this NuGetValidation. Then I could either write AfterTargets so I can do it after the Build target has run, but for this example I'm going to have BeforeTargets and the target that I'm interested in is the GenerateNuspec target.
Now these targets can do anything that you want, pretty much. You can trigger error messages, warning messages, information messages, or you can run custom tasks to do things with build tools. In this example, we're going to be validating that the description is set on our package. To do this, I'm going to write the word Error, signaling that we're going to use the built-in error functionality. I'm going to supply an error code CWS001, the text that is going to be displayed for the error such as "description must be set to something please", and then finally I'm going to use the condition that we learned about earlier to make sure that the description is actually set to something.
Now when I build my project, I should receive the error. As you can see from the error message that we received, we have error CWS001: description must be set to something please. So let's go and do that now, and if we rerun our build again, everything's gonna pass as normal.
If you have targets that rely on other targets, we can set a dependency for that target. In order to do this, I'm going to go to the end of line 12 and add the phrase DependsOnTargets equals, and then the targets I'm interested in. So in this case it's just going to be Build, which is a built-in task for running the build. Now if I run the build, everything works as we're expecting it to.
Now a lot of what we've seen so far has been reflected only in one csproj. There are many situations where we want to take some of the things that we've learned in the video so far and apply them across multiple projects. The MSBuild ecosystem has a number of ways that we can accomplish this, and we're going to take a look at two today. The first of these is called Directory.
Build.props. To create this file, I'm going to click on my source directory, add a new file, and go Directory.Build.props. As the build executes, say the NuGet targets csproj, it will search within that root folder called NuGet targets for this file. If it cannot find it, it will go up to the source directory and have a look to find the Directory.Build.props file. In this case it will, but if it didn't, it will recursively look back through the directories one by one until the root of the drive to find this file. This means that you can declare the file all the way up until your root drive and have it applied across all projects. The same happens with the Directory.Build.targets file, which is usually used for generating custom targets like the one we did earlier.
So let's take a look at the structure of the file. As with our csproj files, we should start off with the Project tags as our wrapper. From here we can then set the properties as if it was in our csproj, and it will get automatically applied across all the projects in the src folder.
In this case, I'm just going to take the PackageOutputPath and the GeneratePackageOnBuild, just quickly remove the condition to set it properly. I'm just going to set this to version 2, and repeat the same in the other one. Now if I build the project, you'll be able to see in the NuGet folder we have version 2 of our packages. The build is otherwise the exact same as we had before, but with all the properties tidied up a little bit to prevent repetition.
In some cases, you may want to add custom build targets or properties in projects that consume a NuGet package that you create. For example, you may wish to run a custom tool or process during the build. So let's take a look at how we can do this now.
Inside our NuGet targets project, we're going to create a new folder called build. Inside of here we need to create a new file, and we're going to call it NuGetTargets.targets. When NuGet installs a package, it looks inside of a couple of special folders for the package name .targets and the package name .props. So in this case it's NuGetTargets.
targets and NuGetTargets.props. When it finds these files, it will automatically include them inside of the build. But now I'm just going to reuse the same NuGet validation task that we had earlier.
Now that this is done, we need to tell the build system how to package this correctly inside of our NuGet package. So I'm going to switch across to our csproj and we're going to remove the target that we created earlier. Then I'm going to declare an ItemGroup, and inside this ItemGroup I'm going to modify the collection None. I'm going to include anything that is inside of the build folder, I'm going to tell MSBuild to pack it, and then tell MSBuild the path is going to be the build folder. Then to cover off multi-targeted builds, we need to repeat the same step or change the package path to buildMultiTargeting.
If the package that you're building is only used at compile time, then you can set the development dependency property to true so that the library does not end up inside of the output of the project. Now I'm going to change the version of this project to 3 and build.
Now that it's built, I'm going to open up the NuGet folder in my explorer and I'm going to double click on the new NuGet package to open it in NuGet Package Explorer. I'll leave a link to this in the description below. Inside of here you can see that we have the buildMultiTargeting folder and the build folder, both containing the targets. We can double click and open this and then we can see our target there. We can also see that the development dependency has been set to yes.
Switching back to our sample project, we're going to include this new NuGet package. I'm going to type it out, but you could equally use the command line to do this. Now if I run this, you'll be able to see that our build fails due to a missing description property. So we are now going to set this, just change the version number, and rerun our build.
And that pretty much is it for this video. There's a load more stuff that you can do with this, and I'm going to put some links down in the description below, so be sure to check them out.
If you enjoyed this video, consider subscribing to the YouTube channel for more content like this.