4 Comments

A Feature-Oriented Directory Structure For C# Projects

After working on .NET applications for the past six years, I recently spent a few months using Ember.js and AngularJS. Both originally supported organizing files in a project by type: separate top-level directories for models, controllers, views, etc. But this has changed over the past few years to prefer organizing by feature area—Ember with pods Angular with modules.

It’s worked well for Javascript apps, so I recently experimented by converting one of my C# apps to follow the same principles.

Angular Project Layout

Consider the structure of the angular-seed app:


app/                    --> all of the source files for the application
  app.css               --> default stylesheet
  components/           --> all app specific modules
    version/              --> version related components
      version.js                 --> version module declaration and basic "version" value service
      version_test.js            --> "version" value service tests
      version-directive.js       --> custom directive that returns the current app version
      version-directive_test.js  --> version directive tests
      interpolate-filter.js      --> custom interpolation filter
      interpolate-filter_test.js --> interpolate filter tests
  view1/                --> the view1 view template and logic
    view1.html            --> the partial template
    view1.js              --> the controller logic
    view1_test.js         --> tests of the controller
  view2/                --> the view2 view template and logic
    view2.html            --> the partial template
    view2.js              --> the controller logic
    view2_test.js         --> tests of the controller
  app.js                --> main application module
  index.html            --> app layout file (the main html template file of the app)
  index-async.html      --> just like index.html, but loads js files asynchronously
karma.conf.js         --> config file for running unit tests with Karma
e2e-tests/            --> end-to-end tests
  protractor-conf.js    --> Protractor config file
  scenarios.js          --> end-to-end scenarios to be run by Protractor

Note that instead of grouping view1.html and view2.html together in a views directory, they’re placed beside the corresponding controller and tests. You can read more about comparing this with the traditional Angular project structures in this post by Cliff Meyers. I’ll use the rest of this post to discuss the concepts in a .NET context.

Typical C# Project Structure

Here’s an example of your typical WPF MVVM project structure. There’s a Models directory, a Views directory, and a ViewModels directory. Easy, right? If you need to find the Customer model, go check the models directory.


App.sln
  Proj1.csproj
    Views/
      Customer.xaml
      Order.xaml
    ViewModels/
      CustomerViewModel.cs
      OrderViewModel.cs
    Models/
      Customer.cs
      Order.cs
    Common/
      Utilities.cs
      SharedCode.cs
  Proj1UnitTests.csproj
    ViewModels/
      CustomerViewModelTests.cs
      OrderViewModelTests.cs
    Models/
      CustomerTests.cs
      OrderTests.cs
    UtilityTests.cs

What happens if we need to add a new feature for shopping cart functionality? We just add the new ShoppingCart.cs tests and model, the new viewmodel files, and finally the view itself. This looks like 90+% of the .NET projects I’ve ever come across. How does it compare to a module oriented approach like that of the angular-seed app?

Feature-Oriented C# Project Structure


App.sln
  Proj1.csproj
    Customer/
      Customer.cs
      Customer.xaml
      CustomerViewModel.cs
    Order/
      Order.cs
      Order.xaml
      OrderViewModel.cs
    Common/
      Utilities.cs
      SharedCode.cs
  Proj1UnitTests.csproj
    Customer/
      CustomerViewModelTests.cs
      CustomerTests.cs
    Order/
      OrderTests.cs
      OrderViewModelTests.cs
    UtilityTests.cs

Now our directories are organized by feature (Customer, Order) instead of by type (Model, View, ViewModel). Need to find the Customer model? It’s right where you’d expect, in the Customer directory. Instead of being beside other models on disk, it’s now co-located with the other Customer files that actually use it. What if we need to add in new shopping cart functionality? Just create the new ShoppingCart*.cs files all in the same folder and follow the same pattern for the tests.

If using the feature approach appeals to you, I think there’s one more logical step to take. The project structure above still has a duplicate directory tree for the tests. What if we move the unit tests beside the code they’re testing?


App.sln
  Proj1.csproj
    Customer/
      Customer.cs
      CustomerTests.cs
      Customer.xaml
      CustomerViewModel.cs
      CustomerViewModelTests.cs
    Order/
      Order.cs
      OrderTests.cs
      Order.xaml
      OrderViewModel.cs
      OrderViewModelTests.cs
    Common/
      Utilities.cs
      UtilityTests.cs
      SharedCode.cs

Now it’s trivial to find related files without needing to traverse the entire app’s directory structure. It’s also clear exactly which files have corresponding unit tests and which ones are missing. Further, if we add in the ShoppingCart files, they all land in a single folder.

Handling Namespaces & Deployment

There are two related issues that still need to be addressed:

  1. Should namespaces still match the directory layout on disk? For a small example project it won’t matter much either way. For now, code in my Common directory is in the default namespace where it is automatically in scope for any source code in nested namespaces. As my project grows, I’ll see what works best.
  2. Do you deploy the unit tests in your dll? For my current app there’s no harm in doing so and it’s default result unless I put in extra effort. In a more commercial/public facing project, the safe answer is to just #ifdef your test code and exclude it for production builds. If references added for testing dlls aren’t actually used by code in the production compilation, the compiler can skip emitting those references in the output dll.

If this feature-oriented approach becomes more popular for .NET code, I’d expect the tooling to improve to support it directly. With the new project.json files for dnx projects, the production build could simply be to include *.cs and exclude *Test.cs files, whereas the test build could default to include all files.

Conclusions

Using a feature-oriented project structure appears as appropriate if not more so than the traditional .NET project structure. I’ll be continuing to experiment with it as my new default going forward. It’s simpler for many scenarios, and I expect it will scale as projects grow since features can easily be refactored into sub-features that use the same recursive directory layout.