ASP.NET Core updates in .NET 6 Release Candidate 2

Daniel Roth

.NET 6 Release Candidate 2 (RC2) is now available. .NET 6 RC2 is very close to the final build of .NET 6 that we expect to ship in November this year in time for .NET Conf 2021. It’s also a “go live” release, so you’re welcome to use it in production. .NET 6 RC2 primarily contains quality improvements and bug fixes, although it does also include a few new features as well.

Here’s what’s new in this preview release:

  • Native dependencies support for Blazor WebAssembly apps
  • Minimal API updates

Get started

To get started with ASP.NET Core in .NET 6 RC2, install the .NET 6 SDK.

If you’re on Windows using Visual Studio, install the latest preview of Visual Studio 2022, which includes .NET 6 RC2. Mac users should install the latest preview of Visual Studio for Mac 2022.

Upgrade an existing project

To upgrade an existing ASP.NET Core app from .NET 6 Preview RC1 to .NET 6 RC2:

  • Update all Microsoft.AspNetCore.* package references to 6.0.0-rc.2.*.
  • Update all Microsoft.Extensions.* package references to 6.0.0-rc.2.*.

See the full list of breaking changes in ASP.NET Core for .NET 6.

Native dependencies support for Blazor WebAssembly apps

Blazor WebAssembly apps in .NET 6 can now use native dependencies that have been built to run on WebAssembly. You can statically link native dependencies into the .NET WebAssembly runtime using the .NET WebAssembly build tools, the same tools that you can use in .NET 6 to ahead-of-time (AOT) compile your Blazor app to WebAssembly or to relink the runtime to remove unused features.

To install the .NET WebAssembly build tools, select the optional component in the Visual Studio installer, or run dotnet workload install wasm-tools from an elevated command prompt. The .NET WebAssembly build tools are based on Emscripten, a compiler toolchain for the web platform.

You add native dependencies to your Blazor WebAssembly app by adding NativeFileReference items in your project file. When you build the project each NativeFileReference gets passed to Emscripten by the .NET WebAssembly build tools so that they are compiled and linked into the runtime. You can then p/invoke into the native code from your .NET code.

Generally any portable native code can be used as a native dependency with Blazor WebAssembly. You can add native dependencies to C/C++ code or code previously compiled using Emscripten: object files (.o), archive files (.a), bitcode (.bc), or standalone WebAssembly modules (.wasm). Prebuilt dependencies typically need to be built using the same version of Emscripten used to build the .NET WebAssembly runtime (currently 2.0.23).

Using native code from a Blazor WebAssembly app

Let’s add a simple native C function to a Blazor WebAssembly app:

  1. Create a new Blazor WebAssembly project.
  2. Add a Test.c file to the project.
  3. Add a C function in Test.c for computing factorials:
int fact(int n)
    if (n == 0) return 1;
    return n * fact(n - 1);
  1. Add a NativeFileReference for Test.c in your project file:
  <NativeFileReference Include="Test.c" />
  1. In Pages/Index.razor add a DllImport for the fact function in generated Test library:
@using System.Runtime.InteropServices


@code {
    static extern int fact(int n);
  1. Call fact method from your .NET code.

When you build the app with the .NET WebAssembly build tools installed, the native C code gets compiled and linked into dotnet.wasm. This may take a few minutes. Once the app is done building, run it to see the rendered factorial value.

Note: You may get a build error on subsequent builds saying that the output assembly is being used by another process. This is a known issue that will be addressed for the .NET 6 release. To workaround the issue, rebuild the project again.

Using libraries with native dependencies

NuGet packages can contain native dependencies for use on WebAssembly. These libraries and their native functionality can then be used from any Blazor WebAssembly app. The files for the native dependencies should be built for WebAssembly and packaged in the browser-wasm architecture-specific folder. WebAssembly-specific dependencies won’t get referenced automatically and need to be referenced manually as a NativeFileReference. Package authors can choose to add the native references by including a .props file in the package with the references.

SkiaSharp is a cross-platform 2D graphics library for .NET based on the native Skia graphics library, and it now has preview support for Blazor WebAssembly. Let’s give it a try!

To use SkiaSharp in a Blazor WebAssembly app:

  1. Add a package reference to the SkiaSharp.Views.Blazor package from your Blazor WebAssembly project.

    dotnet add package –prerelease SkiaSharp.Views.Blazor

  2. Add a SKCanvasView component to your app.

<SKCanvasView OnPaintSurface="OnPaintSurface" />
  1. Add some drawing logic:
@code {
    void OnPaintSurface(SKPaintSurfaceEventArgs e)
        var canvas = e.Surface.Canvas;
        using var paint = new SKPaint
            Color = SKColors.Black,
            IsAntialias = true,
            TextSize = 24
        canvas.DrawText("SkiaSharp", 0, 24, paint);
  1. Run the app to see your custom drawing with SkiaSharp!


You can find a complete sample for using SkiaSharp with Blazor WebAssembly in the SkiaSharp repo.

Minimal API updates

Parameter Binding

In RC2 we added support in TryParse and BindAsync for inherited methods. We also check for public TryParse and BindAsync methods that aren’t in the correct format and throw an error so you will know that you used the wrong syntax for your method(s). Additionally, the BindAsync method now has another supported overload that doesn’t require the ParameterInfo: public static ValueTask<T?> BindAsync(HttpContext context).

You can add a static TryParse method on your custom type as shown below if you want to bind values from route, header attributes, and query strings. The TryParse method must be of the following forms:

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

Below is an example of the TryParse for a complex type Point:

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;

The request /map?point=(10.1, 11.4) to the above endpoint will return a Point object with the following property values X=10.1, Y=11.4.

Furthermore, if you’d like to take control of the binding process for your type, you can use the following forms of BindAsync:

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

Want to bind a complex type using inheritance? BindAsync allows you to do it as demonstrated in the example below:

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>
    public string? Name { get; set; }

    public int Id { get; set; }

We also added support for optional custom parameter types with BindAsync, including nullable structs as shown below:

app.MapGet("/webhook", (CloudEventStruct? cloudEvent) =>

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


We added enhancements to allow you to describe whether the request body is required or not using the Accepts<TRequest>() extension method or [Consumes(typeof(Todo), "application/json", IsRequired = false)] attribute. The Accepts extension method and Consumes attribute allow you to express both the type Todo and the contentType application/json for your generated open-api docs as indicated in the example below.

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();

If you’d like to group related endpoints into different collections in OpenAPI docs (Swagger), you can do so using the WithTags extension method that allows you to provide the grouping tags metadata. See usage example below:

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

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

Source Code Analysis

In RC2, we added a few analyzers to help you quickly find issues with your route handlers or warn you when there are misconfiguration issues for middleware. The analyzer will verify middleware ordering for WebApplicationBuilder and warn you when an incorrect middleware configuration or order is detected.

We also added support to detect when a type that implements IActionResult is returned from the route handler and warn you about the unintentional serialization of the result as JSON. See example below:

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

Furthermore, we introduced a new analyzer that will detect when attributes are put on a method called by a lambda instead of the lambda itself. for example, the following code will produce a warning :

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

Last but not least, for optional parameter binding, we added an analyzer that will throw an exception when there is a mismatched parameter optionality. Notice that the route string below defines the uuid parameter as optional but it’s defined as required in the lambda function (string uuid) => $"{uuid}".

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

Breaking Changes (APIs Renames)

We renamed the following APIs to provide clarity and properly describe their intents:

  • DelegateEndpointConventionBuilder -> RouteHandlerBuilder
  • OpenApiDelegateEndpointConventionBuilderExtensions -> OpenApiRouteHandlerBuilderExtensions
  • DelegateEndpointRouteBuilderExtensions merged with the existing EndpointRouteBuilderExtensions.

The above changes replaced the DelegateEndpoint with RouteHandler and removed Convention in the class names.

Give feedback

We hope you enjoy this preview release of ASP.NET Core in .NET 6. We’re eager to hear about your experiences with this release. Let us know what you think by filing issues on GitHub.

Thanks for trying out ASP.NET Core!