While there will never be a one-size-fits-all solution when I work with teams to help them identify productive engineering practices one topic that we will tend to discuss is solution structure. Visual Studio's solution file represents a collection of projects that form the core and supporting pieces for a product. For most developers the solution file is the identifiable entry point into the product that we look for after retrieving a copy of the source code from version control. When you first open a solution the structure tells a story about the logical structure of the application. Naming conventions and folder structures tell you right away about the patterns being used. Consistent key files within the team's suite of products help you pick out important configuration information such as versioning along with what is required to build and run the solution. By having a meaningful structure that is readily apparent to another experienced developer you build confidence in the quality of the application. Below are some of the common elements I take with me into new projects.
File System Layout
The first thing you encounter before you open up the solution in the IDE is the file system structure. As with many things in life you only get one chance to make a good first impression and this is the starting point where others develop that sense of craftsmanship. While the Java developers have Maven to drive a convention, .NET does not offer any explicit guidance. For projects that I work on I look to create a few easily recognizable buckets within the root:
|(root)||Small number of very important global files -- solution file, Readme.txt, build script, and shared team settings which are discussed later in the post.|
|Build||Artifacts from the build process. This folder and it's contents are temporary in nature.|
|Build\Results||Output from the various tools run through out the build process such as test runner, static code anlaysis results|
|Build\Packages||Artifacts from the application packaging process such as a web deplooy package, MSI file, etc...|
|Documentation||Architectural artifacts, wireframes, and other documentation that a developer would reference on a regular basis when building a specific version|
|Libraries||Third party componetns and libraries that are not available through NuGet|
|Packages||Third party components and libraries managed through NuGet|
|Source||Within this folder are individual folders for each projects as well as shared source files such as the solution assembly information, strong name private key, and static analysis rule sets.|
|Tools||Scripts and utilities that are used as part of the build, packaging and deployment process that are not part of the standard developement environment. For example, MSBuild custom tasks, mocking framework runnner for build agent, sample application dataset generator.|
I prefer to keep names descriptive but if you choose to use "src" instead of "source" and "lib" instead of "libraries" then that is fine as well -- the important point is to drive relatively consistency in structure across all of your products.
Project Naming and Division
I cringe when I see technology acronyms in projects like "WpfApplication" or "WebServices". The technology used for the implementation is not as important as describing the purpose of the project. When navigating through a solution structure I find myself searching for specific functionality rather than focused on if the project is a WPF, WCF, or web project. On a related note I also want to see a degree of commonality across products managed by an organization to enable more effective ad-hoc pairing and engineering swaps amongst teams. While you might not use all of these projects in your solution here are a few names I use in my convention:
|Core||Domain, workflow, persistence and all other non-UI/service interface logic resides within this project. By separating out the UI/service interface pieces you are confronted with expressing concepts in terms of the application domain rather than a particular interface paradigm.|
|Services||Contains logic to wire up the business logic in the Core project to a services interface. This is where you might find specific infrastructure to enable a WCF pipeline or ASP.NET Web API hosting layer.|
|Services.Contracts||For WCF projects where both the consumer and service are within the same realm of ownership I prefer shared assemblies for contracts instead of relying on code generation. The generated proxies tend to be overly complex and do a poor job at representing my domain. In addition the contracts assembly can more easily carry code-based documentation on the interfaces. If you have other projects that require proxy generation then consider the [WCF Extras] project to add WSDL documentation.|
|UI||Rarely do I use acronyms, but this is one place where the word "UserInterface" feels a bit clunky to me given the well known status of UI. Some projects have several UI projects in which case I would describe the projects based on their pupose (i.e. "ManagementConsole", "ClientPortal", "ScoreDashboard").|
|UnitTests||On smaller projects I will stick to a straight "Tests" project and use namespaces to separate out the different types of tests for easier filtering in the various test runners. As projects grow and the unit test stubs become more involved or integration test fixtures become more important I would separate out the buckets. One of the driving forces for separation by test type tends to be breaking out the build pipeline into specific stages (commit, integration test, deploy to test, smoke test, load test, deploy to production).|
|IntegrationTests||On larger projects the tests that combine muliple components interacting, requiring some sort of infrastructure to be deployed (i.e. database schema with sample data set, hardware simulator, etc...) would be fall under the category of integration tests. When breaking out the different types of tests I often reflect on the [Software Engineering Body of Knowledge defintions] to guide me.|
|SystemTests||Because of the tooling used to drive end-to-end system tests, such as Telerik Test Studio, I tend to keep these separate. When projects, such as web-based applications, need to have their test suite exercised from different machine configurations it is easier to deploy the codified system tests to a test runner installed on each machine type.|
|Specifications||Contains the specifications written in partnership with the business and turned into tests that validate their behavior. In smaller projects this would reside in a separate namespace within a single "Tests" project.|
|Database||Depending on how you manage your database schema you may need a separate project type (i.e. using the Visual Studio database project). I prefer database migrations (a la [FluentMigrator]) where possible, so this is an optional one depending on how the team manages their schema.|
|Setup||For applications that are deployed using installation platforms such as Windows Installer this project captures the layout of the package and installation process. This could be a Windows Installer XML, Advanced Installer, NullSoft Installer, or InstallShield project depending on the tooling choice of the team.|
|AdministrationGuide||Adminsitrator-oriented documentation is often left until right before deployment in many corporate development teams. I prefer to make it a first class citizen in the project so that it retains visibility and the authoring tasks are considered in sprint planning. For projects which require multiple languages the help authoring tools often provide a project file format to manage the various aspects of the artifact.|
|Help||Depending on the structure of the team and tools chosen online help content may be authored in a separate tool. This project is created by the help authoring tool.|
Separating code into projects should only be undertaken when the need, value and constraints of physical separation is understood. Project barries are expensive relative to internal division techniques such as folders and namespaces. One product I encountered had over 300 projects in the solution which caused tooling to struggle to keep up. As Jimmy Bogard points out - deferring abstraction decisions until the need and value present itself is preferred and in some cases you may be able to reapproach the problem in a different manner to deal with the complexity.
README.TXT (or README.MD if you prefer)
Text file at the root that contains the notes for other developers such as:
- Development tools that should be installed to work on the product
- Summary of the targets in the build file
- Steps to configure the development environment for specific scenarios (i.e. installing sample data sets, troubleshooting common issues)
The main solution file located in the root folder named after the product to make it clear that this is the primary starting point for developers.
ProductName.msbuild (and Go.cmd)
Targets that capture each stage in the continuous integration process, such as:
- Clean: Clean up build output
- Compile: Clean up build output, updates the version number and builds the solution
- Test: Run tests
- Analyze: Run static code analysis
- Package: Create deployment package
The Go.cmd file is a shortcut to locate MSBuild on the machine and execute it with the build file and desired target. Working with CI servers like TeamCity, Team Foundation Server, and CruiseControl I focus their usage on coordinating the execution of the steps. Storing the build configuration within source control enables anyone to run the same steps that the build agent completes locally to troubleshoot potential build-related issues. While MSBuild is my default tool other build systems such as rake, psake, FinalBuilder should be considered.
Contains the common attributes that should be shared across projects within the solution:
- AssemblyCompany: Name of the product ownership organization
- AssemblyProduct: Product name
- AssemblyCopyright: Legal stuff to keep the lawyers happy
- AssemblyInformationalVersion: Version displayed in the About dialog driven by marketing (i.e. "2013 Q1 Service Pack 4")
- AssemblyVersion: Binding version used in strong name references, often kept to just major and minor elements with revision and build zeroed out (i.e. X.Y.0.0).
- AssemblyFileVersion: Version number generated during the build process driven off a fixed major, minor and revision value with an incrementing build number.
- AssemblyConfiguration: Information about the machine on which the solution is built populated by the build script (machine name, date, time)
For versioning the semantic versioning rules are a good guideline to when to increment major, minor and revision numbers.
How do you structure your solution?
This is just one of many approaches to how you could structure your solution. I am always interested in how others approach it. What I find is that regardless of how you approach it, the important piece is that the convention is relatively consistent across products within an organization. Establishing some basic guidelines will go a long way to helping developers swap between product teams and new hires ramp up.
Sample code for this post is available at https://github.com/colinbowern/Spikes