Rules to Better .NET Core - 4 Rules
A lot of these rules have originated from the .NET Core Superpowers Tour.
.NET Core works on multiple platforms like MacOS, Linux and Windows. So developers can code on their operating system of choice using Visual Studio, Visual Studio for Mac or Visual Studio Code, working on the same code base as developers on a different operating system, and best of all they can also deploy to Linux.
.NET Core is fast, and you get that performance out of the box.
There are three main frameworks for unit testing. The good news is that they are all acceptable choices:
- They all have test runner packages for running tests directly from Visual Studio
- They all have console-based runners that can run tests as part of a CI/CD pipeline
- They differ slightly in syntax and feature set
xUnit.net is a newer framework – written by the original creator of NUnit v2 to create a more opinionated and restrictive framework to encourage TDD best practice. For example, when running xUnit tests, the class containing the test methods is instantiated separately for each test so that tests cannot share data and can run in parallel.
xUnit.net is currently the most popular framework - and is even used by the .NET Core team.
xUnit.net is the default choice for .NET Core web applications and APIs at SSW.
The NUnit project deserves recognition for being the first powerful and open-source unit test framework for the .NET universe – and it’s still a solid choice today.
NUnit has undergone large changes in the last 10 years with its NUnit3 version. The most notable is the Assert Constraints, which is a built-in Fluent Assertion library, allowing you to write readable asserts like
Assert.That(x, Is.EqualTo(42).Within(0.1)). It has also adopted the lifetime scopes of XUnit, but you can choose which one to use.
NUnit differs from XUnit in being more flexible and more adaptable versus XUnit being more restrictive and opinionated.
Because NUnit has an open-source .NET UI control for running tests, NUnit is still SSW’s preferred choice for embedding unit tests and a runner UI inside a Windows application.
MSTest is Microsoft's testing framework. In the past this was a poor choice as although this was the easiest framework to run from Visual Studio, it was extremely difficult to automate these tests from CI/CD build servers. These problems have been completely solved with .NET Core but for most C# developers this is “too little, too late” and the other unit testing frameworks are now more popular.
Respawn is a lightweight utility for cleaning up a database to a known state before running integration tests. It is specifically designed for .NET developers who use C# for testing purposes. By intelligently deleting only the data that has changed, Respawn can dramatically reduce the time it takes to reset a test database to its initial state, making it an efficient tool for improving the speed and reliability of integration tests. Respawn supports SQL Server, PostgreSQL, and MySQL databases.
Testcontainers for .NET! is a library that enables C# .NET developers to create, manage, and dispose of throwaway instances of database systems or other software components within Docker containers for the purpose of automated testing.
It provides a programmatic API to spin up and tear down containers, ensuring a clean and isolated environment for each test run. Testcontainers supports various containers, including databases like SQL Server, PostgreSQL, and MongoDB, as well as other services like Redis, Kafka, and more, making it a versatile tool for integration testing in a .NET environment.
Dotnet is a flexible ecosystem, and that also applies to the use of test frameworks. There is nothing preventing you from using multiple different test frameworks in the same project. They will coexist with no pain. That way one can get the best from each, and not be locked-in with something that doesn't allow you to do your job efficiently.
With .NET Core, we've got a new, extensible configuration system for our projects. This is easily extended and has out-of-the-box support for many configuration sources including JSON files, per-environment overrides, command-line parameters, and environment variables.
A common source of pain when working in a team is when different team members require different connection strings in order to run the project locally. If the developer modifies settings and then accidentally pushes that change into source control, the app might break for other developers.
Resolve this by:
Now, any new developer that needs a custom connection string (or any other configuration setting) can create their own appsettings.Local.json file without affecting any other team member’s configuration.
Long Term Support (LTS) versions of .NET Core / .NET 6+ are officially supported by Microsoft for 3 years following release.So when considering which version to target for your application, the “Latest LTS when we first deploy to Production” is often the safest choice.Non-LTS versions have much shorter support lifetimes.
Deciding which version to target is not always as simple as "always choose LTS".
In many cases, the expected lifetime of a project is longer than this LTS lifetime so future upgrades to your project must always be considered.
The ongoing resources planned to work on this product must also be evaluated.
To help in these decisions, the .NET Core team has released a roadmap for upcoming releases over the next few years. All even-numbered releases will be LTS.
Important questions to consider include:
- What is the planned lifecycle of the project?
- Will there be ongoing development in the future?
How are we planning to distribute and support this project?
- Web services that are automatically deployed to the cloud will be easier to continually update than a desktop app installed on customer PCs
- How does my project’s lifecycle align with the .NET release cycle?
All supported versions of .NET will receive servicing releases and the work to apply these updates should always be factored into the Total Cost of Ownership for any project.
A few invented scenarios are presented below:
"It’s November 2020 and we are planning to launch the final version of our ASP.NET Core 3.1 API next week. There is no further planned development work in the next 12 months. If we upgrade to .NET 5 in the final Sprint, we would get a performance boost."
Recommendation: Although moving to .NET 5 could introduce a performance improvement, .NET 5 is not LTS and there is little future development planned. In this situation staying with the LTS .Net Core 3.1 is recommended.
"It’s November 2020 and Build 4463 of our ASP.NET Core API has just been deployed to Azure via GitHub Actions. We plan to continually develop new feature requests over the next 15 months, Finishing in January 2022”
Recommendation: When “continual development” is planned, it’s much easier to recommend working against the latest .NET version as it’s released. Upgrading incrementally during active development is often less painful than a larger planned upgrade between LTS versions
"Once finished, we have no plans for further features for the next 2 years. Our planned launch date is October 2021"
Recommendation: The plan to reach “feature complete and done” followed by no planned subsequent work seems to suggest that development with an LTS version would be best.
But if you look at the planned launch date, it’s 1 month before the next LTS release (.NET 6). In this scenario, I would strongly advocate Developing against .NET 5, but leave some spare capacity to perform an update to .NET 6 LTS soon after launch. Then the final, "stable" version will have a much longer support window.