微软.NET 6将于11月9日发布。在过去的几个月里,我们得到了几个预览版和两个候选版。每个预览版都提供了.NET 6的各种功能 ,用于开发服务器/云、桌面、物联网和移动应用程序。
在这篇文章中,我回顾了我认为非常棒的6个功能。我还没有深入介绍性能等主题。然而,我试图涵盖直接影响开发过程的性能主题。 好吧,让我们深入了解一下。
1、.NET 6新项目模板
随着 .NET 6预览版7 的推出,出现了几个新的项目模板,这真的很有趣。从本质上讲,这些模板是基于.NET 6预览版所带来的新变化。 C# 10 带来的。这些模板更干净、更简单。值得注意的是,微软的工程师们选择了这条模板之路,向用户建议新功能,但不是强制执行。例如,从现在开始,当你创建一个控制台应用程序时,你将得到的只是简单的单行字:
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

这是个好消息。我的意思是,它只是比在当前版本中为一行有效代码获得所有这些更有意义。
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
通过项目模板启用这些功能,我们得到了两全其美的结果:新代码开始时启用了这些功能,但升级时现有代码不会受到影响。
那么,这里发生了什么?嗯,.NET 6有三个新的功能,使之成为现实:
- 顶层语句-- 这个功能在C#9中已经存在,但在这里它被利用于模板。简而言之,从C#9开始,你不必 在一个控制台应用程序项目中 明确包含Main 方法,但你可以使用 顶级语句 功能。这正是本模板所使用的,它极大地减少了代码。
- 全局使用-每个C#文件的开头都有一个实现所需的使用列表。然而,有时这也是多余的。C # 10引入了关键字global。使用这个关键字,你能够在一个单独的文件中为整个项目定义全局使用,该文件将包含这些导入(例如usings.cs)。有了这个,项目中的其他文件就可以被简化。C# 设计者将此称为消除垂直浪费。
- 隐式使用指令-- 该功能迫使编译器根据项目类型自动导入一组使用指令。从本质上讲,对于每个项目类型,都定义了一组隐含的全局使用,所以不需要在每个文件中明确说明。对于控制台应用程序,这些是:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
1.1 ASP.NET 6网络模板
使用相同的功能和类似的过程,Web应用程序的模板也被最小化了。现在它只包含几行代码:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/", () => "Hello World!");
app.Run();
1.2 ASP.NET 6 MVC模板
MVC模板也被现代化了:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
2.包验证工具
包验证工具是立即引起我注意的功能之一,它将是非常有用的东西。 这个功能是在 .NET 6预览版5 中提出的,它是一个可以提供给所有用户的内部工具的伟大例子。我的意思是,在dotnet/runtimerepo中,已经有工具可以确保多目标的软件包是良好的形式。

包验证工具允许开发者在包开发过程中验证他们的包是否一致和格式良好。它允许开发者根据他们以前的版本、框架的版本和运行时间来验证包。随着.NET生态系统的发展,这变成了一个大问题,特别是在跨目标包和新平台上,因为社区还不够强大。有了这个工具,你甚至不能创建一个未经验证的软件包,这使得整个.NET生态系统更加安全。
我们已经在野外看到了这个问题,甚至在第一方,例如Azure AD库。
目前,软件包验证工具可以作为MSBuild SDK包使用。这意味着你需要像这样把它包含在你的项目中。
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />
<PropertyGroup>
<TargetFrameworks>net5;net6.0</TargetFrameworks>
</PropertyGroup>
</Project>
注意,这也是新的语法。上面的例子描述了一个针对 .NET 5和 .NET 6 的包。这将创建一组任务,在donet pack或donet build命令中运行,对包进行验证。正如我们所提到的,有三种验证可以用这个工具来完成,所以让我们来看看。
2.1 包的验证--兼容框架
一般来说,包验证工具在打包的时候会对错误进行缓存。在这个特殊的例子中,这个工具需要验证针对一个框架编译的代码可以在另一个框架中运行。正如我们在前面的例子中看到的,我们可能想要一个针对 .NET 5 和 .NET 6的 包。当构建这样一个包时,我们可能会尝试这样的方法。
public class TestClass
{
#if NET6_0_OR_GREATER
public void DoSomething(ReadOnlySpan<char> input)
{
//Some .NET 6 Operations
}
#else
public void DoSomething(string input)
{
//Some .NET 5 Operations
}
#endif
}
如果我们试图用dotnet pack在上面的代码上创建一个包,我们会收到以下错误:
C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error : net5.0 assembly api surface area should be compatible with net6.0 assembly surface area so we can compile against net5.0
and run on net6.0 .framework. [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj]
C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error : API Compatibility errors between lib/net5.0/TestPackage.dll (left) and lib/net6.0/TestPackage.dll (right): [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj]
C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error CP0002: Member 'TestPackage.TestClass.DoSomething(string)' exists on the left but not on the right [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj]
发生了什么事?嗯,你的代码与这两个框架不兼容。从这些信息中,我们可以理解,我们不应该用字符串输入排除DoSomething,只需提供.NET 6中额外的 ReadOnlySpan。 像这样:
public class TestClass
{
#if NET6_0_OR_GREATER
public void DoStringManipulation(ReadOnlySpan<char> input)
{
// use spans to do string operations.
}
#endif
public void DoStringManipulation(string input)
{
// Do some string operations.
}
}
再次运行donet包,你会看到这样的东西:
Microsoft (R) Build Engine version 17.0.0-preview-21378-03+d592862ed for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
All projects are up-to-date for restore.
You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
TestPackage -> C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\Debug\net5\TestPackage.dll
TestPackage -> C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\Debug\net6.0\TestPackage.dll
Successfully created package 'C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\Debug\TestPackage.1.0.0.nupkg'.
2.2 包的验证 - 包的版本
有多少次你破坏了包的前一个版本的行为,尽管从代码的角度看一切都很好?例如,你可以打破Liskov替代原则,创造一个二进制的破坏性变化。这发生在你改变了你的包的公共API,所以针对你的库的旧版本编译的程序集不再能够调用API。包验证工具帮助你检测这种情况。
例如,让我们假设我们把TestClass迁移到了*.NET 6*,并在包内发布了它。为了确保你未来的变化不会产生任何二进制制动的变化,你应该对你的项目使用PackageValidationBaselineVersion。工具会根据已经创建的包的先前版本来检测任何制动变化。该项目是这样定义的:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<PackageVersion>2.0.0</PackageVersion>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>
</Project>

TestClass的代码看起来像这样:
public class TestClass
{
public void DoSomething(string input)
{
//Some .NET 6 Operations
}
}
现在,如果我们想用额外的输入来扩展DoSomething,情况是这样的:
public class TestClass
{
public void DoSomething(string input, string input2)
{
//Some .NET 6 Operations
}
}
有了这个,我们肯定会产生二进制的破坏性变化。然而,由于我们使用了PackageValidationBaselineVersion,我们在打包时收到一个错误。
C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error :
API Compatibility errors between lib/net5.0/TestPackage.dll (left) and lib/net6.0/TestPackage.dll (right): [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj]
There are braking changes between versions. Please add or modify apis in the reicent version or supress the intentional braking changes.
正如信息所提示的,我们需要添加DoSomething函数的额外覆盖。
public class TestClass
{
public void DoSomething(string input)
{
//Some .NET 6 Operations
}
public void DoSomething(string input, string input2)
{
//Some .NET 6 Operations
}
}
另外,通过使用PackageValidationBaselinePath也可以实现同样的目的。
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<PackageVersion>2.0.0</PackageVersion>
<PackageValidationBaselinePath>"C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\DebugV1\net6.0\"</PackageValidationBaselinePath>
</PropertyGroup>
</Project>
2.3 包的验证--运行时
我们可以决定更进一步,决定让我们的包在Unix和Windows上工作。也许我们的DoSomething方法在这些系统中需要做一些不同的事情,或者它们需要执行不同的代码。观察一下新的TestClass。
public class TestClass
{
#if Unix
public void DoSomething(string input, bool input2)
{
// Unix specific stuff
}
#else
public void DoSomething(string input)
{
// Windows specific stuff
}
#endif
}
然而,这段代码包含一个错误。正如你所看到的,我们在Unix中缺少只有一个参数的DoSomething(string input)。包验证工具会准确地告诉我们这一点。这段代码应该是这样的:
public class TestClass
{
#if Unix
public void DoSomething(string input, bool input2)
{
// Unix specific stuff
}
public void DoSomething(string input)
{
// Unix specific stuff
}
#else
public void DoSomething(string input)
{
// Windows specific stuff
}
#endif
}
3. .NET 6多平台应用程序用户界面(MAUI)
从 .NET 5 之前,微软就经常谈及 "大统一"。一般来说,这个想法是只有一个.NET,用来针对Windows、Linux、macOS、iOS、Android、tvOS、watchOS和WebAssembly。在 .NET 5 中,他们已经开始了这个过程,并产生了一个单一的.NET运行时和框架,可以在任何地方使用,并具有统一的运行时行为和开发者体验。

在 .NET 6 中,他们进一步推进了这一想法,并将Xamarin 与安卓 和iOS一起纳入伞状结构中。在Build 2020上,微软宣布了 .NET多平台应用程序用户界面 (MAUI),这是一个建立在Xamarin之上的现代UI工具包。当然,这个决定在一定程度上是由社区驱动的。已经有很多人要求在你的移动和桌面应用程序之间共享代码。因此,通过.NET MAUI,你可以从一个代码库中构建可以在 Android、iOS、macOS和Windows 上运行的应用程序。
Xamarin一直是.NET的一部分,但现在它作为一个核心工作负载,与Blazor等其他工作负载共享相同的基础类库,并采用现代SDK Style项目系统以获得一致的工具体验。
.NET 6 有一个库,叫做基类库(BCL)。这个库是不同的特定框架的基础,如 .NET for Android、.NET for iOS、.NET for macOS和Windows UI 。所有这些框架都会访问BCL,反过来,它抽象了平台的细节。所以,当你写代码时,你不需要知道这些细节。这一切都被抽象化了,所以你可以毫无压力地编写跨平台的代码。
除此之外,Xamarin.Forms工具包和 Xamarin.Essentials 是 .NET MAUI 的核心。这些工具得到了扩展,所以它们可以用于开发移动和桌面应用程序。由于它是已经建立和长期使用的技术,拥有一个庞大的社区,所以它是 .NET MAUI 的基础的合理选择 。

要在Visual Studio 2022预览版中启用这一功能,有以下工作负载。

3.1 .NET MAUI单一项目
微软用Single Project 这个名字来形容项目文件,它将所有平台的具体信息都收集在一个屋檐下。基本上,一个可以针对所有平台的共享项目文件。 这个项目是按照.NET 6中项目的SDK式精神建立的。这个项目被分成几个部分。 资源(图片、字体、图标等)、应用清单、NuGet包、平台专用代码。下面是在Visual Studio 2022中的样子。

3.2 .NET MAUI基础知识
.NET MAUI项目使用.NET通用主机,这是一个工作服务,是由 HostBuilder.对于每个平台来说,都有一个入口点来初始化这个构建器。这样一来,应用程序就被通用主机引导了。一旦构建器被初始化,平台就会调用Startup 类中的Configure方法。这个类可以被看作是应用程序的入口点,它创建了应用程序的初始页面。这里就是了:
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
public class Startup : IStartup
{
public void Configure(IAppHostBuilder appBuilder)
{
appBuilder
.UseMauiApp<App>();
}
}

App类是你的应用程序的类。它必须派生自应用程序 类,并且必须覆盖CreateWindow 方法。像这样的东西
using Microsoft.Maui;
using Microsoft.Maui.Controls;
public partial class App : Application
{
protected override IWindow CreateWindow(IActivationState activationState)
{
return new Window(new MainPage());
}
}
了解更多关于 .NET MAUI 的信息 这里.
4.SDK工作负载
在 .NET 6预览版4 中 , 我们首次引入了SDK工作负载。这个功能允许用户只安装必要的SDK,而不是完整的 "一体化 "SDK。因此,如果你想只下载和安装ASP.NET Core,或只安装Xamarin,你可以这样做。从本质上讲,这个功能只是一个SDK的包管理器。所有这些都是通过新的CLI关键字-- 工作负载来支持的。
这个功能也是大事件的一部分。当 .NET核心 最初开发时,需要一个多合一的单体SDK。对于一些 .NET Core 2.x 和 .NET Core 3.x 的功能来说,情况尤其如此。然而,随着.NET SDK支持的工作负载的增加,出现了新的需求--对小型和集中的SDK的需求。

如前所述,这个功能是由CLI工作负载命令支持的。它通过 .NET 6预览版4、5和6 逐渐增长 。 首先,安装命令被引入。
4.1 .NET 6 SDK Workload CLI
该命令允许你安装任何SDK工作负载。
dotnet workload install name_of_the_workload
其他命令扩展了这种类似软件包管理器的体验。有一个命令,你可以列出所有已安装的工作负载。
dotnet workload list
至关重要的是,.NET源码构建功能应扩展到包括新的可选工作负载模型,以便社区能够为他们的方案构建所有的工作负载。事实上,我们希望有了这个模型,源码构建将变得更简单、更高效。
还有一个命令是将所有已安装的SDK工作负载更新到最新版本。
dotnet workload update
你也可以列出可供安装的工作负载。
dotnet workload search
当然,你可以卸载工作负载。
dotnet workload uninstall
如果在工作负载安装过程中出了问题(比如你的互联网连接坏了),你可以用修复它。
dotnet workload repair
最后,.NET 6 工作负载CLI提供了一种方法来检查SDK和Runtimes的新版本是否有新的命令。
dotnet sdk check
5.内循环性能
当有一个新版本的框架时,你有点期待性能会得到改善。.NET 的情况也是如此。该框架的前几个版本(也包括这个版本)给用该工具构建的应用程序带来了不同的性能改进。除此之外,.NET 6 也专注于改善内循环性能,即改善CLI、运行时、 MSBuild等 的性能 。 因此,重点放在优化工具和工作流程上,以改善开发者的体验。

我喜欢 .NET 和它的功能。它对所有事情都有好处吗?当然不是。然而,我确实喜欢用它工作,并建议将它作为许多解决方案的首选平台。这经常会受到其他开发者的反击,因为他们对它没有良好的体验。这些经验大多与这些内循环的性能有关。
许多其他平台的普通内循环操作的性能明显好于 .NET 。这是非常令人反感的,特别是对于刚接触 .NET 的人。这就是为什么我非常高兴 .NET 6 在这方面带来了改进。从 预览版2 开始,我们就有了很多关于一些改进的信息。
内循环的性能对于提供开发者的体验至关重要,而这些体验是开发者越来越期待/要求的。开发人员能以多快的速度进行代码修改并在他们的应用程序中看到由此产生的影响,与他们的工作效率成正比。
如果要涵盖 .NET 6 的所有新的改进,就需要写一篇全新的文章,所以我将只介绍少数几个。不过,这里是完整的改进列表:
- 提高与开发者内循环有关的CLI/SDK/MSBuild的性能。
- 改进并更新启动/调试/更新循环
在开发内循环中从.NET CLI热重载应用程序的变化。 - Blazor组件的热重新加载
- 用于ASP.NET Core的热重新加载
- 使用源码生成器重新实现Razor编译器并逐步引入
- Blazor WebAssembly热重启
支持代码热重启(EnC)的Razor - ASP.NET Core热重启
- Xamarin开发者在.NET 6上拥有更好的开发体验
- 确保BCL支持热重启
- 通过编译的模型减少EF Core应用程序的启动时间
5.1.NET 6热重启
Hot Reload是最大的内循环性能之一,肯定会提供更快的开发。这个想法很简单--允许在应用程序仍在运行时进行代码修改。这将极大地提高开发人员的生产力。从现在开始,你将不必进行修改,构建解决方案,再次运行 ,并返回到应用程序中的那个点,而那个点正是导致修改的原因。我从未想过我能在*.NET* 中看到这一天 。 这真是太棒了!
对于具有各种功能的.NET来说,支持不同的平台和不同类型的应用程序,等等,这是一个很大的特点。变化涉及到运行时(coreclr和mono)、C# Roslyn编译器、应用程序模型(例如Blazor、.NET MAUI)和开发者工具(例如CLI、Visual Studio)。
5.2更快的MSBuild
这项改进是在预览版2中提出的,它是CLI、SDK和MSBuild应该改进的更大史诗的一部分。目前,MSBuild得到了改进,更快的构建将对开发者的体验产生积极的影响。在下面的图片中,你可以看到与.NET 5相比,.NET 6的构建速度有多快 。

5.3尽量减少JITing
微软在这个版本中也专注于消除不必要的JITing。例如,由于一些新增加的SDK,一些程序集没有被预编译。安装程序负责这个工作。这就造成了较高的JIT时间。
6、.NET 6预览功能--窥视.NET 7
.NET 6将包含两个功能,它们将以预览形式出现。其目的是为了尽快从社区获得反馈,并在此基础上加以改进。预览功能是选择进入的,因为它预计会发生变化,很可能是以破坏性的方式。我认为这真的很酷,而且从现在起这将成为标准做法。根据微软的说法,他们启用了一个 "预览 "API机制,这将是一种情况。
目前,我们还不能真正尝试这些功能,因为它们不是当前.NET 6预览版7的一部分,不过,它们将在.NET RC1中提供。该配置将通过 RequiresPreviewFeatures 属性和相应的分析器来实现。 我决定在这篇文章中包括它们,因为它们看起来真的很有趣,而且以后可能会有更大的影响。所以,让我们看看它们都是什么。
强烈建议你尝试一下这个功能,如果有你觉得缺失的场景或功能,或者有其他方面可以改进,请提供反馈。
为了尝试它们,你应该像这样配置项目文件。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<LangVersion>preview</LangVersion>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
</ItemGroup>
</Project>
6.1 接口中的静态摘要
是的,我知道,当我看到上面的文字结构时,我的头也开始发痒。然而,这个想法很简单--声明静态抽象方法作为接口的一部分,并在派生类型中实现它们。
这个功能将不适用于其他抽象,比如抽象类, 你将无法通过接口访问这些方法。另外,因此静态和虚拟将不是一个有效的组合。

下面是代码中的样子:
public interface ISomeInterface<TSelf>
where TSelf : ISomeInterface<TSelf>
{
static abstract TSelf DoSomething(string input);
}
public readonly struct SomeImplementation : ISomeInterface<Guid>
{
public static Guid DoSomething(string input);
{
/* .NET 6 Implementation */
}
}
我的感觉是,这个功能可以说是一种 "剩余功能"。似乎它只是我们将要讨论的下一个预览功能--通用数学的前提条件。
6.2 通用数学
你是否遇到过这样的情况:你可以在通用类型的变量之间使用操作数,但这是不可能的?这是一个已经被.NET社区要求了很久的功能,就我个人而言,我有好几种情况,这可能是非常有用的。
这个功能基本上是通过创建新的静态抽象接口来实现的。这些接口是为语言中的不同操作者而建立的。这些接口是细化的,有可能对其进行扩展或修改。在这里,我使用了微软的代码,因为我认为它非常整洁,显示了通用数学的好处。这是一个使用通用数学的标准偏差的实现。
public static TResult Sum<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : INumber<TResult>
{
TResult result = TResult.Zero;
foreach (var value in values)
{
result += TResult.Create(value);
}
return result;
}
public static TResult Average<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : INumber<TResult>
{
TResult sum = Sum<T, TResult>(values);
return TResult.Create(sum) / TResult.Create(values.Count());
}
public static TResult StandardDeviation<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : IFloatingPoint<TResult>
{
TResult standardDeviation = TResult.Zero;
if (values.Any())
{
TResult average = Average<T, TResult>(values);
TResult sum = Sum<TResult, TResult>(values.Select((value) => {
var deviation = TResult.Create(value) - average;
return deviation * deviation;
}));
standardDeviation = TResult.Sqrt(sum / TResult.Create(values.Count() - 1));
}
return standardDeviation;
}
结论
在这篇文章中,我们介绍了6个有趣的.NET 6功能,我认为这些功能将改变我们进行日常开发的方式。我对所有这些功能都感到非常兴奋,并迫不及待地想在11月9日尝试它们的全部魅力。
谢谢你的阅读!