Προσαρμοσμένη σειριοποίηση JSON με μετατροπείς System.Text.Json – Steve Gordon

Προσαρμοσμένη σειριοποίηση JSON με μετατροπείς System.Text.Json – Steve Gordon

Januar 24, 2023 0 Von admin

Αυτή η ανάρτηση είναι η συνεισφορά μου στο .NET Advent ημερολόγιο. Βεβαιωθείτε ότι έχετε ελέγξει τις άλλες εκπληκτικές αναρτήσεις για τα Χριστούγεννα!

Τη στιγμή που γράφω, είμαι βαθιά στη δουλειά για ορισμένες σημαντικές αλλαγές στο Πελάτης Elasticsearch .NET. Μία από τις αλλαγές είναι η μετάβαση στο System.Text.Json ως προεπιλεγμένο σειριακό πρόγραμμα που χρησιμοποιείται μέσα στον υπολογιστή-πελάτη.

Υπάρχουν δύο „επίπεδα“ σειριοποίησης που εμπλέκονται στο .NET Client. Πρώτον, έχουμε τη σειριοποίηση των δικών μας τύπων, τα μοντέλα αιτημάτων και απόκρισης, μαζί με το ερώτημα DSL. Για αυτό, θα βασιστούμε 100% στο System.Text.Json. Πρέπει επίσης να λάβουμε υπόψη τη σειριοποίηση των τύπων καταναλωτή, όπως το μοντέλο για τα δεδομένα που διαβάζονται ή γράφονται στο Elasticsearch. Από προεπιλογή, θα χρησιμοποιήσουμε το System.Text.Json, ωστόσο, οι καταναλωτές ενδέχεται να επιλέξουν να χρησιμοποιήσουν διαφορετικό σειριακό πρόγραμμα, όπως το Newtonsoft JSON.

Με τον πελάτη 8.0, τώρα δημιουργούμε τα περισσότερα μοντέλα από ένα κοινό σχήμα. Αυτό σημαίνει ότι μπορούμε επίσης να δημιουργήσουμε προσαρμοσμένη λογική σειριοποίησης που διαφορετικά θα ήταν πολλή δουλειά για να κωδικοποιήσουμε και να διατηρήσουμε με μη αυτόματο τρόπο.

Σε αυτήν την ανάρτηση, θέλω να καλύψω μια από τις πιο σύνθετες έννοιες που έπρεπε να χειριστώ σχετικά με τη σειριοποίηση. συναθροίσεις.

ΣΗΜΕΙΩΣΗ: Η τελική σχεδίαση για τους τύπους και τους μετατροπείς που εμφανίζονται σε αυτήν την ανάρτηση είναι ακόμη ένα έργο σε εξέλιξη. Η τρέχουσα σχεδίαση είναι επαρκής για την απεικόνιση προσαρμοσμένων τεχνικών σειριοποίησης χρησιμοποιώντας το System.Text.Json.

Elasticsearch Aggregations

Οι συναθροίσεις μπορούν να συμπεριληφθούν στο σώμα των αιτημάτων αναζήτησης JSON για τη σύνοψη και την ομαδοποίηση δεδομένων. Τα αιτήματα μπορεί να περιλαμβάνουν μηδέν ή περισσότερες συναθροίσεις που εκτελεί το Elasticsearch. Τα προκύπτοντα συγκεντρωτικά δεδομένα περιλαμβάνονται στη συνέχεια στην απόκριση JSON. Παραδείγματα περιπτώσεων χρήσης περιλαμβάνουν την ομαδοποίηση ενός συνόλου αναρτήσεων ιστολογίου για να λάβετε έναν αριθμό αναρτήσεων σε κάθε κατηγορία ή τη συγκέντρωση δεδομένων για την κατανόηση του μέσου χρόνου φόρτωσης μιας ιστοσελίδας για μια καθορισμένη χρονική περίοδο.

Σειριοποίηση συγκεντρώσεων

Δεδομένου ότι κάθε συνάθροιση σε ένα αίτημα έχει μοναδικό όνομα, μια λογική κατασκευή για τη μοντελοποίησή τους στο αίτημα είναι η χρήση ενός λεξικού. Το „AggregationDictionary“ χρησιμοποιεί μια συμβολοσειρά ως κλειδί και ένα „AggregationContainer“ ως τιμή. Ένα δοχείο συγκέντρωσης είναι ο τρόπος μας να μοντελοποιήσουμε την πολυμορφική φύση των συναθροίσεων. Το κοντέινερ μπορεί λογικά να αποθηκεύσει τυχόν παραλλαγές συνάθροισης που υποστηρίζονται από το Elasticsearch, οι οποίες στη συνέχεια μοντελοποιούνται με τις κατάλληλες ιδιότητες.

Θα επικεντρωθούμε σε μία προσέγγιση για τον χειρισμό της σειριοποίησης του πολυμορφικού AggregationContainer και της παραλλαγής του για αυτήν την ανάρτηση. Σε μια μελλοντική ανάρτηση, μπορούμε να συζητήσουμε πώς να χειριστούμε την αποζωνοποίηση που είναι λίγο περισσότερο εμπλεκόμενη.

Ο ορισμός για το AggregationContainer είναι πολύ απλός. Περιλαμβάνει μια εσωτερική ιδιότητα που θα περιέχει μια παρουσία της παραλλαγής που υποστηρίζεται από αυτό το κοντέινερ. Σε αυτήν την περίπτωση, όλες οι παραλλαγές αναμένεται να προέρχονται από τον αφηρημένο τύπο AggregationBase.

[JsonConverter(typeof(AggregationContainerConverter))]
public partial class AggregationContainer
{
	public AggregationContainer(AggregationBase variant) => Variant = variant ?? throw new ArgumentNullException(nameof(variant));

	internal AggregationBase Variant { get; }
}

Εδώ είναι που τα πράγματα αρχίζουν να γίνονται ενδιαφέροντα όταν σκεφτόμαστε να κάνουμε σειρές αυτού του τύπου. Πρέπει να σειριοποιήσουμε την παραλλαγή ως αντικείμενο στο JSON. Για να υποστηριχθεί αυτό, απαιτείται ένας αρκετά απλός μετατροπέας. Η πλευρά της σειριοποίησης αυτού του μετατροπέα δεν είναι πολύ περίπλοκη, αλλά η πολυμορφική αποσειροποίηση είναι λίγο πιο δύσκολη. Εστιάζουμε στη σειριοποίηση για αυτήν την ανάρτηση, οπότε ας βουτήξουμε σε αυτό.

Εδώ είναι η κατηγορία μετατροπέα:

internal sealed class AggregationContainerConverter : JsonConverter<AggregationContainer>
{
	public override AggregationContainer Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{		
        // NOT COVERED IN THIS POST!
	}

	public override void Write(Utf8JsonWriter writer, AggregationContainer value, JsonSerializerOptions options)
	{
		if (value is null)
		{
			writer.WriteNullValue();
		}
		else if (value.Variant is not null)
		{
			var type = value.Variant.GetType();
			JsonSerializer.Serialize(writer, value.Variant, type, options);
		}
		else
		{
			throw new JsonException("Invalid container cannot be serialised");
		}
	}
}

Οι μετατροπείς είναι μια δυνατότητα του System.Text.Json που μας επιτρέπει να προσαρμόσουμε τον τρόπο ανάγνωσης και εγγραφής ενός τύπου ή ιδιότητας ως JSON. Πρέπει να προέρχονται από το JsonConverter και να εφαρμόζουν τις μεθόδους Read and Write.

Ο παραπάνω κώδικας γράφει μια τιμή null εάν το AggregationContainer είναι null. Εάν για κάποιο λόγο έχει δημιουργηθεί ένα κενό κοντέινερ, ρίχνει ένα JsonException. Διαφορετικά, σειριοποιούμε την παραλλαγή. Η μέθοδος serialise υποστηρίζει τη μετάδοση σε ένα υπάρχον Utf8JsonWriter και JsonSerializerOptions που μας επιτρέπει να συνεχίσουμε τη σειριοποίηση πολύπλοκων τύπων στον κύριο συντάκτη. Η μέθοδος σειριοποίησης System.Text.Json είναι γενική και ο τύπος χρησιμοποιείται κατά τη σειριοποίηση του αντικειμένου. Από προεπιλογή, αυτό σημαίνει ότι θα προσπαθήσει να σειριοποιήσει απευθείας τον τύπο AggregationBase. Αυτός ο τύπος βάσης μοιάζει με αυτό:

public abstract class AggregationBase
{
	protected AggregationBase(string name) => Name = name;

	[JsonIgnore]
	public Dictionary<string, object>? Meta { get; set; }

	[JsonIgnore]
	public string? Name { get; internal set; }
	
	// Other code omitted for brevity
}

Αυτό είναι ένα πρόβλημα για εμάς, θέλουμε να σειριοποιήσουμε τον παράγωγο τύπο, όχι απλώς να τον αντιμετωπίσουμε ως αυτόν τον αφηρημένο βασικό τύπο. Επειδή και οι δύο ιδιότητες επισημαίνονται ως JsonIgnore, θα δημιουργηθεί ένα κενό αντικείμενο χρησιμοποιώντας την προεπιλεγμένη συμπεριφορά του System.Text.Json.

Κατά τη σειριοποίηση, μπορούμε να το ελέγξουμε όπως έκανα στον παραπάνω προσαρμοσμένο κωδικό μετατροπέα. Αρχικά παίρνουμε τον πραγματικό τύπο του αντικειμένου. Με αυτό στο χέρι, μπορούμε να καλέσουμε μια υπερφόρτωση του Serialize που δέχεται τον τύπο που θέλουμε να χρησιμοποιήσουμε κατά τη σειριοποίηση. Αυτό θα διασφαλίσει ότι η συνάθροισή μας είναι πλήρως σειριοποιημένη.

Θα χρησιμοποιήσουμε μια απλή συνάθροιση ‚min‘ για να εξετάσουμε βαθύτερα την προσαρμοσμένη σειριοποίηση που χρειαζόμαστε.

[JsonConverter(typeof(MinAggregationConverter))]
public partial class MinAggregation : AggregationBase
{
	public MinAggregation(string name, Field field) : base(name) => Field = field;


	public MinAggregation(string name) : base(name)
	{
	}

	public string? Format { get; set; }

	public Field? Field { get; set; }

	public Missing? Missing { get; set; }

	public Script? Script { get; set; }
}

Ο τύπος ελάχιστης συγκέντρωσης περιλαμβάνει πολλές ιδιότητες που αντιπροσωπεύουν επιλογές για αυτήν τη συγκέντρωση. Περιλαμβάνει επίσης μέλη που ορίζονται στη βασική κλάση, όπως η ιδιότητα Meta. Θα παρατηρήσετε ότι αυτός ο τύπος περιλαμβάνει επίσης έναν προσαρμοσμένο μετατροπέα, ο οποίος προσδιορίζεται στον τύπο προσθέτοντας το χαρακτηριστικό JsonConverter.

Για κάθε έναν από τους 50+ τύπους συνάθροισης, η γεννήτρια κώδικα μπορεί να παράγει έναν αντίστοιχο μετατροπέα. Οι προσαρμοσμένοι μετατροπείς περιέχουν τη λογική σε μορφή ιδιότητας τη συγκέντρωση στο αίτημα.

internal sealed class MinAggregationConverter : JsonConverter<MinAggregation>
{
	public override MinAggregation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		// NOT COVERED IN THIS POST!
	}

	public override void Write(Utf8JsonWriter writer, MinAggregation value, JsonSerializerOptions options)
	{
		writer.WriteStartObject();
		writer.WritePropertyName("min");
		writer.WriteStartObject();

		if (!string.IsNullOrEmpty(value.Format))
		{
			writer.WritePropertyName("format");
			writer.WriteStringValue(value.Format);
		}

		if (value.Field is not null)
		{
			writer.WritePropertyName("field");
			JsonSerializer.Serialize(writer, value.Field, options);
		}

		if (value.Missing is not null)
		{
			writer.WritePropertyName("missing");
			JsonSerializer.Serialize(writer, value.Missing, options);
		}

		if (value.Script is not null)
		{
			writer.WritePropertyName("script");
			JsonSerializer.Serialize(writer, value.Script, options);
		}

		writer.WriteEndObject();

		if (value.Meta is not null)
		{
			writer.WritePropertyName("meta");
			JsonSerializer.Serialize(writer, value.Meta, options);
		}

		writer.WriteEndObject();
	}
}

Αυτή τη φορά, ο μετατροπέας συμμετέχει περισσότερο. Χρησιμοποιεί απευθείας το Utf8JsonWriter για να γράψει τα απαιτούμενα διακριτικά JSON. Ξεκινά γράφοντας ένα διακριτικό αντικειμένου έναρξης, τον χαρακτήρα ‚{‚. Στη συνέχεια γράφει μια ιδιότητα όπου η τιμή προσδιορίζει τη συγκεκριμένη συγκέντρωση που γράφεται. Αυτό ευθυγραμμίζεται με το όνομα συνάθροισης που χρησιμοποιείται από το Elasticsearch. Ένα άλλο αντικείμενο ξεκινά, το οποίο θα περιέχει τα πεδία συγκέντρωσης. Καθένα από αυτά γράφεται μόνο εάν έχει οριστεί μια τιμή στο στιγμιότυπο συγκέντρωσης.

Οι μετα-πληροφορίες για συναθροίσεις δεν περιλαμβάνονται στο κύριο αντικείμενο συγκέντρωσης, αλλά σε επίπεδο εξωτερικού αντικειμένου. Στον παραπάνω κώδικα, αυτό αντιμετωπίζεται τερματίζοντας πρώτα το εσωτερικό αντικείμενο και, στη συνέχεια, γράφοντας τη μετα-τιμή, πριν από το διακριτικό του τελικού αντικειμένου. Αυτή η προσαρμοσμένη μορφοποίηση δεν θα ήταν δυνατή με την προεπιλεγμένη συμπεριφορά System.Text.Json, η οποία σειριοποιεί όλες τις ιδιότητες μέσα σε ένα μεμονωμένο αντικείμενο.

Για να δείτε το αποτέλεσμα αυτής της προσαρμοσμένης σειριοποίησης, ας δημιουργήσουμε ένα βασικό αίτημα αναζήτησης με μια απλή συνάθροιση λεπτών. Στο πρόγραμμα-πελάτη Elasticsearch .NET, αυτό μπορεί να επιτευχθεί με τον ακόλουθο κώδικα αρχικοποιητή αντικειμένου.

var request = new SearchRequest("my-index")
{
	Size = 0,
	Query = new TermQuery 
	{ 
		Field = Field<Project>(p => p.Type), 
		Value = "project"
	},	
	Aggregations = new MinAggregation("min_last_activity", Field<Project>(p => p.LastActivity))
	{
		Format = "yyyy",
		Meta = new Dictionary<string, object> { { "item_1", "value_1" } }
	};
};

Όταν το επίπεδο μεταφοράς πελάτη ξεκινά τη σειριοποίηση της αίτησης, το System.Text.Json θα χρησιμοποιήσει τους κατάλληλους προσαρμοσμένους μετατροπείς για να χειριστεί τη σειριοποίηση. Σε αυτό το παράδειγμα, το τελικό JSON έχει ως εξής.

{
    "aggregations": {
        "min_last_activity": {
            "min": {
                "format": "yyyy",
                "field": "lastActivity"
            },
            "meta": {
                "item_1": "value_1"
            }
        }
    },
    "query": {
        "term": {
            "type": {
                "value": "project"
            }
        }
    },
    "size": 0
}

Όπως μπορούμε να δούμε, η συνάθροιση ελάχ. περιλαμβάνεται από το Λεξικό Συνάθροισης. Οι ιδιότητές του έχουν σειριοποιηθεί ως μέρος του εσωτερικού αντικειμένου. Οι μετα-πληροφορίες γράφονται εντός του εξωτερικού αντικειμένου για να ευθυγραμμιστούν με τη μορφή που αναμένει η Elasticsearch.

Περίληψη

Οι προσαρμοσμένοι μετατροπείς είναι εξαιρετικά ισχυροί και μας επιτρέπουν να ελέγχουμε πλήρως την (απο)σειριοποίηση των τύπων κατά τη χρήση του System.Text.Json. Πολλά από τα πιο σύνθετα στοιχεία του προγράμματος-πελάτη Elasticsearch .NET για v8.0 απαιτούν μετατροπείς είτε κατασκευασμένους με μη αυτόματο τρόπο είτε δημιουργούμενους από κώδικα. Χρησιμοποιώντας αυτές τις τεχνικές, μπόρεσα να ξεπεράσω τις μερικές φορές πολύπλοκες απαιτήσεις JSON που υποστηρίζουν τη μετακίνησή μας να εξαρτόμαστε από το System.Text.Json της Microsoft.