Featured image of post How we enforce .NET coding standards at Workleap to improve productivity, quality and performance

How we enforce .NET coding standards at Workleap to improve productivity, quality and performance

Distributing .editorconfig and MSBuild properties across hundreds of .NET projects is now as easy as adding a single NuGet package. Here's how we did it.

In today’s competitive software development landscape, organizations are actively looking to optimize their Software Development Life Cycle (SDLC) to deliver faster, with better quality, and reduce friction. The rise of Generative AI amplifies this trend even more. Teams that know how to leverage these tools and practices achieve unprecedented velocity.

At Workleap, we decided to take a step back to analyze where we could improve our SDLC as well, in order to reduce friction and help our developers deliver value for our customers faster, without compromising on quality.

Previously, we talked about our improvements in local development with .NET Aspire and how we enforce web APIs standards and guidelines. In this article, we will focus on how we enforce C# code style, quality and performance standards at scale with our open source .NET coding standards NuGet package.

Identifying problems and possible improvements

Many of our services are made of several .NET solutions and hundreds of C# projects. While going through the different code repositories, our platform engineering team found a recurring pattern. Every time developers create a new solution, they are inspired by existing ones and copy some boilerplate code. This boilerplate includes .editorconfig files with dozens if not hundreds of rules, the inclusion of certain packages such as StyleCop, and .csproj properties affecting compiler/MSBuild behavior. Over the years, a certain drift occurred between different projects, resulting in inconsistencies in code style, quality and performance.

We also analyzed more than a hundred pull requests across different repositories, looking for patterns that would influence how long it takes to merge a pull request. We found that a significant number of review comments were related to code style issues. Interviews with developers confirmed that these comments had a negative impact on cycle time, as the back-and-forth could delay the merge to the next day or worse. We also noticed a certain degree of nitpicking, which proved harmful to productivity and relationships among developers. Finally, we detected multiple performance, quality or security issues in these pull requests that went unnoticed, which could be attributed to various factors, such as an overwhelming number of minor style comments that distracted reviewers from more important issues, or too many changes in a single pull request.

Digging deeper in these projects, we also found out that several warnings would pop up at build time, but nothing would prevent the associated code from being merged. This was the beginning of other related MSBuild-related findings, such as:

  • Nullable reference types being disabled by default in some places
  • Roslyn analysis level kept to the default, minimal value
  • Style code not enforced at build time
  • NuGet packages auditing not enabled

Moving forward, we wanted to address these issues in a way that would be sustainable and scalable across all our projects. We needed a solution that would not only enforce code style, quality and performance standards, but that would also be easy to integrate and migrate for existing projects. We knew that one of the most difficult challenges to overcome would be developers’ reluctance to change their habits and accept new rules.

Building reusable .NET coding standards

Once we had identified the problems, we started evaluating if we could build a solution that could be applied to at least 80% of our projects. Being a .NET/C# software company for more than 15 years meant we had a minimum baseline in terms of code style. Instead of relying on StyleCop, which has been useful to many of us for a long time, we decided to leverage the modern Roslyn-based analyzers and code fixes.

What followed were long hours trying to retrofit what would be the ideal coding style for the company in terms of language rules, formatting rules and naming rules. Fortunately, we knew that the code fixes integrated into these Roslyn analysis rules would allow developers to automatically fix them across solutions. The result is more than 200 lines of configuration, fine-tuned for the needs of the majority. This includes details as granular as where to place dots, commas, parentheses, spacing, the order of visibility modifiers, variable and type naming, etc. This is a level of granularity and attention to detail that we had never had before.

Once the code style was defined, we started configuring quality, performance and security rules. This was followed by meticulous work to configure more than 800 individual Roslyn analysis rules. Notable examples include: prevent multiple enumerations of IEnumerable, forwarding CancellationToken parameters to methods, rethrowing to preserve stack details, not using obsolete or weak key derivation functions, awaiting tasks instead of using .Result, etc. While this represented a colossal effort of tweaking hundreds of knobs with different severity levels, we also evaluated the impact of these rules on the build duration using MSBuild Log Viewer, as we did not want to introduce a significant overhead. Surprisingly, we were able to keep the build duration impact at the same level as before or slightly better, while enabling dozens of new relevant rules. We attributed this to the previous use of StyleCop, which was less performant with many rules enabled at low severity levels. As usual, we also considered the impact of these rules on the developer experience, and we made sure that the rules would not be too intrusive or disruptive, unless they were critical for code quality, performance or security.

Debating the severity of more than 800 Roslyn analysis rules took a bit of time.

In parallel with the configuration of these Roslyn analysis rules, we also tweaked multiple MSBuild properties to ensure deterministic, consistent and performant builds. We made sure to set their values only if they were not already set in the project. Notable properties include:

  • AnalysisLevel set to latest-all to enable all the latest Roslyn analysis features, individually tweaked by the rules defined in the .editorconfig mentioned above.
  • TreatWarningsAsErrors set to true when building in a CI environment or in Release configuration, to ensure that no warnings are left unaddressed in production code.
  • EnforceCodeStyleInBuild set to true to ensure that code style is enforced at build time, preventing any code that does not comply with the defined style from being merged.
  • NuGetAudit and several auditing-related properties enabled to ensure that all NuGet packages used in the project are audited for vulnerabilities.
  • AccelerateBuildsInVisualStudio and RestoreUseStaticGraphEvaluation opt-in to improve the build performance in specific scenarios.

We also added Microsoft.CodeAnalysis.BannedApiAnalyzers as a dependency to block the usage of certain .NET APIs that are known to be problematic in most web development scenarios, such as using DateTime.Now, invariant culture for string operations or using Newtonsoft.Json for JSON serialization instead of the built-in System.Text.Json.

Finally, nullable reference types were not enabled by default, as migrating existing projects to nullable reference types can be a long and tedious process. However, we strongly believe that nullable reference types are one of the best features introduced in C#, and our default service templates for new projects come with the Nullable MSBuild property enabled.

A single NuGet package to rule them all

None of these efforts would have made sense if we had not been able to redistribute them in a simple and efficient way. If they had to be copied into every solution, these hundreds of lines of manual configuration would have been a maintenance nightmare.

Fortunately, we happened to have the amazing Gérald Barré working with us at that time in our team, and he had previously studied how to redistribute .editorconfig files across projects in a NuGet package. Being aware of this allowed us to advance from a small proof of concept to an actual game-changer initiative for our company.

To put it simply, anyone that wanted to adopt our new .NET coding standards could simply add a single NuGet package to their project, and all the magic would happen automatically. This capability from the .NET SDK to import .editorconfig and other MSBuild properties and targets files is something we had never seen before in other languages and runtimes.

We use the EditorConfigFiles MSBuild property to import EditorConfig files globally.

Reception and adoption

When building internal tools, one of the most important aspects is adoption. No matter how great the tool is, if it is not adopted by the developers, it will not bring any value.

Knowing this, we used a communication technique that was… interesting to say the least. We applied our coding standards to several key company products and services projects, and through a document explaining our motivations, how it works and the expected benefits, we used a “positive shaming” approach where dozens of examples of non-compliant code were presented. The idea was to show how this new standard could detect thousands of potential errors and improvements in our existing projects, and create a desire to fix them.

This approach was a real success, and we quickly received approval from several technical leaders and managers to proceed with the adoption of our new coding standards across the entire company.

What came out of this was a relatively small effort of migrating more than 80% of our projects to the new coding standards. Small effort, because most Roslyn analysis rules came with code fixes, or because we provided documentation and guidance on migration. Here are some of the benefits we have seen so far:

  • Reduction of time spent in code reviews. Code style issues are now addressed automatically while writing code, or reported by CI checks to authors. This means reviewers spend less time on nitpicking and more time on verifying the actual logic and functionality of the code.
  • Improvement of code quality, performance and security. .NET has all these amazing Roslyn analysis rules that can help catch potential issues early in the development process. Enabling these rules by default has helped us catch many issues that would have otherwise gone unnoticed.
  • Increased developer knowledge. By being exposed to new rules and code suggestions in their IDEs, developers end up learning more about the language and its best practices. It contributed to make them better C# developers.
  • Less boilerplate configuration. Dozens of large, unmaintained .editorconfig files were removed across our repositories. Several Directory.Build.props and *.csproj files were simplified. Developers no longer ask themselves why a certain rule is enabled or not, as it is now centralized in a single place with exhaustive documentation for each rule.
  • Uniformity across projects. Developers can now expect the same code style across all projects, regardless of how ancient they are. This has helped reduce the friction when switching between projects or collaborating with other teams.
  • Faster builds. A few huge solutions have seen their build time reduced by up to 20% thanks to the removal of unnecessary StyleCop rules, in favor of built-in performant .NET rules. This translates to faster feedback loops, better developer satisfaction and less compute time.

Without our .NET coding standards, this poorly written code example would have triggered only one warning!

Try it yourself

Our .NET coding standards are open source and available on GitHub. You can install the package from NuGet.org and try it out in your own projects. We have proper tooling to automatically keep the analysis rules up-to-date as new rules or rule changes are made in the .NET SDK. We have several tests that ensure that several rules or code style settings work as expected. Sometimes, we harden the rules’ severity as the NuGet package adoption increases and we get more feedback from developers.

A few other companies have started using our coding standards and have provided positive feedback. They are grateful for no longer having to think about several aspects that were previously a source of friction, and for being able to focus on delivering value faster.

Code styling is subjective, and these coding standards are what work best for us. If you have different preferences, you can easily fork the repository. You can also modify the behavior of the analysis rules and the compiler with your own .editorconfig and MSBuild properties in your project.


Photo of our office in Montréal by Claude-Simon Langlois

Licensed under CC BY 4.0
Ko-fi donations Buy me a coffee