Telerik blogs
DotNetT2 Light_1200x303

In this article, let’s recap three talks from Build 2022 that show off some great features that are coming with ASP.NET Core 7.

Build 2022 took place last week, giving Microsoft developers a lot to get excited about—whether it’s new Azure bells and whistles, Windows development, developer productivity updates, or tons of other content you can view from the session list.

In this article, I’d like to recap three talks that highlighted what’s new in the .NET world—specifically for ASP.NET Core web applications.

  • C# 11 Updates
  • Output caching in ASP.NET Core 7
  • Minimal API updates

Note: This post includes code samples. The samples are subject to minor syntax changes before .NET 7 releases in November 2022.

C# 11 Updates

With .NET pivoting to annual November releases the last few years, the languages are also following suit. With C# 11 development in full swing, Build is always a good time to get a preview of what’s coming. With that in mind, Mads Torgersen led the session, What’s Next in C# 11, to give us a preview of some new features.

Here are a few of them:

  • Static abstract members
  • Pattern matching updates support list patterns
  • Required properties
  • Raw string literals

Static Abstract Members

C# 11 will include the ability to add static abstract members in interfaces, where you can include static properties, overloadable operators or other static members. A big use case is with using mathematical operators, and Mads showed off an example.

Imagine you’ve got a quick program that adds all items in a given array and it returns the total (for brevity, this uses top-level statements, which eliminates the need for a Main method):

var result = AddAll(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(result);

int AddAll(int[] values)
{
    int result = 0;

    foreach (var value in values)
    {
        result += value;
    }
    return result;
}

What if we could have numeric types implement interfaces? That’s what we can do with static abstract members with this modified AddAll method:

T AddAll<T>(T[] values) where T : INumber<T>
{
    T result = T.AdditiveIdentity;

    foreach (var value in values)
    {
        result += value;
    }
    return result;
}

Now, we are taking T, which is any type that implements the INumber<TSelf> interface (int, short, long, float, decimal or any type that represents a number). The interface provides access to APIs like Abs, Min, Max, TryParse and the typical mathematical operators, which in turn allows developers to write code that relies on the interfaces with these abstract members as a constraint.

If you would change the result array to a mix of int and decimal types, it would calculate correctly without having to explicitly declare the specific type, through the beauty of interfaces. For more details on static abstract interfaces check out the Microsoft documentation.

Pattern Matching Updates Support List Patterns

Using the same example that Mads provided, C# 11 provides updates to pattern matching that helps with processing incoming list items. It can greatly simplify things, like in the following example:

T AddAll<T>(T[] values) where T : INumber<T> => values switch
{
    [] => T.AdditiveIdentity,
    [var t1, .. var middle] => t1 + AddAll(middle),
};

Required Properties

Mads also demonstrated something we’ve all been impatiently waiting for: required properties. To illustrate the benefits, let’s build out a trusty Person class:

public class Person 
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
}

To enforce instantiating certain fields, you would need to rely on a constructor-based approach. If you want to enforce a caller to instantiate a FirstName and LastName, there’s not a way to do it with object initialization. For example, what if a developer writes code before their morning coffee and accidentally makes the LastName the MiddleName?

var person = new Person()
{
    FirstName = "Saul",
    MiddleName = "Goodman"
};

With required properties, we can enforce this using object initialization, like so:

public class Person 
{
    public required string FirstName { get; set; }
    public string MiddleName { get; set; }
    public required string LastName { get; set; }
}

In this case, the compiler will throw an error when LastName is not initialized.

I’m tremendously excited for this feature—it was originally slated for C# 10, and is finally here. It really should have been shipped with the nullability features in the past few releases, as developers have used hacky workarounds like = null! to accommodate these situations. With this update, who needs constructors? Not me.

Raw String Literals

Mads also showed off raw string literals. We’ve all dealt with the pain of escaping pesky characters inside our strings, like quotes, double quotes, white space and backslashes. With C# 11, it’s much easier to create multi-line strings, or characters that previously required you to escape them.

According to the documentation, raw string literals:

  • Start and end with a sequence of at least three double quote characters ("""). You can also include more than three consecutive characters to support strings that contain three or more repeated quotes characters.
  • Single-line raw string literals require the opening and closing quote characters on the same line and multi-line raw string literals require both opening and closing quote characters on their own line.
  • In multi-line raw string literals, any white space to the left of the closing quotes is removed.

Here’s a simple example—note that string interpolation is also supported:

var author = "Andy Dwyer";  
string longMessage = $"""  
"Leslie, I typed your symptoms  
into this thing here and  
it says you have 'internet  
'connectivity problems.'"  
  
— {author}  
""";  
  
Console.WriteLine(longMessage);  

The value is immediate when working with JSON or XML-like structures, like this:

string html = """
           <body style="normal">
              <div class="book-content">
               This is information about the "C# 11" book.
           </body>
           <footer>
               This is information about the author of the "C# 11" book.
           </footer>
       </element>
       """;

Output Caching Middleware in ASP.NET Core 7

In his session, Output Caching in ASP.NET Core 7, Sebastian Ros talked about new middleware for caching endpoints.

If you aren’t familiar, ASP.NET Core already has response caching middleware that enables developers to enable caching sever responses based on HTTP cache headers. This has limited value for UI apps like Blazor and Razor Pages, as browsers often set request headers to prevent caching. Also, the response caching middleware has limited customization options.

The new output caching middleware will allow ASP.NET Core developers to:

  • Store the results in a web application instead of executing it repeatedly, providing performance benefits
  • Configure caching without having to think about HTTP headers
  • Storage options (like saving to disk or Azure Storage)
  • Use tags to invalidate cache entries
  • Have resource locking enabled by default

You’ll be able to use this through a variety of ASP.NET Core web apps, but let’s see how it works using a Minimal API. To start, let’s cache the root endpoint. To tweak Sebastian’s example ever so slightly, let’s say we’ve got a lawn care shop called Mandy’s Mowers, where we fetch details of the lawn mowers for sale.

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.AspNetCore.OutputCaching.Policies;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCaching();

var app = builder.Build();

app.UseOutputCaching();
app.MapGet("/", MowerInventory.GetMowers).OutputCache();
await app.RunAsync();

If I want to disable caching for an endpoint, I would do this:

app.MapGet("/nocache", MowerInventory.GetMowers).OutputCache(cache => cache.NoStore());

If I want to enable caching for all endpoints, I can add a policy to the configuration.

builder.Services.AddOutputCaching(options =>
{
  options.Policies.Add(new OutputCachingPolicy());
});

You can also assign policies to specific endpoints. Let’s say I want to cache product detail results pages for just 15 seconds:

builder.Services.AddOutputCaching(options =>
{
  options.Policies.Add(new OutputCachingPolicy());
  options.Profiles["QuickCache"] = new OutputCachePolicyBuilder().Expires(TimeSpan.FromSeconds(15)).Build();
});

// other code omitted for brevity

app.MapGet("/results", MowerInventory.GetMowers).OutputCache(cache => cache.Profile("QuickCache"));

If you want to cache by query string value, you can do that too. Consider this endpoint where a user filters by type and color.

https://mandysmowers.com/results?type=push&color=red

If I wanted to only specify a caching policy by color, I can use the VaryByQuery API:

app.MapGet("/results", MowerInventory.GetMowers).OutputCache(p => p.VaryByQuery("color"));

Of course, you can also work with nested endpoints, too. A common scenario would be pages for getting all mowers, and also displaying a specific one.

We’d first set up a policy:

builder.Services.AddOutputCaching(options =>
{
    options.Policies.Add(new OutputCachingPolicy());
    options.Policies.Add(new OutputCachePolicyBuilder().Path("/mowers").Tag("mowers").Build());
});

Then, apply a tag to the endpoints:

app.MapGet("/mowers", MowerInventory.GetMowers).OutputCache(cache => cache.Tag("mowers"));
app.MapGet("/mowers/{id}", MowerInventory.GetMowers).OutputCache(cache => cache.Tag("mowers"));

Then, you could perform actions on a tag in a single action, like with performing cache eviction. As shown in the talk, you could build a /purge endpoint to do this:

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag))
{
    await cache.EvictByTagAsync(tag);
};

There’s a lot more to come to this exciting feature, and you can follow it on GitHub.

Minimal API Updates

In Stephen Halter and Safia Abdalla’s talk, Minimal APIs: Past Present and Future, they discussed how Minimal APIs have evolved and what’s in store for .NET 7.

If you aren’t familiar with Minimal APIs, you use them to create HTTP APIs with minimal dependencies and overhead without the bloat that often comes with ASP.NET Core MVC. (Of course, MVC is going nowhere and it’s up to you to determine what best fits your use case.)

I initially wrote about Minimal APIs in their early days and also wrote about their capabilities with .NET 6. As discussed in the talk, .NET 6 was when Minimal APIs truly arrived, taking advantage of top-level statements, lambdas and method groups, attributes on lambdas, new Map methods, and more. Looking forward to .NET 7, here are some of my favorites.

Endpoint Filters

With .NET 7, Minimal APIs will support endpoint filters, which allow you to inspect and modify input parameters before executing a route handler. This will allow developers to intercept a request, much like in an MVC controller, and perform logic based on the parameters.

This involves a new IRouteHandlerFilter interface that takes a RouteHandlerFilterContext which developers can implement for custom filters. The RouteHandlerFilterContext contains HttpContext and Parameters properties—the Parameters is an IList so developers can perform updates on the fly.

Route Groups

.NET 7 also ships route groups that allow you to define a single route prefix for a group of endpoints, which allow a variety of IEndpointConventionBuilder extension methods (like RequireCors and RequireAuthorization). You can say goodbye to manually adding authorization to single endpoints. It also allows nesting.

Typed Results

With .NET 7 Preview 4, typed results should make working with and testing route handlers simpler and easier. To borrow from the talk, here’s a quick example.

In .NET 6, here’s how you could return a specific response based on if a record with a specific type is found:

app.MapGet("/todo/{id}", async (int id, ToDoDb db) =>
    await db.Todos.FindAsync(id) is Todo todo
        ? Results.Ok(todo)
        : Results.NotFound());

And here’s how it looks with typed results in .NET 7:

app.MapGet("/todo/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, ToDoDb db) =>
    await db.Todos.FindAsync(id) is Todo todo;

This makes testing a lot easier, as this example shows:

[Fact]
public async Task GetAllTodos_ReturnsOk()
{
    var db = CreateDbContext();
    var result = await TodosApi.GetAllTodos(db);
    Assert.IsType<Ok<object>>(result);
}

Wrap-up

I’ve only scratched the surface on all the great .NET web content from Build 2022. The .NET team put together a YouTube playlist of .NET Build content, and you can also check out all 389 on-demand sessions. Here are a few other talks I loved but didn’t have space to address:

Stay tuned, as we’ll cover many of these topics as we get closer to the .NET 7 release in November. Happy coding!


Dave-Brock-headshot-sq
About the Author

Dave Brock

Dave Brock is a software engineer, writer, speaker, open-source contributor and Microsoft MVP. With a focus on Microsoft technologies, Dave enjoys advocating for modern and sustainable cloud-based solutions. He writes regularly at daveabrock.com. To reach Dave, follow him on Twitter, where his dad jokes are a parent.

Related Posts

Comments

Comments are disabled in preview mode.