今天我们发布了.NET 7预览版6。这个.NET 7预览版包括对类型转换器的改进、JSON合约的定制、System.Formats.Tar API的更新、对.NET模板创作的约束以及CodeGen领域的性能提升。
你可以下载.NET 7预览6,适用于Windows、macOS和Linux:
.NET 7预览版6已经用Visual Studio 17.3 Preview 3进行了测试。如果你想用Visual Studio家族产品尝试.NET 7,我们建议你使用预览通道构建。如果你是在macOS上,我们建议使用最新的Visual Studio 2022 for Mac预览版。现在,让我们进入这个版本中的一些最新更新。
类型转换器
对于新增加的原始类型DateOnly,TimeOnly,Int128,UInt128, 和Half ,现在有暴露的类型转换器:
namespace System.ComponentModel
{
public class DateOnlyConverter : System.ComponentModel.TypeConverter
{
public DateOnlyConverter() { }
}
public class TimeOnlyConverter : System.ComponentModel.TypeConverter
{
public TimeOnlyConverter() { }
}
public class Int128Converter : System.ComponentModel.BaseNumberConverter
{
public Int128Converter() { }
}
public class UInt128Converter : System.ComponentModel.BaseNumberConverter
{
public UInt128Converter() { }
}
public class HalfConverter : System.ComponentModel.BaseNumberConverter
{
public HalfConverter() { }
}
}
使用实例
TypeConverter dateOnlyConverter = TypeDescriptor.GetConverter(typeof(DateOnly));
// produce DateOnly value of DateOnly(1940, 10, 9)
DateOnly? date = dateOnlyConverter.ConvertFromString("1940-10-09") as DateOnly?;
TypeConverter timeOnlyConverter = TypeDescriptor.GetConverter(typeof(TimeOnly));
// produce TimeOnly value of TimeOnly(20, 30, 50)
TimeOnly? time = timeOnlyConverter.ConvertFromString("20:30:50") as TimeOnly?;
TypeConverter halfConverter = TypeDescriptor.GetConverter(typeof(Half));
// produce Half value of -1.2
Half? half = halfConverter.ConvertFromString(((Half)(-1.2)).ToString()) as Half?;
TypeConverter Int128Converter = TypeDescriptor.GetConverter(typeof(Int128));
// produce Int128 value of Int128.MaxValue which equal 170141183460469231731687303715884105727
Int128? int128 = Int128Converter.ConvertFromString("170141183460469231731687303715884105727") as Int128?;
TypeConverter UInt128Converter = TypeDescriptor.GetConverter(typeof(UInt128));
// produce UInt128 value of UInt128.MaxValue which equal 340282366920938463463374607431768211455
UInt128? uint128 = UInt128Converter.ConvertFromString("340282366920938463463374607431768211455") as UInt128?;
JSON合约定制
在某些情况下,对JSON进行序列化或反序列化的开发者发现他们不想或不能改变类型,因为它们要么来自外部库,要么会极大地污染代码,但需要做一些影响序列化的改变,如删除属性、改变数字的序列化方式、对象的创建方式等等。他们经常被迫编写包装器或自定义转换器,这不仅是一个麻烦,而且会使序列化变得更慢。
JSON合约的定制允许用户对哪些类型被序列化或反序列化进行更多控制。
选择自定义
开发者有两种基本的方式可以 "插入 "定制,它们最终都会分配给JsonSerializerOptions.TypeInfoResolver ,并需要分配给解析器。
- 开发者可以使用
DefaultJsonTypeInfoResolver并添加他们的修改器,所有的修改器将被串行调用。
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers =
{
(JsonTypeInfo jsonTypeInfo) =>
{
// your modifications here, i.e.:
if (jsonTypeInfo.Type == typeof(int))
{
jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
}
}
}
}
};
Point point = JsonSerializer.Deserialize<Point>(@"{""X"":""12"",""Y"":""3""}", options);
Console.WriteLine($"({point.X},{point.Y})"); // (12,3)
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
- 通过实现
System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver编写自己的自定义解析器。 - 当类型未被处理时,代码应返回
null。 IJsonTypeInfoResolver可以与其他解析器结合起来,形成有效的解析器,它将返回第一个非空的答案。例如, 。JsonTypeInfoResolver.Combine(new MyResolver(), new DefaultJsonTypeInfoResolver())
自定义
IJsonTypeInfoResolver 工作是为任何 序列器请求提供 - 这在每个选项的每个类型中只发生一次。 将决定开发者可以改变哪些旋钮,并根据转换器来决定,而转换器是根据提供给选项的转换器来决定的。例如 意味着 可以被添加/修改,而 意味着没有一个旋钮被保证使用 - 这在类型有自定义转换器时可能发生。Type JsonTypeInfo``JsonTypeInfo.Kind JsonTypeInfoKind.Object Properties JsonTypeInfoKind.None
JsonTypeInfo DefaultJsonTypeInfoResolver JsonTypeInfo.CreateJsonTypeInfo - 从头开始创建意味着用户还需要设置 。JsonTypeInfo.CreateObject
定制属性
属性只有在JsonTypeInfo.Kind == JsonTypeInfoKind.Object ,在DefaultJsonTypeInfoResolver 的情况下才会被预先填充。它们可以通过使用JsonTypeInfo.CreateJsonPropertyInfo 来修改或创建,并添加到属性列表中,也就是说,假设你从单独的库中得到一个类,它有奇怪的设计API,你不能改变:
class MyClass
{
private string _name = string.Empty;
public string LastName { get; set; }
public string GetName() => _name;
public void SetName(string name)
{
_name = name;
}
}
在这个功能存在之前,你需要包装你的类型层次,或者为该类型创建你自己的自定义转换器。现在你可以简单地修复它:
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers = { ModifyTypeInfo }
}
};
MyClass obj = new()
{
LastName = "Doe"
};
obj.SetName("John");
string serialized = JsonSerializer.Serialize(obj, options); // {"LastName":"Doe","Name":"John"}
static void ModifyTypeInfo(JsonTypeInfo ti)
{
if (ti.Type != typeof(MyClass))
return;
JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), "Name");
property.Get = (obj) =>
{
MyClass myClass = (MyClass)obj;
return myClass.GetName();
};
property.Set = (obj, val) =>
{
MyClass myClass = (MyClass)obj;
string value = (string)val;
myClass.SetName(value);
};
ti.Properties.Add(property);
}
属性的有条件序列化
在一些使用场景中,需要对一些默认值不进行序列化。也就是说,你不希望0 ,在JSON中显示出某些属性。以前可以通过使用JsonIgnoreAttribute和JsonIgnoreCondition.WhenWritingDefault 。当你的默认值不是0 ,而是不同的东西,即-1 ,或者它取决于一些外部设置时,问题就出现了。
现在可以用你喜欢的任何条件来设置你自己的谓词ShouldSerialize 。例如,你有string 属性,你想让N/A 不在JSON中显示出来。
// string property you'd like to customize
JsonPropertyInfo property = ...;
property.ShouldSerialize = (obj, val) =>
{
// in this specific example we don't use parent but it's available if needed
MyClass parentObj = (MyClass)obj;
string value = (string)val;
return value != "N/A";
};
实例:忽略具有特定名称或类型的属性
var modifier = new IgnorePropertiesWithNameOrType();
modifier.IgnorePropertyWithType(typeof(SecretHolder));
modifier.IgnorePropertyWithName("IrrelevantDetail");
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers = { modifier.ModifyTypeInfo }
}
};
ExampleClass obj = new()
{
Name = "Test",
Secret = new SecretHolder() { Value = "MySecret" },
IrrelevantDetail = 15,
};
string output = JsonSerializer.Serialize(obj, options); // {"Name":"Test"}
class ExampleClass
{
public string Name { get; set; }
public SecretHolder Secret { get; set; }
public int IrrelevantDetail { get; set; }
}
class SecretHolder
{
public string Value { get; set; }
}
class IgnorePropertiesWithNameOrType
{
private List<Type> _ignoredTypes = new List<Type>();
private List<string> _ignoredNames = new List<string>();
public void IgnorePropertyWithType(Type type)
{
_ignoredTypes.Add(type);
}
public void IgnorePropertyWithName(string name)
{
_ignoredNames.Add(name);
}
public void ModifyTypeInfo(JsonTypeInfo ti)
{
JsonPropertyInfo[] props = ti.Properties.Where((pi) => !_ignoredTypes.Contains(pi.PropertyType) && !_ignoredNames.Contains(pi.Name)).ToArray();
ti.Properties.Clear();
foreach (var pi in props)
{
ti.Properties.Add(pi);
}
}
}
System.Formats.Tar API更新
在预览4中,引入了System.Formats.Tar 集合。它提供了操作TAR档案的API。
在Preview 6中,为了涵盖一些特殊情况,做了一些改变。
全局扩展属性专用类
最初的设计是假设只有PAX TAR档案可以在第一个位置包含一个单一的全局扩展属性(GEA)条目,但人们发现TAR档案可以包含多个GEA条目,这可以影响所有后续条目,直到遇到一个新的GEA条目或档案的结束。
我们还发现,GEA条目不应该只出现在只包含PAX条目的档案中:它们可以出现在不同格式的条目混合的档案中。因此增加了一个新的类别来描述GEA条目。
+ public sealed partial class PaxGlobalExtendedAttributesTarEntry : PosixTarEntry
+ {
+ public PaxGlobalExtendedAttributesTarEntry(IEnumerable<KeyValuePair<string, string>> globalExtendedAttributes) { }
+ public IReadOnlyDictionary<string, string> GlobalExtendedAttributes { get { throw null; } }
+ }
条目格式,而不是档案格式
由于发现不同格式的条目可以在一个TAR档案中混合使用,TarFormat 枚举被重新命名为TarEntryFormat :
-public enum TarFormat
+public enum TarEntryFormat
{
...
}
并为TarEntry 添加了一个新的属性,以显示条目的格式:
public abstract partial class TarEntry
{
...
+ public TarEntryFormat Format { get { throw null; } }
...
}
写入和读取的变化
Format 属性已从TarReader 中删除,因为没有任何存档会以单一格式显示其所有条目。
由于GEA条目现在被用他们自己的专业类来描述,而且这种类型的多个条目可以在一个档案中找到,所以TarReader 中的字典属性也被删除:
public sealed partial class TarReader : IDisposable
{
...
- public TarFormat Format { get { throw null; } }
- public IReadOnlyDictionary<string, string>? GlobalExtendedAttributes { get { throw null; } }
...
}
增加专门的GEA类也影响了TarWriter 。
- 过去用于获取单个第一位置GEA条目的字典的构造函数被删除。
- 增加了一个新的构造函数,它只接受流和
leaveOpen布尔值。 - 保留了获取
TarFormat的构造函数,但重命名了枚举,并将默认值设置为Pax。该方法的文档被修改,以解释指定的格式参数只适用于从文件中添加条目的TarWriter.WriteEntry方法。
public sealed partial class TarWriter : IDisposable
{
...
- public TarWriter(Stream archiveStream, IEnumerable<KeyValuePair<string, string>>? globalExtendedAttributes = null, bool leaveOpen = false) { }
+ public TarWriter(Stream archiveStream, bool leaveOpen = false) { }
- public TarWriter(Stream archiveStream, TarFormat archiveFormat, bool leaveOpen = false) { }
+ public TarWriter(Stream archiveStream, TarEntryFormat format = TarEntryFormat.Pax, bool leaveOpen = false) { }
public void WriteEntry(string fileName, string? entryName) { }
...
}
模板创作
约束条件
预览6为.NET模板引入了约束的概念。约束允许你定义允许你的模板的上下文 - 这可以帮助模板引擎确定它应该在dotnet new list 等命令中显示哪些模板。在这个版本中,我们增加了对三种约束的支持:
- 操作系统 - 根据用户的操作系统限制模板的使用
- 模板引擎主机 - 根据执行模板引擎的主机来限制模板 - 这通常是指.NET CLI本身,或者像Visual Studio/Visual Studio for Mac中的新项目对话框这样的嵌入式方案。
- 已安装的工作负载--要求在模板可用之前,指定的.NET SDK工作负载已经安装。
在所有情况下,描述这些约束条件就像在你的模板配置文件中添加一个新的constraints 部分一样简单:
"constraints": {
"web-assembly": {
"type": "workload",
"args": "wasm-tools"
},
}
这些模板可以被命名,在通知用户为什么不能调用你的模板时,我们会使用这个名字。
目前,这些约束条件在.NET CLI中得到了支持,我们正在与Visual Studio团队的合作伙伴合作,将它们纳入你已经知道的项目和项目创建体验中。
我们希望这个功能将为SDK的用户带来更一致的体验,无论他们选择什么样的编辑器,都能更容易地指导用户使用必要的模板前提条件,同时也能帮助我们为常见的场景(如dotnet new list )简化模板列表。在未来的.NET 7预览版中,我们计划增加对基于常见的MSBuild属性的约束的支持。
更多的例子请参见约束条件的文档,关于新类型的约束条件的讨论请加入模板引擎仓库的讨论。
多选参数
预览6还为choice 参数增加了一个新的能力--用户可以在一次选择中指定一个以上的值。这可以用与Flags 风格的枚举相同的方式来使用。这种类型的参数的常见例子可能是:
- 在
web模板上选择多种形式的认证 - 在
maui模板中一次选择多个目标平台(ios、android、web)。
选择这种行为就像在你的模板配置中的参数定义中添加"allowMultipleValues": true 一样简单。一旦你这样做,你将获得一些辅助函数,在你的模板内容中使用,以帮助检测用户选择的特定值。
关于该功能的完整解释,请参见多选参数的文档。
退出代码的统一和报告
预览6还统一了由模板引擎报告的退出代码。这应该可以帮助那些在他们选择的shell中依赖脚本的用户有一个更一致的错误处理经验。此外,由.NET CLI报告的错误现在包括一个链接,以找到有关每个退出代码的详细信息:
➜ dotnet new unknown-template
No templates found matching: 'unknown-template'.
To list installed templates, run:
dotnet new list
To search for the templates on NuGet.org, run:
dotnet new search unknown-template
For details on the exit code, refer to https://aka.ms/templating-exit-codes#103
动态PGO
- github.com/dotnet/runt…中增加了对委托调用的防护性去虚拟化的支持。当动态PGO被启用时,它允许JIT在确定可能有利可图时对委托调用进行专门化和内联。这可以极大地提高性能,下面的微测试证明了这一点,动态PGO现在比没有PGO的情况下快了大约5倍(之前大约是2.5倍)。
目前只支持与实例方法绑定的委托。我们期望对静态方法的支持将在.NET 8的早期预览中出现。
public class Benchmark
{
private readonly long[] _nums;
public Benchmark()
{
_nums = Enumerable.Range(0, 100000).Select(i => (long)i).ToArray();
}
[Benchmark]
public long Sum() => _nums.Sum(l => l * l);
}
| 方法 | 工作 | 工具链 | 意思是 | 误差 | StdDev | 比值 |
|---|---|---|---|---|---|---|
| 总数 | 工作-QWNDLL | \nopgo\corerun.exe | 406.65 us | 0.718 us | 0.560 us | 1.00 |
| 总数 | 工作-PNPEDU | \Ttieredpgo_no_delegate_gdv\corerun.exe | 172.77 us | 0.819 us | 0.766 us | 0.42 |
| 总数 | 工作-KFFWQK | \ティンバーション_ドライト_グループラン.exe | 91.38 us | 0.263 us | 0.219 us | 0.22 |
- 我们开始实施冷热分割,github.com/dotnet/runt…,这是它的第一部分。
Arm64
- github.com/dotnet/runt…在Windows Arm64中启用了LSE原子学。它将锁相关操作的性能提高了78%。

- github.com/dotnet/runt…在Arm64上启用了gc类型的寻址模式,获得了高达45%的性能。
- github.com/dotnet/runt…为16字节的SIMD16对齐arm64数据部分。
- github.com/dotnet/runt…优化了i % 2,使吞吐量提高17%。
循环优化
- 由类型测试驱动的循环克隆:https://github.com/dotnet/runtime/pull/70377,使循环克隆基于循环不变的类型测试,如GDV增加的那些。这有效地允许快速路径循环将类型检查从循环中提升出来,从而提高性能。比如说:

- 在github.com/dotnet/runt…,开始将不变量从多层嵌套循环中吊出。

一般优化
- PRgithub.com/dotnet/runt…改进了JIT中对矢量常量的处理,包括对值编号的支持、常量传播以及其他已经可用于其他常量的优化。
以.NET 7为目标:
要以.NET 7为目标,你需要在你的项目文件中使用一个.NET 7目标框架名称(TFM)。比如说:
<TargetFramework>net7.0</TargetFramework>
全套的.NET 7 TFMs,包括具体操作的TFMs如下:
net7.0net7.0-androidnet7.0-iosnet7.0-maccatalystnet7.0-macosnet7.0-tvosnet7.0-windows
我们希望从.NET 6升级到.NET 7应该是很简单的。请报告您在用.NET 7测试现有应用程序过程中发现的任何破坏性变化。
支持
.NET 7是一个**短期支持(STS)**版本,这意味着它将在发布之日起的18个月内获得免费支持和补丁。值得注意的是,所有版本的质量都是一样的。唯一的区别是支持的长度。关于.NET支持政策的更多信息,请参阅.NET和.NET Core官方支持政策。
我们最近最近将 "当前 "的名称改为 "短期支持(STS)"。我们正在推出这一变化。
突破性变化
你可以通过阅读《.NET 7中的突破性变化》文件,找到最新的.NET 7突破性变化列表。它按领域和版本列出了突破性变化,并附有详细解释的链接。
要想知道哪些破坏性变化是被提议的,但仍在审查中,请关注提议的.NET破坏性变化GitHub问题。
路线图
.NET的发布包括产品、库、运行时间和工具,并代表了微软内部和外部多个团队的合作。你可以通过阅读产品路线图了解这些领域的更多信息:
最后
我们赞赏并感谢您对.NET的支持和贡献。请试一试.NET 7预览版6,并告诉我们你的想法!