Visual Studio Project files and coupling

The way that we’re told to use Visual Studio is that we create a solution file and add into it one or more project files.  Each project file then gets filled with different development artefacts.  When you build inside of Visual Studio each project represents a compiled distributable file (exe, dll, etc).  Many people carry this practice over into their build scripts.  You might be one of them.  I’m here to tell you why you’re wrong to be doing this.

Let’s say you’re starting a project.  You open Visual Studio, select File | New Project and get things rolling.  In a few minutes you have a Solution that contains a few Projects.  Maybe you have one for the UI, one for the business logic and one for the data access layer.  All is good.  A few months later, after adding many artefacts to the different projects, something triggers the need to split the artefacts into one assembly from one DLL into two DLLs. 

You set off to make this happen.  Obviously you need to add a new Project to your Solution, modify some references, and shift some files from one Project into another.  Say you’re stuck using an exclusive locking source control system (like VSS…shudder).  You *must* have exclusive access to all the files necessary including:

  • the sln so you can add the new project
  • at least one existing cs/vb/fsproj which you’ll be removing existing code artefacts from
  • any cs/vb/fs files that will be moved
  • any cs/vb/fs files that reference the ones moving (using statements will need updating when you change the namespacing on the files being moved)
  • possibly some resx files that need to be moved
  • possibly config files that need to be changed or moved
  • any automated tests that make use of the moving cs/vb/fs files

It’s a pretty damn big list of files that you will need to exclusively lock during this process.  Chances are you will need to push all of your co-workers out of the development environment so that you can gain access to all of those files.  Essentially you are, at this point, halting the development process so that you can do nothing more than split one DLL into two.  That in quite inefficient in the short term and it’s completely unsustainable in the long term.

I can hear you now, “Well I use <git/mercurial/svn/etc> so we won’t have those issues”.  Really?  Think it through for a second.  Go ahead, I’ll wait.

With the volume of changes that I listed above, you’ll likely want to be working in some kind of isolation, whether that is local or central.  So yes, you can protect yourself from blocking the ongoing development of your co-workers by properly using those version control systems.  But remember, you do have to integrate your changes with their work at some point.  How are you going to do that?  You’ve moved and modified a significant number of files.  You will have to merge your changes into a branch (or the trunk) locally or otherwise.  Trust me, this will be a merge conflict nightmare.  And it won’t be a pain just for you.  What about the co-worker that has local changes outstanding when you commit your merged modification?  They’re going to end up with a massive piece of merge work on their plate as well.  So instead of being blocked while you do the work, you’re actually creating a block for them immediately after you have completed your work.  Again, the easiest way to achieve the changes would be to prevent any other developers from working in the code while modifications are occurring.  Doesn’t that sound an awful lot like exclusive locking?

Now, I know you’re thinking “Pfft..that doesn’t happen often”.  This is where you’re wrong.  When you started that application development cycle (remember File | New Project?) you likely didn’t have all of the information necessary to determine what your deployables requirements were.  Since you didn’t have all of that information, chances were good, right from the outset, that you were going to be doing the wrong thing.  With that being the case, it means that chances were good that you were going to have to make changes like the one described above.  To me that indicates that you are, by deciding to tie your Visual Studio Projects to your deployables, accepting that you will undertake this overhead.

People, possibly you, accept this overhead on every software project they participate in.  This is where you’re wrong.  There is a way to avoid all of this, but people shrug it off as “not mainstream” and “colouring outside the lines”.  The thing is it works, so ignore it at your own peril.

There is a lot of talk in some development circles about decoupling code.  It’s generally accepted that tightly coupled code is harder to modify, extend and maintain.  When you say that a Visual Studio Project is the equivalent of a deployable, you have tightly coupled your deployment and development structures.  Like code, and as the example above shows, it makes it hard to modify, extend and maintain your deployment.  So why not decouple the Visual Studio Project structure from the deployables requirements?

It’s not that hard to do.  You’ll need to write a build script that doesn’t reference the cs/vb/fsproj files at all.  The .NET Framework kindly provides configurable compiler access for us.  The different language command line compilers (vbc.exe/csc.exe/fsc.exe) allow you to pass in code files, references, resources, etc.  By using this capability, you can build any number of assemblies that you want simply by passing a listing of artefacts into the compiler.  To make it even easier, most build scripting tools provide built in capability to do this.  NAnt and MSBuild both provide (for C#) <csc> tasks that can accept wild carded lists of code files.  This means you can end up with something like this coming out of a solution-project structure that has only one project in it:

<csc output="MyApp.DAL.dll" target="library" debug="${debug}">
  <sources>
    <include name="MyApp.Core/DAL/**/*.cs"/>
  </sources>
  <references>
    <include name="log4net.dll"/>
  </references>
</csc>

<csc output="MyApp.Core.dll" target="library" debug="${debug}">
  <sources>
    <include name="MyApp.Core/Business/**/*.cs"/>
  </sources>
  <references>
    <include name="log4net.dll"/>
    <include name="MyApp.DAL.dll"/>
  </references>
</csc>

<csc output="MyApp.UI.exe" target="winexe" debug="${debug}">
  <sources>
    <include name="MyApp.Core/**/*.cs"/>
    <exclude name="MyApp.Core/DAL/*.cs"/>
    <exclude name="MyApp.Core/Business/*.cs"/>
  </sources>
  <references>
    <include name="log4net.dll"/>
    <include name="MyApp.DAL.dll"/>
    <include name="MyApp.Core.dll"/>
  </references>
</csc>

Likewise, we could consolidate code from multiple projects (really file paths is what the build script sees them as) into one deployable.

<csc output="MyApp.UI.exe" target="winexe" debug="${debug}">
  <sources>
    <include name="MyApp.DAL/**/*.cs"/>
    <include name="MyApp.Business/**/*.cs"/>
    <exclude name="MyApp.UI/**/*.cs"/>
  </sources>
  <references>
    <include name="log4net.dll"/>  
  </references>
</csc>

Now, when it comes time to change to meet new deployables needs, you just need to modify your build script.  Modify the inputs for the different compiler calls and/or add new compilations simply by editing one file.  While you’re doing this the rest of your co-workers can continue doing what they need to provide value to the business.  When it comes time for you to commit the changes to how things are getting compiled, you only have to worry about merging one file. Because the build script is far less volatile than the code files in your solution-project structure, that merge should be relatively painless.

Another way to look at this is that we are now able to configure and use Visual Studio and the solution-project structure in a way that is optimal for developers to write and edit code.  And, in turn, we configure and use the build script in a way that allows developers to be efficient and effective at compiling and deploying code.  This is the decoupling that we really should have in our process and ecosystem to allow us to react quickly to change, whether it comes from the business or our own design decisions.

Comments

Dylan Smith
06/09/2010 06:55 PM by
Dylan Smith

I remember you going on about this at PrairieDevCon, it's an interesting idea. I'm definately going to give it a try on that large project when I have some time and see how practical it is. I'll let you know how it goes.

Stuart Cam
06/10/2010 10:49 AM by
Stuart Cam

This is the exact approach I recommended on a complex C# project.

Granted, we were using SubVersion, so the whole exclusive-lock problem was less apparent, but still, the coupling to the sln and csproj files was there.

By using the CSC task in Nant we were forced to think a little more about the dependancies required for compilation. As a side bonus you also shielded from "Add Referenctitus". How easy is it for a developer to think "Oh, I need a reference to SharePoint... Add Reference..." - minutes later you are now throwing around that 10Mb dll during your build and deployment process.

The other benefit is reducing the amount of *.cs 'rubble'. You know, that dead code which was removed from the *.csproj but that still lives on disk? By using the wildcarding */.cs in Nant we found that dead code wouldn't compile (changed type names, references, etc...) and would be alerted to it.

It's more work, but it pays dividends in the longer term.