New W poprzednim roku dokładnie śledziłem zmiany związane z C# 10 i .NET 6. Minęło już 3 miesiące od mojej prezentacji na konferencji 4Developers na ten temat.

Korzystając z małej przerwy od mojego kanału YouTube postanowiłem przerobić swoją prelekcję na ten wpis na blogu.

O ile zmiany w C# 10 mogą się wydawać kosmetyczne to wraz z nimi pojawił się nowy minimalny styl pisania oprogramowania. Język C# chce być trendy i pokazać jak nie wiele linek kodu wystarczy, aby napisać jakąś prostą aplikację. Chodzi o to, aby ten kod wyglądał fajowo na tle NodeJs, GO czy Python, które słyną ze swojego minimalizmu.

W tym wpisie także spojrzy na szereg dodatkowych klas i metod w .NET 6. Większość z tych zmian ma sprawić, że będziemy pisać jeszcze mniej kodu.

No to idziemy.

Chociaż zdałem sobie sprawę, że w formie tekstowej to głupio brzmi. To nie jest filmik na YouTube gdzie możesz usłyszeć mnie krzyczącego w pełnej energii "No to idziemy".

Nowe klasy i metody w .NET 6

W C# w konstruktorach często wyrzucamy wyjątki, gdy parametr przyszedł jako NULL.

void Example1(string id)
{
    if (id is null)
    {
        throw new ArgumentNullException(nameof(id));
    }
}

Teraz w .NET 6 masz na to gotową metodę statyczną.

void Example2(string id)
{
    ArgumentNullException.ThrowIfNull(id);
}

Do pobrania Id obecnego procesu, jak i jego ścieżki służył wcześniej taki kod.

Process process  = Process.GetCurrentProcess();

string path = process.MainModule.FileName;
int id = process.Id;

Console.WriteLine(path);
Console.WriteLine(id);

Teraz masz na to gotowe pola statyczne w klasie Environment

string path2 = Environment.ProcessPath;
int id2 = Environment.ProcessId;

Jak można inaczej tworzyć liczby losowe?  W .NET 6 powstał nowy sposób do tworzenia losowych liczb, który jest wolniejszy niż klasa Random.

Jednak nie chodzi tutaj o szybkość tylko o dokładność tworzenia tych wartości losowych np. do podpisów cyfrowych i generowania kluczy.

using System.Security.Cryptography;
                        
//for
//- key generation
//- nonces
//- salts in certain signature schemes
var random = RandomNumberGenerator.
    GetInt32(1, 1000000);

var bytes = RandomNumberGenerator.
    GetBytes(255);

PeriodicTimer to klasa, która szybko da Ci możliwość uruchamiania fragmentu kodu raz na jakiś czas bez pisania pętl i wyrażeń Task.Delay.

using PeriodicTimer timer = 
    new(TimeSpan.FromSeconds(2));

while (await timer.WaitForNextTickAsync())
{
    Console.WriteLine(DateTime.UtcNow.Second);
}

Klasa NativeMemory daje możliwość alokacji pamięci, jak i jej uwalniania. Niestety nie programuje nisko poziomo w C#, aby zrozumieć, co dalej można zrobić z tą pamięcią

unsafe
{
    byte* buffer = (byte*)NativeMemory.Alloc(255);
    NativeMemory.Free(buffer);
}

W .NET 6 pojawiły się także nowe kolekcje.

Problem z PriorityQueue

Nazwa tej nowej kolekcji jest myląca. Jak zaraz się przekonasz ta kolekcja ma nie wiele wspólnego z kolejką. 

Bardziej jest to torba, w której możesz określać priorytety wyciąganych elementów z tej kolekcji.

Taki przykład nie zaskakuje. 

PriorityQueue queue = new PriorityQueue();

queue.Enqueue("A", 0);
queue.Enqueue("B", 3);
queue.Enqueue("C", 2);
queue.Enqueue("D", 1);

while (queue.TryDequeue(out string item, out int priority))
{
    Console.WriteLine($"Popped Item : {item}. " +
        $"Priority Was : {priority}");
}

Zgodnie z poziomem priorytetów elementy zostały wyjęte z tej kolekcji.

PriorityQueue działanie w praktyce

Co, jeśli jednak poziomy priorytetów się powtarzają. Myślisz sobie no to wtedy ta kolekcja powinna zachować się jak normalna kolejka.

PriorityQueue<string, int> queue = new PriorityQueue<string, int>();

queue.Enqueue("A", 0);
queue.Enqueue("B", 3);
queue.Enqueue("C", 2);
queue.Enqueue("D", 1);
queue.Enqueue("E", 0);
queue.Enqueue("F", 1);

while (queue.TryDequeue(out string item, out int priority))
{
    Console.WriteLine($"Popped Item : {item}. " +
        $"Priority Was : {priority}");
}

Gdyby jednak dla elementów o tym samych priorytetach zachowanie było typowo kolejkowe to wynik byłby taki: A,E,D,F,C,B

Tymczasem nasz wynik może potwierdzić, że jeśli priorytety są takie same to kolejność wyciągania elementów jest losowa. Pierwszy element z tym samym priorytetem dodany do kolejki nie wyszedł co znaczy, że tak naprawdę nie jest to kolejka.

PriorityQueue nie jest kolejką

Jeśli chcesz posłuchać mojego zdziwienia oto filmik na YouTube.

Kolejne klasy i metody w .NET 6

Jak widać programistom brakowało struktury, która by opisywała tylko datę albo tylko czas.

Mój Twitter eksplodował, gdy podzieliłem się tą zmianą w .NET 6.

Chociaż wiem, że wielu osobom nie podobają się nazwy tych struktur. 

var dateOnly = new DateOnly(2021, 8, 20);
                    
var timeOnly = new TimeOnly(08, 43, 57);
                    
DateTime dateTime = dateOnly.ToDateTime(timeOnly);

W .NET 6 pojawiła się klasa statyczna "BitOperations", która ma szereg gotowych metod do robienia operacji bitowych.

uint number = 235;

if (!BitOperations.IsPow2(235))
{
    number = 
    BitOperations.RoundUpToPowerOf2(number);
}

Console.WriteLine(number);

Metoda EnsureCapacity jest ciekawa. Jej zadaniem jej zagwarantowanie, że w twojej kolekcji w tym przypadku listy w pamięci od górnie będzie zarezerwowane miejsce.

Tak, aby twoja aplikacja nie miała czkawki, gdy wiesz, że twoja lista będzie potrzebować miejsca na milion elementów to dlaczego od razu tego nie zakomunikować. 

Dużo się dzieje w tym fragmencie kodu

var numbers = new List<int> { 1, 2 };
Console.WriteLine(numbers.Capacity < 100);

numbers.EnsureCapacity(100);

Console.WriteLine(numbers.Count == 2);
Console.WriteLine(numbers.Capacity == 100);

numbers.EnsureCapacity(50);
Console.WriteLine(numbers.Capacity == 100);

for (int i = numbers.Count; i < 98; i++)
{
    numbers.Add(i);
}

Console.WriteLine(numbers.Count == 100);
Console.WriteLine(numbers.Capacity == 100);

Na początku tworzę listę, która ma dwa elementy. Capacity tej listy na pewno będzie mniejsze niż 100. Potem korzystam z metody EnsureCapacity, która sprawi, że Capacity zwiększy się do 100, mimo iż ciągle na liście mam tylko dwa elementy

Tej operacji nie można cofnąć. Wywołanie "EnsureCapacity" z wartością 50 nie obniży rezerwacji w pamięci.

Na końcu wypełniam swoją listę tak, aby było w niej 100 elementów. Capacity nie powinno ulec wtedy zmianie.

W .NET 6 pojawiła się także metoda "Chunk()". Rozbija ona jedną kolekcję na pod kolekcje.

Mam przykładowo kolekcję 100 liczb i chcemy, aby co dziesiątą liczbę wyświetlić jakiś napis.

Korzystając z metody Chunk() mogę rozbić swoją kolekcję 100 liczb na 10 kolekcji, które zawierają 10 liczb co daje mi większą kontrolę nad ich wyświetlaniem.

var list = Enumerable.Range(1, 100);

foreach (var chunk in list.Chunk(10))
{
    Console.WriteLine("This next chunk");
    foreach (var item in chunk)
    {
        Console.WriteLine(item);
    }
}

A co nowego w LINQ ?

Co nowego w LINQ ?

W LINQ pojawiły się nowe metody jak : MaxBy, MinBy, DistinctBy, UnionBy, IntersectBy, ExceptBy.

Co ciekawe wszystkie te metody w jakieś formie już istniały w paczkach pomocniczych NuGet, a teraz te metody doszły permanentnie do .NET 6.

var products1 = new[] {
    (Name: "Q", Price: 17),
    (Name: "W", Price: 100),
    (Name: "E", Price: 70),
    (Name: "Y", Price: 17),
 };
 
 var b1 = products1.MaxBy(p => p.Price)
     .Name == "W";
 var b2 = products1.MinBy(p => p.Price)
     .Name == "Q";

MaxBy i MinBy łatwo jest wyjaśnić. Normalnie metody Max i Min szukają metody Equals() i Compare() dla całego obiektu klasy i na tej podstawie określają maksymalną wartość lub minimalną wartość w kolekcji elementów.

W tym przypadku kolekcje klasy anonimowej i w sumie nie wiem jak zadziała w niej metoda Equals() i Compare(). 

Używając MaxBy i MinBy możesz znaleźć odpowiednie elementy w kolekcji na podstawie danej właściwości. W tym przypadku szukamy w liście produktów - produktu z maksymalna cenną i minimalną cenną.

Moim zdaniem użycie tych metod jest bardzo czytelne, bo wiem czym jest maksimum lub minimum, które tutaj szukam.

Jakby co wszystkie zmienne w tym kodzie zwracają prawdę tak, abyś mógł zobaczyć, jakie są wyniki tych metod.

var products1 = new[] {
   (Name: "Q", Price: 17),
   (Name: "W", Price: 100),
   (Name: "E", Price: 70),
   (Name: "Y", Price: 17),
};

var products2 = new[] {
   (Name: "R", Price: 17),
   (Name: "T", Price: 12), };

var b3 = products1.DistinctBy(p => p.Price)
   .Select(p => p.Name).SequenceEqual
   (new[] { "Q", "W", "E" });


var b4 = products1.UnionBy(products2,
    p => p.Price)
   .Select(p => p.Name).SequenceEqual
   (new[] { "Q", "W", "E", "T" });

// Q : 17
var b5 = products1.IntersectBy(products2.
    Select(p => p.Price), p => p.Price)
   .Select(p => p.Name).SequenceEqual
   (new[] { "Q" });

var b6 = products1.ExceptBy(products2.Select
    (p => p.Price),
    p => p.Price)
   .Select(p => p.Name).SequenceEqual
   (new[] { "W", "E" });

Metody DistinctBy, UnionBy, IntersectBy, ExceptBy analogicznie działają na podstawie danej właściwości.

Mogę w ten sposób przy użyciu DistinctBy łatwo znaleźć np. produkty z unikatową ceną.

TryGetNonEnumeratedCount

Istnieje pewien problem z kolekcją IEnumerable. Jeśli chcesz pobrać jej rozmiar no to niestety musisz wykonać pobranie wszystkich elementów tej leniwej kolekcji.

Postała metoda "TryGetNonEnumeratedCount", która może pobrać rozmiar całej kolekcji bez pobrania wszystkiego od razu.

Oto przykład użycia tej metody

IEnumerable<int> seq1 = new[] { 0, 1, 2, 3, 4 };

seq1.TryGetNonEnumeratedCount(out int count1);
Console.WriteLine(count1 == 5);

IEnumerable<int> seq2 = new
    BetterCollectionBeacuseItsMine
    <int>();

var result =
    seq2.TryGetNonEnumeratedCount(out int count2);
Console.WriteLine(result);

class BetterCollectionBeacuseItsMine<T> : IEnumerable<T>
{
    public IEnumerator<T> GetEnumerator()
    { throw new NotImplementedException(); }

    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

W LINQ doszło także przeciążenie metody "FirstOrDefault". Teraz możesz określić jaka wartość ma się pojawić, gdy nie znajdziemy szukanego elementu.

var arr2 = new[] { 0, 2, 4, 6, 8, 10 };

var c1 = arr2.FirstOrDefault(x => x > 11) == 0;
                        
var c2 = arr2.FirstOrDefault(x => x > 11, -1) == -1;

Od .NET 6 i C# 10 możesz korzystać ze specjalnego operatora "Range" wewnątrz metod LINQ jak "ElementAt()" czy "Take()"

var arr = new[] { 0, 1, 2, 3, 4, 5 };

var a1 = arr.ElementAt(^2) == 4;

var a2 = arr.ElementAtOrDefault
    (^10) == default;

var a3 = arr.Take(^2..).
    SequenceEqual(new[] { 4, 5 });

Co nowego w asynchroniczności ?

W asynchroniczności pojawiły się dwie nowe metody.

Pierwsza metoda "WaitAsync" daje Ci możliwość określenia ile chcesz czekać na danych proces asynchroniczny, aby się zakończył.

Jeśli czas zostanie przekroczony jak w tym przypadku to dostaniesz wyjątek "TimeOut Exception".

await ExampleAsync();
Console.Read();

static async Task<int> ExampleAsync()
{
    var operation = SomeLongRunningOperationAsync();

    return await operation.WaitAsync
        (TimeSpan.FromSeconds(3));
}

static async Task<int> SomeLongRunningOperationAsync()
{
    await Task.Delay(5000);
    return 1;
}

Pojawiła się także metoda "Parallel.ForEachAsync", która daje Ci możliwość wykonania asynchronicznie listy zadań, które tak jak widzisz są fragmentami bloku kodu.

static async Task Example2Async()
{
    var urlsToDownload = new[]
    {
                "https://cezarywalenciuk.pl/",
                "https://twitter.com/WalenciukC",
    };

    var client = new HttpClient();

    await Parallel.ForEachAsync(urlsToDownload,
        async (url, token) =>
        {
            var response = await client.GetAsync(url);

            if (response.IsSuccessStatusCode)
            {
                Console.WriteLine
                (await response.Content.ReadAsStringAsync());
            }
        });
}

Co nowego w System.Text.Json ?

Zapewne nie jesteś fanem System.Text.Json?

Mnie to nie dziwi, bo sam nie jestem jego fanem po tym, jak właśnie na tym blogu wyrzucał on ciche wyjątki i restartował cały serwer. Mówimy tutaj o blogu, a co by było z poważną aplikacją korporacyjną.

Tak czy siak, Microsoft chce Cię przekonać do użycia System.Text.Json, abyś na zawsze porzucił paczkę NuGet "Newsoft Json"

Doszły nam atrybuty, które określają kolejność deserializacji właściwości do formatu JSON

public class Game
{
    [JsonPropertyOrder(1)]
    public int Year { get; set; }

    public string Name { get; set; } // 0

    //serialize after Year
    [JsonPropertyOrder(2)] 
    public string Company { get; set; }

    // serialize before other properties
    [JsonPropertyOrder(-1)] 
    public int Id { get; set; }
}

Przetestujmy tę kolejność 

Game game = new()
{
    Company = "Konami",
    Name = "Silent Hill",
    Id = 564,
    Year = 1999
};

JsonSerializerOptions options =
    new () { WriteIndented = true};

string json = JsonSerializer.Serialize(game,options);

Console.WriteLine(json);

Jak widzisz atrybut działają poprawnie.

Serializacja JSON Przykład

JsonNode, JsonObject, JNode

.NET 6 oferuje zbiór nowych klas, które określają nam gałąź, obiekt, tablice JSON i tak dalej.

Potem na postawie tych opakowanych wartości tekstowych JSON możemy zajrzeć głębiej. 

JsonNode jNode = JsonNode.Parse
("{\"array\":[1,6,7],\"name\":\"Cezary\"}");
        
string name = (string)jNode["name"];
Console.WriteLine(name);
        
string name1 = jNode["name"].GetValue<string>();
        
int number = jNode["array"][2].GetValue<int>();

Dzięki tym klasom mogę łatwiej utworzyć zawartość tekstową JSON.

var jObject = new JsonObject()
{
    ["name"] = "Konrad",
    ["array"] = new JsonArray(2, 3, 4, 5)
};

string json = jObject.ToJsonString();
Console.WriteLine(json);

Do większej kontroli nad procesem deserializacji i serializacji JSON zostały stworzone następujące interfejsy. 

Dają one możliwość walidacji procesu, zanim on się zakończy

public class TShirt : IJsonOnDeserialized, 
IJsonOnDeserializing,
IJsonOnSerializing,
IJsonOnSerialized
{
    public void OnDeserialized()
    { }

    public void OnDeserializing()
    { Validate(); }

    public void OnSerialized()
    { }
    public void OnSerializing()
    { Validate(); }

    public string Color { get; set; }
    private void Validate()
    {
        if (Color is null)
            throw new InvalidOperationException
                ($"{nameof(Color)} Can not be null");
    }
}

W .NET 6 System.Text.Json wspiera IAsyncEnumerable

IAsyncEnumerable to kosmiczna bomba, która za prostym interfejsem na pewno ukrywa skomplikowane rozwiązanie wielowątkowe

Dzięki temu możesz napisać taki kod,  który reprezentuje asynchronicznym strumień, który da Ci 5 liczb z przerwą co dwie sekundy

public static async IAsyncEnumerable<int> GetNumbersAsync()
{
    for (int i = 0; i < 5; i++)
    {
        await Task.Delay(2000);
        yield return i;
    }
}

Co ciekawe teraz możesz taki strumień serializować do JSON-a i wygląda to tak.

JsonSerializerOptions options = new() { WriteIndented = true };

using Stream stream = Console.OpenStandardOutput();

var data = new { Number = GetNumbersAsync() };

await JsonSerializer.SerializeAsync(stream, data, options);

Robi wrażenie prawda

bn

System.Text.Json wspiera strumienie "Streams"

Oto przykład ze strumieniami z klasy Stream

public class ToRemember
{
    public string Conference { get; set; }
}

Oto przykład serializacji JSON do strumienia w konsoli.

JsonSerializerOptions options = new(){ WriteIndented = true };

using Stream outputStream = Console.OpenStandardOutput();

ToRemember to = new() { Conference = "5Developers" };

JsonSerializer.Serialize<ToRemember>(outputStream,to,options);

Oto przykład deserializacji do strumienia MemoryStream.

string json = "{\"Conference\":\"4Developers\"}";

byte[] bytes = Encoding.UTF8.GetBytes(json);

using MemoryStream ms = new MemoryStream();

ToRemember toRemember =
    JsonSerializer.Deserialize<ToRemember>(ms);

Console.WriteLine(toRemember.Conference);

Co nowego w C# 10? Nowy styl programowania?

W końcu zakończyliśmy gadanie o nowych klasach i metodach w .NET 6.

A co się zmieniło w C# 10? Tak jak pisałem na początku tego wpisu do C# 10 doszło parę zmian, które wydają się banalne, ale umożliwiają pisanie bardzo krótkiego kodu bez dodatkowych kosmetycznych symboli.

Pierwsza zmiana nazywa się "File-Scoped Namespaces".

Wcześniej przed C# 10 musiałeś tak deklarować przestrzeń nazw w danym pliku.

namespace ViewerApp.Models
{
    public class Viewer
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        public string? StreetAddress { get; set; }
    }
}

Teraz możesz to zrobić tak. Bez nawiasów klamrowych

namespace ViewerApp.Models;

public class Viewer2
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? StreetAddress { get; set; }
}

Ma to oczywiście pewne ograniczenia. Po pierwsze nie możesz w jednym pliku mieszać obu styli deklaracji przestrzeń nazwy.

FileScopedNamespaces nie mogą się powtarzać

W jednym pliku nie możesz użyć dwa razy deklaracji "File-Scoped Namespaces".

FileScopedNamespaces nie mogą być dwa

Jeśli z jakiegoś powodu chcesz dwie różne przestrzenie nazw w jednym pliku to musisz to zrobić to po staremu.

Takie przestrzenie nazw są poprawne

Swoją drogą możesz zadeklarować przestrzenie nazw wewnątrz innej przestrzeni nazw.

przestrzenie nazw zagnieżdżone są okej

C# to nie Python więc oczywiście nie możesz zadeklarować wielu przestrzeni nazw po nowemu w taki sposób. W Pythonie tabulacje i spację są formą nawiasów klamrowych. 

CSharp to nie Python

Implicit global usings

Druga zmiana w C# 10, która skraca kod to globalne using-i.

Przed C# 10 każdy plik musiał mieć deklaracje, z jakich przestrzeń nazw on korzysta. Wygląda to brzydko, gdy wiesz, że po raz setny musisz deklarować użycie przestrzeni nazw "System" 

Globalne using ten problem chcą rozwiązać. 

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

Console.WriteLine("Daj pozytwna ocene");
await Task.Delay(2000);

List<string> list = new List<string>();
list.Add("Tej prelekcj");

Deklarujesz raz dany using i jest on potem przestrzeganych we wszystkich plikach w danym projekcie. .NET 6 domyślnie takie globalne using-i nawet dodaje za Ciebie.

Sprawdź najpierw, czy w twoim projekcie .NET 6 lub wyżej są one uruchomione.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    </PropertyGroup>

</Project>

Potem możesz zobaczyć, że pewne deklaracje przestrzeni nazw nie musisz już robić, a kod nadal się skompiluje.

//using System;
//using System.Collections.Generic;
//using System.Threading.Tasks;

Console.WriteLine("Daj pozytwna ocene");
await Task.Delay(2000);

List<string> list = new List<string>();
list.Add("Tej prelekcj");

.NET 6 za ciebie w zależności od konkretnego szablonu projektu wygenerował niewidoczny dla Ciebie plik. Ten plik ma właśnie deklaracja globalnych using-ów.

Gdzie generuje pliki z global using Visual Studio 2022

Dla aplikacja konsolowej wygląda to tak:

// <auto-generated/>
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;

Dla aplikacji ASP.NET Core domyślne globalne using-i wyglądają tak.

// <autogenerated />
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 global::System.Net.Http.Json;
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;

Oczywiście możesz dodać swoje globalne deklaracje używania przestrzeni nazw. Możesz to nawet zrobić na dwa sposoby.

W projekcie:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    </PropertyGroup>
    <ItemGroup>
    <Using Include="System.Text.Json" />
    <Using Include="System.Text.Json.Serialization" />
    <Using Remove="System.IO" />
    <Using Include="System.Console" Static="True" />
    <Using Include="System.DateTime" Alias="DT" />
    </ItemGroup>
</Project>

Jak i bezpośrednio w kodzie. Nie ma zasady gdzie takie deklaracje zrobisz. Ja tworze plik "global.cs" i w nim zawieram te deklaracje.

global using ViewerApp.Models;
global using static System.Console;
global using DT = System.DateTime;

Na dwa sposoby zrobiliśmy globalne deklaracje użycia przestrzeni : ViewerApp.Models,

Jak widzisz zrobiliśmy także deklaracje użycia przestrzeni do metod statycznych z System.Console

Co daje nam możliwość napisania takiego kodu wszędzie w całym projekcie.

Globalne using dają taki efekt

Zrobiliśmy także globalny alias "DT" do struktury "System.DataTime".

Taka prosta rzecz, a ja dużo ona usuwa niepotrzebnego kodu.

Szablon projektu konsolowego

Pora na bardziej kontrowersyjną zmianę, która może oburzyć starego programistę C#.

Te dwie nowe funkcjonalności w połączeniu z "Top-Level statements" z C# 9.0 dają możliwość na bardzo krótkie napisanie aplikacji "Hello World" w C#. 

Wcześniej nasz hello world wyglądał tak:

using System;

namespace ConsoleApplication87
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

Teraz jednak gdy utworzymy w Visual Studio 2022 projekt konsolowy to zobaczymy znacznie skróconą jego formę.

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

Jaki jest cel takiej zmiany.

Tak jak mówiłem na początku C# chce przykuć uwagę nowych młodych programistów, którzy widzieli jak nie wiele kodu wystarczy napisać w Pythonie, NodeJS, GO, aby zacząć swoją przygodę.

Dlatego C# poszedł także w tym kierunku.

Oczywiście rodzi to już znany problem z języka C# pod tytułem "Na ile sposobów mogę zrobić dokładnie to samo w tym języku programowania"

Dlaczego minimalne aplikacje w C#?

  • Łatwo jest teraz pokazać kod na Twitterze czy YouTube
  • C# wygląda jak NodeJs, GO, Python 
  • Dużo kodu Boilerplate jest usuwane
  • Jest to zmiana kierowana do nowych programistów więc to normalne, że może Ci się to nie podobać.
  • W przyszłości ten styl programowanie będzie super wyglądał w Interaktywnych Notebookach znanych z języka Python
  • Te wszystkie zmiany są fantastyczne dla Source Generators

Szablon aplikacji ASP.NET Core

A jak się zmienił szablon ASP.NET Core w .NET 6? 

Dla przypomnienia uruchamiam Visual Studio 2019

Visual Studio 2019 okno startowe

Wybieram szablon "ASP.NET Core Empty".

Wybranie szablonu ASP.NET Core Empty w Visual Studio 2019

Wybieram ".NET 5.0".

Tworzenie pustego szablonu w Visual Studio 2019 z .NET 5

Nadaje nazwę swojemu projektowi.

Tworzenie pustego szablonu w Visual Studio 2019

...i oto szablon ASP.NET CORE z Visual Studio 2019.

Jak widzisz mamy klasę Program.cs i w niej był kod związany ze startem serwera naszej aplikacji HTTP.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace DotNetEmptyTemplate
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

W klasie Startup.cs mieliśmy deklaracje co ma być wstrzyknięte do kontenera i jak ma być obsłużmy potok HTTP.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DotNetEmptyTemplate
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

Najczęściej była to deklaracja użycia Kontrolerów w tej aplikacji do obsługi żądań HTTP.

Pusty szablon dla ASP.NET Core w .NET 5

A jak wygląda ten szablon w .NET 6.0.

Wybieramy .NET 6.0 w Visual Studio 2022

Jak widzisz klasy "Startup.cs" nie ma.

Pusty Szablony ASP.NET Core w .NET 6

W związku z tymi minimalnymi zmianami w C# 10 szablon ASP.NET CORE Empty także uległ zmianie.

Cały kod startu serwera, wstrzykiwania zależności, definicji obsługi potoku HTTP znajduje się teraz w jednym pliku Program.cs.

Zawartość pliku wygląda tak:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

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

app.Run();

W szablonie dla kontrolerów MVC ten plik Program.cs wygląda tak:

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Ta zmiana może wzbudzić różne emocje, zwłaszcza jeśli tworzysz tutoriale do ASP.NET Core.

Mam kolejną niby małą zmianę, która sprawia, że wszystkie poprzednie tutoriale na temat podstaw ASP.NET Core są teraz nieaktualne i mylące.

Miejmy nadzieje, że te podstawy w ASP.NET Core nie będą już ulegać zmianie

Jak łatwo jest stworzyć aplikację ASP.NET Core z niczego ?

Skoro pozbyliśmy się tylu ceremonii w pisaniu aplikacji ASP.NET Core to chciałem Ci właśnie pokazać jak łatwo nawet bez szablonu taką aplikację stworzyć.

Ktoś by powiedział, że tak powinno być od początku, ale pamiętaj C# wywodzi się od starej Javy, a nie od najnowszych wspaniałych języków programowania jak : GO

W Visual Studio 2022 wybiorę szablon Console-App

Wybranie szablonu Console App w Visual Studio 2022

Mam już tyle tych aplikacji konsolowych, że ten projekt ma numer 87.

Tworzenie projektu dodanie nowej nazwy

Wybieram .NET 6 co automatycznie da mi minimalny szablon aplikacji konsolowej, który omówiliśmy wcześniej.

Tworzenie projektu wybranie .NET 6.0

Do aplikacji chce dodać paczkę NuGet z ASP.NET Core.

Dodanie paczki NuGet w Visual Studio 2022

Znajduje odpowiednią paczkę i...

Paczka NuGet Microsoft.AspNetCore.App dla ASP.NET Core

...dodaje prosty kod obsługi adresu "/", który wyświetli mi napis "Daj dobrą ocenę".

Minimalny kod aplikacji ASP.NET Core w .NET 6

Ten kod mogę nawet rozszerzyć, aby było wstrzykiwanie zależności.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<HelloService>
    (new HelloService());

var app = builder.Build();

app.MapGet("/", () => "Daj dobrą ocenę");
app.MapGet("/hello", (HttpContext context, HelloService service)
     => service.SayHello(context.Request.Query["name"]
     .ToString()));

await app.RunAsync();

public class HelloService
{
    public string SayHello(string name)
    {
        return $"Hello {name}";
    }
}

Daj komentarz pod tym wpisem, aby dać mi sugestię, abym zrobił więcej wpisów na temat minimalnych aplikacji ASP.NET Core.

Jedno pllikowy bałagan ?

Czy takie jedno-plikowe aplikacje nie zrobią bałaganu? 

Pamiętaj, że nadal możesz korzystać kontrolerów. Jeśli jednak chcesz pisać aplikację w nowym stylu to nic nie szkodzi, abyś stworzył klasę statyczną, które odseparują kod od tego jednego pliku Program.cs

public static class Routes
{
    public static IEndpointRouteBuilder UseHelloEndpoint
        (this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapPost("api/hello", async context =>
        {

        });

        return endpoints;
    }
}

Możesz to nawet zrobić jeszcze lepiej.

Lepsze rozwiązania dla minimalnych aplikacji

O tym rozwiązaniu zrobiłem filmik na YouTubie

Oto te rozwiązanie w skrócie:

W wielu projektach/plikach tworzysz swoje definicje Endpointów. Każda taka definicja musi implementować interfejs IEndpointDefinition

public class EndpointDefinition : IEndpointDefinition
{
    public void DefineEndpoints(WebApplication app)
    {
        app.MapPost("api/hello", async context =>
        {

        });
    }

    public void DefineServices(IServiceCollection services)
    {
        
    }
}

Taka definicja endpointu ma dwie metody. 

public interface IEndpointDefinition
{
    void DefineServices(IServiceCollection services);

    void DefineEndpoints(WebApplication app);
}

Metoda "DefineServices" zadeklaruje, z jakich wstrzykiwań zależności dany endpoint korzysta.

Metoda "DefineEndpoint" zadeklaruje jak dany adres HTTP powinien być obsłużony.

public static class EndPointDefinitionExtension
{
    public static void AddEndpointDefinitions(
        this IServiceCollection services,params Type[] scanMarkers)
    {
        var endpoints = new List<IEndpointDefinition>();

        foreach (var scanMarker in scanMarkers)
        {
            endpoints.AddRange(
                scanMarker.Assembly.ExportedTypes
                .Where(x => typeof(IEndpointDefinition)
                .IsAssignableFrom(x) && (!x.IsInterface && !x.IsAbstract))
                .Select(Activator.CreateInstance)
                .Cast<IEndpointDefinition>()
            );
        }

        foreach (var endpoint in endpoints)
        {
            endpoint.DefineServices(services);
        }

        services.AddSingleton
            (endpoints as IReadOnlyCollection<IEndpointDefinition>);
    }

    public static void UseEndpointDefinitions(this WebApplication app)
    {
        var defs = app.Services.
        GetRequiredService<IReadOnlyCollection<IEndpointDefinition>>();

        foreach (var def in defs)
        {
            def.DefineEndpoints(app);
        }
    }
}

Teraz chcemy to wszystko skleić. Stworzyłem metodę rozszerzeniową, która przeskanuje dany projekt w poszukiwaniu klas, które implementują interfejs IEndpointDefinition.

Będę miał kolekcję takich klas. W każdej takiej klasie użyje metody "DefineService" , aby zarejestrować wszystkie potrzebne wstrzykiwania zależności potrzebne dla danego EndPointa.

Na koniec zachowam całą kolekcję klas EndPointów do kontenera wstrzykiwania zależności. 

Gdy zbuduje aplikację ASP.NET Core wtedy skorzystam z metody "UseEndpointDefinitions", która zatwierdzi wszystkie adresy i metody HTTP z tej mojej kolekcji Endpointu.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointDefinitions
(typeof(EndpointDefinition));

var app = builder.Build();

app.UseEndpointDefinitions();
app.Run();

Tak możesz uniknąć bałaganu jednego pliku.

Oczywiście rodzi się pytanie, czy te rozwiązanie jest szybsze niż kontrolery.

Odpowiedź może Cię zaskoczyć, ale w moim przypadku rozwiązanie minimalnych aplikacji okazało się szybsze niż kontrolery. W twoim jednak przypadku radzę jednak to zweryfikować.

Jak łatwo jest stworzyć aplikację SignalR od zera?

Dzięki tym zmianom w C# 10 chce Ci także pokazać jak szybko można od zera napisać aplikację klient/serwer SignalR.

Tworzymy więc projekt konsolowy i dodajemy do niego paczkę NuGet SignalR.

Paczka NuGet Microsoft.AspNetCore.SignalR

Oto nasz kod, który zarejestruje prosty strumień danych.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();
builder.Services.AddCors(options =>
{
    options.AddPolicy("CorsPolicy", builder => builder
        .WithOrigins("http://localhost:56675")
        .AllowAnyMethod()
        .AllowAnyHeader()
        .AllowCredentials());
});

var app = builder.Build();

app.UseCors("CorsPolicy");
app.MapHub<MyHub>("/chat");
app.Run();

Nasz strumień będzie co sekundę być może wyśle nam obecny czas. Dlaczego być może?

Aby za symulować, nie przewidywalność takiego wysyłania danych do strumienia...dodałem do niego sprawdzenie także wartości losowej, która ma szanse w 50% być odrzucona.

public class MyHub : Hub
{
    public async IAsyncEnumerable<string> Streaming
    (CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (_random.Next(0, 10) < 5)
                yield return DateTime.UtcNow.ToString("hh:mm:ss");

            try
            {
                await Task.Delay(1000,stoppingToken);
            }
            catch (OperationCanceledException)
            {
                yield break;
            }
        }
    }

    private Random _random = new Random();
}

Jak będzie wyglądał kod po stronie klienta?

Jeśli interesuje Cię cały projekt to polecam swój filmik na YouTube.

Projekt klient serwer SignalR

Po stronie klienta instaluje odpowiednią paczkę NuGet. 

Paczka NuGet Microsoft.AspNetCore.SignalR.Client

Klientem oczywiście minimalny projekt konsolowy z .NET 6

using Microsoft.AspNetCore.SignalR.Client;

var uri = "http://localhost:5000/chat";

await using var connection = new HubConnectionBuilder()
    .WithUrl(uri)
    .Build();

await connection.StartAsync();

await foreach (var item in
    connection.StreamAsync<string>("Streaming"))
{
    Console.WriteLine(item);
}

Tyle kodu wystarczy, aby wysłać dane do strumienia i potem je odczytać.

Inne zmiany w C# 10 

Oczywiście mamy także inne zmiany w naszym języku programowania. 

Możesz teraz korzystać ze słowa kluczowego "with" w strukturach. Wcześniej te słowo kluczowe było zarezerwowane tylko dla "rekordów".

Drink drink = new () { Fat = 20, TotalVolume= 200};

Drink green = drink with { Fat = 40};

public struct Drink
{
    public int Fat { get; set; }
    public int TotalVolume { get; set; }
}

Jeżeli twoja struktura inicjalizuje właściwości w taki sposób to kompilator stworzy bezparametrowy konstruktor. 

public struct Drink
{
    public int Fat { get; set; } = 1;
    public int TotalVolume { get; set; } = 2;
}

Do C# 10 doszły struktury zapisane jako rekord.

record struct DrinkRecord(int fat, int totalVolume);

Możliwe też jest jawne określenie typów zwracanych w wyrażeniach Lambda.

var a1 = string () => string.Empty;

var a2 = int () => int.MaxValue;

var a3 = static void () => { };

var a4 = string? () => null;

public static void Example<T>()
{
    var a5 = T () => default;
}

Można teraz też dodawać swoje atrybuty do wyrażeń lambda w taki sposób.

Func<int> f1 = [MyAttribute] () => { return 0; };
Func<int> f2 = [return: MyAttribute] () => { return 0; };
Func<int,int> f3 = ([MyAttribute] x)=> { return x; };

class MyAttribute : Attribute
{

}

W C# 10 można deklarować i przypisywać zmienne do Dekonstruktora. 

int a;int b;
(a, int r, int g,b) = (255, 100, 0, 150);

Generyczne atrybuty istniały w wersji Preview i z tego, co wiem ta zmiana przeszła do C# 11.

Generyczne atrybuty w CSharp 11. Być może

Teraz też możesz dodać słowo kluczowe "sealed" do przeciążonych metod w rekordach.

public record Game
{
    public sealed override string ToString()
    {
        return base.ToString();
    }
}

Został także uproszczona referencja do pół i właściwości przez technikę "property pattern".

var speech = new { Speaker = new { Name = "C" } };

//C# 9
if (speech is { Speaker: { Name :"C"} }) { }

//C# 10
if (speech is { Speaker.Name: "C"}) { }

Jest możliwe łączenie napisów stałych ("const") w taki sposób, ale wszystkie zmienne muszą być typu string.

Za komentowany kod pokazuje co nie zadziała, ponieważ konwersja liczby do napisu wymaga uruchomienia kodu.

const string NAME = "MyApplication";
const string FULLNAME = $"{NAME} 1.20";

const double VERSIONP = 1.20;
//const string FULLNAME2 = $"{NAME} {VERSIONP}";

Statyczne abstrakcyjne metody w interfejsach

Na koniec pokaże Ci zmianę, która nie weszła oficjalnie do C# 10. Są to abstrakcyjne statyczne metody. Wygląda to dziwnie, ale ma to swoje zastosowanie.

Oto moja taka metoda.

public interface ICreatable<TSelf, TArg1, TArg2, TArg3> where TSelf : ICreatable<TSelf, TArg1, TArg2, TArg3>
{
    static abstract TSelf Create(TArg1 arg1,
        TArg2 arg2, TArg3 arg3);
}

Jej implementacja wygląda tak.

public class Game : ICreatable<Game, string, string, int>
{
    public Game(string name, string desc, int year)
    {
        Name = name; Description = desc; Year = year;
    }

    public string Name { get; set; }
    public string Description { get; set; }
    public int Year { get; set; }

    public static Game Create
        (string name, string desc, int year)
    {
        return new Game(name, desc, year);
    }
}

Game.Create("a", "a", 1);

Ma to jednak drugie dno. Wyobraź sobie, że wszystkie liczby w .NET implementują interfejs, który informuje Cię, że dany typ rzeczywiście jest liczbą.

Mógłbyś dzięki temu napisać taki kod dodawania wartości liczbowy nie wnikając jakim dokładnym programistycznym typem jest ta liczba.

static T Add<T>(T left, T right) where T : INumber<T>
{
    return left + right;
}

static T ParseInvariant<T>(string s) where T : IParseable<T>
{
    return T.Parse(s,
        CultureInfo.InvariantCulture);
}

Console.Write("First number: ");
var left = ParseInvariant<float>(Console.ReadLine());

Console.Write("Second number: ");
var right = ParseInvariant<float>(Console.ReadLine());

Console.WriteLine($"Result: {Add(left, right)}");

Analogicznie mógłbyś napisać metodę parsującą, która działa dla wszystkich typów, które by implementowały interface IParseable<T>.

A co nowego w Visual Studio 2022 ?

W Visual Studio 2022 największa zmiana polega na tym, że nie jest już to program 32-bitowy. Programy 32-bitowe mogły zajmować tylko 4GB Ramu pamięci.

Oczywiście możesz zadać mi takie dobre pytanie:

"..ale Czarek moje Visual Studio już wcześniej jadło więcej Ramu".

Szkopuł polegał na tym, że Visual Studio 2019 32-bitowe musiał rozbijać się na pod procesy, aby korzystać z więcej Ramu. Te procesy też musiały marnować czas, aby gadać ze sobą.

Teraz gdy Visual Studio 2022 jest 64-bitowe takiego problemu nie ma. 

Synchronizowanie przestrzeń nazw w Visual Studio 2022

Warto też spojrzeć na nowe rozwiązanie "Hot Reload", które pojawiło się Visual Studio 2022.

Jeśli chodzi o inne zmiany w VS 2022 nie ma co się bardziej rozpisywać. Co chwilę pojawiają się nowe opcje w programie i chyba tak zostanie, zwłaszcza że Visual Studio ma teraz konkurencje w postaci programu "Rider" od firmy JetBrains.

Inne wpisy z cyklu co nowego w C#: