Arrow icon
Back to Insights

Automating Code Style Guides

August 12, 2021
Jason Gravell

This is some C# I've written for querying a record from Entity Framework Core.

entity = await context.Entities.Include(e => e.SubEntities)
    .ThenInclude(s => s.SubSubEntity)
    .AsNoTrackingWithIdentityResolution()
    .FirstOrDefaultAsync(e => e.Id == id);

Here's that same code again.

entity = await context
    .Entities
    .Include(
        e => e.SubEntities
    )
    .ThenInclude(
        s => s.SubSubEntity
    )
    .AsNoTrackingWithIdentityResolution()
    .FirstOrDefaultAsync(
        e => e.Id == id
    );

And again.

entity = await context.Entities.Include(e => e.SubEntities).ThenInclude(s => s.SubSubEntity)
.AsNoTrackingWithIdentityResolution().FirstOrDefaultAsync(e => e.Id == id);

If a developer wants to write the above code, they need to use the same letters, punctuation, and symbols as any other developer writing that code. Changing any of those characters could change the functionality of the program, potentially making that code objectively "wrong".

But the whitespace they use to space those characters has no right answer (in C# at least). By manipulating whitespace there are literally an infinite number of ways to write my snippet that will all compile to the same instructions and fetch the record in exactly the same way.

This freedom poses a problem for team software development.

Imagine you've got two developers, Jeff and Scott, tasked with implementing the "fetch this entity" code above. Jeff and Scott are both good developers, and therefore write code that is functionally identical to what you see above. However, they also have different preferences on how lines should be broken up. When it comes time to merge their otherwise identical code together into the main codebase, git sees that the lines of text are not equivalent, and a merge conflict emerges. Developer time that could be spent on writing new code is instead wasted sorting through merge conflicts.

Let's try that again. This time, before these two developers attempt to merge their code, a third developer, Mike, goes through both sets of code and applies his own particular whitespace preferences to it. Both versions of code are now identical prior to the merge, and as a result the code flows seamlessly into the main branch.

Ok, but we're just trading Jeff or Scott parsing merge issues for Mike applying whitespace preferences. If only there was some way to automate Mike's job...

Automating Mike's Job

Unfortunately for Mike, he's about to be out of work. As it turns out, automating the process of automating code formatting to a common style is very feasible through the use of code formatters.

There are a number of code formatting tools out there that can, among other things, solve your whitespace woes. At the time of writing this article, the gold standard is Prettier. It supports a number of languages and frameworks including Typescript, Vue, Angular, and CSS. C#, however, is not one of them. Fortunately for our fictional development team, the web is a big place, and a C# fan tribute exists in the form of CSharpier. Both of these tools can be run from a command line and can apply formatting fixes to a single file or an entire workspace in seconds.

While that gives us the tool to format to a common style, a tool is useless unless it gets used. Trusting developers to run a formatter on their own code is a fools errand. Developers are human beings (well, mostly), and that means they have all the trademark failings of humans. They get lazy, they get forgetful, they even occasionally get malicious. All of these traits are impediments to our frictionless merge Utopia.

The best way to ensure that this tool gets run is to integrate it directly into the workflow. If you're reading this in 2021 or the foreseeable future, you're probably using git as your change tracking software, and it comes with a number of hooks that you can utilize to trigger your formatter. In particular, the pre-commit hook is probably the most useful for this purpose. It defines logic will be run prior to every commit you make to a repository.

Below is a simple pre-commit script that will apply formatting to any currently staged files immediately prior to the commit being created (adopted from an example on github). This one is a shell script, but you can write git hook scripts in any language your machine knows how to execute (shell scripts are popular due to their ubiquity).

#!/bin/sh
LC_ALL=C
# Select files to format from staged
FILES=$(git diff --cached --name-only --diff-filter=ACM | sed 's| |\\ |g')
[ -z "$FILES" ] && exit 0

if [[ $(echo "$FILES" | sed -n '/.*\.cs$/p') ]]; then
  # CSharpier .cs selected files
  # .razor/.cshtml formatting is a work in progress on CSharpier at the moment,
  # so we skip those
  # https://github.com/belav/csharpier/issues/35
  echo "$FILES" | sed -n '/.*\.cs/p' | xargs dotnet csharpier  
fi

if [[ $(echo "$FILES" | sed '/.*\.cs$/d' | sed '/.*\.cshtml$/d' | sed '/.*\.razor$/d') ]]; then 
  # Prettier format all non-C# files
  echo "$FILES" | sed '/.*\.cs/d' | sed '/.*\.cshtml$/d' | sed '/.*\.razor$/d' | xargs npx prettier --write 
fi

# Add back the modified files to staging
echo "$FILES" | xargs git add

exit 0

There is a hurdle to overcome, however. By default, git hooks are a local concept. They reside in your .git/hooks folder, and are therefore not checked in to your repository. CSharpier/Prettier are also tools that need to be installed on the developer's machine (usually via dotnet tool and npm i, respectively). If you're working with a team, every developer on the team will need to configure the pre-commit script in their own hooks folder and run the tool installation. If they clone the repository onto a new machine, they'll need to set it up again. This has all the same failings of simply trusting a developer to run the formatter (albeit with less such opportunities for mistakes).

But just like we can automate the running of code formatters so too can we automate the installation of the formatter and pre-commit hooks. The trick is that we need to hook onto something every developer needs to do in order to work on the project: the package restore process. At Cloud Construct, most of our projects require that the developer made use of either dotnet or node to build projects, both of which provide an opportunity to run post-restore commands.

Here's an example .csproj file that assumes that the pre-commit hook is located at ./hooks/pre-commit relative to the project file:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
    </PropertyGroup>

    <Target Name="AdditionalRestores" AfterTargets="Restore">
        <!-- Restore node packages as defined by ./package.json -->
        <Exec Command="npm i" ConsoleToMsBuild="true" />
        <!-- Restore dotnet tools as specified by ./.config/dotnet-tools.json -->
        <Exec Command="dotnet tool restore" ConsoleToMsBuild="true" />
        <!-- Set git hook path to the ./hooks folder, a folder tracked by the repository -->
        <Exec Command="git config core.hooksPath ./hooks" ConsoleToMsBuild="true" WorkingDirectory="..\.." />    
  </Target>
</Project>

In this example, Prettier has been installed to the repository via npm i prettier, and CSharpier has been installed to the repository via dotnet tool's local tool installation. The Target restores (or installs) both packages, then points the user's git hooks folder to the repository hooks folder, ensuring that the pre-commit script will run the next time a commit is created.

Closing Thoughts

You don't need to take this exact approach to code formatting, but you should keep this one principal in mind as you set up your team's workflow: It's very important that all this automation be frictionless. A developer shouldn't need to manually install these tools to their machine, they shouldn't need to execute them, and absolutely under no circumstances should they ever be blocked from committing by a pre-commit hook. If your hook fails, it should do so silently. If it can't format a particular piece of code automatically, you can spit out a warning, but you never want to impede development for the sake of formatting.

Both of the formatters here are opinionated code formatters, meaning that the code style configuration options available are extremely limited, and most styling choices are made by the developer of the formatter rather than the user. As someone whose answer to the "Tabs vs Spaces" debate was always "whatever one lets me think about formatting less", I LOVE that opinionated code formatters remove the possibility of style debates within the development team.

If the commands used in this article are not your particular cup of tea though, you should be able to substitute them with whatever your tool works best for you.

Author Photo
Jason Gravell
Senior Software Engineer
Arrow icon
Back to Insights

Let's talk about your project

Drop us a note
Arrow
Or call us at:  
1.617.903.7604

Let's talk about
your project

Drop us a note
Arrow
Or call us at:
1.617.903.7604

Let's talk about your project

Drop us a note
Arrow
Or call us at:  
1.617.903.7604