.NET6、C#10到.NET8、C#12

522 阅读6分钟

个人总结,内容相较于官方文档有较大程度删减,不适合学习使用

C#

C#11

泛型属性

泛型可用于属性

public class GenericAttribute<T> : Attribute { }

有点奇怪,反编译成7版本,好像没啥变化?

image.png

原始字符串文本

以至少三个"""开始、结束,相当于@$,接受换行、{}嵌入参数

反编译

image.png

结构自动初始化

以往版本,要求在值类型构造函数中必须初始化所有成员变量,现在不会要求显式初始化所有变量,没有显式初始化的会被设为默认值

image.png

image.png

required

将required修饰符添加到字段或属性,表示必须在构造函数中对其进行初始化

反编译结果 image.png

ref 字段和 ref scoped 变量

从效果来看,类似于指针 ref字段和ref scoped变量 | Easy-DotNET

ref struct

可以在结构类型的声明中使用 ref 修饰符。 ref struct 类型的实例是在堆栈上分配的,不能转义到托管堆。 为了确保这一点,编译器将 ref struct 类型的使用限制如下:

  • ref struct 不能是数组的元素类型。
  • ref struct 不能是类或非 ref struct 的字段的声明类型。
  • ref struct 不能实现接口。
  • ref struct 不能被装箱为 System.ValueType 或 System.Object
  • ref struct 不能是类型参数。
  • ref struct 变量不能由 Lambda 表达式本地函数捕获。
  • ref struct 变量不能在 async 方法中使用。 但是,可以在同步方法中使用 ref struct 变量,例如,在返回 Task 或 Task<TResult> 的方法中。
  • ref struct 变量不能在迭代器中使用。

可以定义一次性的 ref struct。 为此,请确保 ref struct 符合一次性模式。 也就是说,它有一个实例 Dispose 方法,该方法是可访问、无参数的并且具有 void 返回类型。 可以将 using 语句或声明与可释放的 ref struct 的实例一起使用。 行内容(包括插值的开始位置)必须以与最后一行相同的空格开头

行内容(包括插值的开始位置)必须以与最后一行相同的空格开头

var x = $"""
    Hello
{1 + 1}    这行是违法的,
    World
    """;
var x = $""" 
    Hello 
    {1 + 1}  正确
    World 
    """;    

params 在委托类型转换的问题

早期版本中

void Method(int i = 0, params int[] xs) { } 
var action = Method;//var推断为 System.Action<int, int[]> 

但在.NET SDK 7.0.200 或更高版本中,此类方法被推断为匿名合成委托类型 具有相同的默认参数值和修饰符。

void Method(int i = 0, params int[] xs) { } 
var action = Method;  // delegate void <anonymous delegate>(int arg1 = 0, params int[] arg2)

推断得到的类型不一致

局部变量必须在当前线程中赋值才能被使用

以前可以,现在不行(不见兔子不撒鹰)

public async Task M()
    {
        bool a;
        await M1();
        Console.WriteLine(a); //现在不行,报错 error CS0165: Use of unassigned local variable 'a'  

        async Task M1()
        {
            if ("" == String.Empty)
            {
                throw new Exception();
            }
            else
            {
                a = true;
            }
        }
    }

无法通过引用返回 out 参数

对于语言版本 C# 11 或更高版本,或者 .NET 7.0 或更高版本,无法通过引用返回参数。out

static ref T ReturnOutParamByRef<T>(out T t)
{
    t = default;
    return ref t; // error CS8166: Cannot return a parameter by reference 't' because it is not a ref parameter
}    

上下文关键字 var 不能用作显式 lambda 返回类型。

C#12

主构造函数

public class NamedItem(string parama,string paramb,String paramc)
{
}

相当于

public class NamedItem  
{  
    public NamedItem(string parama, string paramb, string paramc)  
    {  
    }  
}

此外,任何显式编写的构造函数都必须使用 this(...) 初始化表达式语法来调用主构造函数。 这可确保主构造函数参数绝对由所有构造函数分配。

如果对记录类型使用主构造函数,会自动生成字段

image.png

集合表达式

// Create an array:
int[] a = [1, 2, 3, 4, 5, 6, 7, 8];

// Create a list:
List<string> b = ["one", "two", "three"];

// Create a span
Span<char> c  = ['a', 'b', 'c', 'd', 'e', 'f', 'h', 'i'];

// Create a jagged 2D array:
int[][] twoD = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

// Create a jagged 2D array from variables:
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[][] twoDFromVariables = [row0, row1, row2];

int[] single = [.. row0, .. row1, .. row2]; 
foreach (var element in single) 
{ 
    Console.Write($"{element}, "); 
} 
// output: 
// 1, 2, 3, 4, 5, 6, 7, 8, 9,

ref readonly

不太能理解,并且这不应该作为一个新功能而是应该作为建议出现

labmda 中的参数列表可以像方法一样设置默认参数

var IncrementBy = (int source, int increment = 1) => source + increment;

Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7

可以使用using为元组、数组等类型设置别名

太强了

using Point = (int x, int y);

内联数组

简单来说,预先设好一个长度固定的值数组类型,如果不是对性能有极高的要求好像没必要使用

[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
    private int _element0;
}
var buffer = new Buffer();
for (int i = 0; i < 10; i++)
{
    buffer[i] = i;
}

foreach (var i in buffer)
{
    Console.WriteLine(i);
}

ref参数可以传递给参数in

早期版本不可以,但现在支持,注意因此变动,导致的调用多态方法可能发生的变动

var i = 5;
System.Console.Write(new C().M(ref i)); // prints "E" in C# 11, but "C" in C# 12
System.Console.Write(E.M(new C(), ref i)); // workaround: prints "E" always

class C
{
    public string M(in int i) => "C";
}
static class E
{
    public static string M(this C c, ref int i) => "E";
}
var i = 5;
System.Console.Write(C.M(null, ref i)); // prints "1" in C# 11, but fails with an ambiguity error in C# 12
System.Console.Write(C.M((I1)null, ref i)); // workaround: prints "1" always

interface I1 { }
interface I2 { }
static class C
{
    public static string M(I1 o, ref int x) => "1";
    public static string M(I2 o, in int x) => "2";
}

.NET

.NET7

ASP.NET Core 7.0

速率限制中间件

ASP.NET Core 中的速率限制中间件 | Microsoft Learn
顾名思义,限制某个路径上请求数量,超速会阻塞而不是返回错误信息

身份验证

现在如果只注册了单个身份验证方案,该方案会自动用作 DefaultScheme,而无需指定。

DI范围扩大

当类型被配置为服务时,不只是构造函数,API控制器中方法的参数也默认通过DI注入实例,相当于标注了FromServices
若要禁用,在配置中写入

builder.Services.Configure<ApiBehaviorOptions>(options => 
{ 
    options.DisableImplicitFromServicesParameters = true; 
});

验证错误中的 JSON 属性名

默认情况下,当发生验证错误时,模型验证会生成一个 ModelStateDictionary,其中属性名用作错误键。 某些应用(例如单页应用)受益于使用 JSON 属性名来处理 Web API 生成的验证错误。

builder.Services.AddControllers(options => { 
    options.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider()); 
});

不懂有什么作用,测试了几次,开了提示信息中的属性名都被转换为小写,不开都被转换为大写,没其他区别???

最小 API 筛选器应用

最小 API 筛选器应用 | Microsoft Learn
个人认为,功能和行为筛选器功能重复了,甚至和中间件一个粒度,和中间件相比唯一特殊的点在保证在终结点方法前后执行

支持将查询字符串绑定到基元类型、字符串数组和 StringValues 数组

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

将请求正文绑定为 Stream 或 PipeReader

todo

IResult改为共用类型

在.NET 7中,实现IResult的接口或类型是公共的,这意味着它可以从其他程序集中被引用和使用。这使得其他开发者或测试框架可以轻松地利用IResult来扩展或测试其功能
这里不是太理解这个单元测试的含义

运行时检测ITesult类型

HttpResult 接口提供了六个接口,对应五种返回值类型,用于通过模式在运行时检测IResult类型

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

使用参数调用 WithOpenApi

首先下载Microsoft.AspNetCore.OpenApiNuGet包,WithOpenApi 方法接受可用于修改 OpenAPI 注释的函数。

app.MapPost("/todoitems/{id}", async (int id) =>
{
    return "";
})
.WithOpenApi(generatedOperation =>
{
    var parameter = generatedOperation.Parameters[0];
    parameter.Description = "The ID associated with the created Todo";
    return generatedOperation;
});

image.png

文件上传

最小 API 现在支持使用 IFormFile 和 IFormFileCollection 上传文件。 以下代码使用 IFormFile 和 IFormFileCollection 上传文件:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

AsParametersAttribute

AsParameters应用示例
将多个参数统合到一个对象中,简化参数列表

路由组

用于为一组route统一配置前缀、EndPointFilter等组件

app.MapGroup("/public/todos") 
.MapTodosApi() 
.WithTags("Public")
.AddEndpointFilterFactory(QueryPrivateTodos) 
.RequireAuthorization();
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

Json序列化

1). 可自定义json协定,比较复杂
自定义序列化和反序列化协定 - .NET | Microsoft Learn
What's new in System.Text.Json in .NET 7 - .NET Blog (microsoft.com)

2). 用户定义类型层次结构的多态序列化
[JsonDerivedType(typeof(calss))]标记的类,在序列化时会尝试序列化为目标class,同时指定多个的情况下,从子类向父类遍历,逐个尝试序列化

[JsonDerivedType(typeof(WeatherForecastBase), typeDiscriminator: "base")]序列化后加一个值为base的$type字段,在反序列化时如果包含这个字段,会尝试反序列化为目标class

若要处理未知派生类型,必须使用基类型上的注释[JsonPolymorphic( UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]来加入此类支持,否则将会导致运行异常。 该枚举类提供三个枚举值:

  1. 失败 FailSerialization
  2. 回退到基本类型 FallBackToBaseType
  3. 回退到最近的被支持的父类 FallBackToNearestAncestor

3). [JsonRequired]属性标注的属性序列化时不能为空

中断性更新

以 ASPNET 为前缀的环境变量优先级

ASPNET_ 为前缀的环境变量和以 DOTNET_ 为前缀的环境变量优先级做了调整。读取 WebApplicationBuilder 的默认主机配置时,命令行参数和以 DOTNET_ 为前缀的环境变量将替代以 ASPNET_ 为前缀的环境变量。

.NET8

ASP.NET Core8

自定义区域性格式

app.UseRequestLocalization(options =>
    {
        options.CultureInfoUseUserOverride = false;
    });

当 CultureInfoUseUserOverride 设置为 true 时,ASP.NET Core 应用程序会考虑用户的区域性设置来格式化日期、时间和数字。这允许应用程序根据用户的区域性偏好(例如美国或法国)来显示日期、时间或数字。

反之,如果设置为 false(默认值),则应用程序将使用应用程序的默认区域性设置来格式化这些值。 示例

public void ConfigureServices(IServiceCollection services) 
{ 
    services.AddLocalization(options => options.ResourcesPath = "Resources"); 
    services.AddRequestLocalization(options => { options.DefaultRequestCulture = new RequestCulture("en-US"); // 默认文化设置为美国英语 
    options.CultureInfoUseUserOverride = true; // 启用用户覆盖 
}); 
// 其他配置... 
}
public class MyController : ControllerBase
{
    private readonly IStringLocalizer<MyController> _localizer;

    public MyController(IStringLocalizer<MyController> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Index()
    {
        var formattedDate = DateTime.Now.ToString("d"); // 格式化日期为短日期格式(例如:"09/13/2023")  
        var dateString = _localizer["The current date is {0}.", formattedDate]; // 这里将根据用户的区域性设置显示日期格式(例如:美国英语格式为 "The current date is September 13, 2023.")  
        return View(dateString);
    }
}

表单参数绑定

1).现在支持使用 [FromForm] 属性显式绑定到表单值。 通过 [FromForm] 绑定到请求的参数包括防伪令牌。 处理请求时会验证防伪令牌。
没看懂,和前面区别不大?????

2).支持使用 IFormCollectionIFormFile 和 IFormFileCollection 类型推断绑定到表单。 OpenAPI 元数据针对表单参数进行推断,以支持与 Swagger UI 集成。
最小 API 应用程序中的参数绑定 | Microsoft Learn

反伪令牌的中间件

此版本添加了用于验证反伪令牌的中间件,用于缓解跨站点请求伪造攻击。 调用 AddAntiforgery 以在 DI 中注册防伪服务。 在 DI 容器中注册防伪服务时,WebApplicationBuilder 会自动添加防伪中间件。 防伪令牌用于减少跨站点请求伪造攻击

ASP.NET Core 8.0 的新增功能 | Microsoft Learn

builder.Services.AddAntiforgery();

优化ObjectPool

在ObjectPool中新建了IResttable接口,提供可重用类型返回对象池时,自动重置状态功能 通过 ASP.NET Core 中的 ObjectPool 重用对象 | Microsoft Learn

支持自定义IOC的建

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

IExceptionHander

IExceptionHandler is a new interface that gives the developer a callback for handling known exceptions in a central location.

和异常过滤器功能上有些重合 但粒度上
exceptionHander是一个中间件,粒度是整个请求
异常过滤器是最后一个中间件的组件,粒度是终结点方法

IExceptionHandler是在请求处理管道中处理异常的中间件组件,而异常Filter是在Action执行过程中处理异常的组件。两者都可以用于处理异常,但它们的执行时机和方式有所不同。

好像区别也不太大?为什么不能都用IEctptionHander,异常过滤器既不是方法级aop,又不是功能强大的全局异常处理器,多少有点不伦不类

短路

app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();

只要与这个map匹配,配置在其前后的中缉拿缉拿都不会被执行,直接执行该map

序列化

处理缺失的成员

提供了[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]属性,skip表示跳过,disallow表示引发异常

接口层次序列化

没啥说的,当一个新功能看,本该如此的感觉
.NET 8 的新增功能 | Microsoft Learn

自定义格式策略

.NET 8 的新增功能 | Microsoft Learn

序列化只读属性或字段

.NET 8 的新增功能 | Microsoft Learn

也可以使用 JsonIncludeAttribute 和 JsonConstructorAttribute 特性注释将非公共成员加入到给定类型的序列化协定中。