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.