CodeGym /Kursy /C# SELF /Kontrola nad procesem przez atrybuty

Kontrola nad procesem przez atrybuty

C# SELF
Poziom 45 , Lekcja 2
Dostępny

1. Po co są atrybuty przy serializacji?

Kiedy serializujesz obiekty do JSON lub XML, dzieje się coś podobnego do skanowania twojego pokoju robotem-sprzątaczem. Wszystko, co wyraźnie leży na widoku (publiczne właściwości/pola), trafia do „worka” (do pliku lub stringa), a reszta jest ignorowana. Ale czasami nie chcesz, żeby robot pokazał zawartość twojej torby, albo chcesz ładniej nazwać rzeczy — na przykład nie po-rosyjsku, a po-angielsku.

Tu wchodzą atrybuty! Pozwalają sterować procesem serializacji: ukrywać zbędne rzeczy, zmieniać nazwy, nakładać porządek, ignorować części obiektu albo mówić serializatorowi, że masz specjalne reguły. To jak naklejki na rzeczach: „nie dotykać”, „ważne”, „zmienić nazwę w JSON”.

Typowe zadania kontroli serializacji

  • Zmień nazwę właściwości lub pola w dokumencie wyjściowym (np. w JSON zamiast FirstName zrobić "first_name")
  • Ukryć niektóre pola/właściwości przed serializacją lub deserializacją (np. hasła, wewnętrzne liczniki)
  • Sterować traktowaniem wartości domyślnych lub null-wartości
  • Ustawić kolejność elementów (ważne dla XML)
  • Opisać dodatkowe parametry (atrybuty) dla elementów XML

Każda platforma serializacyjna — czy to System.Text.Json, Newtonsoft.Json czy XmlSerializer — używa swoich atrybutów do tych celów.

2. Atrybuty dla System.Text.Json: nowoczesny i modny

Klasyczny serializator JSON w systemie .NET obsługuje niezły zestaw atrybutów, które żyją w przestrzeni nazw System.Text.Json.Serialization.

Najbardziej użyteczne atrybuty:

Atrybut Do czego służy Przykład użycia
JsonPropertyName("...")
Zmienia nazwę właściwości w JSON
[JsonPropertyName("id")]
JsonIgnore
Całkowicie wyklucza właściwość z serializacji
[JsonIgnore]
JsonInclude
Serializuje publiczne pole (a nie tylko właściwość)
[JsonInclude]
JsonIgnore(Condition = ...)
Ignorować właściwość w określonych warunkach
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]

Przykłady użycia:

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("first_name")]
    public string FirstName { get; set; } // Nazwa w JSON będzie 'first_name'

    [JsonIgnore]
    public string Password { get; set; } // Nie trafi do JSON

    [JsonPropertyName("born_year")]
    public int? YearOfBirth { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Nickname { get; set; } // Jeśli null — nie trafi do JSON
}

Zserializujmy taką osobę:

var person = new Person
{
    FirstName = "Ivan",
    Password = "123456",
    YearOfBirth = 2000,
    Nickname = null
};

string json = JsonSerializer.Serialize(person);
// json: {"first_name":"Ivan","born_year":2000}

Zauważ: i hasło, i ksywka zniknęły z JSON-a! (Hasło — bo jest zawsze ignorowane, a ksywka — tylko jeśli jest null).

Dlaczego to ważne? W praktyce często nie chcesz, żeby strona kliencka (albo ktoś niepowołany!) zobaczył krytyczne dane jak hasła, tokeny, liczniki wewnętrzne, timestampy. Dzięki [JsonIgnore] i podobnym atrybutom zrobisz to jedną linią.

3. Atrybuty dla Newtonsoft.Json: lider elastyczności

Jeśli pracujesz z Newtonsoft.Json (dokumentacja tutaj), masz jeszcze więcej możliwości (i trochę prostsze API, jeśli pochodzisz ze starych projektów).

Główne atrybuty:

Atrybut Przeznaczenie
JsonProperty("...")
Ustawia nazwę właściwości w JSON, oraz kolejność, obowiązkowość itp.
JsonIgnore
Wyklucza pole/właściwość z serializacji
JsonRequired
Wymaga obecności przy deserializacji
JsonConverter
Pozwala wskazać własny konwerter dla skomplikowanych przypadków
DefaultValueHandling
Steruje serializacją wartości domyślnych

Przykład w praktyce:

using Newtonsoft.Json;

public class UserProfile
{
    [JsonProperty("login")]
    public string Username { get; set; }

    [JsonIgnore]
    public string InternalNotes { get; set; }

    [JsonProperty(Required = Required.Always)]
    public string Email { get; set; }
}

Dodatkowe możliwości JsonProperty — obowiązkowość (Required), kolejność (Order) itd. Na przykład:

[JsonProperty("id", Order = 1, Required = Required.Always)]
public int Id { get; set; }

4. Atrybuty dla XML: styl "classic"

XmlSerializer używa całego arsenału własnych atrybutów z przestrzeni nazw System.Xml.Serialization.

Najczęściej spotykane:

Atrybut Do czego służy
[XmlElement("...")]
Element w XML z inną nazwą
[XmlAttribute("...")]
Mapuje właściwość na atrybut elementu XML
[XmlIgnore]
Wyklucza właściwość lub pole z serializacji do XML
[XmlArray("...")]
Dla kolekcji — ustawia nazwę tablicy XML
[XmlArrayItem("...")]
Dla kolekcji — ustawia nazwę elementu tablicy
[XmlRoot("...")]
Zmienia nazwę korzeniowego taga XML

Przykład w praktyce:

using System.Xml.Serialization;

[XmlRoot("human")]
public class Person
{
    [XmlElement("firstname")]
    public string Name { get; set; }

    [XmlAttribute("years")]
    public int Age { get; set; }

    [XmlIgnore]
    public string Secret { get; set; }
}

var person = new Person { Name = "Anna", Age = 32, Secret = "42" };

Po serializacji XML będzie mniej więcej taki:

<human years="32"><firstname>Anna</firstname></human>

Zwróć uwagę, że pole Secret nie trafiło do XML, a Age zostało zserializowane jako atrybut, a nie jako zagnieżdżony tag.

5. Przydatne niuanse

Pod maską: jak działają atrybuty

Kiedy serializator napotka twoją klasę, dosłownie „czyta” ją przez reflection (magia .NET, więcej w System.Reflection). Serializator odczytuje metadane: na przykład, czy właściwość ma atrybut JsonIgnore albo XmlElement. W zależności od tego dodaje dane do końcowego dokumentu (albo je pomija).

To wygodny sposób oddzielić „schemat danych” od logiki biznesowej. Twoja klasa — to twoja logika biznesowa, a atrybuty — to w zasadzie paszport do serializacji.

Tabela zgodności kluczowych atrybutów

Funkcja System.Text.Json Newtonsoft.Json XmlSerializer
Zmień nazwę
[JsonPropertyName]
[JsonProperty]
[XmlElement]
[XmlAttribute]
Ignoruj
[JsonIgnore]
[JsonIgnore]
[XmlIgnore]
Własny format
[JsonConverter]
[JsonConverter]
— (przez IXmlSerializable, boli)
Korzeń obiektu
[XmlRoot]
Kolekcja
[XmlArray]
[XmlArrayItem]

6. Zaawansowane scenariusze

Czasem potrzebujesz serializować obiekt inaczej niż potrafi standardowy serializer. Na przykład chcesz przechowywać datę jako UNIX timestamp, a nie w standardowym formacie ISO. W takim przypadku są atrybuty, które pozwalają podłączyć własne konwertery.

W System.Text.Json:

public class UnixDateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime;

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        => writer.WriteNumberValue(new DateTimeOffset(value).ToUnixTimeSeconds());
}

public class LogEntry
{
    [JsonConverter(typeof(UnixDateTimeConverter))]
    public DateTime EventTime { get; set; }
}

Teraz przy serializacji pole EventTime zamieni się w liczbę, a nie string z datą.

W Newtonsoft.Json:

public class BoolToYesNoConverter : JsonConverter<bool>
{
    public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer)
        => writer.WriteValue(value ? "yes" : "no");

    public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer)
        => (string)reader.Value == "yes";
}

public class Answer
{
    [JsonConverter(typeof(BoolToYesNoConverter))]
    public bool IsCorrect { get; set; }
}

Teraz wartość bool będzie serializowana jako "yes" lub "no", a z powrotem zamieniana na true lub false.

7. Cechy i pułapki

Kiedy dodajesz atrybuty, pamiętaj: różne serializatory używają różnych atrybutów. Jeśli pracujesz i z JSON, i z XML (albo — co gorsza — jednocześnie z Newtonsoft.Json i System.Text.Json), nie zapomnij dodać obu potrzebnych atrybutów, inaczej czeka cię niespodzianka.

Standardowa nazwa właściwości będzie użyta przez serializator, chyba że ją nadpiszesz przez atrybut.

Uważaj przy dziedziczeniu: klasy potomne dziedziczą publiczne pola/właściwości i jeśli na rodzicu był atrybut, będzie on stosowany także w klasie potomnej. To często zaskakuje, ale tak jest zaprojektowane.

Typowy błąd: Często zdarza się, że deweloperzy przez przypadek oznaczą pole, które musi trafić do serializacji, atrybutem JsonIgnore (albo odwrotnie — zapomną zignorować wrażliwe dane). Albo np. dodają XmlElement do prywatnego pola — i dziwią się, czemu serializator go nie widzi (XmlSerializer obsługuje tylko public członków!).

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION