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.
Note: This post includes code samples. The samples are subject to minor syntax changes before .NET 7 releases in November 2022.
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:
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.
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),
};
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.
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:
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>
""";
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:
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.
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.
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.
.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.
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);
}
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 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.