.NET 6候选版本2中ASP.NET Core更新教程

132 阅读5分钟

.NET 6候选发布版2(RC2)现在已经推出。.NET 6 RC2非常接近于.NET 6的最终版本,我们预计将在今年11月为2021年的.NET会议及时发货。这也是一个 "上线 "版本,所以欢迎你在生产中使用它。.NET 6 RC2主要包含质量改进和错误修复,尽管它也包括一些新功能。

以下是这个预览版中的新功能:

  • 对Blazor WebAssembly应用程序的本地依赖性支持
  • 极少的API更新

开始使用

要开始使用.NET 6 RC2中的ASP.NET Core,请安装.NET 6 SDK

如果你在Windows上使用Visual Studio,请安装Visual Studio 2022的最新预览版,其中包括.NET 6 RC2。Mac用户应安装Visual Studio for Mac 2022的最新预览版

升级一个现有的项目

要将现有的ASP.NET Core应用程序从.NET 6 Preview RC1升级到.NET 6 RC2:

  • 将所有Microsoft.AspNetCore.*包的引用更新为6.0.0-rc.2.*
  • 将所有Microsoft.Extensions.*包的引用更新到6.0.0-rc.2.*

请参阅ASP.NET Core for .NET 6中的全部中断变化列表。

对Blazor WebAssembly应用程序的本地依赖性支持

.NET 6中的Blazor WebAssembly应用程序现在可以使用在WebAssembly上运行的本地依赖项。你可以使用.NET WebAssembly构建工具将本地依赖关系静态地链接到.NET WebAssembly运行时,这些工具与你在.NET 6中用来提前(AOT)编译Blazor应用程序到WebAssembly或重新链接运行时以删除未使用的功能的工具相同。

要安装.NET WebAssembly构建工具,请在Visual Studio安装程序中选择可选的组件,或在高位命令提示符下运行dotnet workload install wasm-tools 。.NET WebAssembly构建工具是基于Emscripten,这是一个用于网络平台的编译器工具链。

你可以通过在项目文件中添加NativeFileReference 项目来为你的Blazor WebAssembly应用程序添加本地依赖项。当你构建项目时,每个NativeFileReference ,由.NET WebAssembly构建工具传递给Emscripten,以便它们被编译和链接到运行时。然后你可以从你的.NET代码中p/invoke到本地代码。

一般来说,任何可移植的本地代码都可以作为Blazor WebAssembly的本地依赖项。你可以在C/C++代码或以前用Emscripten编译的代码中添加本地依赖关系:对象文件(.o)、存档文件(.a)、比特码(.bc)或独立的WebAssembly模块(.wasm)。预构建的依赖通常需要使用与构建.NET WebAssembly运行时相同的Emscripten版本(目前是2.0.23)来构建。

使用来自Blazor WebAssembly应用程序的本地代码

让我们在Blazor WebAssembly应用程序中添加一个简单的本地C函数:

  1. 创建一个新的Blazor WebAssembly项目。
  2. 在该项目中添加一个Test.c文件。
  3. Test.c中添加一个C函数,用于计算阶乘。
int fact(int n)
{
    if (n == 0) return 1;
    return n * fact(n - 1);
}
  1. 在你的项目文件中为Test.c添加一个NativeFileReference
<ItemGroup>
  <NativeFileReference Include="Test.c" />
</ItemGroup>
  1. Pages/Index.razor中,为生成的Test库中的fact 函数添加一个DllImport
@using System.Runtime.InteropServices

...

@code {
    [DllImport("Test")]
    static extern int fact(int n);
}
  1. 从你的.NET代码中调用fact 方法:
<p>@fact(3)</p>

当你用安装的.NET WebAssembly构建工具构建应用程序时,本地C代码会被编译并链接到dotnet.wasm中。这可能需要几分钟的时间。一旦应用程序完成构建,运行它以查看渲染的阶乘值。

注意:在随后的构建中,你可能会得到一个构建错误,说输出程序集正在被另一个进程使用。这是一个已知的问题,将在.NET 6版本中解决。要解决这个问题,请再次重建该项目。

使用具有本地依赖性的库

NuGet包可以包含在WebAssembly上使用的本地依赖性。这些库和它们的本地功能可以在任何Blazor WebAssembly应用程序中使用。本机依赖的文件应该为WebAssembly构建,并打包在browser-wasm 架构专用文件夹中。WebAssembly特定的依赖文件不会被自动引用,需要手动引用NativeFileReference 。软件包作者可以选择在软件包中加入一个.props文件来添加本地引用。

SkiaSharp是一个基于本地Skia图形库的.NET跨平台2D图形库,现在它对Blazor WebAssembly有预览支持。让我们试一试吧

要在Blazor WebAssembly应用程序中使用SkiaSharp。

  1. 从你的Blazor WebAssembly项目中添加一个对SkiaSharp.Views.Blazor包的引用。

    dotnet add package -prerelease SkiaSharp.Views.Blazor

  2. 在你的应用程序中添加一个SKCanvasView 组件:

<SKCanvasView OnPaintSurface="OnPaintSurface" />
  1. 添加一些绘图逻辑:
@code {
    void OnPaintSurface(SKPaintSurfaceEventArgs e)
    {
        var canvas = e.Surface.Canvas;
        canvas.Clear(SKColors.White);
        using var paint = new SKPaint
        {
            Color = SKColors.Black,
            IsAntialias = true,
            TextSize = 24
        };
        canvas.DrawText("SkiaSharp", 0, 24, paint);
    }
}
  1. 运行应用程序,查看你用SkiaSharp定制的绘图

SkiaSharp

你可以在SkiaSharp repo中找到一个使用SkiaSharp与Blazor WebAssembly的完整示例

最小的API更新

参数绑定

在RC2中,我们在TryParseBindAsync 中增加了对继承方法的支持。我们还检查了公共TryParseBindAsync 方法的正确格式,并抛出一个错误,这样你就知道你的方法使用了错误的语法。此外,BindAsync 方法现在有另一个支持的重载,不需要ParameterInfo:public static ValueTask<T?> BindAsync(HttpContext context)

如果你想从路由、标题属性和查询字符串中绑定值,你可以在你的自定义类型上添加一个静态的TryParse 方法,如下所示。TryParse 方法必须是以下形式:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

下面是一个复杂类型的TryParse 的例子:

app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider, out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
} 

对上述端点的请求/map?point=(10.1, 11.4) ,将返回一个具有以下属性值的Point 对象X=10.1, Y=11.4

此外,如果你想控制你的类型的绑定过程,你可以使用以下形式的BindAsync

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo? parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

想使用继承来绑定一个复杂的类型吗?BindAsync ,你可以做到这一点,正如下面的例子所演示的:

app.MapPost("/widget", (CreateWidgetDTO dto) =>
{
    // Use the DTO
});

public abstract class DTO<T>
{
    // typeof(T) must equal ParameterInfo.Type otherwise we throw
    public static T BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Use reflection to bind the properties on T
    }
}

public class CreateWidgetDTO : DTO<CreateWidgetDTO>
{
    [FromRoute]
    public string? Name { get; set; }

    [FromQuery]
    public int Id { get; set; }
}

我们还通过BindAsync 增加了对可选的自定义参数类型的支持,包括如下所示的可忽略的结构:

app.MapGet("/webhook", (CloudEventStruct? cloudEvent) =>
{
    redis.Publish(cloudEvent);
});

public struct CloudEventStruct
{
    public static async ValueTask<CloudEventStruct?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        return await CloudEventParser.ReadEventAsync(context, parameter.Name);
    }
}

OpenAPI

我们增加了一些增强功能,允许你使用Accepts<TRequest>() 扩展方法或[Consumes(typeof(Todo), "application/json", IsRequired = false)] 属性来描述请求主体是否需要。Accepts 扩展方法和Consumes 属性允许你为你生成的open-api文档同时表达类型Todo 和内容类型application/json ,如下面的例子所示:

app.MapPost("/todo", async (HttpRequest request) =>
{
    var todo =  await request.Body.ReadAsJsonAsync<Todo>();
    return todo is Todo ? Results.Ok(todo) : Results.Ok();
})
.Accepts<Todo>(isRequired: false, contentType: "application/json");

app.MapPost("/todo", HandlePostTodo);
[Consumes(typeof(Todo), "application/json", IsRequired = false)]
IResult HandlePostTodo(HttpRequest request)
{
    var todo =  await request.Body.ReadAsJsonAsync<Todo>();
    return todo is Todo ? Results.Ok(todo) : Results.Ok();
}

如果你想在OpenAPI文档(Swagger)中把相关的端点分成不同的集合,你可以使用WithTags 扩展方法来实现,该方法允许你提供分组标签的元数据。见下面的使用例子:

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

app.MapGet("/todos/sample", () => new[]
    {
        new Todo { Id = 1, Title = "Do this" },
        new Todo { Id = 2, Title = "Do this too" }
    })
    .WithTags("Examples", "TodoApi");

源代码分析

在RC2中,我们增加了一些分析器,以帮助你快速发现路由处理程序的问题,或在中间件存在错误配置问题时向你发出警告。分析器将为WebApplicationBuilder ,并在检测到不正确的中间件配置或顺序时警告你。

我们还增加了对检测从路由处理程序返回的实现IActionResult 的类型的支持,并对无意中将结果序列化为JSON的情况发出警告。见下面的例子:

app.Map("/", () => new JsonResult(new { Hello = "World" }));

此外,我们引入了一个新的分析器,它将检测到属性被放在一个由lambda调用的方法上,而不是lambda本身。例如,下面的代码将产生一个警告:

app.Map("/payment", () => SubmitPayment());
[Authorize]
void SubmitPayment() { }  

最后但并非最不重要的是,对于可选的参数绑定,我们增加了一个分析器,当参数的可选性不匹配时,将抛出一个异常。请注意,下面的路由字符串将uuid 参数定义为可选,但在lambda函数(string uuid) => $"{uuid}" 中却定义为必需。

app.MapGet("/todo/{uuid?}", (string uuid) => $"{uuid}");

突破性变化(API重命名)

我们重新命名了以下API,以提供清晰度并正确描述其意图:

  • DelegateEndpointConventionBuilder ->RouteHandlerBuilder
  • OpenApiDelegateEndpointConventionBuilderExtensions ->OpenApiRouteHandlerBuilderExtensions
  • DelegateEndpointRouteBuilderExtensions 与现有的 合并。EndpointRouteBuilderExtensions

上述改动用RouteHandler 替换了DelegateEndpoint ,并删除了类名中的Convention

给予反馈

我们希望你喜欢这个.NET 6中的ASP.NET Core预览版。我们很想听听你对这个版本的体验。通过在GitHub上提交问题,让我们知道你的想法。