Neuroróżnorodność w miejscu pracy
22.11.20238 min
Grzegorz Magrian
ONWELO Sp. z o.o.

Grzegorz MagrianFullstack .NET DeveloperONWELO Sp. z o.o.

Kluczowe zmiany i nowości w .NET 8

Poznaj najnowsze funkcje, usprawnienia i narzędzia, które ogłoszono w platformie .NET 8.

Kluczowe zmiany i nowości w .NET 8

W najnowszej wersji platformy .NET, czyli .NET 8, deweloperzy mogą cieszyć się wieloma innowacyjnymi funkcjami, usprawnieniami i narzędziami. Zwiększają one wydajność, elastyczność oraz ułatwiają proces tworzenia oprogramowania. 

Wprowadzone zmiany poprawiają stabilność, usprawniają biblioteki odpowiedzialne za serializację danych, dodają możliwość nowego typu rejestrowania zależności w mechanizmie wstrzykiwania oraz wdrażają wiele innych udoskonaleń, dzięki którym .NET 8 jest jeszcze bardziej atrakcyjną platformą dla programistów. 

Przeanalizujmy zatem, jakie kluczowe nowości przynosi ostatnia odsłona .NET i jakie korzyści mogą z niej czerpać twórcy aplikacji. Dodatkowo warto zaznaczyć, że .NET 8, będący następcą .NET 7, będzie wspierany przez trzy lata jako wersja długoterminowego wsparcia (LTS). W tym artykule znajdziesz skrót najistotniejszych zmian zawartych w .NET 8.

Uaktualnienia formatu JSON

Zespół .NET wykonał wiele pracy nad narzędziami do obsługi formatu JSON, nie tylko w zakresie dodania obsługi nowych typów. Nowe metody API ułatwiają zapisywanie konkretnych węzłów w dokumencie JSON, podczas gdy inne funkcje poprawiają zarządzanie zawartością JSON-a w .NET.

Te aktualizacje pomagają zapewnić integralność dokumentów JSON i umacniają rolę .NET w rozwoju cloud-native, ponieważ to włąsnie JSON teraz stanowi najczęściej używany format podczas wywoływania interfejsów REST API.

Wybrane zmiany

  • Usprawnienia generatora kodu źródłowego mechanizmu serializacji, które mają na celu wyrównanie natywnego środowiska AOT z serializatorem opartym na odbiciu.
  • Obsługa serializacji hierarchii interfejsu. Hierarchie i ich właściwości są serializowane zarówno z natychmiast zaimplementowanego interfejsu, jak i jego interfejsu podstawowego.
IDerived value = new DerivedImplement { Base = 0, Derived = 1};
JsonSerializer.Serialize(value); // {"Base":0,"Derived":1}
public interface IBase
{
    public int Base { get; set; }
}
public interface IDerived : IBase
{
    public int Derived { get; set; }
}
public class DerivedImplement : IDerived
{
    public int Base { get; set; }
    public int Derived { get; set; }
}
  • JsonNamingPolicy od teraz zwiera również nowe zasady nazewnictwa dla snake_case (z podkreśleniami) i kebab-case (z łącznikami). Można użyć ich w podobny sposób, jak istniejący JsonNamingPolicy.CamelCase.
var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
JsonSerializer.Serialize(new { PropertyName = "value"}, options);
// { "property_name" : "value" }
  • Teraz można wykonać deserializację na polach lub właściwościach tylko do odczytu (czyli na tych, które nie mają zdefiniowanej metody set)
  • Typy JsonNode obejmują nowe metody, ułatwiające modyfikację struktury formatu JSON. Są to m.in.:
// Tworzy głęboką kopię bieżącego węzła wraz z wszystkimi jego potomkami.
public JsonNode DeepClone();

// Zwraca true, jeśli dwa węzły mają równoważne reprezentacje JSON.
public static bool DeepEquals(JsonNode? node1, JsonNode? node2);

// Określa JsonValueKind bieżącego węzła.
public JsonValueKind GetValueKind(JsonSerializerOptions options = null);

// Jeśli węzeł jest wartością właściwości w obiekcie nadrzędnym,
// zwraca jej nazwę. W przeciwnym razie zgłasza wyjątek InvalidOperationException.
public string GetPropertyName();

// Jeśli węzeł jest elementem nadrzędnego JsonArray,
// zwraca jego indeks. W przeciwnym razie zgłasza wyjątek InvalidOperationException.
public int GetElementIndex();

// Zastępuje tę instancję nową wartością,
// aktualizując odpowiednio obiekt/tablicę nadrzędną.
public void ReplaceWith<T>(T wartość);

// Asynchronicznie parsuje strumień jako dane kodowane w UTF-8
// reprezentujące pojedynczą wartość JSON do JsonNode.
public static Task<JsonNode?> ParseAsync(
    Stream utf8Json,
    JsonNodeOptions? nodeOptions = null,
    JsonDocumentOptions documentOptions = default,
    CancellationToken cancellationToken = default);
  • Możliwość włączania do serializowanej wiadomości niepublicznych pól, które chcemy w niej zawrzeć, oznaczając je za pomocą nowego atrybutu [JsonInclude]. Wraz z wcześniej już wspomnianym atrybutem, otrzymaliśmy kolejny, równie przydatny atrybut. Jest nim [JsonConstructor]. Umożliwia zdefiniowanie metody, która ma zostać użyta do serializacji nowo tworzonego obiektu


W tym obszarze pojawiło się więcej zmian, niż te wymienione wyżej. Moim zdaniem warto się nimi zainteresować. Szczegółowe informacje znajdują się w oficjalnym artykule Microsoftu.

Abstrakcja czasu

Klasa TimeProvider i interfejs ITimer, które zostały niedawno wprowadzone, oferują funkcjonalność abstrakcji czasu, ułatwiając symulację czasu w scenariuszach testowych. TimeProvider jest klasą abstrakcyjną, zawierającą wiele funkcji wirtualnych, co czyni ją idealnym kandydatem do integracji z frameworkami służącymi do tworzenia mocków. Pozwala to na bezproblemowe i kompleksowe tworzenie symulacji wszystkich aspektów abstrakcji czasu.

Dobrze, że ta funkcjonalność w końcu została wprowadzona, ponieważ abstrakcje czasu od lat były tematem dyskusji, a ich pojawienie się było od dawna oczekiwane.

Przykład użycia w kodzie znajduje się poniżej. 

static DateTimeOffset AddNDaysToUtcNow(TimeProvider timeProvider, int nbDays){
    return timeProvider.GetUtcNow().AddDays(nbDays);
}
[Test]
public void Test_AddNDaysToNow(){
    var ttp = new TestTimeProvider(new DateTimeOffset(new DateTime(2023,10,30,0,0,0)));
    var result = AddNDaysToUtcNow(ttp, 5);
    Assert.IsTrue(result.Year == 2023);
    Assert.IsTrue(result.Month == 11);
    Assert.IsTrue(result.Day == 4);
}
class TestTimeProvider : TimeProvider {
    private readonly DateTimeOffset m_UtcNow;
    public TestTimeProvider(DateTimeOffset utcNow) { this.m_UtcNow = utcNow; }
    public override DateTimeOffset GetUtcNow() { return m_UtcNow; }
}


Warto zauważyć, że abstrakcję czasu można również wykorzystać do symulowania operacji zadań (Task), które zależą od postępu czasu, takich jak Task.Delay() i Task.WaitAsync().

Nowe typy nastawione na wydajność

.NET 8 oferuje nową przestrzeń nazw System.Collections.Frozen. Zawiera ona nowe klasy kolekcji FrozenSet<T> i FrozenDictionary<TKey,TValue>. Kwalifikator Frozen oznacza, że kolekcje są niezmienne, czyli nie można ich zmienić po utworzeniu. Wewnętrznie implementacja wykorzystuje ten wymóg, aby umożliwić szybsze wyliczanie i szybsze operacje wyszukiwania, takie jak Contains() lub TryGetValue(). Te nowe zamrożone kolekcje okazują się szczególnie cenne w scenariuszach, w których kolekcje są początkowo wypełniane, a następnie utrzymywane przez cały cykl życia długotrwałej aplikacji.

Udostępniona została także do użytku klasa System.Buffers.SearchValues<T>, która sprawdza się szczególnie w scenariuszach, w których stały zestaw wartości jest często odpytywany podczas pracy programu. Podczas tworzenia instancji SearchValues<T> wszystkie istotne dane wymagane do optymalizacji przyszłych wyszukiwań są obliczane z wyprzedzeniem, co usprawnia proces wyszukiwania dla obiektu tej klasy.

Nowa klasa System.Text.CompositeFormat okazuje się nieoceniona przy optymalizacji ciągów formatujących, takich jak "Imię: {0} Nazwisko: {1}", które nie są znane w czasie kompilacji. Chociaż istnieje początkowe obciążenie w zadaniach takich jak analiza ciągów, to podejście proaktywne znacząco redukuje obciążenie obliczeniowe w późniejszym użyciu, poprawiając wydajność i efektywność.

Ułatwione zarządzanie archiwami ZIP

Możliwe jest teraz kompresowanie plików z katalogu przy użyciu strumienia, bez konieczności buforowania ich w pliku tymczasowym. Pozwala to na bezpośrednie zarządzanie wynikiem kompresji w pamięci. Te nowe interfejsy okazują się korzystne w scenariuszach, w których przestrzeń dyskowa jest ograniczona, ponieważ eliminują potrzebę wykorzystania dysku jako kroku pośredniego. Oto nowe interfejsy:

public static partial class ZipFile
{
    public static void CreateFromDirectory(string sourceDirectoryName, Stream destination);
    public static void CreateFromDirectory(string sourceDirectoryName, Stream destination, CompressionLevel compressionLevel, bool includeBaseDirectory);
    public static void CreateFromDirectory(string sourceDirectoryName, Stream destination, CompressionLevel compressionLevel, bool includeBaseDirectory, Encoding? entryNameEncoding);

    public static void ExtractToDirectory(Stream source, string destinationDirectoryName) { }
    public static void ExtractToDirectory(Stream source, string destinationDirectoryName, bool overwriteFiles) { }
    public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding) { }
    public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles) { }
}

Losowanie nigdy nie było tak proste!

.NET 8 wprowadza przełomowe metody, które rewolucjonizują nasze podejście do losowości. Jest to szczególnie przydatne w aplikacjach uczenia maszynowego. Np. nowe metody:

System.Random.GetItems()


oraz

System.Security.Cryptography.RandomNumberGenerator.GetItems()


umożliwiają losowy wybór określonej liczby elementów ze zbioru wejściowego.

Ponadto Microsoft dostarczył nam także dodatkowe dwie metody. Są to:

Random.Shuffle


 oraz

RandomNumberGenerator.Shuffle<T>(Span<T>)


Umożliwiają one losową zmianę kolejności elementów w obszarze pamięci. Te metody są cenne szczególnie w uczeniu maszynowym, gdzie pomogą nam zwiększyć liczbę zróżnicowanych zbiorów danych. Zarówno treningowych, jak i testowych.

Garbage Collector

GC (Garbage Collector) w .NET 8 wprowadza DATAS (Dynamic Adaptation To Application Sizes). Dostosowuje on wykorzystanie pamięci przez aplikację na podstawie Live Data Size (LDS), który obejmuje dane długotrwałe i dane w locie podczas zdarzenia GC. 

DATAS ma dwa główne zastosowania. Jest korzystny w przypadku gwałtownych obciążeń w środowiskach o ograniczonej pamięci, takich jak aplikacje kontenerowe z limitami pamięci. Zmniejsza lub zwiększa rozmiar sterty w zależności od potrzeb. Jest przydatny w przypadku małych obciążeń przy użyciu Server GC, zapewniając, że rozmiar sterty jest zgodny z rzeczywistymi wymaganiami aplikacji.

Chociaż niektórzy początkowo nazywali go „dynamicznym GC”, kluczowym celem DATAS jest dostosowanie do rozmiaru aplikacji. GC zawsze było dynamiczne, ale DATAS dostosowuje je do obciążeń w najnowszym .NET.

Walidacja danych

Przestrzeń nazw System.ComponentModel.DataAnnotations zawiera od teraz nowe atrybuty walidacji danych, przeznaczone dla scenariuszy walidacji w usługach natywnych dla chmury. Istniejące wcześniej walidatory DataAnnotations były ukierunkowane na typową walidację danych, wprowadzanych przez interfejs użytkownika, takich jak pola w formularzu. Nowe atrybuty są przeznaczone do walidacji danych niewprowadzanych przez użytkownika, takich jak opcje konfiguracji.

Oprócz nowych atrybutów do typów RangeAttribute i RequiredAttribute dodano nowe właściwości.

W końcu kompletny mechanizm wstrzykiwania zależności!

Kluczowe usługi wstrzykiwania zależności (DI – Dependency Injection) zapewniają środki do rejestrowania i pobierania usług DI przy użyciu kluczy. Używając kluczy, można określić sposób rejestrowania i korzystania z usług. Oto niektóre z nowych interfejsów API:

  • Interfejs IKeyedServiceProvider.
  • Atrybut ServiceKeyAttribute – może być wykorzystany do wstrzyknięcia klucza użytego do rejestracji/rozwiązania w konstruktorze.
  • Atrybut FromKeyedServicesAttribute – może być użyty w parametrach konstruktora usługi w celu określenia, która usługa kluczowana ma zostać użyta.
  • Różne nowe metody rozszerzeń dla IServiceCollection do obsługi usług z kluczem, np. ServiceCollectionServiceExtensions.AddKeyedScoped.
  • Implementacja ServiceProvider dla IKeyedServiceProvider.


Moim zdaniem wprowadzenie tych zmian pozwala stwierdzić, że mechanizm wstrzykiwania zależności w .NET stał się kompletny i – w większości przypadków – wystarczający.

Hostowane usługi cyklu życia

Hostowane usługi mają teraz więcej możliwości obsługi podczas cyklu życia aplikacji. IHostedService udostępnia tylko metody StartAsync i StopAsync, a nowy interfejs IHostedLifecycleService udostępnia również następujące dodatkowe metody:

  • StartingAsync(CancellationToken)
  • StartedAsync(CancellationToken)
  • StoppingAsync(CancellationToken)
  • StoppedAsync(CancellationToken)


Te metody są uruchamiane odpowiednio przed i po istniejących punktach. Pozwala to programiście na dużo większą kontrolę zachowania podczas uruchamiania oraz zatrzymywania serwisów.

Wiele, wiele więcej!

Najnowsza odsłona platformy .NET wprowadza znacznie więcej nowych ciekawych rozwiązań oraz uprawnień. W artykule omówiłem tylko część z nich. Zainteresowanych zapraszam do zgłębienia dokumentacji .NET 8, gdzie można znaleźć szczegółowe informacje dotyczące wszelkich zmian i usprawnień. 

Niezależnie od tego, czy jesteś programistą z długoletnim doświadczeniem, czy dopiero zaczynasz swoją przygodę z .NET, warto poznać wszystkie aspekty i możliwości najnowszej wersji platformy .NET, aby być na bieżąco.

Podsumowanie

.NET 8 stanowi znaczący krok naprzód, wprowadzając mnóstwo świeżych funkcji i ulepszeń. Nowe interfejsy, klasy i możliwości są dopasowane tak, aby zapewnić konkurencyjność, wydajność i bezpieczeństwo kodu. Mowa tu o najbliższych trzech latach, czyli okresie, kiedy wersja ta będzie wpierana. 

<p>Loading...</p>