Udostępnij za pośrednictwem


Co nowego w środowisku uruchomieniowym platformy .NET 8

W tym artykule opisano nowe funkcje w środowisku uruchomieniowym platformy .NET dla platformy .NET 8.

Ulepszenia wydajności

Platforma .NET 8 zawiera ulepszenia generowania kodu i kompilacji just in time (JIT):

  • Ulepszenia wydajności arm64
  • Ulepszenia SIMD
  • Obsługa rozszerzeń ISA AVX-512 (zobacz Vector512 i AVX-512)
  • Ulepszenia natywne dla chmury
  • Ulepszenia przepływności JIT
  • Pętla i ogólne optymalizacje
  • Zoptymalizowany dostęp dla pól oznaczonych ThreadStaticAttribute
  • Kolejna alokacja rejestru. Arm64 ma dwie instrukcje do wyszukiwania wektorów w tabeli, które wymagają, aby wszystkie elementy w tuple operandów były obecne w kolejnych rejestrach.
  • JIT/NativeAOT mogą teraz rozwijać i automatycznie wektoryzować niektóre operacje pamięci za pomocą SIMD, takie jak porównywanie, kopiowanie i zerowanie, jeśli mogą określić ich rozmiary w czasie kompilacji.

Ponadto optymalizacja dynamiczna sterowana profilem (PGO) została ulepszona i jest teraz domyślnie włączona. Aby ją włączyć, nie trzeba już używać opcji konfiguracji środowiska uruchomieniowego. Dynamiczne PGO działa w parze z kompilacją warstwową w celu dalszej optymalizacji kodu na podstawie dodatkowej instrumentacji zastosowanej na poziomie 0.

Średnio, dynamiczne PGO poprawia wydajność o około 15%. W zestawie porównawczym obejmującym około 4600 testów, 23% wykazało poprawę wydajności o 20% lub więcej.

Promocja struktury Codegen

Platforma .NET 8 zawiera nowe przejście optymalizacji promowania fizycznego dla generowania kodu, które uogólnia zdolność JIT do promowania zmiennych strukturalnych. Ta optymalizacja (nazywana również zamianą agregatów przez elementy skalarne) zastępuje pola zmiennych struktury zmiennymi prostymi, które JIT jest w stanie lepiej zrozumieć i dokładniej zoptymalizować.

Już wcześniej w trybie JIT tę optymalizację obsługiwano, ale wiązało się to z dużymi ograniczeniami, które obejmują:

  • Była obsługiwana tylko w przypadku struktur z czterema lub mniejszą liczbą pól.
  • Obsługiwane było tylko wtedy, gdy każde pole było typem pierwotnym lub prostą strukturą opakowującym typ pierwotny.

Usunięcie ograniczeń fizycznych eliminuje te ograniczenia, co rozwiązuje szereg długotrwałych problemów JIT.

Zbieranie śmieci

Platforma .NET 8 dodaje możliwość dostosowania limitu pamięci na bieżąco. Jest to przydatne w scenariuszach usług w chmurze, w których zapotrzebowanie przychodzi i idzie. Aby zapewnić ekonomiczność, usługi powinny skalować się i dostosowywać zużycie zasobów w zależności od wahania się popytu. Gdy usługa wykryje spadek zapotrzebowania, może skalować zużycie zasobów w dół przez zmniejszenie limitu pamięci. Wcześniej nie powiodło się to, ponieważ moduł odśmiecenia pamięci (GC) nie wiedział o zmianie i może przydzielić więcej pamięci niż nowy limit. Dzięki tej zmianie możesz wywołać interfejs API RefreshMemoryLimit(), aby zaktualizować GC przy użyciu nowego limitu pamięci.

Należy pamiętać o pewnych ograniczeniach:

  • Na platformach 32-bitowych (na przykład Windows x86 i Linux ARM), .NET nie może ustanowić nowego twardego limitu sterty, jeśli taki jeszcze nie istnieje.
  • Interfejs API może zwrócić kod stanu inny niż zero wskazujący, że odświeżanie nie powiodło się. Może się tak zdarzyć, jeśli zmniejszanie skali jest zbyt agresywne i nie pozostawia przestrzeni na manewr dla GC. W takim przypadku rozważ wywołanie GC.Collect(2, GCCollectionMode.Aggressive) w celu zmniejszenia bieżącego użycia pamięci, a następnie spróbuj ponownie.
  • Jeśli przeskalujesz limit pamięci w górę poza rozmiar, który GC uważa, że proces może obsłużyć podczas uruchamiania, wywołanie RefreshMemoryLimit powiedzie się, ale nie będzie w stanie użyć więcej pamięci niż to, co postrzega jako limit.

Poniższy fragment kodu pokazuje, jak wywołać interfejs API.

GC.RefreshMemoryLimit();

Możesz również odświeżyć niektóre ustawienia konfiguracji GC związane z limitem pamięci. Poniższy fragment kodu ustawia twardy limit sterty do 100 miibibajtów (MiB):

AppContext.SetData("GCHeapHardLimit", (ulong)100 * 1_024 * 1_024);
GC.RefreshMemoryLimit();

Interfejs API może zgłosić InvalidOperationException, jeśli twardy limit jest nieprawidłowy, na przykład w przypadku ujemnych wartości procentowych twardego limitu sterty i jeśli twardy limit jest zbyt niski. Może się tak zdarzyć, jeśli sztywny limit sterty, który zostanie ustanowiony w wyniku odświeżenia, z powodu nowych ustawień AppData lub wynikający ze zmian limitu pamięci kontenera, jest niższy niż to, co zostało już przydzielone.

Globalizacja dla aplikacji mobilnych

Aplikacje mobilne w systemach iOS, tvOS i MacCatalyst mogą zdecydować się na nowy hybrydowy tryb globalizacji korzystający z lżejszego pakietu ICU. W trybie hybrydowym dane globalizacji są częściowo pobierane z pakietu ICU i częściowo z wywołań do natywnych interfejsów API. Tryb hybrydowy obsługuje wszystkie lokalizacje obsługiwane przez urządzenia mobilne.

Tryb hybrydowy jest najbardziej odpowiedni dla aplikacji, które nie mogą działać w niezmiennym trybie globalizacji i używają kultur, które zostały przycięte z danych ICU na urządzeniach przenośnych. Można go również użyć, gdy chcesz załadować mniejszy plik danych OIOM. (Plik icudt_hybrid.dat ma rozmiar 34,5 % mniejszy niż domyślny plik danych ICU icudt.dat.

Aby użyć trybu globalizacji hybrydowej, ustaw właściwość HybridGlobalization MSBuild na true:

<PropertyGroup>
  <HybridGlobalization>true</HybridGlobalization>
</PropertyGroup>

Należy pamiętać o pewnych ograniczeniach:

  • Ze względu na ograniczenia interfejsu API natywnego nie wszystkie interfejsy API globalizacji są obsługiwane w trybie hybrydowym.
  • Niektóre z obsługiwanych interfejsów API mają inne zachowanie.

Aby sprawdzić, czy aplikacja jest dotknięta, zobacz Różnice behawioralne.

Międzyoperacyjność COM generowana przez źródło

Platforma .NET 8 zawiera nowy generator źródła, który obsługuje współdziałanie z interfejsami COM. Możesz użyć GeneratedComInterfaceAttribute, aby oznaczyć interfejs jako interfejs COM dla generatora źródłowego. Następnie generator źródła wygeneruje kod umożliwiający wywoływanie kodu C# do kodu niezarządzanego. Generuje również kod umożliwiający wywoływanie kodu napisanego w C# z niezarządzanego środowiska. Ten generator źródła integruje się z LibraryImportAttributei można używać typów z GeneratedComInterfaceAttribute jako parametry i typy zwracane w metodach z atrybutem LibraryImport.

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

[GeneratedComInterface]
[Guid("5401c312-ab23-4dd3-aa40-3cb4b3a4683e")]
partial interface IComInterface
{
    void DoWork();
}

internal partial class MyNativeLib
{
    [LibraryImport(nameof(MyNativeLib))]
    public static partial void GetComInterface(out IComInterface comInterface);
}

Generator źródła obsługuje również nowy atrybut GeneratedComClassAttribute, aby umożliwić przekazywanie typów implementujących interfejsy za pomocą atrybutu GeneratedComInterfaceAttribute do niezarządzanych kodów. Generator źródła wygeneruje kod niezbędny do uwidocznienia obiektu COM, który implementuje interfejsy i przekazuje wywołania do implementacji zarządzanej.

Metody na interfejsach z atrybutem GeneratedComInterfaceAttribute obsługują wszystkie typy, co LibraryImportAttribute, a LibraryImportAttribute teraz obsługuje typy z atrybutami GeneratedComInterfacei GeneratedComClass.

Jeśli kod w języku C# używa tylko interfejsu z atrybutem GeneratedComInterface, aby opakowywać obiekt COM z niezarządzanego kodu lub opakowywać zarządzany obiekt z C# w celu ujawnienia dla niezarządzanego kodu, możesz użyć opcji we właściwości Options, aby dostosować generowany kod. Te opcje oznaczają, że nie trzeba pisać marshallers dla scenariuszy, które wiesz, że nie będą używane.

Generator źródła używa nowego typu StrategyBasedComWrappers do tworzenia i zarządzania otokami obiektów COM oraz otokami obiektów zarządzanych. Ten nowy typ zapewnia oczekiwane środowisko użytkownika .NET dla współpracy z COM, oferując tym samym punkty dostosowywania dla zaawansowanych użytkowników. Jeśli Twoja aplikacja ma własny mechanizm definiowania typów COM lub jeśli musisz obsługiwać scenariusze, które nie są obecnie obsługiwane przez źródło generowane COM, rozważ użycie nowego typu StrategyBasedComWrappers, aby dodać brakujące funkcje do Twojego scenariusza i uzyskać takie samo środowisko użytkownika platformy .NET dla typów COM.

Jeśli używasz programu Visual Studio, nowe analizatory i poprawki kodu ułatwiają konwertowanie istniejącego kodu międzyoperacyjności modelu COM w celu użycia międzyoperacyjności generowanej przez źródło. Obok każdego interfejsu, który ma ComImportAttribute, ikona żarówki oferuje opcję konwersji do interoperation generowanej przez źródło. Poprawka zmienia interfejs tak, aby używał atrybutu GeneratedComInterfaceAttribute. Obok każdej klasy, która implementuje interfejs z GeneratedComInterfaceAttribute, żarówka oferuje opcję dodania atrybutu GeneratedComClassAttribute do typu. Po przekonwertowaniu typów możesz przenieść swoje metody DllImport, aby korzystały z LibraryImportAttribute.

Ograniczenia

Generator źródła COM nie obsługuje afinitetu do apartamentów, korzystania ze słowa kluczowego new do aktywacji CoClass COM oraz następujących interfejsów API:

  • interfejsy oparte na IDispatch.
  • interfejsy oparte na IInspectable.
  • Właściwości i zdarzenia MODELU COM.

Generator źródła powiązania konfiguracji

Platforma .NET 8 wprowadza generator źródeł, aby zapewnić konfigurację przyjazną dla AOT i przycinania w środowisku ASP.NET Core. Generator jest alternatywą dla istniejącej implementacji opartej na odbiciu.

Generator źródłowy sprawdza wywołania Configure(TOptions), Bindi Get, aby pobrać informacje o typie. Gdy generator jest włączony w projekcie, kompilator niejawnie wybiera wygenerowane metody w ramach istniejących implementacji struktury opartej na odbiciu.

Do korzystania z generatora nie są wymagane żadne zmiany kodu źródłowego. Jest ona domyślnie włączona w aplikacjach internetowych skompilowanych przy użyciu protokołu AOT, a gdy PublishTrimmed jest ustawiona na true (aplikacje.NET 8+). W przypadku innych typów projektów generator źródłowy jest domyślnie wyłączony, ale możesz wyrazić zgodę, ustawiając właściwość EnableConfigurationBindingGenerator na true w pliku projektu:

<PropertyGroup>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

Poniższy kod przedstawia przykład wywoływania powiązania.

public class ConfigBindingSG
{
    static void RunIt(params string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        IConfigurationSection section = builder.Configuration.GetSection("MyOptions");

        // !! Configure call - to be replaced with source-gen'd implementation
        builder.Services.Configure<MyOptions>(section);

        // !! Get call - to be replaced with source-gen'd implementation
        MyOptions? options0 = section.Get<MyOptions>();

        // !! Bind call - to be replaced with source-gen'd implementation
        MyOptions options1 = new();
        section.Bind(options1);

        WebApplication app = builder.Build();
        app.MapGet("/", () => "Hello World!");
        app.Run();
    }

    public class MyOptions
    {
        public int A { get; set; }
        public string S { get; set; }
        public byte[] Data { get; set; }
        public Dictionary<string, string> Values { get; set; }
        public List<MyClass> Values2 { get; set; }
    }

    public class MyClass
    {
        public int SomethingElse { get; set; }
    }
}

Podstawowe biblioteki platformy .NET

Ta sekcja zawiera następujące podtematy:

Refleksja

Wskaźniki funkcji zostały wprowadzone w platformie .NET 5, jednak w tym czasie nie dodano odpowiedniego wsparcia dla refleksji. W przypadku używania typeof lub odbicia wskaźnika funkcji, na przykład typeof(delegate*<void>()) lub FieldInfo.FieldType, zwracano IntPtr. Począwszy od platformy .NET 8, zamiast tego zwracany jest obiekt System.Type. Ten typ zapewnia dostęp do metadanych wskaźnika funkcji, w tym konwencji wywoływania, zwracanego typu i parametrów.

Notatka

Instancja wskaźnika funkcji, który jest adresem fizycznym funkcji, nadal jest reprezentowana jako IntPtr. Zmienił się tylko typ odbicia.

Nowe funkcje są obecnie implementowane tylko w środowisku uruchomieniowym CoreCLR i MetadataLoadContext.

Nowe interfejsy API zostały dodane do System.Type, takie jak IsFunctionPointer, oraz do System.Reflection.PropertyInfo, System.Reflection.FieldInfoi System.Reflection.ParameterInfo. Poniższy kod pokazuje, jak korzystać z niektórych nowych API do refleksji.

using System;
using System.Reflection;

// Sample class that contains a function pointer field.
public unsafe class UClass
{
    public delegate* unmanaged[Cdecl, SuppressGCTransition]<in int, void> _fp;
}

internal class FunctionPointerReflection
{
    public static void RunIt()
    {
        FieldInfo? fieldInfo = typeof(UClass).GetField(nameof(UClass._fp));

        // Obtain the function pointer type from a field.
        Type? fpType = fieldInfo?.FieldType;

        // New methods to determine if a type is a function pointer.
        Console.WriteLine(
        $"IsFunctionPointer: {fpType?.IsFunctionPointer}");
        Console.WriteLine(
            $"IsUnmanagedFunctionPointer: {fpType?.IsUnmanagedFunctionPointer}");

        // New methods to obtain the return and parameter types.
        Console.WriteLine($"Return type: {fpType?.GetFunctionPointerReturnType()}");

        if (fpType is not null)
        {
            foreach (Type parameterType in fpType.GetFunctionPointerParameterTypes())
            {
                Console.WriteLine($"Parameter type: {parameterType}");
            }
        }

        // Access to custom modifiers and calling conventions requires a "modified type".
        Type? modifiedType = fieldInfo?.GetModifiedFieldType();

        // A modified type forwards most members to its underlying type.
        Type? normalType = modifiedType?.UnderlyingSystemType;

        if (modifiedType is not null)
        {
            // New method to obtain the calling conventions.
            foreach (Type callConv in modifiedType.GetFunctionPointerCallingConventions())
            {
                Console.WriteLine($"Calling convention: {callConv}");
            }
        }

        // New method to obtain the custom modifiers.
        Type[]? modifiers =
            modifiedType?.GetFunctionPointerParameterTypes()[0].GetRequiredCustomModifiers();

        if (modifiers is not null)
        {
            foreach (Type modreq in modifiers)
            {
                Console.WriteLine($"Required modifier for first parameter: {modreq}");
            }
        }
    }
}

W poprzednim przykładzie są generowane następujące dane wyjściowe:

IsFunctionPointer: True
IsUnmanagedFunctionPointer: True
Return type: System.Void
Parameter type: System.Int32&
Calling convention: System.Runtime.CompilerServices.CallConvSuppressGCTransition
Calling convention: System.Runtime.CompilerServices.CallConvCdecl
Required modifier for first parameter: System.Runtime.InteropServices.InAttribute

Serializacja

Wprowadzono wiele ulepszeń w celu System.Text.Json serializacji i deserializacji funkcji na platformie .NET 8. Na przykład można dostosować obsługę właściwości JSON, których nie ma w obiekcie POCO.

W poniższych sekcjach opisano inne ulepszenia serializacji:

Aby uzyskać więcej informacji na temat serializacji JSON, zobacz serializację i deserializację JSON na platformie .NET.

Wbudowana obsługa dodatkowych typów

Serializator ma wbudowaną obsługę następujących dodatkowych typów.

  • Half, Int128i UInt128 jako typy liczbowe.

    Console.WriteLine(JsonSerializer.Serialize(
        [ Half.MaxValue, Int128.MaxValue, UInt128.MaxValue ]
    ));
    // [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
    
  • Wartości Memory<T> i ReadOnlyMemory<T>. byte wartości są serializowane do ciągów Base64, a inne typy do tablic JSON.

    JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 1, 2, 3 }); // "AQID"
    JsonSerializer.Serialize<Memory<int>>(new int[] { 1, 2, 3 }); // [1,2,3]
    

Generator źródłowy

Program .NET 8 zawiera ulepszenia generatora źródła system.Text.Json , które mają na celu uczynienie natywnego środowiska AOT na równi z serializatorem opartym na odbiciu . Na przykład:

  • Generator kodu źródłowego obsługuje teraz serializację typów z właściwościami required i init. Oba te elementy były już obsługiwane w serializacji opartej na odbiciu.

  • Ulepszone formatowanie kodu wygenerowanego przez źródło.

  • JsonSourceGenerationOptionsAttribute równoważność funkcji z JsonSerializerOptions. Aby uzyskać więcej informacji, zobacz Określanie opcji (generowanie źródła).

  • Dodatkowa diagnostyka (na przykład SYSLIB1034 i SYSLIB1039).

  • Nie uwzględniaj typów ignorowanych lub niedostępnych właściwości.

  • Obsługa zagnieżdżania deklaracji JsonSerializerContext w obrębie dowolnych rodzajów typów.

  • Obsługa typów generowanych przez kompilator lub w scenariuszach generowania źródła o słabym typie. Ponieważ typy generowane przez kompilator nie mogą być jawnie określone przez generator źródła, System.Text.Json teraz wykonuje rozpoznawanie najbliższych przodków w czasie wykonywania. Ta rezolucja określa najbardziej odpowiedni supertyp, z którym ma być serializowana wartość.

  • Nowy typ konwertera JsonStringEnumConverter<TEnum>. Istniejąca klasa JsonStringEnumConverter nie jest obsługiwana w natywnej usłudze AOT. Możesz dodawać adnotacje do typów wyliczenia w następujący sposób:

    [JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))]
    public enum MyEnum { Value1, Value2, Value3 }
    
    [JsonSerializable(typeof(MyEnum))]
    public partial class MyContext : JsonSerializerContext { }
    

    Aby uzyskać więcej informacji, zobacz Serializowanie pól wyliczenia jako ciągi znaków.

  • Nowa właściwość JsonConverter.Type umożliwia wyszukanie typu wystąpienia JsonConverter niegenerycznych:

    Dictionary<Type, JsonConverter> CreateDictionary(IEnumerable<JsonConverter> converters)
        => converters.Where(converter => converter.Type != null)
                     .ToDictionary(converter => converter.Type!);
    

    Właściwość może mieć wartość null, ponieważ zwraca null dla wystąpień JsonConverterFactory oraz typeof(T) dla wystąpień JsonConverter<T>.

Generatory źródeł łańcucha

Klasa JsonSerializerOptions zawiera nową właściwość TypeInfoResolverChain, która uzupełnia istniejącą właściwość TypeInfoResolver. Te właściwości są używane do dostosowywania kontraktu przy łańcuchowym stosowaniu generatorów źródłowych. Dodanie nowej właściwości oznacza, że nie trzeba określać wszystkich powiązanych komponentów w jednym miejscu wywołania — można je dodać później. TypeInfoResolverChain umożliwia również introspekcję łańcucha lub usunięcie z niego składników. Aby uzyskać więcej informacji, zobacz Łączenie generatorów źródeł.

Ponadto JsonSerializerOptions.AddContext<TContext>() jest teraz przestarzałe. Został zastąpiony przez właściwości TypeInfoResolver i TypeInfoResolverChain. Aby uzyskać więcej informacji, zobacz SYSLIB0049.

Hierarchie interfejsu

Platforma .NET 8 dodaje obsługę serializacji właściwości z hierarchii interfejsu.

Poniższy kod przedstawia przykład, w którym właściwości zarówno z natychmiast zaimplementowanego interfejsu, jak i jego interfejsu podstawowego są serializowane.

public static void InterfaceHierarchies()
{
    IDerived value = new DerivedImplement { Base = 0, Derived = 1 };
    string json = JsonSerializer.Serialize(value);
    Console.WriteLine(json); // {"Derived":1,"Base":0}
}

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; }
}

Zasady nazewnictwa

JsonNamingPolicy zawiera nowe zasady nazewnictwa dla konwersji nazw właściwości snake_case (z podkreśleniem) i kebab-case (z łącznikiem). Użyj tych zasad podobnie do istniejących zasad JsonNamingPolicy.CamelCase:

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
JsonSerializer.Serialize(new { PropertyName = "value" }, options);
// { "property_name" : "value" }

Aby uzyskać więcej informacji, zobacz Używanie wbudowanych zasad nazewnictwa.

Właściwości tylko do odczytu

Teraz można dokonać deserializacji na polach lub właściwościach tylko do odczytu (czyli tych, które nie mają akcesora set).

Aby włączyć tę obsługę globalnie, ustaw nową opcję PreferredObjectCreationHandlingna JsonObjectCreationHandling.Populate. Jeśli zgodność jest problemem, można również włączyć funkcjonalność bardziej szczegółowo, umieszczając atrybut [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] na określonych typach, których właściwości mają zostać wypełnione, lub na poszczególnych właściwościach.

Rozważmy na przykład następujący kod, który deserializuje się do typu CustomerInfo, który ma dwie właściwości tylko do odczytu.

public static void ReadOnlyProperties()
{
    CustomerInfo customer = JsonSerializer.Deserialize<CustomerInfo>("""
        { "Names":["John Doe"], "Company":{"Name":"Contoso"} }
        """)!;

    Console.WriteLine(JsonSerializer.Serialize(customer));
}

class CompanyInfo
{
    public required string Name { get; set; }
    public string? PhoneNumber { get; set; }
}

[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
class CustomerInfo
{
    // Both of these properties are read-only.
    public List<string> Names { get; } = new();
    public CompanyInfo Company { get; } = new()
    {
        Name = "N/A",
        PhoneNumber = "N/A"
    };
}

Przed platformą .NET 8 wartości wejściowe zostały zignorowane, a właściwości Names i Company zachowały wartości domyślne.

{"Names":[],"Company":{"Name":"N/A","PhoneNumber":"N/A"}}

Teraz wartości wejściowe są używane do wypełniania właściwości tylko do odczytu podczas deserializacji.

{"Names":["John Doe"],"Company":{"Name":"Contoso","PhoneNumber":"N/A"}}

Aby uzyskać więcej informacji na temat deserializacji przy użyciu zachowania wypełniania, zobacz także Zainicjowane właściwości wypełniania.

Wyłączanie wartości domyślnej opartej na odbiciu

Teraz można wyłączyć używanie serializatora opartego na odbiciu domyślnie. Wyłączenie to jest przydatne, aby uniknąć przypadkowego rootowania komponentów refleksji, które nie są używane, zwłaszcza w aplikacjach przycinanych i aplikacjach natywnych AOT. Aby wyłączyć domyślną serializację opartą na refleksji, wymagając podania argumentu JsonSerializerOptions do metod serializacji i deserializacji JsonSerializer, ustaw właściwość JsonSerializerIsReflectionEnabledByDefault MSBuild na false w pliku projektu.

Użyj nowego interfejsu API IsReflectionEnabledByDefault, aby sprawdzić wartość przełącznika funkcji. Jeśli jesteś autorem biblioteki budującej na podstawie System.Text.Json, możesz polegać na tej właściwości, aby skonfigurować swoje wartości domyślne bez przypadkowego ustanowienia na stałe składników refleksji.

Aby uzyskać więcej informacji, zobacz Wyłącz domyślne ustawienia refleksji.

Nowe metody interfejsu API JsonNode

Typy JsonNode i System.Text.Json.Nodes.JsonArray obejmują następujące nowe metody.

public partial class JsonNode
{
    // Creates a deep clone of the current node and all its descendants.
    public JsonNode DeepClone();

    // Returns true if the two nodes are equivalent JSON representations.
    public static bool DeepEquals(JsonNode? node1, JsonNode? node2);

    // Determines the JsonValueKind of the current node.
    public JsonValueKind GetValueKind(JsonSerializerOptions options = null);

    // If node is the value of a property in the parent
    // object, returns its name.
    // Throws InvalidOperationException otherwise.
    public string GetPropertyName();

    // If node is the element of a parent JsonArray,
    // returns its index.
    // Throws InvalidOperationException otherwise.
    public int GetElementIndex();

    // Replaces this instance with a new value,
    // updating the parent object/array accordingly.
    public void ReplaceWith<T>(T value);

    // Asynchronously parses a stream as UTF-8 encoded data
    // representing a single JSON value into a JsonNode.
    public static Task<JsonNode?> ParseAsync(
        Stream utf8Json,
        JsonNodeOptions? nodeOptions = null,
        JsonDocumentOptions documentOptions = default,
        CancellationToken cancellationToken = default);
}

public partial class JsonArray
{
    // Returns an IEnumerable<T> view of the current array.
    public IEnumerable<T> GetValues<T>();
}

Niepublijni członkowie

Można włączyć elementy członkowskie inne niż publiczne do umowy serializacji dla danego typu przy użyciu adnotacji atrybutów JsonIncludeAttribute i JsonConstructorAttribute.

public static void NonPublicMembers()
{
    string json = JsonSerializer.Serialize(new MyPoco(42));
    Console.WriteLine(json);
    // {"X":42}

    JsonSerializer.Deserialize<MyPoco>(json);
}

public class MyPoco
{
    [JsonConstructor]
    internal MyPoco(int x) => X = x;

    [JsonInclude]
    internal int X { get; }
}

Aby uzyskać więcej informacji, zobacz Użyj typów niezmiennych oraz członków niepublicznych i akcesoriów.

API deserializacji strumieniowej

Platforma .NET 8 zawiera nowe metody rozszerzające deserializację strumieniową, takie jak IAsyncEnumerable<T>, na przykład GetFromJsonAsAsyncEnumerable. Podobne metody istniały, które zwracają Task<TResult>, na przykład HttpClientJsonExtensions.GetFromJsonAsync. Nowe metody rozszerzenia wywołują strumieniowe interfejsy API i zwracają IAsyncEnumerable<T>.

Poniższy kod pokazuje, jak można użyć nowych metod rozszerzenia.

public async static void StreamingDeserialization()
{
    const string RequestUri = "https://api.contoso.com/books";
    using var client = new HttpClient();
    IAsyncEnumerable<Book?> books = client.GetFromJsonAsAsyncEnumerable<Book>(RequestUri);

    await foreach (Book? book in books)
    {
        Console.WriteLine($"Read book '{book?.title}'");
    }
}

public record Book(int id, string title, string author, int publishedYear);

WithAddedModifier, metoda rozszerzenia

Nowa metoda rozszerzenia WithAddedModifier(IJsonTypeInfoResolver, Action<JsonTypeInfo>) umożliwia łatwe wprowadzenie modyfikacji kontraktów serializacji dowolnych wystąpień IJsonTypeInfoResolver.

var options = new JsonSerializerOptions
{
    TypeInfoResolver = MyContext.Default
        .WithAddedModifier(static typeInfo =>
        {
            foreach (JsonPropertyInfo prop in typeInfo.Properties)
            {
                prop.Name = prop.Name.ToUpperInvariant();
            }
        })
};

Nowe przeciążenia JsonContent.Create

Teraz można tworzyć wystąpienia JsonContent przy użyciu kontraktów bezpiecznych przycinania lub generowanych z kodu źródłowego. Nowe metody to:

var book = new Book(id: 42, "Title", "Author", publishedYear: 2023);
HttpContent content = JsonContent.Create(book, MyContext.Default.Book);

public record Book(int id, string title, string author, int publishedYear);

[JsonSerializable(typeof(Book))]
public partial class MyContext : JsonSerializerContext
{
}

Blokowanie wystąpienia JsonSerializerOptions

Następujące nowe metody umożliwiają kontrolowanie, kiedy wystąpienie JsonSerializerOptions jest zamrożone:

  • JsonSerializerOptions.MakeReadOnly()

    To przeciążenie jest przeznaczone do bezpiecznego obsługi przycięć i dlatego zgłasza wyjątek w przypadkach, gdy wystąpienie opcji nie zostało skonfigurowane z rozwiązywaczem.

  • JsonSerializerOptions.MakeReadOnly(Boolean)

    Jeśli przekażesz true do tego przeciążenia, to wypełni ono instancję opcji domyślnym mechanizmem rozwiązywania odbić, jeśli żaden nie jest obecny. Ta metoda jest oznaczona RequiresUnreferenceCode/RequiresDynamicCode i dlatego jest nieodpowiednia dla natywnych aplikacji AOT.

Nowa właściwość IsReadOnly umożliwia sprawdzenie, czy wystąpienie opcji jest zamrożone.

Abstrakcja czasu

Nowa klasa TimeProvider i interfejs ITimer dodają funkcjonalność abstrakcji czasu, co umożliwia symulowanie czasu w scenariuszach testowych. Ponadto można użyć abstrakcji czasu, aby wyśmiewać operacje Task, które polegają na postępie czasu przy użyciu Task.Delay i Task.WaitAsync. Abstrakcja czasu obsługuje następujące niezbędne operacje czasowe:

  • Pobieranie czasu lokalnego i czasu UTC
  • Uzyskiwanie znacznika czasu na potrzeby pomiaru wydajności
  • Tworzenie czasomierza

Poniższy fragment kodu przedstawia kilka przykładów użycia.

// Get system time.
DateTimeOffset utcNow = TimeProvider.System.GetUtcNow();
DateTimeOffset localNow = TimeProvider.System.GetLocalNow();

TimerCallback callback = s => ((State)s!).Signal();

// Create a timer using the time provider.
ITimer timer = _timeProvider.CreateTimer(
    callback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);

// Measure a period using the system time provider.
long providerTimestamp1 = TimeProvider.System.GetTimestamp();
long providerTimestamp2 = TimeProvider.System.GetTimestamp();

TimeSpan period = _timeProvider.GetElapsedTime(providerTimestamp1, providerTimestamp2);
// Create a time provider that works with a
// time zone that's different than the local time zone.
private class ZonedTimeProvider(TimeZoneInfo zoneInfo) : TimeProvider()
{
    private readonly TimeZoneInfo _zoneInfo = zoneInfo ?? TimeZoneInfo.Local;

    public override TimeZoneInfo LocalTimeZone => _zoneInfo;

    public static TimeProvider FromLocalTimeZone(TimeZoneInfo zoneInfo) =>
        new ZonedTimeProvider(zoneInfo);
}

Ulepszenia utF8

Jeśli chcesz włączyć zapisywanie ciągopodobnej reprezentacji typu do docelowego zakresu, zaimplementuj nowy interfejs IUtf8SpanFormattable dla swojego typu. Ten nowy interfejs jest ściśle związany z ISpanFormattable, ale jest przeznaczony dla utF8 i Span<byte> zamiast UTF16 i Span<char>.

IUtf8SpanFormattable został zaimplementowany we wszystkich typach pierwotnych (oraz innych), z identyczną logiką wspólną, niezależnie od tego, czy kieruje się na string, Span<char>czy Span<byte>. Ma pełną obsługę wszystkich formatów (w tym nowych specyfikatora binarnego "B") i wszystkich kultur. Oznacza to, że można teraz formatować bezpośrednio do formatu UTF8 z Byte, Complex, Char, DateOnly, DateTime, DateTimeOffset, Decimal, Double, Guid, Half, IPAddress, IPNetwork, Int16, Int32, Int64, Int128, IntPtr, NFloat, SByte, Single, Rune, TimeOnly, TimeSpan, UInt16, UInt32, UInt64, UInt128, UIntPtri Version.

Nowe metody Utf8.TryWrite zapewniają oparty na protokole UTF8 odpowiednik istniejących metod MemoryExtensions.TryWrite, które są oparte na protokole UTF16. Składnia ciągów interpolowanych umożliwia formatowanie złożonego wyrażenia bezpośrednio w zakresie bajtów UTF8, na przykład:

static bool FormatHexVersion(
    short major,
    short minor,
    short build,
    short revision,
    Span<byte> utf8Bytes,
    out int bytesWritten) =>
    Utf8.TryWrite(
        utf8Bytes,
        CultureInfo.InvariantCulture,
        $"{major:X4}.{minor:X4}.{build:X4}.{revision:X4}",
        out bytesWritten);

Implementacja rozpoznaje IUtf8SpanFormattable w wartościach formatu i używa tej implementacji do bezpośredniego zapisywania ich reprezentacji UTF8 w docelowym zakresie.

Implementacja wykorzystuje również nową metodę Encoding.TryGetBytes(ReadOnlySpan<Char>, Span<Byte>, Int32), która wraz z jej odpowiednikiem Encoding.TryGetChars(ReadOnlySpan<Byte>, Span<Char>, Int32) obsługuje kodowanie i dekodowanie w zakresie docelowym. Jeśli zakres nie jest wystarczająco długi, aby pomieścić stan wynikowy, metody zwracają false zamiast zgłaszać wyjątek.

Metody pracy z losowością

Typy System.Random i System.Security.Cryptography.RandomNumberGenerator wprowadzają dwie nowe metody pracy z losowością.

PobierzElementy<T>()

Nowe metody System.Random.GetItems i System.Security.Cryptography.RandomNumberGenerator.GetItems umożliwiają losowe wybranie określonej liczby elementów z zestawu danych wejściowych. W poniższym przykładzie pokazano, jak używać System.Random.GetItems<T>() (na instancji uzyskanej z właściwości Random.Shared), aby losowo wstawić 31 elementów do tablicy. Ten przykład może być używany w grze "Simon", gdzie gracze muszą pamiętać sekwencję kolorowych przycisków.

private static ReadOnlySpan<Button> s_allButtons = new[]
{
    Button.Red,
    Button.Green,
    Button.Blue,
    Button.Yellow,
};

// ...

Button[] thisRound = Random.Shared.GetItems(s_allButtons, 31);
// Rest of game goes here ...

Shuffle<T>()

Nowe metody Random.Shuffle i RandomNumberGenerator.Shuffle<T>(Span<T>) umożliwiają losowe określanie kolejności zakresu. Te metody są przydatne do zmniejszania stronniczości w trenowaniu w uczeniu maszynowym (więc pierwszą rzeczą nie zawsze jest trenowanie, a ostatnią rzeczą zawsze jest testowanie).

YourType[] trainingData = LoadTrainingData();
Random.Shared.Shuffle(trainingData);

IDataView sourceData = mlContext.Data.LoadFromEnumerable(trainingData);

DataOperationsCatalog.TrainTestData split = mlContext.Data.TrainTestSplit(sourceData);
model = chain.Fit(split.TrainSet);

IDataView predictions = model.Transform(split.TestSet);
// ...

Typy ukierunkowane na wydajność

Platforma .NET 8 wprowadza kilka nowych typów mających na celu poprawę wydajności aplikacji.

  • Nowa przestrzeń nazw System.Collections.Frozen zawiera typy kolekcji FrozenDictionary<TKey,TValue> i FrozenSet<T>. Te typy nie zezwalają na żadne zmiany kluczy i wartości po utworzeniu kolekcji. To wymaganie umożliwia szybsze operacje odczytu (na przykład TryGetValue()). Te typy są szczególnie przydatne w przypadku kolekcji, które są wypełniane przy pierwszym użyciu, a następnie utrwalane przez czas trwania długotrwałej usługi, na przykład:

    private static readonly FrozenDictionary<string, bool> s_configurationData =
        LoadConfigurationData().ToFrozenDictionary();
    
    // ...
    if (s_configurationData.TryGetValue(key, out bool setting) && setting)
    {
        Process();
    }
    
  • Metody takie jak MemoryExtensions.IndexOfAny szukają pierwszego wystąpienia dowolnej wartości w przekazanej kolekcji. Nowy typ System.Buffers.SearchValues<T> jest przeznaczony do przekazania do takich metod. W związku z tym, .NET 8 dodaje nowe przeciążenia metod, takich jak MemoryExtensions.IndexOfAny, które przyjmują instancję nowego typu. Podczas tworzenia wystąpienia SearchValues<T>wszystkie dane niezbędne do optymalizacji kolejnych wyszukiwań są pozyskiwane w tym czasie, co oznacza, że praca jest wykonywana zawczasu.

  • Nowy typ System.Text.CompositeFormat jest przydatny do optymalizowania ciągów formatu, które nie są znane w czasie kompilacji (na przykład jeśli ciąg formatu jest ładowany z pliku zasobu). Na początku spędza się trochę dodatkowego czasu na czynności takie jak analizowanie ciągu, ale oszczędza to pracy przy każdym użyciu.

    private static readonly CompositeFormat s_rangeMessage =
        CompositeFormat.Parse(LoadRangeMessageResource());
    
    // ...
    static string GetMessage(int min, int max) =>
        string.Format(CultureInfo.InvariantCulture, s_rangeMessage, min, max);
    
  • Nowe typy System.IO.Hashing.XxHash3 i System.IO.Hashing.XxHash128 zapewniają implementacje szybkich algorytmów skrótów XXH3 i XXH128.

System.Numerics i System.Runtime.Intrinsics

W tej sekcji opisano ulepszenia przestrzeni nazw System.Numerics i System.Runtime.Intrinsics.

  • Vector256<T>, Matrix3x2i Matrix4x4 poprawiły przyspieszanie sprzętowe na platformie .NET 8. Na przykład Vector256<T> została ponownie zaimplementowana, aby wewnętrznie używać operacji 2x Vector128<T>, w miarę możliwości. Pozwala to na częściowe przyspieszenie niektórych funkcji, gdy Vector128.IsHardwareAccelerated == true, ale Vector256.IsHardwareAccelerated == false, na przykład na Arm64.
  • Funkcje wewnętrzne sprzętu są teraz oznaczone atrybutem ConstExpected. Gwarantuje to, że użytkownicy będą świadomi, kiedy podstawowy sprzęt oczekuje stałej wartości, a więc kiedy zmienna wartość może nieoczekiwanie wpłynąć na wydajność.
  • Interfejs API Lerp(TSelf, TSelf, TSelf)Lerp został dodany do IFloatingPointIeee754<TSelf>, a także do float (Single), double (Double) i Half. Ten interfejs API umożliwia efektywne i poprawne wykonywanie interpolacji liniowej między dwiema wartościami.

Vector512 i AVX-512

Rozszerzono obsługę SIMD w .NET Core 3.0, aby uwzględnić wewnętrzne interfejsy API specyficzne dla platformy x86/x64. .NET 5 dodał obsługę architektury Arm64, a .NET 7 wprowadził międzyplatformowe funkcje wewnętrzne sprzętu. Platforma .NET 8 dodatkowo obsługuje technologię SIMD, wprowadzając Vector512<T> i obsługę rozszerzeń Intel Advanced Vector Extensions 512 (AVX-512) instrukcji.

W szczególności platforma .NET 8 obejmuje obsługę następujących kluczowych funkcji avX-512:

  • Operacje wektorów 512-bitowych
  • Dodatkowe 16 rejestrów SIMD
  • Dodatkowe instrukcje dostępne dla wektorów 128-bitowych, 256-bitowych i 512-bitowych

Jeśli masz sprzęt, który obsługuje funkcjonalność, to teraz Vector512.IsHardwareAccelerated raportuje true.

.NET 8 dodaje również kilka klas specyficznych dla platform w ramach przestrzeni nazw System.Runtime.Intrinsics.X86.

Te klasy są zgodne z tą samą ogólną strukturą co inne architektury zestawu instrukcji (ISA), które udostępniają właściwość IsSupported i zagnieżdżoną klasę Avx512F.X64 dla instrukcji dostępnych tylko dla procesów 64-bitowych. Ponadto każda klasa ma zagnieżdżoną klasę Avx512F.VL, która uwidacznia rozszerzenia Avx512VL (długość wektora) dla odpowiedniego zestawu instrukcji.

Nawet jeśli nie używasz jawnie instrukcji specyficznych dla Vector512lub Avx512Fw swoim kodzie, prawdopodobnie i tak skorzystasz z nowego wsparcia dla AVX-512. JIT może korzystać z dodatkowych rejestrów i instrukcji niejawnie podczas używania Vector128<T> lub Vector256<T>. Biblioteka klas bazowych używa tych wewnętrznych funkcji sprzętowych w większości operacji udostępnianych przez Span<T> i ReadOnlySpan<T> oraz w wielu API matematycznych dla typów pierwotnych.

Walidacja danych

Przestrzeń nazw System.ComponentModel.DataAnnotations zawiera nowe atrybuty weryfikacji danych przeznaczone do scenariuszy weryfikacji w usługach natywnych dla chmury. Chociaż istniejące moduły sprawdzania poprawności DataAnnotations są kierowane do typowej weryfikacji wprowadzania danych interfejsu użytkownika, takiej jak pola w formularzu, nowe atrybuty są przeznaczone do weryfikowania danych spoza wprowadzania przez użytkownika, takich jak opcje konfiguracji . Oprócz nowych atrybutów nowe właściwości zostały dodane do typu RangeAttribute.

Nowy interfejs API Opis
RangeAttribute.MinimumIsExclusive
RangeAttribute.MaximumIsExclusive
Określa, czy ograniczenia są uwzględnione w dozwolonym zakresie.
System.ComponentModel.DataAnnotations.LengthAttribute Określa zarówno dolne, jak i górne granice ciągów lub kolekcji. Na przykład [Length(10, 20)] wymaga co najmniej 10 elementów i co najmniej 20 elementów w kolekcji.
System.ComponentModel.DataAnnotations.Base64StringAttribute Sprawdza, czy ciąg jest prawidłową reprezentacją Base64.
System.ComponentModel.DataAnnotations.AllowedValuesAttribute
System.ComponentModel.DataAnnotations.DeniedValuesAttribute
Określ odpowiednio listy dozwolonych i listy odmowy. Na przykład [AllowedValues("apple", "banana", "mango")].

Metryki

Nowe interfejsy API umożliwiają dołączanie tagów par klucz-wartość do Meter i Instrument obiektów podczas ich tworzenia. Agregatory opublikowanych pomiarów metryk mogą używać tagów do rozróżniania zagregowanych wartości.

var options = new MeterOptions("name")
{
    Version = "version",
    // Attach these tags to the created meter.
    Tags = new TagList()
    {
        { "MeterKey1", "MeterValue1" },
        { "MeterKey2", "MeterValue2" }
    }
};

Meter meter = meterFactory!.Create(options);

Counter<int> counterInstrument = meter.CreateCounter<int>(
    "counter", null, null, new TagList() { { "counterKey1", "counterValue1" } }
);
counterInstrument.Add(1);

Nowe interfejsy API obejmują:

Kryptografia

Platforma .NET 8 dodaje obsługę prymitywów kryptograficznych SHA-3. (Algorytm SHA-3 jest obecnie obsługiwany przez system Linux z programem OpenSSL 1.1.1 lub nowszym oraz systemem Windows 11 Build 25324 lub nowszym). Interfejsy API, w których algorytm SHA-2 jest dostępny, oferują teraz komplement SHA-3. Obejmuje to SHA3_256, SHA3_384i SHA3_512 na potrzeby tworzenia skrótów; HMACSHA3_256, HMACSHA3_384i HMACSHA3_512 dla HMAC; HashAlgorithmName.SHA3_256, HashAlgorithmName.SHA3_384i HashAlgorithmName.SHA3_512 na potrzeby tworzenia skrótów, w których można skonfigurować algorytm; i RSAEncryptionPadding.OaepSHA3_256, RSAEncryptionPadding.OaepSHA3_384i RSAEncryptionPadding.OaepSHA3_512 na potrzeby szyfrowania OAEP RSA.

W poniższym przykładzie pokazano, jak używać interfejsów API, w tym właściwości SHA3_256.IsSupported w celu określenia, czy platforma obsługuje algorytm SHA-3.

// Hashing example
if (SHA3_256.IsSupported)
{
    byte[] hash = SHA3_256.HashData(dataToHash);
}
else
{
    // ...
}

// Signing example
if (SHA3_256.IsSupported)
{
     using ECDsa ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
     byte[] signature = ec.SignData(dataToBeSigned, HashAlgorithmName.SHA3_256);
}
else
{
    // ...
}

Obsługa algorytmu SHA-3 jest obecnie przeznaczona do obsługi kryptograficznych elementów pierwotnych. Konstrukcje i protokoły wyższego poziomu nie powinny początkowo w pełni obsługiwać algorytmu SHA-3. Te protokoły obejmują certyfikaty X.509, SignedXmli COSE.

Nawiązywanie kontaktów / Sieci komputerowe

Obsługa serwera proxy HTTPS

Do tej pory typy serwerów proxy, które obsługiwał HttpClient, umożliwiały atakującemu w stylu "man-in-the-middle" zobaczenie, z którą stroną klient nawiązuje połączenie, nawet w przypadku adresów HTTPS. HttpClient obsługuje teraz serwera proxy HTTPS, który tworzy zaszyfrowany kanał między klientem a serwerem proxy, dzięki czemu wszystkie żądania mogą być obsługiwane z pełną prywatnością.

Aby włączyć serwer proxy HTTPS, ustaw zmienną środowiskową all_proxy lub użyj klasy WebProxy do programowego sterowania serwerem proxy.

Unix: export all_proxy=https://x.x.x.x:3218 Windows: set all_proxy=https://x.x.x.x:3218

Można również użyć klasy WebProxy do programowego sterowania serwerem proxy.

Metody ZipFile oparte na strumieniu

Platforma .NET 8 zawiera nowe przeciążenia ZipFile.CreateFromDirectory, które pozwalają zbierać wszystkie pliki zawarte w katalogu i spakować je, a następnie przechowywać wynikowy plik zip w podanym strumieniu. Podobnie nowe przeciążenia ZipFile.ExtractToDirectory umożliwiają udostępnienie strumienia zawierającego spakowany plik i wyodrębnienie jego zawartości do systemu plików. Są to nowe przeciążenia:

namespace System.IO.Compression;

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) { }
}

Te nowe interfejsy API mogą być przydatne w przypadku ograniczenia miejsca na dysku, ponieważ unikają konieczności używania dysku jako kroku pośredniego.

Biblioteki rozszerzeń

Ta sekcja zawiera następujące podtematy:

Usługi związane z kluczowym DI

Usługi wstrzykiwania zależności z kluczami (DI) zapewniają możliwość rejestrowania i pobierania usług DI przy użyciu kluczy. Za pomocą kluczy można określić zakres sposobu rejestrowania i używania usług. Oto niektóre z nowych interfejsów API:

W poniższym przykładzie pokazano, jak używać usług di z kluczami.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<BigCacheConsumer>();
builder.Services.AddSingleton<SmallCacheConsumer>();
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
WebApplication app = builder.Build();
app.MapGet("/big", (BigCacheConsumer data) => data.GetData());
app.MapGet("/small", (SmallCacheConsumer data) => data.GetData());
app.MapGet("/big-cache", ([FromKeyedServices("big")] ICache cache) => cache.Get("data"));
app.MapGet("/small-cache", (HttpContext httpContext) => httpContext.RequestServices.GetRequiredKeyedService<ICache>("small").Get("data"));
app.Run();

class BigCacheConsumer([FromKeyedServices("big")] ICache cache)
{
    public object? GetData() => cache.Get("data");
}

class SmallCacheConsumer(IServiceProvider serviceProvider)
{
    public object? GetData() => serviceProvider.GetRequiredKeyedService<ICache>("small").Get("data");
}

public interface ICache
{
    object Get(string key);
}

public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

Aby uzyskać więcej informacji, sprawdź dotnet/runtime#64427.

Hostowane usługi cyklu życia

Hostowane usługi mają teraz więcej opcji wykonywania podczas cyklu życia aplikacji. IHostedService dostarczyło StartAsync i StopAsync, a teraz IHostedLifecycleService udostępnia dodatkowe metody:

Te metody są uruchamiane odpowiednio przed i po istniejących punktach.

W poniższym przykładzie pokazano, jak używać nowych interfejsów API.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

internal class HostedLifecycleServices
{
    public async static void RunIt()
    {
        IHostBuilder hostBuilder = new HostBuilder();
        hostBuilder.ConfigureServices(services =>
        {
            services.AddHostedService<MyService>();
        });

        using (IHost host = hostBuilder.Build())
        {
            await host.StartAsync();
        }
    }

    public class MyService : IHostedLifecycleService
    {
        public Task StartingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StopAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
    }
}

Aby uzyskać więcej informacji, sprawdź dotnet/runtime#86511.

Walidacja opcji

Generator kodu źródłowego

Aby zmniejszyć obciążenie uruchamiania i poprawić zestaw funkcji weryfikacji, wprowadziliśmy generator kodu źródłowego, który implementuje logikę walidacji. Poniższy kod przedstawia przykładowe modele i klasy modułu sprawdzania poprawności.

public class FirstModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P1 { get; set; } = string.Empty;

    [Microsoft.Extensions.Options.ValidateObjectMembers(
        typeof(SecondValidatorNoNamespace))]
    public SecondModelNoNamespace? P2 { get; set; }
}

public class SecondModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P4 { get; set; } = string.Empty;
}

[OptionsValidator]
public partial class FirstValidatorNoNamespace
    : IValidateOptions<FirstModelNoNamespace>
{
}

[OptionsValidator]
public partial class SecondValidatorNoNamespace
    : IValidateOptions<SecondModelNoNamespace>
{
}

Jeśli aplikacja używa iniekcji zależności, możesz wstrzyknąć walidację, jak pokazano w poniższym przykładowym kodzie.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<FirstModelNoNamespace>(
    builder.Configuration.GetSection("some string"));

builder.Services.AddSingleton<
    IValidateOptions<FirstModelNoNamespace>, FirstValidatorNoNamespace>();
builder.Services.AddSingleton<
    IValidateOptions<SecondModelNoNamespace>, SecondValidatorNoNamespace>();

Typ ValidateOptionsResultBuilder

Platforma .NET 8 wprowadza typ ValidateOptionsResultBuilder w celu ułatwienia tworzenia obiektu ValidateOptionsResult. Co ważne, ten konstruktor umożliwia gromadzenie wielu błędów. Wcześniej tworzenie obiektu ValidateOptionsResult niezbędnego do wdrożenia IValidateOptions<TOptions>.Validate(String, TOptions) było trudne, a czasami powodowało złożone błędy walidacji. Jeśli wystąpiło wiele błędów, proces weryfikacji często zatrzymywał się przy pierwszym błędzie.

Poniższy fragment kodu przedstawia przykładowe użycie ValidateOptionsResultBuilder.

ValidateOptionsResultBuilder builder = new();
builder.AddError("Error: invalid operation code");
builder.AddResult(ValidateOptionsResult.Fail("Invalid request parameters"));
builder.AddError("Malformed link", "Url");

// Build ValidateOptionsResult object has accumulating multiple errors.
ValidateOptionsResult result = builder.Build();

// Reset the builder to allow using it in new validation operation.
builder.Clear();

Konstruktory LoggerMessageAttribute

LoggerMessageAttribute oferuje dodatkowe przeciążenia konstruktora. Wcześniej należało wybrać konstruktor bez parametrów lub konstruktor, który wymagał wszystkich parametrów (identyfikator zdarzenia, poziom dziennika i komunikat). Nowe przeciążenia zapewniają większą elastyczność określania wymaganych parametrów przy użyciu ograniczonego kodu. Jeśli nie podasz identyfikatora zdarzenia, system wygeneruje je automatycznie.

public LoggerMessageAttribute(LogLevel level, string message);
public LoggerMessageAttribute(LogLevel level);
public LoggerMessageAttribute(string message);

Metryki rozszerzeń

interfejs IMeterFactory

Nowy interfejs IMeterFactory można zarejestrować w kontenerach iniekcji zależności (DI) i używać go do tworzenia obiektów Meter w izolowany sposób.

Zarejestruj IMeterFactory w kontenerze DI przy użyciu domyślnej implementacji fabryki mierników:

// 'services' is the DI IServiceCollection.
services.AddMetrics();

Użytkownicy mogą następnie uzyskać fabrykę mierników i użyć jej do utworzenia nowego obiektu Meter.

IMeterFactory meterFactory = serviceProvider.GetRequiredService<IMeterFactory>();

MeterOptions options = new MeterOptions("MeterName")
{
    Version = "version",
};

Meter meter = meterFactory.Create(options);

Klasa MetricCollector<T>

Nowa klasa MetricCollector<T> umożliwia rejestrowanie pomiarów metryk wraz ze znacznikami czasu. Ponadto klasa oferuje elastyczność korzystania z wybranego dostawcy czasu na potrzeby dokładnego generowania znacznika czasu.

const string CounterName = "MyCounter";
DateTimeOffset now = DateTimeOffset.Now;

var timeProvider = new FakeTimeProvider(now);
using var meter = new Meter(Guid.NewGuid().ToString());
Counter<long> counter = meter.CreateCounter<long>(CounterName);
using var collector = new MetricCollector<long>(counter, timeProvider);

Assert.IsNull(collector.LastMeasurement);

counter.Add(3);

// Verify the update was recorded.
Assert.AreEqual(counter, collector.Instrument);
Assert.IsNotNull(collector.LastMeasurement);

Assert.AreSame(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
Assert.AreEqual(3, collector.LastMeasurement.Value);
Assert.AreEqual(now, collector.LastMeasurement.Timestamp);

System.Numerics.Tensors.TensorPrimitives

Zaktualizowany pakiet System.Numerics.Tensors NuGet zawiera interfejsy API w nowym typie System.Numerics.Tensors.TensorPrimitives, które zapewniają obsługę operacji tensorowych. Prymitywy tensorowe optymalizują obciążenia intensywnie korzystające z danych, związane ze sztuczną inteligencją i uczeniem maszynowym.

Zastosowania sztucznej inteligencji, takie jak wyszukiwanie semantyczne i generacja wspomagana pobieraniem (RAG), rozszerzają możliwości języka naturalnego dużych modeli językowych, takich jak ChatGPT, wzbogacając monity odpowiednimi danymi. W przypadku tych obciążeń operacje na wektorach — na przykład podobieństwo cosinusowe w celu znalezienia najbardziej odpowiednich danych, aby odpowiedzieć na pytanie — mają kluczowe znaczenie. Typ TensorPrimitives udostępnia interfejsy API dla operacji wektorowych.

Aby uzyskać więcej informacji, zobacz wpis na blogu „Ogłoszenie .NET 8 RC 2”.

Natywna obsługa AOT

Opcja publikowania jako natywna AOT została po raz pierwszy wprowadzona na platformie .NET 7. Publikowanie aplikacji przy użyciu natywnej funkcji AOT tworzy w pełni samodzielną wersję aplikacji, która nie wymaga środowiska uruchomieniowego — wszystko jest zawarte w jednym pliku. Platforma .NET 8 oferuje następujące ulepszenia w zakresie publikowania natywnego AOT:

  • Dodaje obsługę architektur x64 i Arm64 w systemie macOS.

  • Zmniejsza rozmiary natywnych aplikacji AOT w systemie Linux o maksymalnie 50%. W poniższej tabeli przedstawiono rozmiar aplikacji "Hello World" opublikowanej za pomocą natywnej funkcji AOT, która obejmuje całe środowisko uruchomieniowe platformy .NET na platformie .NET 7 a .NET 8:

    System operacyjny .NET 7 .NET 8
    Linux x64 (z -p:StripSymbols=true) 3,76 MB 1,84 MB
    Windows x64 2,85 MB 1,77 MB
  • Umożliwia określenie preferencji optymalizacji: rozmiaru lub szybkości. Domyślnie kompilator wybiera generowanie szybkiego kodu przy jednoczesnym zważeniu na rozmiar aplikacji. Można jednak użyć właściwości <OptimizationPreference> MSBuild w celu optymalizacji pod kątem jednej lub drugiej. Aby uzyskać więcej informacji, zobacz Optymalizacja wdrożeń AOT.

Określanie docelowych platform podobnych do systemu iOS za pomocą natywnej funkcji AOT

Platforma .NET 8 uruchamia pracę w celu włączenia natywnej obsługi AOT dla platform podobnych do systemu iOS. Teraz można kompilować i uruchamiać aplikacje .NET iOS i .NET MAUI z natywną funkcją AOT na następujących platformach:

  • ios
  • iossimulator
  • maccatalyst
  • tvos
  • tvossimulator

Wstępne testowanie pokazuje, że rozmiar aplikacji na dysku zmniejsza się o około 35% dla aplikacji platformy .NET dla systemu iOS, które używają natywnego AOT zamiast mono. Rozmiar aplikacji na dysku dla aplikacji .NET MAUI dla systemu iOS zmniejsza się do 50%. Ponadto czas uruchamiania jest również krótszy. Aplikacje platformy .NET dla systemu iOS uruchamiają się około 28% szybciej, a aplikacje .NET MAUI dla systemu iOS mają około 50% lepszą wydajność uruchamiania w porównaniu z Mono. Obsługa platformy .NET 8 jest eksperymentalna i tylko pierwszym krokiem dla funkcji jako całości. Aby uzyskać więcej informacji, zobacz wpis w blogu .NET 8 Performance Improvements in .NET MAUI (Ulepszenia wydajności platformy .NET 8 na platformie .NET MAUI).

Natywna obsługa funkcji AOT jest dostępna jako funkcja zgody przeznaczona do wdrożenia aplikacji; Mono jest nadal domyślnym środowiskiem uruchomieniowym na potrzeby tworzenia i wdrażania aplikacji. Aby skompilować i uruchomić aplikację .NET MAUI z natywną funkcją AOT na urządzeniu z systemem iOS, użyj dotnet workload install maui, aby zainstalować obciążenie .NET MAUI i dotnet new maui -n HelloMaui w celu utworzenia aplikacji. Następnie ustaw właściwość MSBuild PublishAot na true w pliku projektu.

<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>

Po ustawieniu wymaganej właściwości i uruchomieniu dotnet publish, jak pokazano w poniższym przykładzie, aplikacja zostanie wdrożona przy użyciu natywnej AOT.

dotnet publish -f net8.0-ios -c Release -r ios-arm64  /t:Run

Ograniczenia

Nie wszystkie funkcje systemu iOS są zgodne z natywną funkcją AOT. Podobnie nie wszystkie biblioteki powszechnie używane w systemie iOS są zgodne z funkcją NativeAOT. Oprócz istniejących ograniczeń wdrożenia natywnego AOTponiżej przedstawiono niektóre inne ograniczenia dotyczące platform podobnych do systemu iOS:

  • Używanie natywnej funkcji AOT jest włączone tylko podczas wdrażania aplikacji (dotnet publish).
  • Debugowanie kodu zarządzanego jest obsługiwane tylko w przypadku platformy Mono.
  • Zgodność z platformą .NET MAUI jest ograniczona.

Kompilacja AOT dla aplikacji systemu Android

Aby zmniejszyć rozmiar aplikacji, aplikacje .NET i .NET MAUI przeznaczone dla systemu Android używają profilowanych tryb kompilacji przed czasem (AOT), gdy są one wbudowane w tryb wydania. Profilowana kompilacja AOT ma wpływ na mniej metod niż zwykła kompilacja AOT. Platforma .NET 8 wprowadza właściwość <AndroidStripILAfterAOT>, która umożliwia dalsze kompilowanie funkcji AOT dla aplikacji systemu Android w celu jeszcze większego zmniejszenia rozmiaru aplikacji.

<PropertyGroup>
  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
</PropertyGroup>

Domyślnie ustawienie AndroidStripILAfterAOT na true zastępuje domyślne ustawienie AndroidEnableProfiledAot, zezwalając (prawie) na przycinanie wszystkich metod skompilowanych przez AOT. Możesz również użyć profilowanego usuwania AOT i IL, jawnie ustawiając obie właściwości na true:

<PropertyGroup>
  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
  <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
</PropertyGroup>

Aplikacje Windows skompilowane krzyżowo

Podczas tworzenia aplikacji przeznaczonych dla systemu Windows na platformach innych niż Windows wynikowy plik wykonywalny jest teraz aktualizowany przy użyciu dowolnych określonych zasobów Win32 — na przykład ikony aplikacji, manifestu, informacji o wersji.

Wcześniej aplikacje musiały być tworzone w systemie Windows, aby mieć takie zasoby. Naprawienie tej luki w obsłudze obejmującej wiele budynków było popularnym żądaniem, ponieważ był to znaczący punkt bólu wpływający zarówno na złożoność infrastruktury, jak i użycie zasobów.

Zobacz też