Project structure is probably one of the most important things to get right. If things are not setup in a consistent manner you will have a hard time managing your software builds. I find convention over configuration to be of great help especially when creating new builds. We want the project structure to be organized and easy to use, which means it should be easy for new developers to figure out where things are and where new files should go. For this series I will be using the fictitious FoodBankTracker application.
I like to use multiple solution files at the root of my source control repository, this allows me to share projects between different solutions as project references are so much easier to deal with than file based assembly references.
In the SharedLibs folder I have all my 3rd party file based assembly references which are used during the build process, things like: NUnit, Castle, NHibernate, RhinoMocks etc. Other than these 3rd party assemblies, all of my references are project based.
In the Tools folder I keep build tools checked in such as NUnit, Doxygen, and MSBuildCommunityTasks. The Tools folder differs from the SharedLibs folder in one key aspect, the SharedLibs folder contains assemblies that may be deployed with the built application, where the executables and assemblies in the Tools folder are only used during the build process. Tools are checked into source control for several reasons:
- New workstations don't require so many 3rd party tool installs.
- Upgrades across the team are as easy as svn update.
- Relative tool paths are fixed, which makes automating builds that much easier.
I also keep a generic global.targets MSBuild file in the Tools folder which gets included in all my MSBuild project files. Global.targets keeps shared settings and targets that are common to all projects; this saves me time when configuring a new project, but it also allows me to change a tool path or target without touching every single build file in source control. The global.targets also includes the MSBuildCommunityTasks which contains additional targets that will be used; by including it in global.targets its just one less thing I have to include in each build file. Most importantly this target file keeps all the paths to the different tools used during the build process, remember we want to keep things consistent across projects. This target build script also sets some build properties like version, company name, and sets some paths used during the build process.
Before global.targets gets included, the parent build script needs to set the $(SourceDirectory) property which should point to the root of the source control repository. This path is used to find where things are in the source control repository relative to the trunk. The parent build file may also want to set a friendly product name property, but this is purely optional.
<!-- The SourceDirectory property must be set prior to including this file -->
<!-- The ProductName property should be set prior to including this file -->
<PropertyGroup>
<BuildVersion>$(CCNetLabel)</BuildVersion>
<BuildVersion Condition="'$(BuildVersion)' == ''">0.0.0.0</BuildVersion>
<CompanyName>Sneal</CompanyName>
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<ProductName Condition="'$(ProductName)' == ''">$(MSBuildProjectName)</ProductName>
</PropertyGroup>
<PropertyGroup>
<NUnitPath>$(SourceDirectory)\Tools\NUnit\bin\nunit-console.exe</NUnitPath>
<DevEnvPath>"$(VS80COMNTOOLS)..\ide\devenv.com"</DevEnvPath>
<DoxygenPath>$(SourceDirectory)\Tools\Doxygen\doxygen.exe</DoxygenPath>
<MSBuildCommunityTasksPath>
$(SourceDirectory)\Tools\MSBuildCommunityTasks
</MSBuildCommunityTasksPath>
</PropertyGroup>
<Import Project=
"$(SourceDirectory)\Tools\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<PropertyGroup>
<ArtifactDirectory>$(CCNetArtifactDirectory)</ArtifactDirectory>
<ArtifactDirectory Condition="'$(ArtifactDirectory)' == ''">
$(SourceDirectory)\artifacts\$(ProductName)
</ArtifactDirectory>
<PublishDirectory Condition="'$(PublishDirectory)' == ''">
\\buildserver\builds\$(ProductName)\$(BuildVersion)
</PublishDirectory>
<TempZipDirectory Condition="'$(TempZipDirectory)' == ''">
$(ArtifactDirectory)\ZipImage
</TempZipDirectory>
</PropertyGroup>
Notice that this reusable targets file tends to be passive, i.e. it doesn't overwrite any properties if they've already been set. One thing to note is that non-official CC.NET builds will be built using version 0.0.0.0, which would be used if a developer did a local build. When CC.NET runs a build script it will set some additional properties, one of which would be build version. Global.targets include some other paths and reusable targets that are not shown, but we won't talk about the reusable targets just yet.
Besides setting paths to build tools we also set properties for build directories, specifically: $(ArtifactDirectory), $(PublishDirectory), and $(TempZipDirectory). The ArtifactDirectory property should point to where all build artifacts get spit out to. Artifacts are things like XML result files from NUnit, MSBuild, and Doxyen output. If running under CC.NET this will be the CC.NET artifact directory. The TempZipDirectory property points to where all build output gets copied to, to build up a disk image in the proper layout for the zip file which will contain our ready for deployment build. Everything in the temp zip directory gets zipped and then copied to the publish directory which is usually a network accessible location on the build server, here it would be \\buildserver\builds\FoodBankTracker\0.0.0.0
The commons folder contains a reusable library that I use between most, if not all, of my solutions. This contains the actual library and another unit test project. This project is built in the same fashion as the FoodBankTracker application, but will focus just on the one application.
Now for the FoodBankTracker folder, without the FoodBankTracker application there would be no need for everything else we've talked about up to this point. As you can see in the image, the application is broken out into four main logical areas: core, data, presentation, and web. You will also notice that the main application and unit test projects are all at the same folder level and the database folder which contains the database scripts used to build the application's database. There is generally a 1:1 ratio of unit test projects versus normal projects, but the important thing to note is that the test code is in a separate folder and project from the application code.
All of the projects are class library projects except for the web project and the database project. The web project is a normal web application project that only contains very thin display logic, while the database is a (.dbp) database project where we keep our SQL scripts.
Inside the solution the different project types are logically organized between the core application and the Tests solution folder. Separating out projects logically inside the solution instead of on disk allows me to easily move things around in the future without adversely effecting the build scripts. Also since I've positioned my solution file at the root, I can easily add project references to my Commons library and unit test project. This also allows me to add new shared "common" features that much quicker, because its just as easy to add it to the Commons project as it is to add it to one of the FoodBankTracker projects.