How to add Health Checks to ASP.NET Core project. A coding story. dotnet coding-stories csharp
How to add OpenAPI to ASP.NET Core project. A coding story. dotnet coding-stories csharp

TL;DR

This blog post is a compilation of the latest and greatest additions from the .NET 6 release. Also, I’ve created a coding story that will help you to learn new improvements.

minimal-api-banner


Please check the coding story πŸ‘‡:

https://codingstories.io/stories/6139f4a2f2cd3d0031cd8ef1/617b06b5d2ec28001acfe618

post-banner Source code πŸ‘‡:

https://github.com/NikiforovAll/whats-new-in-dotnet6

Also, see my blog post. It explains how to run .NET 6 inside devcontainer: https://nikiforovall.github.io/productivity/devcontainers/2021/10/14/devcontainer-for-dotnet6.html


βš πŸ‘€ Please note that the content below consists of excerpts from the actual coding story. Please check the coding story if you haven’t already.

Part 1. C# 10

Full list of changes:

Also, see:

Global usings

global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;

global using static System.Console;
global using static System.Math;
WriteLine(Sqrt(3 * 3 + 4 * 4));
$ dotnet run 
5

Reference

File-scoped namespaces

// IProductRepository.cs
namespace MyNamespace;

public interface IProductRepository
{
    Task<Product> GetProductAsync(int id);
}

public record class Product(int Id, string Name, ProductWarranty Warranty);
// Program.cs
using MyNamespace;

var repository = new ProductRepository();
var product = await repository.GetProductAsync(Parse(args[0]));
WriteLine(product);
$ dotnet run 12
Product { Id = 1, Name = Name, Warranty = OneYear }

Reference

Record structs

public record class Point(double X, double Y, double Z);

// public record class Point
// {
//     public double X {  get; init; }
//     public double Y {  get; init; }
//     public double Z {  get; init; }
// }

public readonly record struct Point2(double X, double Y, double Z);

// public record struct Point2
// {
//     public double X {  get; init; }
//     public double Y {  get; init; }
//     public double Z {  get; init; }
// }

public record struct Point3(double X, double Y, double Z);

// public record struct Point3
// {
//     public double X { get; set; }
//     public double Y { get; set; }
//     public double Z { get; set; }
// }

Reference

Part 2. .NET API

βž• System.Numerics.BitOperations

Minimal API

// ================================================================
// Main Components
// ================================================================

// WebApplication
public static Microsoft.AspNetCore.Builder.WebApplicationBuilder CreateBuilder ();
// WebApplicationBuilder
public Microsoft.AspNetCore.Builder.WebApplication Build ();
// WebApplication
public void Run (string? url = default);
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

services
    .AddEndpointsApiExplorer()
    .AddSwaggerGen();

var app = builder.Build();
app
    .UseSwagger()
    .UseSwaggerUI();

app.MapGet("ka/{pow}", (int pow) => IsPow2(pow));

app.Run();
$ dotnet run
Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7225
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5069
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\home\dev\codingstories\whats-new-in-dotnet6\MinimalAPI

Reference

LINQ Improvements

using Bogus;
var faker = new OrderFaker();
var orders = faker.Generate(100);

// ================================================================
Header("Chunking");
// ================================================================

foreach (var chunk in orders.Chunk(25))
{
    var sum = chunk.Sum(o => o.Quantity);

    WriteLine($"chunk[{chunk.Length}].Quantity={sum}");
}
Chunking
chunk[25].Quantity=154
chunk[25].Quantity=138
chunk[25].Quantity=119
chunk[25].Quantity=138
// ================================================================
Header("Index Support for ElementAt");
// ================================================================

var order1 = orders.ElementAt(^5);

WriteLine(order1);
Index Support for ElementAt
Order { OrderId = 15, Address = 604 Leffler Glens, Romaport, Cape Verde, Quantity = 2, Index = 96 }
// ================================================================
Header("Range Support for Take");
// ================================================================

var orders2 = orders.Take(^5..);

WriteLine(string.Join(Environment.NewLine, orders2));
Range Support for Take
Order { OrderId = 15, Address = 604 Leffler Glens, Romaport, Cape Verde, Quantity = 2, Index = 96 }
Order { OrderId = 8, Address = 35694 Cummings Ville, Turnerville, Republic of Korea, Quantity = 5, Index = 97 }
Order { OrderId = 13, Address = 8711 Vita Burgs, East Jamaalberg, Zambia, Quantity = 4, Index = 98 }
Order { OrderId = 83, Address = 11506 Skiles Curve, Lake Abbeychester, Suriname, Quantity = 8, Index = 99 }
Order { OrderId = 89, Address = 9444 Schamberger Burgs, Wisokyland, Costa Rica, Quantity = 2, Index = 100 }
// ================================================================
Header("Three way zipping");
// ================================================================

string[] a1 = { "1", "2", "3" };
string[] a2 = { "One", "Two", "Three" };
float[] a3 = { 1f, 2f, 3f };

foreach ((string i, string i2, float i3) in a1.Zip(a2, a3))
{
    WriteLine($"{i}-{i2}-{i3}");
}
Three-way zipping
1-One-1
2-Two-2
3-Three-3
// ================================================================
Header("Default Parameters for Common Methods");
// ================================================================

var order2 = orders.FirstOrDefault(
    o => o.Address.Contains("Odesa"), defaultValue: orders.ElementAt(^10));

WriteLine(order2);
Default Parameters for Common Methods
Order { OrderId = 14, Address = 49026 Huels Groves, Armandville, Bhutan, Quantity = 3, Index = 91 }
// ================================================================
Header("Avoiding Enumeration with TryGetNonEnumeratedCount");
// ================================================================

if (orders.TryGetNonEnumeratedCount(out int count))
    WriteLine($"The count is {count}");
Avoiding Enumeration with TryGetNonEnumeratedCount
The count is 100
// ================================================================
Header("MaxBy and MinBy");
// ================================================================

WriteLine(orders.MaxBy(o => o.Quantity));
MaxBy and MinBy
Order { OrderId = 16, Address = 30901 Madilyn Meadow, Lake Marlenfort, Malawi, Quantity = 10, Index = 2 }

Reference

Parallel.ForEachAsync

βž• Task.WaitAsync

βž• Random.Shared

public static Task ForEachAsync<TSource>(
    IAsyncEnumerable<TSource> source,
    Func<TSource, CancellationToken, ValueTask> body);

public static Task ForEachAsync<TSource>(
    IEnumerable<TSource> source,
    Func<TSource, CancellationToken, ValueTask> body);
await Parallel.ForEachAsync(Generate(), Handle)
    .WaitAsync(TimeSpan.FromMilliseconds(200))
    .ContinueWith(Report);

static async IAsyncEnumerable<int> Generate()
{
    while (true)
    {
        var delay = Random.Shared.Next(100);
        WriteLine($"Issued {delay}");
        await Task.Delay(delay);
        yield return delay;
    }
}

static async ValueTask Handle(int i, CancellationToken ct)
{
    await Task.Delay(i, ct);
    WriteLine($"Handled {i}");
}

static void Report(Task t) => WriteLine(
    $"Finished at: {DateTime.Now:T}, task.Status {t.Status}");
$ dotnet run
Issued 56
Issued 23
Issued 81
Handled 23
Handled 56
Issued 75
Finished at: 10:59:08 PM, task.Status Faulted

Reference

PeriodicTimer

βž• ArgumentNullException.ThrowIfNull

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));

while (await timer.WaitForNextTickAsync())
{
    LogEntry? l = Random.Shared.Next(128) is int r && IsPow2(r)
        ? default
        : new(r);

    ProcessLogEntry(l);
}

static void ProcessLogEntry(LogEntry? entry)
{
    ArgumentNullException.ThrowIfNull(entry, nameof(entry));
    WriteLine(entry);
}

public record class LogEntry(int Value);
$ dotnet run
LogEntry { Value = 104 }
LogEntry { Value = 115 }
LogEntry { Value = 21 }
LogEntry { Value = 110 }
LogEntry { Value = 106 }
LogEntry { Value = 63 }
LogEntry { Value = 81 }
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'entry')
   at System.ArgumentNullException.Throw(String paramName)
   at System.ArgumentNullException.ThrowIfNull(Object argument, String paramName)
   at Program.<<Main>$>g__ProcessLogEntry|0_0(LogEntry entry) in PeriodicTimer\Program.cs:line 18
   at Program.<Main>$(String[] args) in PeriodicTimer\Program.cs:line 12
   at Program.<Main>(String[] args)

Priority Queue

var queue = new PriorityQueue<Job, int>(ReverseComparer<int>.Default);

foreach (var i in 10)
{
    var p = Random.Shared.Next(100);
    queue.Enqueue(new($"Job{i}", p), p);
}

using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(500));

while (await timer.WaitForNextTickAsync())
{
    if (!queue.TryDequeue(out var job, out _))
        break;

    WriteLine(job);
}

public record struct Job(string Name, int Priority);

public sealed class ReverseComparer<T> : IComparer<T>
{
    public static readonly ReverseComparer<T> Default = new(Comparer<T>.Default);

    public static ReverseComparer<T> Reverse(IComparer<T> comparer) =>
        new ReverseComparer<T>(comparer);

    private readonly IComparer<T> comparer = Default;

    private ReverseComparer(IComparer<T> comparer) =>
        this.comparer = comparer;

    public int Compare(T? x, T? y) => comparer.Compare(y, x);
}

public static class ProduceNumericEnumeratorExtensions
{
    public static IEnumerator<int> GetEnumerator(this int number)
    {
        for (var i = 0; i < number; i++)
        {
            yield return i;
        }
    }
}
$ dotnet run 
Job { Name = Job1, Priority = 75 }
Job { Name = Job5, Priority = 66 }
Job { Name = Job2, Priority = 61 }
Job { Name = Job0, Priority = 58 }
Job { Name = Job4, Priority = 53 }
Job { Name = Job9, Priority = 34 }
Job { Name = Job7, Priority = 34 }
Job { Name = Job3, Priority = 29 }
Job { Name = Job8, Priority = 17 }
Job { Name = Job6, Priority = 8 }

Reference

DateOnly and TimeOnly

DateOnly date = DateOnly.MinValue;
Log(date, nameof(date)); //Outputs 01/01/0001 (With no Time)

TimeOnly time = TimeOnly.MinValue;
Log(time, nameof(time)); //Outputs 12:00 AM

TimeOnly startTime = TimeOnly.Parse("11:00 PM");
var hoursWorked = 2;
var endTime = startTime.AddHours(hoursWorked);
Log(endTime, nameof(endTime)); //Outputs 1:00 AM

var isBetween = TimeOnly.Parse("12:00 AM").IsBetween(startTime, endTime); 

Log(isBetween, nameof(isBetween)); //Outputs true.

static void Log<T>(T value, string name = "") => WriteLine($"{name} {value}");
$ dotnet run 
date 1/1/0001
time 12:00 AM
endTime 1:00 AM
isBetween True

System.Text.Json

// ================================================================
Header("Serialization Notification");
// ================================================================

string invalidOrderJson = "{}";

var success = IgnoreErrors(() => JsonSerializer.Deserialize<Order2>(invalidOrderJson));
WriteLine($"Exception thrown: {!success}");

// IJsonOnDeserialized, IJsonOnDeserializing, IJsonOnSerialized, IJsonOnSerializing
public record class Order2 : Order, IJsonOnDeserialized
{
    public void OnDeserialized() => this.Validate();

    private void Validate()
    {
        if (this.OrderId <= 0)
            throw new ArgumentException();
    }
}

public record class Order
{
    public int OrderId { get; init; }
    public string Address { get; init; }
    public int Quantity { get; init; }
}

static bool IgnoreErrors(Action operation)
{
    if (operation == null)
        return false;

    try
    {
        operation.Invoke();
    }
    catch
    {
        return false;
    }

    return true;
}
$ dotnet run 
Serialization Notification
Exception thrown: True
// ================================================================
Header("Property Ordering");
// ================================================================

Order3 order = new()
{
    OrderId = 1,
    Address = "Address",
    Quantity = 1,
    Comments = new() { "Cool", "Awesome" }
};
var serializedOrder = JsonSerializer.Serialize(order, options);
WriteLine(serializedOrder);

public record class Order3
{
    [JsonPropertyOrder(-1)]
    public int OrderId { get; init; }

    [JsonPropertyOrder(1)]
    public string Address { get; init; }

    [JsonPropertyOrder(2)]
    public int Quantity { get; init; }

    [JsonPropertyOrder(99)]
    public List<string> Comments { get; init; }
}
$ dotnet run 
Property Ordering
{
  "OrderId": 1,
  "Address": "Address",
  "Quantity": 1,
  "Comments": [
    "Cool",
    "Awesome"
  ]
}
// ================================================================
Header("IAsyncEnumerable support && Working with Streams");
// ================================================================

var data = new { Data = RangeAsync(1, 5) };

// SerializeAsync
using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, RangeAsync(1, 5), options);
stream.Position = 0;

// DeserializeAsyncEnumerable
await foreach (var i in JsonSerializer.DeserializeAsyncEnumerable<int>(stream, options))
{
    WriteLine(i);
}

static async IAsyncEnumerable<int> RangeAsync(int start, int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(i);
        yield return start + i;
    }
}
$ dotnet run 
1
2
3
4
5
// ================================================================
Header("Working with JSON DOM");
// ================================================================

// Parse
var node = JsonNode.Parse(serializedOrder);

WriteLine($"OrderId: {node["OrderId"].GetValue<int>()}");
WriteLine($"Order.Comments[0]: {node["Comments"][0].GetValue<string>()}");

// Create DOM Object via API
var jObjectOrder = new JsonObject
{
    ["OrderId"] = 1,
    ["Discounts"] = new JsonArray(
        new JsonObject
        {
            ["DiscountId"] = 1,
            ["Value"] = .05,
        },
        new JsonObject
        {
            ["DiscountId"] = 2,
            ["Value"] = .1,
        }
    ),
};

WriteLine(jObjectOrder.ToJsonString(options));
OrderId: 1
Order.Comments[0]: Cool
{
  "OrderId": 1,
  "Discounts": [
    {
      "DiscountId": 1,
      "Value": 0.05
    },
    {
      "DiscountId": 2,
      "Value": 0.1
    }
  ]
}

Summary

Thank you very much. I hope you find this blog post useful πŸ‘.


Oleksii Nikiforov

Jibber-jabbering about programming and IT.