Come creare un'istanza di istanze di JsonSerializerOptions con System.Text.Json

Questo articolo illustra come evitare problemi di prestazioni quando si usa JsonSerializerOptions. Viene inoltre illustrato come usare i costruttori con parametri disponibili.

Riutilizzare le istanze JsonSerializerOptions

Se si usa JsonSerializerOptions ripetutamente con le stesse opzioni, non creare una nuova istanza JsonSerializerOptions ogni volta che viene usata. Riutilizzare la stessa istanza per ogni chiamata. Queste indicazioni si applicano al codice scritto per i convertitori personalizzati e quando si chiama JsonSerializer.Serialize o JsonSerializer.Deserialize. Usare la stessa istanza tra più thread è sicuro. Le cache dei metadati nell'istanza delle opzioni sono al sicuro dai thread e l'istanza non è modificabile dopo la prima serializzazione o deserializzazione.

Il codice seguente illustra la riduzione delle prestazioni per l'uso di nuove istanze di opzioni.

using System.Diagnostics;
using System.Text.Json;

namespace OptionsPerfDemo
{
    public record Forecast(DateTime Date, int TemperatureC, string Summary);

    public class Program
    {
        public static void Main()
        {
            Forecast forecast = new(DateTime.Now, 40, "Hot");
            JsonSerializerOptions options = new() { WriteIndented = true };
            int iterations = 100000;

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                Serialize(forecast, options);
            }
            watch.Stop();
            Console.WriteLine($"Elapsed time using one options instance: {watch.ElapsedMilliseconds}");

            watch = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                Serialize(forecast);
            }
            watch.Stop();
            Console.WriteLine($"Elapsed time creating new options instances: {watch.ElapsedMilliseconds}");
        }

        private static void Serialize(Forecast forecast, JsonSerializerOptions? options = null)
        {
            _ = JsonSerializer.Serialize<Forecast>(
                forecast,
                options ?? new JsonSerializerOptions() { WriteIndented = true });
        }
    }
}

// Produces output like the following example:
//
//Elapsed time using one options instance: 190
//Elapsed time creating new options instances: 40140

Il codice precedente serializza un piccolo oggetto 100.000 volte usando la stessa istanza di opzioni. Serializza quindi lo stesso oggetto lo stesso numero di volte e crea ogni volta una nuova istanza di opzioni. Una tipica differenza di tempo di esecuzione è di 190 contro 40.140 millisecondi. La differenza è ancora maggiore se il numero di iterazioni aumenta.

Il serializzatore viene sottoposto a una fase di riscaldamento durante la prima serializzazione di ogni tipo nell'oggetto grafico quando viene passata una nuova istanza di opzioni. Questo riscaldamento include la creazione di una cache di metadati necessari per la serializzazione. I metadati includono delegati ai getter delle proprietà, ai setter, agli argomenti del costruttore, agli attributi specificati e così via. Questa cache dei metadati viene archiviata nell'istanza delle opzioni. Lo stesso processo di riscaldamento e la stessa cache viene applicata alla deserializzazione.

Le dimensioni della cache dei metadati in un'istanza JsonSerializerOptions dipendono dal numero di tipi da serializzare. Se si passano numerosi tipi, ad esempio tipi generati dinamicamente, al serializzatore, le dimensioni della cache continueranno a crescere e possono causare un OutOfMemoryException.

La proprietà JsonSerializerOptions.Default

Se l'istanza di JsonSerializerOptions che è necessario usare è l'istanza predefinita (con tutte le impostazioni predefinite e i convertitori predefiniti), usare la proprietà JsonSerializerOptions.Default anziché creare un'istanza di opzioni. Per altre informazioni, vedere Usare il convertitore di sistema predefinito.

Copiare JsonSerializerOptions

Esiste un costruttore JsonSerializerOptions che consente di creare una nuova istanza con le stesse opzioni di un'istanza esistente, come illustrato nell'esempio seguente:

using System.Text.Json;

namespace CopyOptions
{
    public class Forecast
    {
        public DateTime Date { get; init; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
    };

    public class Program
    {
        public static void Main()
        {
            Forecast forecast = new()
            {
                Date = DateTime.Now,
                TemperatureC = 40,
                Summary = "Hot"
            };

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

            JsonSerializerOptions optionsCopy = new(options);
            string forecastJson =
                JsonSerializer.Serialize<Forecast>(forecast, optionsCopy);

            Console.WriteLine($"Output JSON:\n{forecastJson}");
        }
    }
}

// Produces output like the following example:
//
//Output JSON:
//{
//  "Date": "2020-10-21T15:40:06.8998502-07:00",
//  "TemperatureC": 40,
//  "Summary": "Hot"
//}
Imports System.Text.Json
Imports System.Text.Json.Serialization

Namespace CopyOptions

    Public Class Forecast
        Public Property [Date] As Date
        Public Property TemperatureC As Integer
        Public Property Summary As String
    End Class

    Public NotInheritable Class Program

        Public Shared Sub Main()
            Dim forecast1 As New Forecast() With {
                .[Date] = Date.Now,
                .Summary = Nothing,
                .TemperatureC = CType(Nothing, Integer)
            }

            Dim options As New JsonSerializerOptions() With {
                .DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
            }

            Dim optionsCopy As New JsonSerializerOptions
            Dim forecastJson As String = JsonSerializer.Serialize(forecast1, optionsCopy)

            Console.WriteLine($"Output JSON:{forecastJson}")
        End Sub

    End Class

End Namespace

' Produces output like the following example:
'
'Output JSON:
'{
'  "Date": "2020-10-21T15:40:06.8998502-07:00",
'  "TemperatureC": 40,
'  "Summary": "Hot"
'}

La cache dei metadati dell'istanza esistente JsonSerializerOptions non viene copiata nella nuova istanza. Pertanto, l'uso di questo costruttore non equivale a riutilizzare un'istanza esistente di JsonSerializerOptions.

Impostazioni predefinite Web per JsonSerializerOptions

Le opzioni seguenti hanno impostazioni predefinite diverse per le app Web:

In .NET 9 e versioni successive è possibile usare il JsonSerializerOptions.Web singleton per serializzare con le opzioni predefinite usate da ASP.NET Core per le app Web. Nelle versioni precedenti chiamare il costruttore JsonSerializerOptions constructor per creare una nuova istanza con le impostazioni predefinite Web, come illustrato nell'esempio seguente:

using System.Text.Json;

namespace OptionsDefaults
{
    public class Forecast
    {
        public DateTime? Date { get; init; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
    };

    public class Program
    {
        public static void Main()
        {
            Forecast forecast = new()
            {
                Date = DateTime.Now,
                TemperatureC = 40,
                Summary = "Hot"
            };

            JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
            {
                WriteIndented = true
            };

            Console.WriteLine(
                $"PropertyNameCaseInsensitive: {options.PropertyNameCaseInsensitive}");
            Console.WriteLine(
                $"JsonNamingPolicy: {options.PropertyNamingPolicy}");
            Console.WriteLine(
                $"NumberHandling: {options.NumberHandling}");

            string forecastJson = JsonSerializer.Serialize<Forecast>(forecast, options);
            Console.WriteLine($"Output JSON:\n{forecastJson}");

            Forecast? forecastDeserialized =
                JsonSerializer.Deserialize<Forecast>(forecastJson, options);

            Console.WriteLine($"Date: {forecastDeserialized?.Date}");
            Console.WriteLine($"TemperatureC: {forecastDeserialized?.TemperatureC}");
            Console.WriteLine($"Summary: {forecastDeserialized?.Summary}");
        }
    }
}

// Produces output like the following example:
//
//PropertyNameCaseInsensitive: True
//JsonNamingPolicy: System.Text.Json.JsonCamelCaseNamingPolicy
//NumberHandling: AllowReadingFromString
//Output JSON:
//{
//  "date": "2020-10-21T15:40:06.9040831-07:00",
//  "temperatureC": 40,
//  "summary": "Hot"
//}
//Date: 10 / 21 / 2020 3:40:06 PM
//TemperatureC: 40
//Summary: Hot
Imports System.Text.Json

Namespace OptionsDefaults

    Public Class Forecast
        Public Property [Date] As Date
        Public Property TemperatureC As Integer
        Public Property Summary As String
    End Class

    Public NotInheritable Class Program

        Public Shared Sub Main()
            Dim forecast1 As New Forecast() With {
                .[Date] = Date.Now,
                .TemperatureC = 40,
                .Summary = "Hot"
                }

            Dim options As New JsonSerializerOptions(JsonSerializerDefaults.Web) With {
                .WriteIndented = True
                }

            Console.WriteLine(
                $"PropertyNameCaseInsensitive: {options.PropertyNameCaseInsensitive}")
            Console.WriteLine(
                $"JsonNamingPolicy: {options.PropertyNamingPolicy}")
            Console.WriteLine(
                $"NumberHandling: {options.NumberHandling}")

            Dim forecastJson As String = JsonSerializer.Serialize(forecast1, options)
            Console.WriteLine($"Output JSON:{forecastJson}")

            Dim forecastDeserialized As Forecast = JsonSerializer.Deserialize(Of Forecast)(forecastJson, options)

            Console.WriteLine($"Date: {forecastDeserialized.[Date]}")
            Console.WriteLine($"TemperatureC: {forecastDeserialized.TemperatureC}")
            Console.WriteLine($"Summary: {forecastDeserialized.Summary}")
        End Sub

    End Class

End Namespace

' Produces output like the following example:
'
'PropertyNameCaseInsensitive: True
'JsonNamingPolicy: System.Text.Json.JsonCamelCaseNamingPolicy
'NumberHandling: AllowReadingFromString
'Output JSON:
'{
'  "date": "2020-10-21T15:40:06.9040831-07:00",
'  "temperatureC": 40,
'  "summary": "Hot"
'}
'Date: 10 / 21 / 2020 3:40:06 PM
'TemperatureC: 40
'Summary: Hot