Στη C#, δύο τρόποι αρχικοποίησης μιας ιδιότητας μπορεί να φαίνονται ισοδύναμοι:
// Εναλλακτική Α:
public string Name { get; init; } = null!;
// Εναλλακτική Β:
public string Name { get; init; } = string.Empty;
Και οι δύο αποφεύγουν την προειδοποίηση για μη αρχικοποιημένη non-nullable ιδιότητα. Δημιουργούν όμως διαφορετική αρχική κατάσταση.
Το null! αναθέτει πραγματικό null. Το ! καταστέλλει την προειδοποίηση του compiler. Δεν προσθέτει προστασία κατά την εκτέλεση ούτε εγγυάται ότι η τιμή θα συμπληρωθεί αργότερα.
Πηγή: Microsoft — null-forgiving operator.
Το string.Empty αναθέτει "": μια πραγματική συμβολοσειρά χωρίς χαρακτήρες. Είναι διαφορετικό από το " ", που περιέχει έναν χαρακτήρα κενού.
Πηγή: Microsoft — String.Empty.
Για να επιλέξουμε, χρειάζεται να εξετάσουμε τον ρόλο του DTO: δέχεται δεδομένα που πρέπει να ελέγξουμε ή μεταφέρει δεδομένα που η εφαρμογή υπόσχεται ότι έχει ήδη συμπληρώσει;
Στα request/command DTOs, όπου η απουσία και το κενό αντιμετωπίζονται ισοδύναμα, προτιμώ string.Empty μαζί με FluentValidation.
public sealed record CreateZoneExceptionCommand
{
public string Description { get; init; } = string.Empty;
}
Το αντικείμενο ξεκινά με μη-null συμβολοσειρά. Αυτό δεν σημαίνει ότι περιέχει επιχειρησιακά έγκυρη περιγραφή.
Αυτή είναι η ευθύνη του validator:
public sealed class CreateZoneExceptionCommandValidator
: AbstractValidator<CreateZoneExceptionCommand>
{
public CreateZoneExceptionCommandValidator()
{
RuleFor(x => x.Description)
.NotEmpty()
.WithMessage("Η περιγραφή είναι υποχρεωτική.");
}
}
Για συμβολοσειρές, το NotEmpty() απορρίπτει:
| Τιμή | Αποτέλεσμα |
|---|---|
null |
Αποτυχία |
"" |
Αποτυχία |
" " |
Αποτυχία |
"Εξαίρεση για διεθνείς πτήσεις" |
Επιτυχία |
Αυτό επιβεβαιώνεται από την τεκμηρίωση του FluentValidation και τον πηγαίο κώδικα του validator.
Προσοχή: το NotNull() μόνο του επιτρέπει το "". Για ένα υποχρεωτικό πεδίο κειμένου, αυτή η διαφορά έχει σημασία.
Η προτίμησή μου προς το string.Empty αφορά τη συγκεκριμένη χρήση του request DTO:
- Αρχικοποίηση: το πεδίο ξεκινά με πραγματική, μη-null συμβολοσειρά.
- Validation: ελέγχουμε αν το περιεχόμενο επαρκεί για τη λειτουργία.
Το validation δεν συμπληρώνει την περιγραφή. Αναφέρει το πρόβλημα και η εφαρμογή σταματά τη λειτουργία.
Αυτή είναι μια συνειδητή σύμβαση, όχι καθολικός κανόνας της C#.
Ο έλεγχος της εισόδου πρέπει να γίνεται πριν από την αποθήκευση.
Σε μια εφαρμογή με validation pipeline, μια ενδεικτική ροή είναι:
API → Request/Command → Validation → Handler → Domain → Αποθήκευση
Αν ο έλεγχος αποτύχει, το API μπορεί να επιστρέψει 400 Bad Request με κατανοητά σφάλματα.
Η επίσημη τεκμηρίωση του FluentValidation δείχνει έλεγχο και επιστροφή σφαλμάτων πριν από την κλήση αποθήκευσης.
Πηγή: FluentValidation — Manual validation.
Το «πριν από τον handler» ισχύει όταν έχουμε υλοποιήσει και συνδέσει αντίστοιχο pipeline. Η ύπαρξη μιας κλάσης validator από μόνη της δεν εξασφαλίζει ότι θα εκτελεστεί.
Το Domain προστατεύει τους δικούς του απαραίτητους κανόνες, ενώ οι περιορισμοί της βάσης διατηρούν την ακεραιότητα των δεδομένων. Μια κενή υποχρεωτική περιγραφή πρέπει να απορρίπτεται από την εφαρμογή πριν επιχειρηθεί η αποθήκευσή της.
Πηγή: Microsoft — Domain model validations.
Το string.Empty ορίζει μόνο την αρχική τιμή.
Ένα εισερχόμενο JSON με "description": null μπορεί να την αντικαταστήσει, ανάλογα με τις ρυθμίσεις του serializer.
Με ενεργό RespectNullableAnnotations, σε υποστηριζόμενες περιπτώσεις το System.Text.Json μπορεί να απορρίψει το ρητό null ήδη κατά το deserialization.
Η προεπιλογή επομένως δεν αποτελεί προστασία από κάθε εξωτερική είσοδο.
Πηγή: Microsoft — Nullable annotations στο System.Text.Json.
Όταν το null έχει ξεχωριστή σημασία στο request, το μοντελοποιούμε ρητά:
public string? Description { get; init; }
Ένα μη ελεγμένο request μπορεί να μην περιέχει περιγραφή, ακόμη κι όταν η λειτουργία απαιτεί να συμπληρωθεί πριν συνεχίσει.
Στα response DTOs, όπως τα αποτελέσματα ενός query, η ευθύνη αλλάζει.
Η εφαρμογή πρέπει να συμπληρώσει όσα υπόσχεται το συμβόλαιο της απάντησης.
Για παράδειγμα, αν κάθε αποτέλεσμα πρέπει να περιέχει το label της περιόδου λειτουργίας, μια συνειδητή επιλογή για DTO που συμπληρώνεται από mapper είναι:
public sealed record ZoneExceptionDto
{
public string OperatingPeriodLabel { get; init; } = null!;
}
Εδώ χρησιμοποιούμε το null! με την προσδοκία ότι το mapping θα συμπληρώσει την ιδιότητα. Αν η ανάθεση παραλειφθεί, η τιμή παραμένει null.
Αποφεύγουμε έτσι να δώσουμε αυθαίρετα το "" ως προεπιλεγμένο label, όταν το συμβόλαιο απαιτεί πραγματική τιμή.
Το null! όμως δεν εντοπίζει την παράλειψη.
Δεν πετάει exception κατά τη δημιουργία του DTO και δεν ελέγχει αν ολοκληρώθηκε το mapping.
Το mapping πρέπει να αποδίδει την πραγματική τιμή:
var dto = new ZoneExceptionDto
{
OperatingPeriodLabel = entity.OperatingPeriod.Label
};
Αν το label είναι υποχρεωτικό, ένα αυθαίρετο fallback μπορεί να κρύψει το πρόβλημα:
OperatingPeriodLabel =
entity.OperatingPeriod.Label ?? string.Empty;
Το ?? string.Empty δικαιολογείται όταν το κενό αποτελεί αποδεκτή τιμή του response.
Για να ελέγξουμε τη χαρτογράφηση, χρησιμοποιούμε tests με γνωστά δεδομένα και συγκεκριμένες προσδοκίες:
dto.OperatingPeriodLabel
.Should()
.Be("Θερινή περίοδος");
Αυτό το test θα εντόπιζε την παράλειψη είτε η αρχική τιμή ήταν null είτε ήταν "".
Την επιβεβαίωση της σωστής χαρτογράφησης την παρέχει ο έλεγχος, όχι ο initializer. Όπου χρειάζεται εγγύηση κατά την εκτέλεση, προσθέτουμε αντίστοιχους ελέγχους ή επιβάλλουμε την αρχικοποίηση κατά τη δημιουργία του αντικειμένου.
Δεν είναι επίσης δεδομένο ότι μια ξεχασμένη ιδιότητα θα εμφανιστεί ως null στο JSON.
Ανάλογα με τις ρυθμίσεις του serializer, μπορεί:
- Να συμπεριληφθεί ως
null. - Να παραλειφθεί από το JSON.
- Να προκαλέσει σφάλμα serialization λόγω παραβίασης της non-nullable δήλωσης.
Δεν βασιζόμαστε στον frontend developer για να ανακαλύψει το λάθος.
Πηγές: Microsoft — Παράλειψη null ιδιοτήτων, έλεγχος nullable annotations.
Όπου το επιτρέπει ο τρόπος δημιουργίας του response DTO, μπορούμε να ενισχύσουμε την αρχικοποίηση με constructor ή, από C# 11, με required:
public required string OperatingPeriodLabel { get; init; }
Το required απαιτεί ανάθεση κατά τη συνήθη δημιουργία του αντικειμένου από C# κώδικα. Δεν εγγυάται από μόνο του ότι το string περιέχει αποδεκτό κείμενο.
Πηγή: Microsoft — required modifier.
Οι διαφορετικές τεχνικές απόψεις αναδεικνύουν την ίδια ανάγκη για προσοχή: ο Maarten Balliauw αποθαρρύνει την απόκρυψη του προβλήματος με default!, ενώ ο Ian Griffiths επισημαίνει ότι και το κενό string μπορεί να κρύψει ελλιπή αρχικοποίηση όταν χρησιμοποιείται απλώς για να εξαφανιστεί μια προειδοποίηση.
Η διάκριση αφορά DTO εισόδου και DTO εξόδου, όχι αποκλειστικά commands και queries.
Ένα query request μπορεί επίσης να χρειάζεται validation. Ένα command μπορεί να επιστρέφει response DTO που απαιτεί πλήρη χαρτογράφηση.
Η Clean Architecture οργανώνει αυτές τις ευθύνες χωρίς να επιβάλλει συγκεκριμένο initializer.
Πηγή: Microsoft — Clean Architecture.
Εσείς ποιες συμβάσεις ακολουθείτε στα request και response DTOs; Πώς εξασφαλίζετε ότι τα ελλιπή αιτήματα απορρίπτονται έγκαιρα και ότι τα λάθη στο mapping εντοπίζονται πριν φτάσουν στον client;
Top comments (0)