The release of .NET Core 2 was the first “real” version of .NET Core. The previous version (.NET Core 1) really wasn’t ready for primetime. But .NET Core 2 was a solid product. And in many ways, it was better than .NET Framework.
First off, .NET Core 2 had all that was good about .NET Core. The hosting model was more obvious, some of the magic of ASP.NET was gone, and it had more of a NodeJS feel to hosting (which eliminated some of that hard-to-understand magic that existed before). With .NET Core 2, all bootstrapping occurs in Startup.cs.
The second is the performance of .NET Core. From the beginning, .NET Core has had a better performance profile than the original .NET Framework.
The third is some of the new language features such as expression body members and local functions. Expression body members don’t enable anything new, but they make writing simple methods even simpler and with less code.
Local functions allow for the creation of functions to be nested within another function. This allows you to create a function that is only used by another of your functions. This minimizes the scope of your function, which is an excellent way to improve the quality of a system.
NET Core 2/2.1 wasn’t perfect. While it felt like real software, many libraries and tools were still lagging.
.NET Framework 2.0 (Generics, Partial Classes, Nullable Types, Anonymous Methods)
.NET Framework 3.0/3.5 (WPF, WF, WCF, Auto-Implemented Properties)
.NET Framework 3.0 (LINQ)
.NET Framework 3.5 SP1 (ADO.NET Entity Framework)
.NET Framework 4.0 (Parallel, Dynamic)
.NET Framework 4.5 (Async)
.NET Core 2.0/2.1
.NET Core 3.0/3.1
.NET Core 5.0