Υπάρχει μια απαίτηση που εμφανίζεται σχεδόν σε κάθε εφαρμογή:
«Θέλω να βλέπω μια λίστα με τα δεδομένα μου».
Μπορεί να είναι προϊόντα σε ένα ηλεκτρονικό κατάστημα, παραγγελίες σε ένα διαχειριστικό περιβάλλον, πελάτες σε ένα CRM ή εργασίες σε ένα σύστημα διαχείρισης έργων.
Στην αρχή, η υλοποίηση φαίνεται απλή. Δημιουργούμε ένα endpoint, διαβάζουμε μερικές εγγραφές από τη βάση και τις επιστρέφουμε.
Στη συνέχεια αρχίζουν οι πραγματικές ανάγκες:
«Να μπορώ να ψάχνω με βάση το όνομα».
«Να εμφανίζω μόνο συγκεκριμένες κατηγορίες».
«Να ταξινομώ με βάση την τιμή».
«Να βλέπω είκοσι αποτελέσματα κάθε φορά».
«Να μπορώ να τα συνδυάζω όλα αυτά».
Κάπως έτσι, ένα μικρό endpoint αποκτά πολλές συνθήκες, διαφορετικές διαδρομές εκτέλεσης και επαναλαμβανόμενο κώδικα. Όταν η ίδια απαίτηση εμφανιστεί σε πέντε ή δέκα διαφορετικές λίστες, το πρόβλημα γίνεται και θέμα συντήρησης.
Το Sieve προσφέρει έναν κοινό τρόπο να περιγράφουμε και να εφαρμόζουμε filtering, sorting και pagination σε ένα .NET API.
Για να το αξιοποιήσουμε σωστά, όμως, χρειάζεται να καταλάβουμε περισσότερα από το πώς εγκαθίσταται ένα NuGet package. Χρειάζεται να δούμε πώς συνδέεται με το LINQ, πότε εκτελείται το query στη βάση, ποια πεδία επιτρέπουμε στον client να χρησιμοποιήσει και ποιο είναι το κόστος αυτής της ευελιξίας.
Σε αυτό το άρθρο θα ξεκινήσουμε από τις βασικές έννοιες και θα προχωρήσουμε σταδιακά σε ένα ολοκληρωμένο παράδειγμα με ASP.NET Core, EF Core και SQLite.
Ο στόχος είναι διπλός: ένας αρχάριος να μπορεί να παρακολουθήσει τη λογική και ένας πιο έμπειρος developer να μπορεί να αξιολογήσει τις σχεδιαστικές αποφάσεις.
1. Τι είναι το Sieve
Το Sieve είναι μια βιβλιοθήκη για .NET που μας επιτρέπει να εφαρμόζουμε:
- Filtering: ποια δεδομένα θέλουμε.
- Sorting: με ποια σειρά τα θέλουμε.
- Pagination: ποιο τμήμα των αποτελεσμάτων θέλουμε.
Η περιγραφή αυτών των επιλογών μπορεί να έρχεται μέσα από τις παραμέτρους ενός HTTP request.
Για παράδειγμα:
GET /api/products?filters=category==Accessories&sorts=priceCents&page=1&pageSize=20 Το request διαβάζεται ως:
«Επέστρεψε την πρώτη σελίδα, με έως είκοσι προϊόντα της κατηγορίας Accessories, ταξινομημένα κατά αύξουσα τιμή».
Το Sieve μετατρέπει αυτή την περιγραφή σε LINQ εκφράσεις πάνω σε ένα IQueryable.
Όταν το IQueryable προέρχεται από EF Core, ο database provider μεταφράζει τις υποστηριζόμενες εκφράσεις σε SQL.
Μπορούμε να σκεφτούμε το Sieve ως έναν μηχανισμό σύνθεσης queries με μια μικρή, συγκεκριμένη γλώσσα.
Ο τεχνικός όρος για μια τέτοια γλώσσα είναι DSL — Domain-Specific Language: μια γλώσσα σχεδιασμένη για έναν συγκεκριμένο σκοπό.
Στην περίπτωσή μας, ο σκοπός είναι η περιγραφή φίλτρων, ταξινομήσεων και σελίδων.
2. Οι βασικές έννοιες χωρίς προϋποθέσεις
Πριν περάσουμε στον κώδικα, αξίζει να ξεκαθαρίσουμε τους όρους που θα χρησιμοποιήσουμε.
API και endpoint
Ένα API είναι ένας τρόπος επικοινωνίας μεταξύ εφαρμογών.
Ένα endpoint είναι ένα συγκεκριμένο σημείο αυτού του API:
GET /api/products Ένα frontend μπορεί να καλέσει αυτό το endpoint για να ζητήσει τα προϊόντα που θα εμφανίσει στην οθόνη.
Query parameters
Είναι οι παράμετροι που εμφανίζονται μετά το ? σε ένα URL:
?page=2&pageSize=20 Το & χωρίζει τις διαφορετικές παραμέτρους.
Στο συγκεκριμένο παράδειγμα:
- Το
pageέχει τιμή2. - Το
pageSizeέχει τιμή20.
Filtering
Το φιλτράρισμα καθορίζει ποια δεδομένα θα συμπεριληφθούν.
Παράδειγμα:
«Θέλω προϊόντα με τιμή μεγαλύτερη ή ίση από 50 ευρώ».
Sorting
Η ταξινόμηση καθορίζει τη σειρά των αποτελεσμάτων.
Παράδειγμα:
«Θέλω πρώτα τα φθηνότερα προϊόντα».
Pagination
Η σελιδοποίηση χωρίζει τα αποτελέσματα σε μικρότερα τμήματα.
Αν υπάρχουν 1.000 προϊόντα και ζητάμε 20 ανά σελίδα:
- Η πρώτη σελίδα αντιστοιχεί στις πρώτες 20 θέσεις.
- Η δεύτερη στις επόμενες 20.
- Η τρίτη στις επόμενες 20.
Έτσι περιορίζουμε τον όγκο δεδομένων που επιστρέφει κάθε request και διευκολύνουμε την παρουσίασή τους.
Entity
Ένα entity είναι ένα αντικείμενο που αναπαριστά δεδομένα του συστήματος.
Σε μια εφαρμογή με σχεσιακή βάση, συχνά αντιστοιχεί σε εγγραφές ενός πίνακα.
Για παράδειγμα:
public class Product { public int Id { get; set; } public string Name { get; set; } = ""; } DTO
Ένα DTO — Data Transfer Object είναι το αντικείμενο που επιλέγουμε να μεταφέρουμε προς τον client.
Μπορεί να περιέχει λιγότερα ή διαφορετικά πεδία από το entity.
Η βάση μπορεί να γνωρίζει εσωτερικές πληροφορίες που δεν χρειάζεται να εκτίθενται στο response.
LINQ
Το LINQ μάς επιτρέπει να περιγράφουμε queries σε C#:
products.Where(p => p.PriceCents >= 5000); Το συγκεκριμένο query εκφράζει:
«Κράτησε μόνο τα προϊόντα με τιμή τουλάχιστον 5.000 λεπτά».
Entity Framework Core
Το EF Core είναι ένα ORM.
Συνδέει τα αντικείμενα της εφαρμογής με τα δεδομένα της βάσης και μπορεί να μεταφράζει LINQ queries σε SQL.
Το Sieve συνεργάζεται με αυτή τη διαδικασία. Η σύνδεση με τη βάση και η εκτέλεση του SQL παραμένουν ευθύνη του EF Core και του provider.
3. Ποιο πρόβλημα λύνει
Ας υποθέσουμε ότι κατασκευάζουμε ένα endpoint προϊόντων χωρίς Sieve:
var query = db.Products.AsQueryable(); if (!string.IsNullOrWhiteSpace(category)) { query = query.Where(p => p.Category == category); } if (minPriceCents.HasValue) { query = query.Where( p => p.PriceCents >= minPriceCents.Value); } if (maxPriceCents.HasValue) { query = query.Where( p => p.PriceCents <= maxPriceCents.Value); } if (!string.IsNullOrWhiteSpace(name)) { query = query.Where( p => p.Name.Contains(name)); } Για ένα μικρό endpoint, αυτή είναι μια λογική και συχνά επαρκής λύση.
Η πολυπλοκότητα αυξάνεται όταν προσθέτουμε:
- Περισσότερα πεδία.
- Διαφορετικούς operators ανά πεδίο.
- Πολλαπλά κριτήρια ταξινόμησης.
- Συνδυασμούς
ANDκαιOR. - Παρόμοια συμπεριφορά σε πολλά endpoints.
Με το Sieve, η απαίτηση μπορεί να περιγραφεί ως:
filters=category==Accessories,priceCents>=5000,priceCents<=15000 Και να εφαρμοστεί με έναν κοινό μηχανισμό:
var result = sieve.Apply(model, query); Η αξία του βρίσκεται στην τυποποίηση μιας επαναλαμβανόμενης κατηγορίας queries.
Αυτό μπορεί να προσφέρει:
- Λιγότερο επαναλαμβανόμενο κώδικα.
- Κοινές συμβάσεις μεταξύ endpoints.
- Ευκολότερη προσθήκη επιτρεπόμενων πεδίων.
- Πιο προβλέψιμη συνεργασία backend και frontend.
Υπάρχει όμως και κόστος: το API αποκτά μια γλώσσα queries που πρέπει να τεκμηριώνεται, να περιορίζεται και να συντηρείται.
4. Πώς λειτουργεί κάτω από την επιφάνεια
Η βασική ροή είναι:
HTTP request ↓ Παραλαβή και έλεγχος παραμέτρων ↓ Δημιουργία βασικού IQueryable ↓ Εφαρμογή filtering ↓ Εφαρμογή sorting ↓ Εφαρμογή pagination ↓ Επιλογή πεδίων για το response ↓ Μετάφραση από τον EF Core provider ↓ Εκτέλεση SQL ↓ JSON response Το Sieve συνθέτει εκφράσεις πάνω στο query.
Η βάση εκτελεί το query όταν φτάσουμε σε μια λειτουργία που απαιτεί πραγματικά αποτελέσματα, όπως:
ToListAsync() ή:
CountAsync() Αυτό μας οδηγεί στην πιο σημαντική τεχνική έννοια του άρθρου.
Η διαφορά ανάμεσα σε IQueryable και φορτωμένα δεδομένα
Εξετάζουμε τον παρακάτω κώδικα:
var query = db.Products .Where(p => p.IsActive); Σε αυτό το σημείο συνήθως δεν έχουμε φορτώσει προϊόντα από τη βάση.
Έχουμε δημιουργήσει την περιγραφή ενός query.
Μπορούμε να συνεχίσουμε:
query = query.Where( p => p.PriceCents >= 5000); Και αργότερα να εκτελέσουμε:
var products = await query.ToListAsync(); Αυτή η αναβολή της εκτέλεσης ονομάζεται deferred execution.
Το Sieve αξιοποιεί αυτή τη δυνατότητα για να προσθέσει τις εκφράσεις του πριν εκτελεστεί το query.
Το λάθος που πρέπει να αποφεύγουμε
var products = await db.Products.ToListAsync(); var result = sieve.Apply( model, products.AsQueryable()); Εδώ τα προϊόντα έχουν ήδη φορτωθεί στη μνήμη.
Το AsQueryable() πάνω σε μια List<Product> δεν επαναφέρει το query στη βάση.
Το Sieve εφαρμόζεται πλέον στα αντικείμενα που έχουν ήδη φορτωθεί.
Για ένα endpoint που διαβάζει από βάση, η επιθυμητή σειρά είναι:
var query = db.Products.AsNoTracking(); query = sieve.Apply(model, query); var products = await query.ToListAsync(); Αυτό προϋποθέτει ότι οι εκφράσεις που προστίθενται μπορούν να μεταφραστούν από τον συγκεκριμένο database provider.
5. Οι εκδόσεις του παραδείγματος
Το άρθρο χρησιμοποιεί:
| Στοιχείο | Έκδοση |
|---|---|
| Target framework | .NET 10 |
| Sieve | 2.5.5 |
| EF Core SQLite provider | 10.0.14 |
Η Sieve 2.5.5 είναι έκδοση του 2022 και το package στοχεύει το .NET Standard 2.1.
Η ηλικία ενός release είναι στοιχείο που πρέπει να λαμβάνουμε υπόψη όταν αξιολογούμε μια εξάρτηση.
Πριν από υιοθέτηση σε πραγματικό project, εξετάζουμε:
- Τις λειτουργίες που χρειαζόμαστε.
- Την κατάσταση των releases και των σχετικών issues.
- Τη συμπεριφορά με τον δικό μας database provider.
- Την ευκολία μελλοντικής αντικατάστασης.
- Το κόστος συντήρησης του query contract.
Η συμβατότητα του target framework δεν αποδεικνύει από μόνη της ότι κάθε συνδυασμός runtime, EF Core και provider λειτουργεί όπως περιμένουμε.
Ο κώδικας που ακολουθεί είναι εκπαιδευτικό παράδειγμα και δεν έχει εκτελεστεί ή δοκιμαστεί στο πλαίσιο αυτού του άρθρου.
6. Η σύνταξη του Sieve
Οι βασικές παράμετροι είναι:
filters sorts page pageSize Filtering
Η βασική μορφή είναι:
πεδίο operator τιμή Στο πραγματικό query:
priceCents>=5000 Στο παράδειγμά μας, οι τιμές αποθηκεύονται ως ακέραια λεπτά του ευρώ:
5000 λεπτά = 50 ευρώ Οι βασικοί operators
| Operator | Σημασία | Παράδειγμα |
|---|---|---|
== | Ίσο | category==Accessories |
!= | Διαφορετικό | category!=Displays |
> | Μεγαλύτερο | priceCents>5000 |
>= | Μεγαλύτερο ή ίσο | priceCents>=5000 |
< | Μικρότερο | priceCents<15000 |
<= | Μικρότερο ή ίσο | priceCents<=15000 |
@= | Περιέχει | name@=Keyboard |
_= | Αρχίζει με | name_=Laptop |
_-= | Τελειώνει σε | name_-=Mouse |
!@= | Δεν περιέχει | name!@=Cover |
!_= | Δεν αρχίζει με | name!_=Laptop |
!_-= | Δεν τελειώνει σε | name!_-=Mouse |
Για αρκετές λειτουργίες κειμένου υπάρχει και μορφή χωρίς διάκριση πεζών και κεφαλαίων:
name@=*keyboard Το @=* εκφράζει contains χωρίς διάκριση πεζών και κεφαλαίων.
Υπάρχουν αντίστοιχα μορφές όπως:
==* !=* _=* _-=* AND μεταξύ φίλτρων
Το κόμμα συνδέει διαφορετικά φίλτρα:
category==Accessories,priceCents>=5000 Διαβάζεται ως:
Category == Accessories AND PriceCents >= 5000 OR μεταξύ τιμών
Το | περιγράφει εναλλακτικές τιμές:
category==Accessories|Displays Διαβάζεται ως:
Category == Accessories OR Category == Displays OR μεταξύ πεδίων
Μπορούμε να εφαρμόσουμε την ίδια συνθήκη σε περισσότερα πεδία:
(name|category)@=*keyboard Η σημασία είναι:
Το Name περιέχει keyboard OR το Category περιέχει keyboard Η σύνταξη καλύπτει συγκεκριμένες μορφές συνδυασμών.
Για αυθαίρετα επίπεδα ομαδοποίησης και σύνθετους Boolean κανόνες χρειάζεται ξεχωριστή αξιολόγηση ή ειδικό query contract.
Μια παγίδα με το «διαφορετικό»
Το παρακάτω μπορεί να οδηγήσει σε λάθος προσδοκίες:
category!=Accessories|Displays Οι εναλλακτικές τιμές συνδέονται με OR.
Αν θέλουμε να αποκλείσουμε και τις δύο κατηγορίες, η σαφής μορφή είναι:
category!=Accessories,category!=Displays Δηλαδή:
Category != Accessories AND Category != Displays Sorting
Αύξουσα ταξινόμηση:
sorts=priceCents Φθίνουσα ταξινόμηση:
sorts=-priceCents Πολλαπλά κριτήρια:
sorts=priceCents,name Η σειρά των κριτηρίων έχει σημασία:
- Πρώτα ταξινόμηση κατά τιμή.
- Σε ισοβαθμία, ταξινόμηση κατά όνομα.
Pagination
page=2&pageSize=20 Ζητάμε τη δεύτερη σελίδα, με είκοσι αποτελέσματα ανά σελίδα.
Η offset σελιδοποίηση χρησιμοποιεί τον υπολογισμό:
skip = (page - 1) × pageSize Για τη δεύτερη σελίδα:
skip = (2 - 1) × 20 = 20 Παραλείπουμε τις πρώτες είκοσι θέσεις και παίρνουμε τις επόμενες είκοσι.
Escaping και URL encoding
Αν μια τιμή περιέχει χαρακτήρες με ειδική σημασία για το Sieve, χρειάζεται escaping:
name@=ACME\, Inc Το \, δηλώνει ότι το κόμμα ανήκει στην τιμή.
Αυτό είναι διαφορετικό από το URL encoding, που αφορά τη μεταφορά της παραμέτρου μέσα στο URL.
Στην πράξη χρειαζόμαστε:
- Σωστή σύνταξη για το Sieve.
- Σωστό encoding για το HTTP request.
Στα παραδείγματα θα χρησιμοποιήσουμε το --data-urlencode του curl.
7. Τι θα κατασκευάσουμε
Θα δημιουργήσουμε ένα API καταλόγου προϊόντων:
GET /api/products Θα υποστηρίζει:
- Filtering με βάση όνομα, κατηγορία και τιμή.
- Sorting με βάση αναγνωριστικό, όνομα, τιμή και ημερομηνία δημιουργίας.
- Pagination.
- Συνολικό πλήθος αποτελεσμάτων.
- Σταθερή ταξινόμηση στις ισοβαθμίες.
- Όρια στα εισερχόμενα queries.
- Επιστροφή μόνο ενεργών προϊόντων.
Θα χρησιμοποιήσουμε SQLite ώστε η τοπική βάση να βρίσκεται σε ένα αρχείο.
Οι τιμές θα αποθηκεύονται ως ακέραια λεπτά του ευρώ.
Αυτή είναι μια συγκεκριμένη επιλογή για έναν κατάλογο ενός νομίσματος με δύο δεκαδικά ψηφία. Ένα σύστημα πολλών νομισμάτων ή σύνθετων οικονομικών υπολογισμών χρειάζεται ευρύτερο μοντέλο χρημάτων και κανόνες στρογγυλοποίησης.
Η δομή του project θα είναι:
SieveCatalog.Api/ ├── Contracts/ │ └── ProductContracts.cs ├── Controllers/ │ └── ProductsController.cs ├── Data/ │ └── CatalogDbContext.cs ├── Models/ │ └── Product.cs ├── Properties/ │ └── launchSettings.json ├── Querying/ │ ├── CatalogSieveProcessor.cs │ └── ProductQueryPolicy.cs ├── Program.cs └── appsettings.json 8. Βήμα 1 — Δημιουργία του project
Χρειαζόμαστε εγκατεστημένο .NET 10 SDK.
Δημιουργούμε ένα κενό ASP.NET Core project:
dotnet new web --name SieveCatalog.Api --framework net10.0 Μπαίνουμε στον φάκελο:
cd SieveCatalog.Api Προσθέτουμε το Sieve:
dotnet add package Sieve --version 2.5.5 Προσθέτουμε τον SQLite provider του EF Core:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.14 Στη συνέχεια δημιουργούμε τους φακέλους και τα αρχεία της προηγούμενης δομής.
9. Βήμα 2 — Το μοντέλο Product
Στο αρχείο Models/Product.cs:
namespace SieveCatalog.Api.Models; public sealed class Product { public int Id { get; set; } public string Name { get; set; } = ""; public string Category { get; set; } = ""; public int PriceCents { get; set; } public DateTime CreatedAtUtc { get; set; } public bool IsActive { get; set; } } Το IsActive θα χρησιμοποιείται από τον server για να αποφασίζει ποια προϊόντα εμφανίζονται.
Η ημερομηνία CreatedAtUtc θα χρησιμοποιείται για ταξινόμηση και στο παράδειγμα περιέχει τιμές UTC.
10. Βήμα 3 — Το DbContext και τα αρχικά δεδομένα
Στο αρχείο Data/CatalogDbContext.cs:
using Microsoft.EntityFrameworkCore; using SieveCatalog.Api.Models; namespace SieveCatalog.Api.Data; public sealed class CatalogDbContext( DbContextOptions<CatalogDbContext> options) : DbContext(options) { public DbSet<Product> Products => Set<Product>(); protected override void OnModelCreating( ModelBuilder modelBuilder) { var product = modelBuilder.Entity<Product>(); product.Property(p => p.Name) .HasMaxLength(200) .IsRequired(); product.Property(p => p.Category) .HasMaxLength(80) .IsRequired(); product.HasIndex(p => new { p.IsActive, p.PriceCents, p.Id }); product.HasIndex(p => new { p.IsActive, p.CreatedAtUtc, p.Id }); product.HasData( Seed(1, "Mechanical Keyboard", "Accessories", 8500, true), Seed(2, "Wireless Mouse", "Accessories", 3500, true), Seed(3, "USB-C Dock", "Accessories", 12000, true), Seed(4, "27-inch Monitor", "Displays", 26000, true), Seed(5, "Laptop Pro", "Computers", 140000, true), Seed(6, "Laptop Air", "Computers", 95000, true), Seed(7, "Keyboard Cover", "Accessories", 1500, true), Seed(8, "Retired Keyboard", "Accessories", 6500, false), Seed(9, "Travel Mouse", "Accessories", 3500, true) ); } private static Product Seed( int id, string name, string category, int priceCents, bool isActive) { return new Product { Id = id, Name = name, Category = category, PriceCents = priceCents, IsActive = isActive, CreatedAtUtc = new DateTime( 2026, 1, id, 12, 0, 0, DateTimeKind.Utc) }; } } Το DbContext είναι το σημείο μέσω του οποίου συνθέτουμε και εκτελούμε queries για τα δεδομένα μας.
Το HasData ορίζει τα αρχικά δεδομένα του παραδείγματος.
Τα indexes είναι ενδεικτικοί υποψήφιοι για τα queries που μας ενδιαφέρουν. Η αποτελεσματικότητά τους εξαρτάται από τα πραγματικά φίλτρα, την ταξινόμηση και το execution plan.
Η ύπαρξη ενός index πρέπει να συνδέεται με συγκεκριμένο workload.
11. Βήμα 4 — Ορίζουμε τα πεδία που εκθέτει το Sieve
Το API χρειάζεται σαφή λίστα επιτρεπόμενων πεδίων.
Για το παράδειγμά μας:
| Όνομα στο API | Ιδιότητα C# | Filtering | Sorting |
|---|---|---|---|
id | Id | Όχι | Ναι |
name | Name | Ναι | Ναι |
category | Category | Ναι | Όχι |
priceCents | PriceCents | Ναι | Ναι |
created | CreatedAtUtc | Όχι | Ναι |
Θα χρησιμοποιήσουμε το Fluent API του Sieve.
Στο αρχείο Querying/CatalogSieveProcessor.cs:
using Microsoft.Extensions.Options; using Sieve.Models; using Sieve.Services; using SieveCatalog.Api.Models; namespace SieveCatalog.Api.Querying; public sealed class CatalogSieveProcessor( IOptions<SieveOptions> options) : SieveProcessor(options) { protected override SievePropertyMapper MapProperties( SievePropertyMapper mapper) { mapper.Property<Product>(p => p.Id) .CanSort() .HasName("id"); mapper.Property<Product>(p => p.Name) .CanFilter() .CanSort() .HasName("name"); mapper.Property<Product>(p => p.Category) .CanFilter() .HasName("category"); mapper.Property<Product>(p => p.PriceCents) .CanFilter() .CanSort() .HasName("priceCents"); mapper.Property<Product>(p => p.CreatedAtUtc) .CanSort() .HasName("created"); return mapper; } } Οι βασικές λειτουργίες είναι:
-
CanFilter(): επιτρέπει filtering. -
CanSort(): επιτρέπει sorting. -
HasName(): καθορίζει το εξωτερικό όνομα του πεδίου.
Γιατί χρησιμοποιούμε aliases
Ο client γνωρίζει το όνομα:
created Η εφαρμογή γνωρίζει την ιδιότητα:
CreatedAtUtc Αν αλλάξει η ονομασία της ιδιότητας C#, μπορούμε να διατηρήσουμε το εξωτερικό όνομα και να προσαρμόσουμε το mapping.
Έτσι τα query fields αντιμετωπίζονται ως μέρος του API contract.
Η εναλλακτική με attributes
Το Sieve υποστηρίζει και attributes:
using Sieve.Attributes; public sealed class Product { public int Id { get; set; } [Sieve( CanFilter = true, CanSort = true, Name = "name")] public string Name { get; set; } = ""; } Για μικρά projects, αυτή είναι μια απλή επιλογή.
Το Fluent API είναι χρήσιμο όταν θέλουμε οι αποφάσεις για τα queries να συγκεντρώνονται σε ξεχωριστό σημείο.
Στο project του άρθρου συνεχίζουμε με το Fluent API.
12. Βήμα 5 — Σχεδιάζουμε request και response
Στο αρχείο Contracts/ProductContracts.cs:
using System.ComponentModel.DataAnnotations...