.Net8新特性

本文整理.Net8新特性的使用方法。
当前包括:

  • Route ShortCircuit
  • Exception Throw Helper
  • HttpLoggingMiddleware 的改进
  • C# 12 中的 InlineArray 特性

Route ShortCircuit

有些请求,如浏览器会自动请求 favicon.ico,这些请求即使很简单,往往也会完整地运行中间件管道,但实际上可能并不需要,在 .NET 8 中引入了一个 Route ShortCircuit 的功能,也就是路由短路,可以在处理结束之后马上中断请求,不再执行后面的中间件了,这样会使得这样的路由或者 API 更加高效。

在 route 后添加 ShortCircut() 来启用,使用方法如下:

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();
app.Use(async (HttpContext context, RequestDelegate next) =>
{
    context.Response.Headers["Value"] = "123";
    await next(context);
});
app.MapGet("/", () => "Hello .NET 8!");

// ShortCircuit
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
app.MapGet("/short-circuit-status", () => "Short circuiting!")
            .ShortCircuit(401);
// MapShortCircuit
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
app.MapShortCircuit(403, "admin");

await app.RunAsync();

Exception Throw Helper

在 .NET 6 中,引入了一个 ArgumentNullException.ThrowIfNull(object? argument, string? paramName = default) 的方法,在 .NET 7/8 中引入了更多的支持,我们可以在代码里使用这些 exception helper 来简化一些代码。

常用的 Argument exception:

ArgumentNullExceptionSample(null);

ArgumentExceptionThrowIfNullOrEmptySample(null);
ArgumentExceptionThrowIfNullOrEmptySample(string.Empty);

ArgumentExceptionThrowIfNullOrWhiteSpaceSample(null);
ArgumentExceptionThrowIfNullOrWhiteSpaceSample(string.Empty);
ArgumentExceptionThrowIfNullOrWhiteSpaceSample(" ");

public static void ArgumentNullExceptionSample(string? value)
{
    InvokeHelper.TryInvoke(() => ArgumentNullException.ThrowIfNull(value));
}

public static void ArgumentExceptionThrowIfNullOrEmptySample(string? value)
{
    InvokeHelper.TryInvoke(() => ArgumentException.ThrowIfNullOrEmpty(value));
}

public static void ArgumentExceptionThrowIfNullOrWhiteSpaceSample(string? value)
{
    InvokeHelper.TryInvoke(() => ArgumentException.ThrowIfNullOrWhiteSpace(value));
}

ArgumentNullException.ThrowIfNull(object? obj, string? paramName = default) 是 .NET 6 开始支持的

在 .NET 7 里支持了指针的判断 ArgumentNullException.ThrowIfNull(void* argument, string? paramName = default)(not CLS-compliant)

.NET 7 还引入了 ArgumentException.ThrowIfNullOrEmpty(string? value, string? paramName = default), 增加判断空字符串的场景

.NET 8 引入了 ArgumentException.ThrowIfNullOrWhiteSpace(string? value, string? paramName = default) 增强了判断空字符串的场景

HttpLoggingMiddleware 的改进

.NET 6 开始引入了一个 http logging 的中间件,我们可以借助于 http logging 的中间件记录请求和响应的信息,但是扩展性不是很强,在 .NET 8 版本中进行了一些优化,引入了一些新的配置和 HttpLoggingInterceptor 使得它更加容易扩展了

HttpLoggingFields 中新增了一个 Duration 枚举值,会记录请求处理的耗时

在 HttpLoggingOptions 中增加了一个 CombineLogs 的配置,默认是 false,默认 request/response/duration 的 log 都是分开的,配置为 true 之后就会合并成一条日志。

HttpLoggingInterceptor

.NET 8 还引入了 IHttpLoggingInterceptor,借助于此可以更好的扩展 http logging

可以根据 Request 或者 Response 信息来动态地调整要记录的 field 或者动态调整 RequestBodyLogLimit/ResponseBodyLogLimit

来看一个 HttpLoggingInterceptor 示例:

file sealed class MyHttpLoggingInterceptor: IHttpLoggingInterceptor
{
    public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext.Request.Path.Value?.StartsWith("/req-") == true)
        {
            logContext.LoggingFields = HttpLoggingFields.ResponsePropertiesAndHeaders;
            logContext.AddParameter("req-path", logContext.HttpContext.Request.Path.Value);
        }
        
        return ValueTask.CompletedTask;
    }

    public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext is { Response.StatusCode: >=200 and < 300, Request.Path.Value: "/hello" })
        {
            logContext.TryDisable(HttpLoggingFields.All);
        }
        return ValueTask.CompletedTask;
    }
}

使用示例如下,使用 AddHttpLoggingInterceptor() 来注册:

var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHttpLogging(options =>
{
    options.LoggingFields = HttpLoggingFields.All;
    options.CombineLogs = true;
});
builder.Services.AddHttpLoggingInterceptor<MyHttpLoggingInterceptor>();

var app = builder.Build();
app.UseHttpLogging();
app.MapGet("/hello", () => "Hello");
app.MapGet("/crash", () => Results.BadRequest());
app.MapGet("/req-intercept", () => "Hello .NET 8");
app.MapControllers();
await app.RunAsync();

可以看到每个请求的 log 输出的结果都有所不同,第一个请求虽然我们设置了 ogContext.TryDisable(HttpLoggingFields.All) 但是还是有输出结果这是因为 httpLogging 目前的实现就是这样,在 Response 里处理的时候 request 信息已经被记录好了,详细可以参考 http logging middleware 的 实现

如果想要完全 disable 需要在 OnRequestAsync 方法里处理

public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
{
    if ("/no-log".Equals(logContext.HttpContext.Request.Path.Value, StringComparison.OrdinalIgnoreCase))
    {
        logContext.LoggingFields = HttpLoggingFields.None;
    }
    //
    return ValueTask.CompletedTask;
}

这样请求就不会有日志打印了

最后一个 req-intercept 在 request 的处理中设置了 ResponsePropertiesAndHeaders 并且加了一个自定义的 Parameter 从输出结果可以看到有输出到日志

More
大家可以自己尝试一下,比之前会好用一些,但是觉得还是有所欠缺

比如日志级别目前还都是 Information 不能动态的改变日志级别

另外就是前面提到的即使使用 CombineLogs 在 response 中设置为 HttpLoggingFields.None 时,依然会记录 request 信息,希望后面还会继续优化一下

C# 12 中的 InlineArray 特性

C# 12 引入了一个 InlineArray 特性,利用这一特性,我们可以更方便地类数组的结构体,可以代替原来要使用非安全代码的 fixed size buffer

[System.Runtime.CompilerServices.InlineArray(10)]
file struct MyArray
{
    // required
    private int _element;
}

使用 InlineArray 需要指定 size,也就是 array 的长度,并且我们需要声明一个字段

使用示例如下:

var arr = new MyArray();
for (var i = 0; i < 10; i++)
{
    arr[i] = i;
}
foreach (var i in arr)
{
    Console.Write(i);
    Console.Write(",");
}

Console.WriteLine();

ReadOnlySpan<int> span = arr;
foreach (var i in span)
{
    Console.Write(i);
    Console.Write(",");
}

Console.WriteLine();

foreach (var i in arr[^2..])
{
    Console.Write(i);
    Console.Write(",");
}
Console.WriteLine();

Console.WriteLine(arr[^1]);

// error CS0021: Cannot apply indexing with [] to an expression of type 'MyArray'
// if (arr is [0,1,..])
//     Console.WriteLine("StartsWith 0, 1");

if (span is [0,1,..])
    Console.WriteLine("StartsWith 0, 1");

// error CS9174: Cannot initialize type 'MyArray' with a collection expression because the type is not constructible.
// arr = [1, 2, 3, 4, 5];

span = [1,2,3,4,5];
foreach (var item in span)
{
    Console.Write(item);
    Console.Write(",");
}

运行结果:

0,1,2,3,4,5,6,7,8,9,
0,1,2,3,4,5,6,7,8,9,
8,9,
9
StartsWith 0, 1
1,2,3,4,5,

从这个示例可以看得出来,我们可以像使用数组一样使用,同时我们可以直接隐式转换成 Span 和 ReadOnlySpan,
并且可以使用 Index 和 Range 操作符,但是目前暂时不能直接使用集合表达式和 list pattern,但是我们可以转成 span 之后再使用
文章来源

posted @ 2023-11-25 16:01  cdboy  阅读(219)  评论(0编辑  收藏  举报