<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nikosst</title>
    <description>The latest articles on DEV Community by nikosst (@__b63657).</description>
    <link>https://dev.to/__b63657</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2710936%2F20075e34-3f33-44d9-a08b-c1703ff98457.jpg</url>
      <title>DEV Community: nikosst</title>
      <link>https://dev.to/__b63657</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__b63657"/>
    <language>en</language>
    <item>
      <title>Fake και Mock στα Unit Tests με παραδείγματα και Clean Architecture</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:05:05 +0000</pubDate>
      <link>https://dev.to/__b63657/fake-kai-mock-sta-unit-tests-me-paradeigmata-kai-clean-architecture-4cj9</link>
      <guid>https://dev.to/__b63657/fake-kai-mock-sta-unit-tests-me-paradeigmata-kai-clean-architecture-4cj9</guid>
      <description>&lt;p&gt;Γράφουμε ένα unit test. Ο κώδικάς μας χρησιμοποιεί ένα repository, μια βάση δεδομένων ή μια υπηρεσία ειδοποιήσεων.&lt;/p&gt;

&lt;p&gt;Και κάπου εκεί εμφανίζεται η ερώτηση:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Να χρησιμοποιήσω fake ή mock; Ποια είναι η διαφορά και ποιο κάνει καλύτερο το test μου;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένα fake μάς δίνει μια απλοποιημένη υλοποίηση που λειτουργεί. Ένα mock μάς επιτρέπει να επαληθεύσουμε συγκεκριμένες κλήσεις προς έναν συνεργάτη.&lt;/p&gt;

&lt;p&gt;Η επιλογή εξαρτάται από αυτό που θέλουμε να αποδείξουμε:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Αν ενδιαφερόμαστε για το αποτέλεσμα μιας αλλαγής, ένα fake συχνά κάνει το test πιο φυσικό.&lt;/li&gt;
&lt;li&gt;Αν ενδιαφερόμαστε για μια συγκεκριμένη εξωτερική ενέργεια, ένα mock μπορεί να εκφράζει ακριβώς την απαίτηση.&lt;/li&gt;
&lt;li&gt;Αν μια πραγματική υλοποίηση είναι ήδη απλή, γρήγορη και προβλέψιμη, μπορούμε να τη χρησιμοποιήσουμε απευθείας.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ας δούμε τι σημαίνουν αυτά, πάνω στο ίδιο παράδειγμα. Ο κώδικας χρησιμοποιεί &lt;strong&gt;Python 3.10+&lt;/strong&gt; και το ενσωματωμένο &lt;code&gt;unittest&lt;/code&gt;, χωρίς επιπλέον βιβλιοθήκες. Οι αρχές εφαρμόζονται και σε άλλες γλώσσες και frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Τι ελέγχει ένα unit test
&lt;/h2&gt;

&lt;p&gt;Ένα unit test ελέγχει μια συγκεκριμένη συμπεριφορά μιας μικρής μονάδας λογισμικού.&lt;/p&gt;

&lt;p&gt;Η μονάδα μπορεί να είναι μια συνάρτηση, μια κλάση ή ένα μικρό σύνολο αντικειμένων που συνεργάζονται. Δεν υπάρχει ένας καθολικά αποδεκτός κανόνας ότι «ένα unit test επιτρέπεται να εκτελεί μόνο μία κλάση». Ο Martin Fowler περιγράφει τόσο tests με πραγματικούς συνεργάτες όσο και tests που τους αντικαθιστούν με test doubles. &lt;a href="https://martinfowler.com/bliki/UnitTest.html" rel="noopener noreferrer"&gt;Unit Test — Martin Fowler&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, σε μια εφαρμογή διαχείρισης εργασιών, διαφορετικές συμπεριφορές είναι:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Μια ανολοκλήρωτη εργασία γίνεται ολοκληρωμένη.&lt;/li&gt;
&lt;li&gt;Η νέα κατάσταση αποθηκεύεται.&lt;/li&gt;
&lt;li&gt;Στέλνεται ειδοποίηση ολοκλήρωσης.&lt;/li&gt;
&lt;li&gt;Μια εργασία που δεν υπάρχει προκαλεί συγκεκριμένο σφάλμα.&lt;/li&gt;
&lt;li&gt;Η επανάληψη της ενέργειας δεν στέλνει δεύτερη ειδοποίηση.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Παρότι όλες μπορεί να σχετίζονται με μία μέθοδο, αξίζουν ξεχωριστά, εστιασμένα tests.&lt;/p&gt;

&lt;p&gt;Ένα καλό test περιγράφει &lt;strong&gt;ποια απαίτηση ισχύει και υπό ποιες συνθήκες&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Η οικογένεια των test doubles
&lt;/h2&gt;

&lt;p&gt;Το &lt;strong&gt;test double&lt;/strong&gt; είναι ο γενικός όρος για ένα αντικείμενο που αντικαθιστά κάποιο άλλο κατά τη διάρκεια ενός test.&lt;/p&gt;

&lt;p&gt;Δεν είναι όλα τα test doubles mocks.&lt;/p&gt;

&lt;p&gt;Η παρακάτω ορολογία ακολουθεί τη διάκριση που παρουσιάζει ο Fowler, βασισμένος στο έργο του Gerard Meszaros. &lt;a href="https://martinfowler.com/articles/mocksArentStubs.html" rel="noopener noreferrer"&gt;Mocks Aren’t Stubs&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Είδος&lt;/th&gt;
&lt;th&gt;Ρόλος&lt;/th&gt;
&lt;th&gt;Απλό παράδειγμα&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dummy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Συμπληρώνει μια παράμετρο και δεν χρησιμοποιείται στο συγκεκριμένο test.&lt;/td&gt;
&lt;td&gt;Ένα αντικείμενο που απαιτεί ο constructor, αλλά δεν καλείται.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Δίνει προκαθορισμένες απαντήσεις.&lt;/td&gt;
&lt;td&gt;«Όταν ζητηθεί η εργασία, επέστρεψε αυτή την τιμή».&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fake&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Παρέχει μια απλοποιημένη λειτουργική υλοποίηση.&lt;/td&gt;
&lt;td&gt;Repository που αποθηκεύει αντικείμενα σε ένα dictionary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Καταγράφει κλήσεις ώστε να τις εξετάσουμε.&lt;/td&gt;
&lt;td&gt;Καταγραφέας των ειδοποιήσεων που ζητήθηκαν.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Χρησιμοποιείται για επαλήθευση προσδοκιών σχετικά με κλήσεις.&lt;/td&gt;
&lt;td&gt;«Η ειδοποίηση πρέπει να κληθεί μία φορά με αυτό το ID».&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Οι ονομασίες των βιβλιοθηκών δεν αντιστοιχούν πάντα ακριβώς σε αυτούς τους ρόλους.&lt;/p&gt;

&lt;p&gt;Ένα αντικείμενο που δημιουργήθηκε από το &lt;code&gt;unittest.mock&lt;/code&gt; μπορεί να χρησιμοποιείται μόνο ως stub. Αν απλώς επιστρέφει μια τιμή, κάνουμε stubbing. Αν επαληθεύουμε πώς κλήθηκε, κάνουμε interaction verification.&lt;/p&gt;

&lt;p&gt;Αντίστοιχα, ένα χειροποίητο αντικείμενο που καταγράφει κλήσεις μπορεί να λειτουργεί ως spy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ο ρόλος μέσα στο test έχει περισσότερη σημασία από τον τρόπο δημιουργίας του αντικειμένου.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Η βασική διαφορά Fake και Mock
&lt;/h2&gt;

&lt;p&gt;Ας υποθέσουμε ότι ολοκληρώνουμε μια εργασία.&lt;/p&gt;

&lt;p&gt;Με fake repository μπορούμε να ελέγξουμε:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Μετά την εκτέλεση, το repository επιστρέφει την εργασία ως ολοκληρωμένη.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Με mock repository μπορούμε να ελέγξουμε:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Το use case κάλεσε τη μέθοδο αποθήκευσης με την ολοκληρωμένη εργασία.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Στην πρώτη περίπτωση εξετάζουμε την κατάσταση που προέκυψε, δηλαδή κάνουμε &lt;strong&gt;state verification&lt;/strong&gt;. Στη δεύτερη εξετάζουμε μια αλληλεπίδραση, δηλαδή κάνουμε &lt;strong&gt;interaction verification&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Αυτά είναι διαφορετικά αποδεικτικά στοιχεία.&lt;/p&gt;

&lt;p&gt;Το fake repository μάς επιτρέπει να αποθηκεύσουμε και μετά να διαβάσουμε δεδομένα. Το mock repository καταγράφει ότι ζητήθηκε μια αποθήκευση.&lt;/p&gt;

&lt;p&gt;Κανένα από τα δύο, από μόνο του, δεν αποδεικνύει ότι λειτουργεί το πραγματικό SQL query.&lt;/p&gt;

&lt;p&gt;Επίσης, οι άξονες δεν είναι απόλυτοι: ένα test μπορεί να χρησιμοποιεί fake και ταυτόχρονα να ελέγχει αλληλεπιδράσεις σε έναν δεύτερο συνεργάτη.&lt;/p&gt;

&lt;h2&gt;
  
  
  Το παράδειγμα που θα χρησιμοποιήσουμε
&lt;/h2&gt;

&lt;p&gt;Θα υλοποιήσουμε το use case &lt;code&gt;CompleteTask&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Οι απαιτήσεις του είναι:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Φορτώνει την εργασία.&lt;/li&gt;
&lt;li&gt;Αν δεν υπάρχει, σηκώνει &lt;code&gt;TaskNotFound&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Αν είναι ήδη ολοκληρωμένη, την επιστρέφει χωρίς νέα ειδοποίηση.&lt;/li&gt;
&lt;li&gt;Διαφορετικά, την ολοκληρώνει και την αποθηκεύει.&lt;/li&gt;
&lt;li&gt;Μετά την επιτυχημένη αποθήκευση, ζητά την αποστολή ειδοποίησης.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Για το repository ορίζουμε ένα μικρό συμβόλαιο:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Το &lt;code&gt;get()&lt;/code&gt; επιστρέφει &lt;code&gt;None&lt;/code&gt; όταν το ID δεν υπάρχει.&lt;/li&gt;
&lt;li&gt;Το &lt;code&gt;save()&lt;/code&gt; δημιουργεί ή αντικαθιστά την εργασία με το συγκεκριμένο ID.&lt;/li&gt;
&lt;li&gt;Μια αποτυχία διαθεσιμότητας αποθήκευσης μπορεί να εκφραστεί ως &lt;code&gt;RepositoryUnavailable&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Θα χρησιμοποιήσουμε δύο αρχεία:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tasks.py
test_tasks.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Όλα τα blocks των tests που ακολουθούν μπαίνουν διαδοχικά στο ίδιο &lt;code&gt;test_tasks.py&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ο κώδικας της εφαρμογής
&lt;/h3&gt;

&lt;p&gt;Στο &lt;code&gt;tasks.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RepositoryUnavailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TaskNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

        &lt;span class="n"&gt;completed_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completed_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completed_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;completed_task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;Task&lt;/code&gt; εκφράζει την κατάσταση και τη μετάβαση ολοκλήρωσης.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;CompleteTask&lt;/code&gt; συντονίζει τη φόρτωση, την αποθήκευση και την ειδοποίηση.&lt;/p&gt;

&lt;p&gt;Τα &lt;code&gt;TaskRepository&lt;/code&gt; και &lt;code&gt;CompletionNotifier&lt;/code&gt; περιγράφουν τις δυνατότητες που χρειάζεται το use case. Δεν περιέχουν λεπτομέρειες SQL, HTTP ή κάποιου SDK.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;Protocol&lt;/code&gt; επιτρέπει να περιγράψουμε αυτή τη δομή χωρίς να απαιτούμε κοινή βασική κλάση από κάθε υλοποίηση. Οι δηλώσεις τύπων δεν επιβάλλουν από μόνες τους το επιχειρησιακό συμβόλαιο κατά την εκτέλεση.&lt;/p&gt;

&lt;h2&gt;
  
  
  Πρώτα δοκιμάζουμε τον απλό κώδικα χωρίς doubles
&lt;/h2&gt;

&lt;p&gt;Η ολοκλήρωση του ίδιου του &lt;code&gt;Task&lt;/code&gt; δεν χρειάζεται repository ή υπηρεσία ειδοποιήσεων.&lt;/p&gt;

&lt;p&gt;Στην αρχή του &lt;code&gt;test_tasks.py&lt;/code&gt; βάζουμε τα imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_autospec&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RepositoryUnavailable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TaskNotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και προσθέτουμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_task_can_be_completed_without_test_doubles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το test ελέγχει την πραγματική υλοποίηση της συμπεριφοράς.&lt;/p&gt;

&lt;p&gt;Ελέγχει επίσης ότι το αρχικό αντικείμενο παραμένει ανολοκλήρωτο: η μέθοδος επιστρέφει νέο αντικείμενο.&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται να αντικαταστήσουμε με mock κάτι που είναι ήδη εύκολο να χρησιμοποιήσουμε.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ο ίδιος έλεγχος με Fake
&lt;/h2&gt;

&lt;p&gt;Για να ελέγξουμε το use case χωρίς πραγματική βάση, δημιουργούμε ένα repository που κρατά δεδομένα στη μνήμη.&lt;/p&gt;

&lt;p&gt;Στο &lt;code&gt;test_tasks.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InMemoryTaskRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό είναι fake επειδή έχει λειτουργική συμπεριφορά:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Αποθηκεύει διαφορετικές εργασίες.&lt;/li&gt;
&lt;li&gt;Αναζητά με βάση το ID.&lt;/li&gt;
&lt;li&gt;Διατηρεί αλλαγές μεταξύ κλήσεων.&lt;/li&gt;
&lt;li&gt;Αντικαθιστά υπάρχουσες εγγραφές.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Δεν χρειάζεται να ορίσουμε ξεχωριστά την απάντηση κάθε κλήσης.&lt;/p&gt;

&lt;p&gt;Χρειαζόμαστε επίσης έναν απλό συνεργάτη για τις ειδοποιήσεις:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecordingNotifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent_task_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent_task_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο &lt;code&gt;RecordingNotifier&lt;/code&gt; είναι &lt;strong&gt;spy&lt;/strong&gt;: καταγράφει τις κλήσεις που δέχεται. Δεν υλοποιεί πραγματική παράδοση ειδοποιήσεων.&lt;/p&gt;

&lt;p&gt;Το test με το fake repository είναι:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompleteTaskWithFakeTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_completed_task_is_persisted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Arrange
&lt;/span&gt;        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryTaskRepository&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecordingNotifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Act
&lt;/span&gt;        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Assert
&lt;/span&gt;        &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_missing_task_raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryTaskRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecordingNotifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskNotFound&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent_task_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το πρώτο test περιγράφει μια ολοκληρωμένη ακολουθία:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Υπάρχει μια ανολοκλήρωτη εργασία. Την ολοκληρώνω. Διαβάζω την αποθηκευμένη κατάστασή της και είναι ολοκληρωμένη.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Δεν καθορίζει πόσες φορές επιτρέπεται να γίνει ανάγνωση από το repository.&lt;/p&gt;

&lt;p&gt;Αν η υλοποίηση αλλάξει εσωτερικά, αλλά διατηρήσει το αποτέλεσμα και το συμβόλαιο, το test έχει περισσότερες πιθανότητες να παραμείνει χρήσιμο χωρίς αλλαγές.&lt;/p&gt;

&lt;h3&gt;
  
  
  Μια σημαντική παγίδα με mutable αντικείμενα
&lt;/h3&gt;

&lt;p&gt;Το παράδειγμα χρησιμοποιεί &lt;code&gt;Task&lt;/code&gt; με &lt;code&gt;frozen=True&lt;/code&gt; και πεδία που είναι αμετάβλητες τιμές.&lt;/p&gt;

&lt;p&gt;Αυτό αποτρέπει μια συνηθισμένη παγίδα των fake repositories.&lt;/p&gt;

&lt;p&gt;Αν το repository επέστρεφε το ίδιο mutable αντικείμενο που κρατά εσωτερικά, ο κώδικας θα μπορούσε να κάνει:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η αλλαγή θα φαινόταν αμέσως μέσα στο fake, ακόμη και αν ξεχνούσαμε να καλέσουμε &lt;code&gt;save()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Έτσι, ένα test θα μπορούσε να περνά ενώ η πραγματική αποθήκευση δεν θα λειτουργούσε.&lt;/p&gt;

&lt;p&gt;Στο δικό μας παράδειγμα δημιουργείται νέο &lt;code&gt;Task&lt;/code&gt;. Αν αφαιρέσουμε το &lt;code&gt;save()&lt;/code&gt;, το fake συνεχίζει να περιέχει το παλιό αντικείμενο και το test αποτυγχάνει.&lt;/p&gt;

&lt;p&gt;Η συμπεριφορά του fake πρέπει να σχεδιάζεται με προσοχή.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ο ίδιος έλεγχος με Mock
&lt;/h2&gt;

&lt;p&gt;Ας αντικαταστήσουμε τώρα το repository με ένα mock.&lt;/p&gt;

&lt;p&gt;Στο &lt;code&gt;test_tasks.py&lt;/code&gt; προσθέτουμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompleteTaskWithMockTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_completed_task_is_saved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Arrange
&lt;/span&gt;        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Act
&lt;/span&gt;        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Assert
&lt;/span&gt;        &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_missing_task_does_not_save_or_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskNotFound&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_not_called&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_not_called&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_failed_save_does_not_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RepositoryUnavailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RepositoryUnavailable&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_not_called&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Εδώ η γραμμή:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;κάνει &lt;strong&gt;stubbing&lt;/strong&gt;: ορίζει μια απάντηση.&lt;/p&gt;

&lt;p&gt;Η γραμμή:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;κάνει &lt;strong&gt;interaction verification&lt;/strong&gt;: ελέγχει μια κλήση.&lt;/p&gt;

&lt;p&gt;Το mock δεν αποθηκεύει την εργασία σε κάποιο λειτουργικό αποθετήριο. Καταγράφει ότι η &lt;code&gt;save()&lt;/code&gt; κλήθηκε με το αναμενόμενο αντικείμενο.&lt;/p&gt;

&lt;p&gt;Επίσης, το &lt;code&gt;assert_called_once_with()&lt;/code&gt; προσθέτει την απαίτηση της μίας κλήσης. Αν το μόνο που ενδιαφέρει το προϊόν είναι η τελική κατάσταση, αυτό μπορεί να είναι αυστηρότερος περιορισμός από όσο χρειάζεται.&lt;/p&gt;

&lt;p&gt;Αυτός είναι ένας λόγος να προτιμήσουμε το fake για τον συγκεκριμένο έλεγχο αποθήκευσης.&lt;/p&gt;

&lt;h3&gt;
  
  
  Γιατί χρησιμοποιούμε autospec και spec_set
&lt;/h3&gt;

&lt;p&gt;Το &lt;code&gt;create_autospec()&lt;/code&gt; δημιουργεί doubles με βάση το API που δίνουμε και ελέγχει τις υπογραφές των κλήσεων.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;spec_set=True&lt;/code&gt; περιορίζει την πρόσβαση και την ανάθεση σε γνωστά attributes.&lt;/p&gt;

&lt;p&gt;Έτσι, ένα λάθος όνομα μεθόδου ή λανθασμένος αριθμός παραμέτρων είναι πιθανότερο να αποκαλυφθεί άμεσα. Δεν επαληθεύονται, όμως, οι επιχειρησιακοί κανόνες ή οι τύποι επιστροφής από τα type hints. &lt;a href="https://docs.python.org/3/library/unittest.mock.html#unittest.mock.create_autospec" rel="noopener noreferrer"&gt;Τεκμηρίωση του unittest.mock&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Το autospec μειώνει ορισμένα λάθη στη χρήση του API. Δεν αποδεικνύει ότι το double συμπεριφέρεται όπως ο πραγματικός adapter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Πότε το mock είναι ιδιαίτερα βολικό
&lt;/h3&gt;

&lt;p&gt;Το τρίτο test προκαλεί αποτυχία αποθήκευσης:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RepositoryUnavailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και ελέγχει μια σαφή απαίτηση:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Αν η αποθήκευση αποτύχει, δεν πρέπει να ζητηθεί ειδοποίηση ολοκλήρωσης.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Δεν χρειάζεται να προσθέσουμε στο fake repository μηχανισμούς αποτυχίας για κάθε πιθανό σενάριο.&lt;/p&gt;

&lt;p&gt;Για έναν μεμονωμένο έλεγχο σφάλματος, ένα μικρό stub ή ένα mock με &lt;code&gt;side_effect&lt;/code&gt; μπορεί να είναι η πιο απλή λύση.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fake και Mock στο ίδιο test
&lt;/h2&gt;

&lt;p&gt;Δεν χρειάζεται να διαλέξουμε μία τεχνική για ολόκληρη την εφαρμογή.&lt;/p&gt;

&lt;p&gt;Μπορούμε να χρησιμοποιήσουμε:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fake repository για μια λειτουργική διαχείριση κατάστασης.&lt;/li&gt;
&lt;li&gt;Mock notifier για να ελέγξουμε την απαίτηση αποστολής ειδοποίησης.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Προσθέτουμε στο &lt;code&gt;test_tasks.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompleteTaskHybridTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_completion_sends_one_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryTaskRepository&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_repeated_completion_does_not_notify_again&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryTaskRepository&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;CompletionNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;complete_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Εδώ το mock ελέγχει κάτι που έχει άμεση επιχειρησιακή σημασία: να μη ζητηθεί δεύτερη ειδοποίηση για την ίδια ολοκλήρωση.&lt;/p&gt;

&lt;p&gt;Το fake repository εξασφαλίζει ότι η δεύτερη εκτέλεση διαβάζει την κατάσταση που αποθήκευσε η πρώτη.&lt;/p&gt;

&lt;p&gt;Δεν προγραμματίζουμε χειροκίνητα μια διαφορετική απάντηση για κάθε ανάγνωση. Η αλλαγή προκύπτει από τη λειτουργία του fake.&lt;/p&gt;

&lt;p&gt;Το test αποδεικνύει τη συμπεριφορά για αυτές τις δύο διαδοχικές κλήσεις. Οι ταυτόχρονες κλήσεις είναι διαφορετικό σενάριο.&lt;/p&gt;

&lt;h2&gt;
  
  
  Τα Fakes χρειάζονται δικά τους tests
&lt;/h2&gt;

&lt;p&gt;Ένα fake είναι επιπλέον κώδικας. Μπορεί να περιέχει bugs ή να αποκλίνει από την πραγματική υλοποίηση.&lt;/p&gt;

&lt;p&gt;Χρειάζεται επομένως να ελέγχουμε το συμβόλαιό του.&lt;/p&gt;

&lt;p&gt;Προσθέτουμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InMemoryTaskRepositoryContractTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryTaskRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_id_returns_none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertIsNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_saved_task_can_be_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_save_replaces_task_with_same_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτά ελέγχουν τρία σημεία του συμβολαίου μας:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Την απουσία εγγραφής.&lt;/li&gt;
&lt;li&gt;Την ανάγνωση αποθηκευμένης εγγραφής.&lt;/li&gt;
&lt;li&gt;Την αντικατάσταση εγγραφής με ίδιο ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Προς το παρόν εκτελούνται μόνο στο fake.&lt;/p&gt;

&lt;p&gt;Όταν υπάρχει πραγματικός adapter, μπορούμε να οργανώσουμε αυτούς τους ίδιους ελέγχους ώστε να εκτελούνται και στις δύο υλοποιήσεις, με κατάλληλο setup και καθαρισμό.&lt;/p&gt;

&lt;p&gt;Αυτή είναι η αξία των κοινών &lt;strong&gt;contract tests&lt;/strong&gt;: ελέγχουν ότι οι υλοποιήσεις συμφωνούν στις συμπεριφορές που υπόσχεται το API. Δεν απαιτούν από το fake να αναπαράγει κάθε λεπτομέρεια της βάσης. &lt;a href="https://abseil.io/resources/swe-book/html/ch13.html#fakes_should_be_tested" rel="noopener noreferrer"&gt;Software Engineering at Google — Test Doubles&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, ένα dictionary δεν αποδεικνύει από μόνο του σωστή συμπεριφορά συναλλαγών, constraints ή ταυτόχρονων ενημερώσεων.&lt;/p&gt;

&lt;p&gt;Αυτά χρειάζονται ελέγχους με την κατάλληλη πραγματική υποδομή.&lt;/p&gt;

&lt;h2&gt;
  
  
  Εκτέλεση των παραδειγμάτων
&lt;/h2&gt;

&lt;p&gt;Αφού τοποθετήσουμε όλα τα προηγούμενα blocks στα δύο αρχεία, εκτελούμε από τον φάκελό τους:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η ολοκληρωμένη σουίτα περιλαμβάνει 11 tests.&lt;/p&gt;

&lt;p&gt;Μπορούμε επίσης να δοκιμάσουμε μικρές σκόπιμες αλλαγές:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Αν αφαιρέσουμε το &lt;code&gt;save()&lt;/code&gt;, αποτυγχάνουν τόσο το test αποθήκευσης με fake όσο και εκείνο με mock.&lt;/li&gt;
&lt;li&gt;Αν αφαιρέσουμε το &lt;code&gt;send()&lt;/code&gt;, αποτυγχάνει το test ειδοποίησης.&lt;/li&gt;
&lt;li&gt;Αν αφαιρέσουμε τον έλεγχο &lt;code&gt;task.completed&lt;/code&gt;, αποτυγχάνει το test της επανάληψης.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Αυτή είναι μια χρήσιμη ερώτηση για κάθε test:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ποιο συγκεκριμένο λάθος στον κώδικα θα έκανε αυτό το test να αποτύχει;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Πότε προτιμάμε κάθε προσέγγιση
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ανάγκη&lt;/th&gt;
&lt;th&gt;Συνήθως κατάλληλη επιλογή&lt;/th&gt;
&lt;th&gt;Γιατί&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Απλός υπολογισμός ή domain object&lt;/td&gt;
&lt;td&gt;Πραγματική υλοποίηση&lt;/td&gt;
&lt;td&gt;Έχει μικρό κόστος και ελέγχουμε τον πραγματικό κώδικα.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Αποθήκευση και ανάγνωση μέσα σε ένα σενάριο&lt;/td&gt;
&lt;td&gt;Fake&lt;/td&gt;
&lt;td&gt;Διατηρεί λειτουργική κατάσταση μεταξύ κλήσεων.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Πολλά tests χρειάζονται το ίδιο απλό repository&lt;/td&gt;
&lt;td&gt;Κοινό fake&lt;/td&gt;
&lt;td&gt;Μειώνει επαναλαμβανόμενο setup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Μία συγκεκριμένη απάντηση από dependency&lt;/td&gt;
&lt;td&gt;Stub&lt;/td&gt;
&lt;td&gt;Εκφράζει απλά τη συνθήκη του σεναρίου.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Έλεγχος ότι ζητήθηκε ειδοποίηση&lt;/td&gt;
&lt;td&gt;Mock ή spy&lt;/td&gt;
&lt;td&gt;Η αλληλεπίδραση αποτελεί μέρος της απαίτησης.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Μεμονωμένη αποτυχία μιας εξάρτησης&lt;/td&gt;
&lt;td&gt;Stub ή mock με σφάλμα&lt;/td&gt;
&lt;td&gt;Το σενάριο ρυθμίζεται χωρίς πολύπλοκο fake.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL, mappings, constraints, συναλλαγές&lt;/td&gt;
&lt;td&gt;Integration test&lt;/td&gt;
&lt;td&gt;Χρειάζεται να εκτελεστεί ο πραγματικός adapter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ολόκληρη διαδρομή χρήστη&lt;/td&gt;
&lt;td&gt;Επιλεγμένο end-to-end test&lt;/td&gt;
&lt;td&gt;Ελέγχει τη σύνδεση των επιμέρους τμημάτων.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ο πίνακας είναι πρακτικός οδηγός. Η τελική επιλογή εξαρτάται από το συμβόλαιο και το κόστος του test.&lt;/p&gt;

&lt;p&gt;Ένα fake έχει αρχικό κόστος κατασκευής και συντήρησης. Συμφέρει περισσότερο όταν η απλή συμπεριφορά του επαναχρησιμοποιείται σε αρκετά σενάρια.&lt;/p&gt;

&lt;p&gt;Ένα mock έχει μικρό αρχικό κόστος. Αν όμως κάθε test χρειάζεται μεγάλη λίστα ρυθμίσεων και επαληθεύσεων, το συνολικό κόστος συντήρησης αυξάνεται.&lt;/p&gt;

&lt;p&gt;Η Google επισημαίνει ότι η υπερβολική χρήση mocks μπορεί να δυσκολεύει την κατανόηση και το refactoring, επειδή τα tests αρχίζουν να γνωρίζουν πολλές λεπτομέρειες της υλοποίησης. &lt;a href="https://testing.googleblog.com/2013/05/testing-on-toilet-dont-overuse-mocks.html" rel="noopener noreferrer"&gt;Don’t Overuse Mocks&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Πώς συνδέεται αυτό με την Clean Architecture
&lt;/h2&gt;

&lt;p&gt;Στην Clean Architecture οι εξαρτήσεις του πηγαίου κώδικα κατευθύνονται προς τις εσωτερικές πολιτικές και τους κανόνες της εφαρμογής.&lt;/p&gt;

&lt;p&gt;Το use case δεν χρειάζεται να γνωρίζει αν η αποθήκευση γίνεται σε PostgreSQL, σε αρχείο ή μέσω κάποιου εξωτερικού API. Χρειάζεται ένα συμβόλαιο που εκφράζει τις δικές του ανάγκες. &lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;The Clean Architecture — Robert C. Martin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Μια πιθανή οργάνωση είναι:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;domain/
    task.py

application/
    complete_task.py
    ports.py
    errors.py

infrastructure/
    postgres_task_repository.py
    email_completion_notifier.py

tests/
    unit/
    integration/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Στο άρθρο κρατήσαμε τον κώδικα σε δύο αρχεία για να είναι εύκολος στην εκτέλεση. Δεν χρειάζονται πολλοί φάκελοι για να καταλάβουμε την αρχή.&lt;/p&gt;

&lt;p&gt;Οι εξαρτήσεις του πηγαίου κώδικα μπορούν να φαίνονται ως εξής:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CompleteTask ------------&amp;gt; TaskRepository
PostgresTaskRepository -&amp;gt; TaskRepository
InMemoryTaskRepository -&amp;gt; TaskRepository
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;CompleteTask&lt;/code&gt; χρησιμοποιεί το port.&lt;/p&gt;

&lt;p&gt;Ο πραγματικός adapter και το fake υλοποιούν τη συμπεριφορά που περιγράφει το port. Στην Python αυτή η συμμόρφωση μπορεί να είναι δομική, χωρίς ρητή κληρονομικότητα.&lt;/p&gt;

&lt;p&gt;Στην παραγωγή συνδέουμε το use case με τον πραγματικό adapter. Στα unit tests το συνδέουμε με το κατάλληλο double.&lt;/p&gt;

&lt;p&gt;Αυτή η σύνδεση μέσω constructor είναι &lt;strong&gt;dependency injection&lt;/strong&gt;. Δεν απαιτεί κάποιο ειδικό framework.&lt;/p&gt;

&lt;p&gt;Η Clean Architecture διευκολύνει την επιλογή fake ή mock, επειδή κάνει τα όρια καθαρά. Δεν επιβάλλει μία από τις δύο τεχνικές.&lt;/p&gt;

&lt;h2&gt;
  
  
  Η σχέση με το SOLID
&lt;/h2&gt;

&lt;p&gt;Το SOLID βοηθά να σχεδιάσουμε συνεργασίες που είναι κατανοητές και μπορούν να αντικατασταθούν με ελεγχόμενο τρόπο.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Responsibility Principle
&lt;/h3&gt;

&lt;p&gt;Το &lt;code&gt;Task&lt;/code&gt; διαχειρίζεται τη δική του μετάβαση κατάστασης.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;CompleteTask&lt;/code&gt; συντονίζει το use case.&lt;/p&gt;

&lt;p&gt;Ο adapter αποθήκευσης διαχειρίζεται την επικοινωνία με την υποδομή.&lt;/p&gt;

&lt;p&gt;Αυτό περιορίζει την ανάμειξη διαφορετικών πηγών αλλαγής. Το SRP σχετίζεται με τους λόγους αλλαγής μιας μονάδας, όχι με έναν αυθαίρετο αριθμό μεθόδων ή γραμμών. &lt;a href="https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html" rel="noopener noreferrer"&gt;The Single Responsibility Principle&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open Closed Principle
&lt;/h3&gt;

&lt;p&gt;Μπορούμε να προσθέσουμε έναν adapter που στέλνει ειδοποιήσεις μέσω άλλης υπηρεσίας χωρίς να αλλάξουμε τη λογική ολοκλήρωσης.&lt;/p&gt;

&lt;p&gt;Το σημείο σύνθεσης της εφαρμογής θα επιλέξει τη νέα υλοποίηση.&lt;/p&gt;

&lt;p&gt;Αυτό δεν σημαίνει ότι πρέπει να προβλέψουμε κάθε μελλοντική αλλαγή. Χρειαζόμαστε χρήσιμα σημεία επέκτασης εκεί όπου υπάρχουν πραγματικά όρια. &lt;a href="https://blog.cleancoder.com/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html" rel="noopener noreferrer"&gt;The Open Closed Principle&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Liskov Substitution Principle
&lt;/h3&gt;

&lt;p&gt;Ένα fake πρέπει να μπορεί να αντικαταστήσει τον πραγματικό συνεργάτη μέσα στο συμβόλαιο που ελέγχει.&lt;/p&gt;

&lt;p&gt;Αν το πραγματικό repository επιστρέφει &lt;code&gt;None&lt;/code&gt; όταν δεν βρίσκει εργασία, ενώ το fake επιστρέφει ένα κενό &lt;code&gt;Task&lt;/code&gt;, οι υλοποιήσεις διαφωνούν σημασιολογικά.&lt;/p&gt;

&lt;p&gt;Το ίδιο όνομα μεθόδου δεν αρκεί για ασφαλή υποκατάσταση.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interface Segregation Principle
&lt;/h3&gt;

&lt;p&gt;Το use case χρειάζεται ένα μικρό &lt;code&gt;TaskRepository&lt;/code&gt; και έναν &lt;code&gt;CompletionNotifier&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται ένα τεράστιο interface με διαχείριση χρηστών, migrations, exports και ειδοποιήσεις μαζί.&lt;/p&gt;

&lt;p&gt;Ένα στενό συμβόλαιο περιορίζει και τις δυνατότητες που πρέπει να υλοποιήσει το fake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Inversion Principle
&lt;/h3&gt;

&lt;p&gt;Το use case εξαρτάται από τα ports που εκφράζουν τις ανάγκες της εφαρμογής.&lt;/p&gt;

&lt;p&gt;Οι adapters προσαρμόζονται σε αυτά τα ports.&lt;/p&gt;

&lt;p&gt;Η dependency injection είναι ένας τρόπος να συνδέσουμε τις υλοποιήσεις. Το Dependency Inversion αφορά τον σχεδιασμό και την κατεύθυνση των εξαρτήσεων. &lt;a href="https://staff.cs.utu.fi/~jounsmed/doos_06/material/DesignPrinciplesAndPatterns.pdf" rel="noopener noreferrer"&gt;Design Principles and Design Patterns — Robert C. Martin&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices για καθαρά και χρήσιμα tests
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Γράφουμε ένα test για μια συγκεκριμένη συμπεριφορά
&lt;/h3&gt;

&lt;p&gt;Ένα test μπορεί να έχει περισσότερα από ένα assertions, όταν όλα τεκμηριώνουν την ίδια συμπεριφορά.&lt;/p&gt;

&lt;p&gt;Στο test αποθήκευσης, η επιστρεφόμενη τιμή και η αποθηκευμένη κατάσταση περιγράφουν το ίδιο αποτέλεσμα.&lt;/p&gt;

&lt;p&gt;Αν προσθέσουμε στο ίδιο test ειδοποιήσεις, permissions, audit logs και formatting, η αποτυχία γίνεται δυσκολότερο να ερμηνευτεί.&lt;/p&gt;

&lt;p&gt;Η αντιστοιχία «μία μέθοδος παραγωγής, ένα test» δεν είναι χρήσιμος γενικός κανόνας. &lt;a href="https://testing.googleblog.com/2014/04/testing-on-toilet-test-behaviors-not.html" rel="noopener noreferrer"&gt;Test Behaviors, Not Methods&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Κρατάμε καθαρά τα Arrange Act Assert
&lt;/h3&gt;

&lt;p&gt;Το test πρέπει να επιτρέπει στον αναγνώστη να ξεχωρίζει:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ποιες είναι οι αρχικές συνθήκες.&lt;/li&gt;
&lt;li&gt;Ποια ενέργεια εκτελείται.&lt;/li&gt;
&lt;li&gt;Ποιο αποτέλεσμα αναμένεται.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Δεν χρειάζεται κάθε test να έχει σχόλια &lt;code&gt;Arrange&lt;/code&gt;, &lt;code&gt;Act&lt;/code&gt;, &lt;code&gt;Assert&lt;/code&gt;. Οι κενές γραμμές και τα σαφή ονόματα συχνά αρκούν.&lt;/p&gt;

&lt;h3&gt;
  
  
  Επαληθεύουμε μόνο τις αλληλεπιδράσεις που έχουν σημασία
&lt;/h3&gt;

&lt;p&gt;Αν η απαίτηση είναι να σταλεί μία ειδοποίηση, έχει νόημα να ελέγξουμε τη μία κλήση.&lt;/p&gt;

&lt;p&gt;Αν η απαίτηση είναι η τελική κατάσταση, μπορεί να μη χρειάζεται να ελέγξουμε πόσες φορές έγινε ανάγνωση από το repository.&lt;/p&gt;

&lt;p&gt;Κάθε επαλήθευση κλήσης προσθέτει έναν περιορισμό.&lt;/p&gt;

&lt;p&gt;Πριν τη γράψουμε, εξετάζουμε αν μια διαφορετική ακολουθία κλήσεων, με το ίδιο επιτρεπτό αποτέλεσμα, θα ήταν επίσης σωστή.&lt;/p&gt;

&lt;h3&gt;
  
  
  Δεν αντικαθιστούμε με mock τη συμπεριφορά που εξετάζουμε
&lt;/h3&gt;

&lt;p&gt;Αν θέλουμε να ελέγξουμε το &lt;code&gt;CompleteTask&lt;/code&gt;, εκτελούμε το πραγματικό &lt;code&gt;CompleteTask&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Αν αντικαταστήσουμε τη δική του &lt;code&gt;execute()&lt;/code&gt; με προκαθορισμένη απάντηση, δεν ελέγχουμε την υλοποίησή του.&lt;/p&gt;

&lt;p&gt;Αντίστοιχα, αποφεύγουμε να κάνουμε mock απλά αντικείμενα δεδομένων όπως το &lt;code&gt;Task&lt;/code&gt;. Ένα πραγματικό &lt;code&gt;Task&lt;/code&gt; είναι εύκολο να δημιουργηθεί και δείχνει καθαρά το σενάριο.&lt;/p&gt;

&lt;h3&gt;
  
  
  Κάθε test ξεκινά με δική του κατάσταση
&lt;/h3&gt;

&lt;p&gt;Στα παραδείγματα δημιουργούμε νέο repository και νέο notifier για κάθε test.&lt;/p&gt;

&lt;p&gt;Έτσι αποφεύγουμε:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Δεδομένα που διαρρέουν μεταξύ tests.&lt;/li&gt;
&lt;li&gt;Παλιές καταγραφές κλήσεων.&lt;/li&gt;
&lt;li&gt;Εξάρτηση από τη σειρά εκτέλεσης.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Το γεγονός ότι ένα fake είναι γρήγορο δεν σημαίνει ότι πρέπει να μοιράζεται ως mutable global αντικείμενο.&lt;/p&gt;

&lt;h3&gt;
  
  
  Τα tests παραμένουν απλά
&lt;/h3&gt;

&lt;p&gt;Προτιμάμε σαφείς τιμές εισόδου και σαφή αναμενόμενα αποτελέσματα.&lt;/p&gt;

&lt;p&gt;Αποφεύγουμε να υπολογίζουμε το αναμενόμενο αποτέλεσμα αντιγράφοντας τον ίδιο αλγόριθμο με τον κώδικα παραγωγής. Θα μπορούσαμε να αντιγράψουμε και το ίδιο λάθος.&lt;/p&gt;

&lt;p&gt;Επίσης, λίγη επανάληψη στο setup μπορεί να είναι προτιμότερη από ένα γενικό helper με πολλές παραμέτρους και κρυφές προεπιλογές. &lt;a href="https://testing.googleblog.com/2014/07/testing-on-toilet-dont-put-logic-in.html" rel="noopener noreferrer"&gt;Don’t Put Logic in Tests&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Δεν αφήνουμε το fake να γίνει δεύτερη βάση δεδομένων
&lt;/h3&gt;

&lt;p&gt;Το fake υλοποιεί το απαραίτητο συμβόλαιο.&lt;/p&gt;

&lt;p&gt;Αν αρχίσουμε να γράφουμε query planner, μηχανισμό συναλλαγών και περίπλοκη προσομοίωση locking, το κόστος του μπορεί να ξεπεράσει την αξία του.&lt;/p&gt;

&lt;p&gt;Για συμπεριφορές που εξαρτώνται ουσιαστικά από τη συγκεκριμένη βάση, χρησιμοποιούμε την πραγματική τεχνολογία σε integration tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Κρατάμε τα εξωτερικά SDKs πίσω από χρήσιμα όρια
&lt;/h3&gt;

&lt;p&gt;Αν το use case χρησιμοποιεί βαθιές αλυσίδες από αντικείμενα ενός SDK, τα tests μπορεί να χρειάζονται αντίστοιχες αλυσίδες mocks.&lt;/p&gt;

&lt;p&gt;Ένα μικρό port, όπως το &lt;code&gt;CompletionNotifier&lt;/code&gt;, μπορεί να περιορίσει αυτή τη γνώση στον adapter.&lt;/p&gt;

&lt;p&gt;Ο adapter χρειάζεται δικούς του ελέγχους απέναντι στην πραγματική βιβλιοθήκη ή στο κατάλληλο περιβάλλον δοκιμών. Η σύσταση να αποφεύγουμε mocks για τύπους που δεν ελέγχουμε περιγράφεται και στο Google Testing Blog. &lt;a href="https://testing.googleblog.com/2020/07/testing-on-toilet-dont-mock-types-you.html" rel="noopener noreferrer"&gt;Don’t Mock Types You Don’t Own&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Ελέγχουμε τις αποτυχίες και τα όρια
&lt;/h3&gt;

&lt;p&gt;Το happy path είναι μόνο ένα μέρος της συμπεριφοράς.&lt;/p&gt;

&lt;p&gt;Στο παράδειγμά μας ελέγχουμε επίσης:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Απουσία εργασίας.&lt;/li&gt;
&lt;li&gt;Αποτυχία αποθήκευσης.&lt;/li&gt;
&lt;li&gt;Επανάληψη ολοκλήρωσης.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Σε άλλα use cases μπορεί να έχουν σημασία κενές είσοδοι, όρια ποσότητας, ληγμένες προθεσμίες ή ακυρωμένες ενέργειες.&lt;/p&gt;

&lt;p&gt;Αν η συμπεριφορά εξαρτάται από χρόνο, τυχαιότητα ή δίκτυο, χρειαζόμαστε τρόπο να ελέγχουμε αυτές τις συνθήκες με προβλέψιμες εισόδους.&lt;/p&gt;

&lt;h3&gt;
  
  
  Χρησιμοποιούμε το coverage ως ένδειξη
&lt;/h3&gt;

&lt;p&gt;Το coverage δείχνει ποιος κώδικας εκτελέστηκε. Δεν αποδεικνύει ότι τα assertions ελέγχουν σωστά τις απαιτήσεις.&lt;/p&gt;

&lt;p&gt;Ένα test μπορεί να εκτελεί όλο το use case και να ελέγχει μόνο ότι η επιστρεφόμενη τιμή δεν είναι &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Η δοκιμή με μικρά σκόπιμα λάθη, όπως η αφαίρεση της αποθήκευσης, βοηθά να αξιολογήσουμε αν το test προστατεύει πράγματι τη συμπεριφορά που δηλώνει.&lt;/p&gt;

&lt;h2&gt;
  
  
  Τι δεν αποδεικνύουν τα unit tests του παραδείγματος
&lt;/h2&gt;

&lt;p&gt;Το fake και το mock μάς βοηθούν να ελέγξουμε τη λογική του use case.&lt;/p&gt;

&lt;p&gt;Για την πραγματική εφαρμογή χρειάζονται επιπλέον αποδείξεις:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Επίπεδο&lt;/th&gt;
&lt;th&gt;Τι θέλουμε να αποδείξουμε&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests&lt;/td&gt;
&lt;td&gt;Οι κανόνες και οι αποφάσεις του use case είναι σωστοί.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Κοινά contract tests&lt;/td&gt;
&lt;td&gt;Fake και πραγματικός adapter συμφωνούν στο συμβόλαιο.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration tests&lt;/td&gt;
&lt;td&gt;Ο adapter συνεργάζεται σωστά με βάση, SDK ή υπηρεσία.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Επιλεγμένα end-to-end tests&lt;/td&gt;
&lt;td&gt;Μια κρίσιμη διαδρομή λειτουργεί μέσα στην εφαρμογή.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Υπάρχει επίσης ένα συγκεκριμένο όριο στον κώδικά μας.&lt;/p&gt;

&lt;p&gt;Αποθηκεύουμε πρώτα την ολοκλήρωση και μετά ζητάμε ειδοποίηση. Αν η αποθήκευση πετύχει αλλά η ειδοποίηση αποτύχει, μια επόμενη εκτέλεση θα βρει ήδη ολοκληρωμένη την εργασία.&lt;/p&gt;

&lt;p&gt;Η σημερινή υλοποίηση δεν εγγυάται αξιόπιστη παράδοση της ειδοποίησης σε αυτό το σενάριο. Ούτε ο διαδοχικός έλεγχος επανάληψης αποδεικνύει προστασία απέναντι σε ταυτόχρονες κλήσεις.&lt;/p&gt;

&lt;p&gt;Αν απαιτείται αξιόπιστη σύνδεση αποθήκευσης και δημοσίευσης μηνύματος, μια επιλογή είναι το &lt;strong&gt;transactional outbox&lt;/strong&gt;: αποθηκεύουμε την αλλαγή και την πρόθεση αποστολής στην ίδια συναλλαγή και επεξεργαζόμαστε την αποστολή ξεχωριστά. Χρειάζεται επίσης αντιμετώπιση πιθανών διπλών παραδόσεων. &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html" rel="noopener noreferrer"&gt;Transactional Outbox Pattern — AWS&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Αυτό θα ήταν πρόσθετη απαίτηση της εφαρμογής, με δική της σχεδίαση και tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Πώς αποφασίζουμε στην πράξη
&lt;/h2&gt;

&lt;p&gt;Πριν δημιουργήσουμε ένα test double, μπορούμε να ακολουθήσουμε αυτή τη σειρά σκέψης:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ποια συμπεριφορά θέλω να αποδείξω;&lt;/strong&gt; Διατυπώνω την απαίτηση πριν επιλέξω τεχνική.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Μπορώ να χρησιμοποιήσω την πραγματική υλοποίηση;&lt;/strong&gt; Αν είναι απλή, γρήγορη και προβλέψιμη, τη χρησιμοποιώ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Χρειάζομαι λειτουργική κατάσταση μεταξύ κλήσεων;&lt;/strong&gt; Ένα μικρό fake συχνά βοηθά.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Χρειάζομαι μόνο μία συγκεκριμένη απάντηση ή αποτυχία;&lt;/strong&gt; Ένα stub μπορεί να αρκεί.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Η κλήση προς έναν συνεργάτη είναι μέρος της απαίτησης;&lt;/strong&gt; Χρησιμοποιώ mock ή spy και ελέγχω τις σχετικές λεπτομέρειες.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Η ορθότητα εξαρτάται από την πραγματική υποδομή;&lt;/strong&gt; Προσθέτω integration test στο αντίστοιχο όριο.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Για το δικό μας παράδειγμα, το fake repository είναι μια καλή προεπιλογή για ελέγχους κατάστασης και επαναλαμβανόμενων ενεργειών. Το mock notifier είναι χρήσιμο όταν θέλουμε να επαληθεύσουμε την απαίτηση αποστολής. Ένα stub αποτυχίας διευκολύνει τον έλεγχο σφαλμάτων.&lt;/p&gt;

&lt;p&gt;Ένα χρήσιμο test εκφράζει καθαρά την απαίτηση, εντοπίζει ουσιαστικά λάθη και επιτρέπει να αλλάζουμε την εσωτερική οργάνωση του κώδικα όσο διατηρούμε το συμφωνημένο συμβόλαιο.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>python</category>
      <category>cleancode</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Expression Mapping, Projection και Specification με ένα απλό παράδειγμα</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:01:52 +0000</pubDate>
      <link>https://dev.to/__b63657/expression-mapping-projection-kai-specification-me-ena-aplo-paradeigma-657</link>
      <guid>https://dev.to/__b63657/expression-mapping-projection-kai-specification-me-ena-aplo-paradeigma-657</guid>
      <description>&lt;p&gt;Όταν αναπτύσσουμε ένα API με EF Core, συνήθως ξεκινάμε από μια απλή ανάγκη, να διαβάσουμε κάποια δεδομένα και να τα επιστρέψουμε στον client. Γράφουμε το query, φορτώνουμε τα entities και τα μετατρέπουμε σε DTOs, ώστε η απάντηση να περιέχει τα πεδία που χρειάζεται η συγκεκριμένη λειτουργία.&lt;/p&gt;

&lt;p&gt;Όταν χρειαζόμαστε πληροφορίες από σχετικές οντότητες, προσθέτουμε και ένα &lt;code&gt;Include&lt;/code&gt;. Για παράδειγμα, μια λίστα παραγγελιών μπορεί να χρειάζεται το όνομα του πελάτη. Η φόρτωση της παραγγελίας μαζί με τον πελάτη φαίνεται μια φυσική επιλογή.&lt;/p&gt;

&lt;p&gt;Σε αυτό το σημείο, όμως, υπάρχει μια λεπτομέρεια που εύκολα περνά απαρατήρητη, το ότι επιστρέφουμε ένα μικρό DTO δεν σημαίνει ότι διαβάσαμε και λίγα δεδομένα από τη βάση. Αν η μετατροπή γίνει αφού φορτωθούν τα entities, μπορεί να έχουμε ήδη μεταφέρει πεδία που η συγκεκριμένη απάντηση δεν χρησιμοποιεί.&lt;/p&gt;

&lt;p&gt;Εδώ αποκτά αξία το &lt;strong&gt;projection&lt;/strong&gt;, περιγράφουμε τη μορφή του αποτελέσματος μέσα στο ίδιο το query, ώστε το EF Core να γνωρίζει ποια δεδομένα χρειαζόμαστε πριν το εκτελέσει. Ένα mapping expression μάς επιτρέπει να κρατήσουμε αυτή την περιγραφή επαναχρησιμοποιήσιμη.&lt;/p&gt;

&lt;p&gt;Με μια απλή λίστα παραγγελιών, θα δούμε πώς λειτουργεί αυτή η προσέγγιση, τι αλλάζει στο SQL και πώς συνδέεται με το Specification pattern. Θα εξετάσουμε επίσης πότε αξίζει να χρησιμοποιούμε αυτά τα εργαλεία, τι γίνεται όταν τα &lt;code&gt;Include&lt;/code&gt; είναι πολλά και αν έχουν νόημα ακόμη και όταν τα entities μας είναι μικρά.&lt;/p&gt;

&lt;p&gt;Ένα endpoint χρειάζεται να επιστρέψει τρία πεδία. Το query, όμως, φορτώνει δύο ολόκληρα entities, τα μετατρέπει σε DTOs και τελικά χρησιμοποιεί μόνο ένα μικρό μέρος των δεδομένων.&lt;/p&gt;

&lt;p&gt;Ο κώδικας λειτουργεί. Η βάση και η εφαρμογή, όμως, κάνουν περισσότερη δουλειά από όση απαιτεί η συγκεκριμένη λειτουργία.&lt;/p&gt;

&lt;p&gt;Ας δούμε πώς το projection αλλάζει αυτή τη συμπεριφορά, τι προσφέρει ένα mapping expression και πού συνδέεται το Specification pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Το παράδειγμα θα είναι μια λίστα παραγγελιών&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Έχουμε δύο entities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Order&lt;/code&gt;: &lt;code&gt;Id&lt;/code&gt;, &lt;code&gt;Total&lt;/code&gt;, &lt;code&gt;CustomerId&lt;/code&gt;, &lt;code&gt;CreatedAt&lt;/code&gt;, &lt;code&gt;InternalNotes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Customer&lt;/code&gt;: &lt;code&gt;Id&lt;/code&gt;, &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Email&lt;/code&gt;, &lt;code&gt;Phone&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Κάθε παραγγελία συνδέεται υποχρεωτικά με έναν πελάτη μέσω του navigation property &lt;code&gt;Order.Customer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Για τη λίστα μας χρειαζόμαστε μόνο:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderSummaryDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CustomerName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Μια συνηθισμένη υλοποίηση είναι η εξής:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;minimumTotal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;minimumTotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderSummaryDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Στο &lt;code&gt;ToListAsync()&lt;/code&gt; εκτελείται το query και δημιουργούνται τα &lt;code&gt;Order&lt;/code&gt; και &lt;code&gt;Customer&lt;/code&gt; objects στη μνήμη. Το mapping που ακολουθεί δουλεύει πάνω σε αυτά τα objects.&lt;/p&gt;

&lt;p&gt;Το SQL έχει σχηματικά την παρακάτω μορφή:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InternalNotes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Phone&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Customers&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;minimumTotal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Χρησιμοποιούμε τρία πεδία, αλλά παραλαμβάνουμε περισσότερα.&lt;/p&gt;

&lt;p&gt;Μια απαραίτητη διευκρίνιση, &lt;strong&gt;το &lt;code&gt;Include&lt;/code&gt; δεν φέρνει όλες τις εγγραφές των πινάκων.&lt;/strong&gt; Το &lt;code&gt;Where&lt;/code&gt; εξακολουθεί να περιορίζει τις παραγγελίες. Το &lt;code&gt;Include&lt;/code&gt; ζητά να φορτωθούν και οι σχετικοί πελάτες ως entities, με τις χαρτογραφημένες scalar ιδιότητές τους.&lt;/p&gt;

&lt;p&gt;Επίσης, δεν φορτώνει αυτόματα κάθε άλλη σχέση του &lt;code&gt;Customer&lt;/code&gt;. Αφορά τη σχέση που ζητήσαμε να συμπεριληφθεί.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager" rel="noopener noreferrer"&gt;Τεκμηρίωση: Eager Loading&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Projection: ορίζουμε το αποτέλεσμα μέσα στο query&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Projection είναι η επιλογή και διαμόρφωση των δεδομένων που θα αποτελέσουν το αποτέλεσμα. Στο LINQ εκφράζεται με το &lt;code&gt;Select&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Αντί να φορτώσουμε πλήρη entities και μετά να τα μετατρέψουμε, περιγράφουμε το DTO όσο το query παραμένει &lt;code&gt;IQueryable&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;minimumTotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderSummaryDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το EF Core μπορεί πλέον να μεταφράσει το projection σε SQL που επιλέγει τις απαραίτητες στήλες:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CustomerName&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Customers&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;minimumTotal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το SQL είναι ενδεικτικό· η ακριβής μορφή εξαρτάται από τον provider και το μοντέλο.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;JOIN&lt;/code&gt; παραμένει, επειδή χρειαζόμαστε το όνομα του πελάτη. Η διαφορά είναι ότι δεν μεταφέρουμε τα υπόλοιπα πεδία και δεν δημιουργούμε πλήρη &lt;code&gt;Order&lt;/code&gt; και &lt;code&gt;Customer&lt;/code&gt; entities.&lt;/p&gt;

&lt;p&gt;Το DTO κατασκευάζεται στη .NET από τις τιμές που επιστρέφει το query.&lt;/p&gt;

&lt;p&gt;Δεν χρειαζόμαστε &lt;code&gt;Include&lt;/code&gt; για αυτό το projection. Η έκφραση &lt;code&gt;o.Customer.Name&lt;/code&gt; περιγράφει ήδη ποια πληροφορία της σχέσης απαιτείται.&lt;/p&gt;

&lt;p&gt;Ας το δούμε πιο αναλυτικά. Το &lt;code&gt;Include&lt;/code&gt; έχει νόημα όταν ζητάμε από το EF να φορτώσει ολόκληρο το σχετικό &lt;strong&gt;entity&lt;/strong&gt;, για παράδειγμα όταν θέλουμε να το τροποποιήσουμε και να καλέσουμε &lt;code&gt;SaveChanges&lt;/code&gt;. Στο projection δεν ζητάμε entities, ζητάμε &lt;strong&gt;συγκεκριμένες τιμές&lt;/strong&gt;. Το EF διαβάζει την έκφραση, καταλαβαίνει ότι χρειάζεται το &lt;code&gt;Customer.Name&lt;/code&gt;, δημιουργεί το &lt;code&gt;JOIN&lt;/code&gt; μόνο του και επιλέγει μόνο τις στήλες που χρησιμοποιούνται.&lt;/p&gt;

&lt;p&gt;Αν προσθέσουμε &lt;code&gt;Include&lt;/code&gt; μαζί με ένα &lt;code&gt;Select&lt;/code&gt; που αλλάζει το σχήμα του αποτελέσματος, το EF Core το αγνοεί. Δεν προσφέρει τίποτα και προσθέτει μόνο θόρυβο στον κώδικα.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Σενάριο&lt;/th&gt;
&lt;th&gt;Τι χρησιμοποιούμε&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queries που επιστρέφουν DTOs ή read μοντέλα&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Select&lt;/code&gt;, χωρίς &lt;code&gt;Include&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commands που τροποποιούν entities&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Include&lt;/code&gt; μόνο για τις σχέσεις που χρειάζονται&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying#project-only-properties-you-need" rel="noopener noreferrer"&gt;Τεκμηρίωση: Project only properties you need&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Τι είναι το mapping expression;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αν το ίδιο projection χρησιμοποιείται σε περισσότερα σημεία, μπορούμε να το ορίσουμε μία φορά:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Linq.Expressions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderMappings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderSummaryDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;ToSummary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderSummaryDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και να το χρησιμοποιήσουμε απευθείας στο query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;minimumTotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderMappings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToSummary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο όρος &lt;em&gt;expression mapper&lt;/em&gt; περιγράφει εδώ ένα mapping γραμμένο ως expression tree.&lt;/p&gt;

&lt;p&gt;Η διάκριση έχει σημασία:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Το &lt;code&gt;Func&amp;lt;Order, OrderSummaryDto&amp;gt;&lt;/code&gt; είναι ένας delegate που εκτελείται πάνω σε ένα object.&lt;/li&gt;
&lt;li&gt;Το &lt;code&gt;Expression&amp;lt;Func&amp;lt;Order, OrderSummaryDto&amp;gt;&amp;gt;&lt;/code&gt; αναπαριστά τον κώδικα ως δομή δεδομένων, την οποία το EF Core μπορεί να αναλύσει και να μεταφράσει σε SQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Δεν σημαίνει ότι κάθε C# έκφραση μεταφράζεται. Πρέπει να υποστηρίζεται από το EF Core και τον συγκεκριμένο provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Το ξεχωριστό expression δεν προσφέρει καλύτερο SQL από το ισοδύναμο inline &lt;code&gt;Select&lt;/code&gt;.&lt;/strong&gt; Προσφέρει επαναχρησιμοποίηση, αναγνωσιμότητα και κοινό σημείο συντήρησης.&lt;/p&gt;

&lt;p&gt;Το όφελος στη μεταφορά δεδομένων προκύπτει επειδή το projection συμμετέχει στο query πριν από την εκτέλεσή του.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/" rel="noopener noreferrer"&gt;Τεκμηρίωση: Expression Trees&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Τι γίνεται όταν τα &lt;code&gt;Include&lt;/code&gt; είναι πολλά;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ας υποθέσουμε ότι ένα query χρειάζεται δέκα &lt;code&gt;Include&lt;/code&gt;. Το πρόβλημα δεν βρίσκεται στο mapping expression. Βρίσκεται στο SQL και στον όγκο των δεδομένων που μεταφέρονται:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cartesian explosion.&lt;/strong&gt; Όταν περισσότερα από ένα &lt;code&gt;Include&lt;/code&gt; αφορούν collections (σχέσεις ένα-προς-πολλά), το &lt;code&gt;JOIN&lt;/code&gt; πολλαπλασιάζει τις γραμμές. Με τρία collections των 100 στοιχείων η καθεμία, μία παραγγελία επιστρέφει 100 × 100 × 100 = 1.000.000 γραμμές. Τα πεδία του κύριου entity επαναλαμβάνονται σε κάθε γραμμή.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Πλατιά αποτελέσματα.&lt;/strong&gt; Με δέκα &lt;code&gt;JOIN&lt;/code&gt;, ακόμα και σε απλές σχέσεις many-to-one, κάθε γραμμή περιέχει τις στήλες έντεκα πινάκων. Αυξάνονται η κίνηση στο δίκτυο, η μνήμη και ο χρόνος δημιουργίας των objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Δυσκολότερο query plan.&lt;/strong&gt; Πολλά &lt;code&gt;JOIN&lt;/code&gt; δυσκολεύουν τον optimizer και την αξιοποίηση των indexes, και η απόδοση γίνεται λιγότερο προβλέψιμη όσο μεγαλώνουν τα δεδομένα.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change tracking.&lt;/strong&gt; Όλα τα entities παρακολουθούνται, κάτι που κοστίζει σε μνήμη και CPU. Στα reads αυτό αποφεύγεται με &lt;code&gt;AsNoTracking()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Τι κάνουμε σε αυτή την περίπτωση:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Για reads, projection.&lt;/strong&gt; Ζητάμε μόνο τις στήλες που χρειάζεται το DTO, ακόμα κι αν το μοντέλο έχει δέκα σχέσεις.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Όταν χρειάζονται entities και υπάρχουν πολλά collections, &lt;code&gt;AsSplitQuery()&lt;/code&gt;.&lt;/strong&gt; Το EF εκτελεί ξεχωριστό query για κάθε collection και αποφεύγεται το καρτεσιανό γινόμενο. Το αντίτιμο είναι περισσότερα round trips και πιθανή ασυνέπεια αν τα δεδομένα αλλάξουν ανάμεσα στα queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Όταν ένα DTO χρειάζεται πολλά collections,&lt;/strong&gt; χωρίζουμε το αποτέλεσμα σε περισσότερα queries (π.χ. κύρια δεδομένα και ξεχωριστό query για τις γραμμές) και τα ενώνουμε στην εφαρμογή.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Προσοχή και στο projection με πολλά πεδία. Το EF μεταφράζει σε SQL ό,τι μπορεί, και η τελική κατασκευή του DTO γίνεται στη .NET. Δεν βάζουμε μέσα στο &lt;code&gt;Select&lt;/code&gt; δικές μας μεθόδους ή πολύπλοκη λογική που δεν μεταφράζεται σε SQL, γιατί το EF είτε θα πετάξει exception είτε θα φέρει περισσότερα δεδομένα από όσα χρειάζεται. Κρατάμε το &lt;code&gt;Select&lt;/code&gt; απλό και κάνουμε το υπόλοιπο mapping μετά το &lt;code&gt;ToListAsync()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries" rel="noopener noreferrer"&gt;Τεκμηρίωση: Single vs. split queries&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Πού μπαίνει το Specification;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το Specification pattern οργανώνει κανόνες σε μια ονομασμένη, επαναχρησιμοποιήσιμη μονάδα.&lt;/p&gt;

&lt;p&gt;Στην κλασική του μορφή περιγράφει ένα κριτήριο: για παράδειγμα, «παραγγελίες με ποσό μεγαλύτερο από ένα όριο».&lt;/p&gt;

&lt;p&gt;Σε query-oriented υλοποιήσεις μπορεί να συγκεντρώνει επίσης ταξινόμηση, pagination, includes και projection.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Έννοια&lt;/th&gt;
&lt;th&gt;Τι περιγράφει&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expression tree&lt;/td&gt;
&lt;td&gt;Τον κώδικα σε μορφή που μπορεί να αναλυθεί και να μεταφραστεί.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projection&lt;/td&gt;
&lt;td&gt;Τη μορφή και τα πεδία του αποτελέσματος.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query specification&lt;/td&gt;
&lt;td&gt;Τους κανόνες ενός συγκεκριμένου query, ενδεχομένως μαζί με το projection.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Για παράδειγμα, με τη βιβλιοθήκη &lt;code&gt;Ardalis.Specification&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Ardalis.Specification&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersAboveTotalSpec&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Specification&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderSummaryDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrdersAboveTotalSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;minimumTotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;minimumTotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderMappings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToSummary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Εδώ η specification συγκεντρώνει το κριτήριο αναζήτησης και χρησιμοποιεί το mapping expression για το αποτέλεσμα.&lt;/p&gt;

&lt;p&gt;Τα δύο εργαλεία συνεργάζονται. Δεν χρειάζεται όμως να υιοθετήσουμε specifications για να χρησιμοποιήσουμε projection, ούτε κάθε specification πρέπει να επιστρέφει DTO.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://specification.ardalis.com/features/select.html" rel="noopener noreferrer"&gt;Τεκμηρίωση: Select στο Ardalis.Specification&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Έχει νόημα ακόμη και με μικρά entities;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ναι. Δεν είναι κακή πρακτική να χρησιμοποιούμε expressions επειδή το entity έχει λίγα πεδία.&lt;/p&gt;

&lt;p&gt;Για ένα read endpoint, το projection αποτυπώνει καθαρά τι χρειάζεται η συγκεκριμένη λειτουργία. Αν αργότερα προστεθούν πεδία στο entity, το συγκεκριμένο DTO projection συνεχίζει να ζητά τις ίδιες τιμές.&lt;/p&gt;

&lt;p&gt;Το μετρήσιμο όφελος εξαρτάται από το μέγεθος των πεδίων, τον αριθμό των αποτελεσμάτων και τη συχνότητα εκτέλεσης. Σε ένα μικρό και σπάνιο query, η διαφορά μπορεί να είναι αμελητέα.&lt;/p&gt;

&lt;p&gt;Η χρήσιμη διάκριση είναι:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Χρησιμοποιούμε &lt;code&gt;Select&lt;/code&gt; για να διαμορφώσουμε ένα read result.&lt;/li&gt;
&lt;li&gt;Εξάγουμε mapping expression όταν επαναχρησιμοποιείται ή κάνει τον κώδικα σαφέστερο.&lt;/li&gt;
&lt;li&gt;Προσθέτουμε specification όταν οι κανόνες αναζήτησης χρειάζονται δική τους οργάνωση.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Δεν χρειάζεται ένα απλό query να αποκτήσει πολλές abstractions για να θεωρηθεί σωστό.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Οι λεπτομέρειες που κάνουν τη διαφορά&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το projection πρέπει να παραμένει μέσα στο &lt;code&gt;IQueryable&lt;/code&gt;. Αν προηγηθεί &lt;code&gt;ToList()&lt;/code&gt;, το query έχει ήδη εκτελεστεί. Αν περάσουμε σε &lt;code&gt;AsEnumerable()&lt;/code&gt;, οι επόμενες LINQ πράξεις εκτελούνται στην εφαρμογή.&lt;/p&gt;

&lt;p&gt;Αντίστοιχα, αποφεύγουμε αυτό:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderMappings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToSummary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;.Compile()&lt;/code&gt; μετατρέπει το expression σε delegate. Στη συγκεκριμένη κλήση επιλέγεται το &lt;code&gt;Enumerable.Select&lt;/code&gt;, οπότε το mapping εκτελείται στη μνήμη.&lt;/p&gt;

&lt;p&gt;Επίσης, μια custom μέθοδος μέσα σε expression δεν αποκτά αυτόματα SQL μετάφραση. Στο τελικό projection μπορεί να εκτελεστεί στην εφαρμογή, ενώ μια μη μεταφράσιμη συνθήκη στο &lt;code&gt;Where&lt;/code&gt; συνήθως προκαλεί exception.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/client-eval" rel="noopener noreferrer"&gt;Τεκμηρίωση: Client vs. Server Evaluation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;AsNoTracking()&lt;/code&gt; αφορά διαφορετικό θέμα: το change tracking. Δεν μειώνει τις επιλεγμένες στήλες. Στο δικό μας projection, που επιστρέφει μόνο τιμές μέσα σε DTO και κανένα entity instance, δεν υπάρχουν entities για tracking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/tracking#tracking-and-custom-projections" rel="noopener noreferrer"&gt;Τεκμηρίωση: Tracking and custom projections&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Αν χρησιμοποιούμε AutoMapper, η αντίστοιχη δυνατότητα για query projection είναι το &lt;code&gt;ProjectTo&amp;lt;TDto&amp;gt;()&lt;/code&gt;. Το &lt;code&gt;mapper.Map&lt;/code&gt; πάνω σε ήδη φορτωμένα entities δεν μπορεί να αλλάξει τα δεδομένα που παραλάβαμε από τη βάση.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.automapper.org/en/stable/Queryable-Extensions.html" rel="noopener noreferrer"&gt;Τεκμηρίωση: AutoMapper Queryable Extensions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Για αναγνώσεις που επιστρέφουν DTOs, το projection είναι μια πρακτική προεπιλογή. Όταν χρειάζεται να φορτώσουμε entities για αλλαγές και εκτέλεση domain behavior, η φόρτωση του κατάλληλου entity graph εξακολουθεί να έχει θέση.&lt;/p&gt;

&lt;p&gt;Και για να ελέγξουμε τι ζητά πραγματικά το query μας από τη βάση:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToQueryString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η αξία της επιλογής φαίνεται στο παραγόμενο SQL και στις ανάγκες της λειτουργίας που υλοποιούμε.&lt;/p&gt;




&lt;p&gt;Να θυμάσαι..!&lt;/p&gt;

&lt;p&gt;Το &lt;strong&gt;Specification&lt;/strong&gt; οργανώνει τα κριτήρια αναζήτησης, το &lt;strong&gt;Expression Mapping&lt;/strong&gt; επιτρέπει τη μεταφορά εκφράσεων ανάμεσα σε διαφορετικά μοντέλα και το &lt;strong&gt;Projection&lt;/strong&gt; καθορίζει τη μορφή των δεδομένων που επιστρέφουμε. &lt;/p&gt;

&lt;p&gt;Συνδυάζοντάς τα, μπορούμε να χτίσουμε queries με σαφείς ευθύνες και λιγότερη επανάληψη, που προσαρμόζονται ευκολότερα στις αλλαγές της εφαρμογής. Η πραγματική τους αξία φαίνεται όταν προσθέτουμε ένα νέο φίλτρο ή αλλάζουμε ένα DTO και η υπάρχουσα δομή μάς βοηθά να κάνουμε την αλλαγή σε συγκεκριμένα σημεία. Αυτό είναι και το ζητούμενο, κώδικας που παραμένει κατανοητός και εύκολος στην εξέλιξη, όσο οι απαιτήσεις μεγαλώνουν.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>csharp</category>
      <category>database</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Aggregates στο Domain-Driven Design: Πώς και γιατί τα χρησιμοποιούμε σε C# και .NET</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Mon, 12 Oct 2026 20:21:26 +0000</pubDate>
      <link>https://dev.to/__b63657/aggregates-sto-domain-driven-design-pos-kai-giati-ta-khresimopoioume-se-c-kai-net-1p0p</link>
      <guid>https://dev.to/__b63657/aggregates-sto-domain-driven-design-pos-kai-giati-ta-khresimopoioume-se-c-kai-net-1p0p</guid>
      <description>&lt;p&gt;"Πλήρης οδηγός για Aggregates και Aggregate Roots στο DDD, με πρακτικά παραδείγματα C#, EF Core, Clean Architecture και Business Rules."&lt;br&gt;
tags: dotnet, csharp, architecture, ddd&lt;/p&gt;

&lt;p&gt;Όταν ξεκινάμε την ανάπτυξη μιας εφαρμογής με &lt;strong&gt;C# και ASP.NET Core&lt;/strong&gt;, συνήθως σκεφτόμαστε με όρους Controllers, Services, Repositories, Entities και Database Tables.&lt;/p&gt;

&lt;p&gt;Αυτή η προσέγγιση λειτουργεί αρκετά καλά σε απλές CRUD εφαρμογές.&lt;/p&gt;

&lt;p&gt;Τι συμβαίνει όμως όταν η επιχειρησιακή λογική γίνεται πιο σύνθετη;&lt;/p&gt;

&lt;p&gt;Τι γίνεται όταν μια παραγγελία δεν πρέπει να αλλάζει μετά την ολοκλήρωσή της; Όταν δεν επιτρέπεται να ολοκληρωθεί χωρίς προϊόντα; Ή όταν πολλές διαφορετικές λειτουργίες πρέπει να διατηρούν τα δεδομένα σε έγκυρη κατάσταση;&lt;/p&gt;

&lt;p&gt;Εδώ έρχεται μία από τις σημαντικότερες έννοιες του &lt;strong&gt;Domain-Driven Design (DDD)&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Το Aggregate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Σε αυτό το άρθρο θα δούμε:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Τι είναι το Aggregate και το Aggregate Root.&lt;/li&gt;
&lt;li&gt;Ποια προβλήματα λύνουν στην αρχιτεκτονική μιας εφαρμογής.&lt;/li&gt;
&lt;li&gt;Πώς σχετίζονται με Entities και Value Objects.&lt;/li&gt;
&lt;li&gt;Πώς υλοποιούμε ένα Aggregate με C#.&lt;/li&gt;
&lt;li&gt;Πώς το συνδέουμε με Entity Framework Core.&lt;/li&gt;
&lt;li&gt;Πώς εφαρμόζουμε Repository Pattern και Clean Architecture.&lt;/li&gt;
&lt;li&gt;Πώς επιλέγουμε σωστά τα Aggregate Boundaries.&lt;/li&gt;
&lt;li&gt;Ποια είναι τα συνηθέστερα λάθη.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ας ξεκινήσουμε.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Τι είναι ένα Aggregate;
&lt;/h2&gt;

&lt;p&gt;Στο Domain-Driven Design, ένα &lt;strong&gt;Aggregate&lt;/strong&gt; είναι μια ομάδα από Domain Objects που αντιμετωπίζεται ως μία ενιαία μονάδα επιχειρησιακής συνέπειας.&lt;/p&gt;

&lt;p&gt;Μπορεί να περιλαμβάνει:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ένα Aggregate Root.&lt;/li&gt;
&lt;li&gt;Άλλα Entities.&lt;/li&gt;
&lt;li&gt;Value Objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Το σημαντικότερο χαρακτηριστικό του είναι ότι οι αλλαγές στην εσωτερική του κατάσταση ελέγχονται μέσω του &lt;strong&gt;Aggregate Root&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Παράδειγμα
&lt;/h3&gt;

&lt;p&gt;Ας υποθέσουμε ότι αναπτύσσουμε ένα e-commerce σύστημα.&lt;/p&gt;

&lt;p&gt;Μια παραγγελία περιλαμβάνει:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order (Aggregate Root)
│
├── OrderItem (Entity)
│   ├── ProductId
│   ├── Quantity
│   └── UnitPrice
│
├── OrderItem (Entity)
│   ├── ProductId
│   ├── Quantity
│   └── UnitPrice
│
└── OrderStatus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;Order&lt;/code&gt; είναι το Aggregate Root.&lt;/p&gt;

&lt;p&gt;Αυτό σημαίνει ότι οι αλλαγές στα OrderItems πρέπει να πραγματοποιούνται μέσω του Order και όχι ανεξάρτητα.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;49.90m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αντί για:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;orderItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η διαφορά φαίνεται μικρή, αλλά έχει τεράστια αρχιτεκτονική σημασία.&lt;/p&gt;

&lt;p&gt;Στην πρώτη περίπτωση, το Order ελέγχει αν επιτρέπεται η αλλαγή.&lt;/p&gt;

&lt;p&gt;Στη δεύτερη, οποιοδήποτε σημείο της εφαρμογής μπορεί να τροποποιήσει το Entity χωρίς να εφαρμόσει τους επιχειρησιακούς κανόνες.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Γιατί χρειαζόμαστε Aggregates;
&lt;/h2&gt;

&lt;p&gt;Ας δούμε ένα συνηθισμένο παράδειγμα από μια εφαρμογή που βασίζεται σε CRUD Services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;UpdateQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderItemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderItems&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderItemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Item not found"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αρχικά, ο κώδικας φαίνεται σωστός.&lt;/p&gt;

&lt;p&gt;Αλλά υπάρχουν αρκετά προβλήματα.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Πρόβλημα 1: Δεν προστατεύονται οι Business Rules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Τι γίνεται αν η παραγγελία έχει ήδη ολοκληρωθεί;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Πρόβλημα 2: Η επιχειρησιακή λογική βρίσκεται στο Application Service.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αν χρειαστεί η ίδια λειτουργία από διαφορετικό σημείο της εφαρμογής, υπάρχει κίνδυνος να αντιγραφούν ή να παρακαμφθούν οι έλεγχοι.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Πρόβλημα 3: Το Domain Model είναι Anemic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Τα Entities περιέχουν δεδομένα, αλλά όχι την επιχειρησιακή συμπεριφορά που τα αφορά.&lt;/p&gt;

&lt;p&gt;Με το Aggregate, η ενέργεια μεταφέρεται στο Domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChangeItemQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το ίδιο το Order αναλαμβάνει να αποφασίσει αν επιτρέπεται η αλλαγή.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Αυτός είναι ο βασικός στόχος: να προστατεύουμε τους επιχειρησιακούς κανόνες μέσα στο Domain Model.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Aggregate Root vs Entity vs Value Object
&lt;/h2&gt;

&lt;p&gt;Πριν προχωρήσουμε στην υλοποίηση, πρέπει να κατανοήσουμε τρεις βασικές έννοιες.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entity
&lt;/h3&gt;

&lt;p&gt;Ένα Entity είναι ένα αντικείμενο με μοναδική ταυτότητα.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Δύο Customers με το ίδιο όνομα δεν είναι απαραίτητα ο ίδιος Customer.&lt;/p&gt;

&lt;p&gt;Η ταυτότητά τους καθορίζεται από το &lt;code&gt;Id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Value Object
&lt;/h3&gt;

&lt;p&gt;Ένα Value Object δεν έχει δική του επιχειρησιακή ταυτότητα.&lt;/p&gt;

&lt;p&gt;Η ισότητά του καθορίζεται από τις τιμές του.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Σε production υλοποίηση θα προσθέταμε validation για το νόμισμα, το ποσό και τις επιτρεπόμενες πράξεις.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aggregate Root
&lt;/h3&gt;

&lt;p&gt;Το Aggregate Root είναι ένα Entity που αποτελεί το κεντρικό σημείο πρόσβασης σε ένα Aggregate.&lt;/p&gt;

&lt;p&gt;Έχει την ευθύνη να προστατεύει τους κανόνες που πρέπει να ισχύουν σε όλη τη μονάδα συνέπειας.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, ένα Order Aggregate μπορεί να επιβάλλει τους κανόνες:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Δεν επιτρέπονται προϊόντα με αρνητική ποσότητα.&lt;/li&gt;
&lt;li&gt;Δεν επιτρέπονται αλλαγές σε ολοκληρωμένη παραγγελία.&lt;/li&gt;
&lt;li&gt;Μια παραγγελία πρέπει να περιέχει τουλάχιστον ένα προϊόν για να ολοκληρωθεί.&lt;/li&gt;
&lt;li&gt;Το συνολικό ποσό υπολογίζεται από τα OrderItems.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. Υλοποιώντας το OrderItem Entity
&lt;/h2&gt;

&lt;p&gt;Αρχικά, δημιουργούμε το OrderItem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderItem&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;UnitPrice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// EF Core&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="nf"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Invalid product."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Quantity must be positive."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unitPrice&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Unit price cannot be negative."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;UnitPrice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ChangeQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Quantity must be positive."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;TotalPrice&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;UnitPrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Παρατηρούμε κάτι σημαντικό.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;Quantity&lt;/code&gt; έχει private setter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Επίσης, η μέθοδος αλλαγής ποσότητας δεν είναι public:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ChangeQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό περιορίζει τη δυνατότητα εξωτερικής τροποποίησης.&lt;/p&gt;

&lt;p&gt;Η χρήση του &lt;code&gt;internal&lt;/code&gt; αποτελεί έναν πρώτο περιορισμό πρόσβασης σε επίπεδο assembly. Δεν εξασφαλίζει από μόνη της ότι μόνο το Aggregate Root μπορεί να καλέσει τη μέθοδο.&lt;/p&gt;

&lt;p&gt;Για αυστηρότερη προστασία μπορούμε να χρησιμοποιήσουμε επιπλέον encapsulation ή διαφορετική οργάνωση των Domain Types.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Υλοποιώντας το Order Aggregate Root
&lt;/h2&gt;

&lt;p&gt;Ας δημιουργήσουμε πρώτα το OrderStatus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Draft&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Completed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Cancelled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Στη συνέχεια δημιουργούμε το Aggregate Root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsReadOnly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;TotalAmount&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalPrice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// EF Core&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customerId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Invalid customer."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;EnsureDraft&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Product already exists."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ChangeItemQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;EnsureDraft&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Product not found."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChangeQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RemoveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;EnsureDraft&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Product not found."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;EnsureDraft&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Cannot complete an empty order."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Completed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;EnsureDraft&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EnsureDraft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Order cannot be modified."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Τι κερδίσαμε;
&lt;/h3&gt;

&lt;p&gt;Πλέον δεν μπορούμε να αλλάξουμε ανεξέλεγκτα την κατάσταση της παραγγελίας.&lt;/p&gt;

&lt;p&gt;Όλες οι αλλαγές περνούν από συγκεκριμένες Domain Methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChangeItemQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αν προσπαθήσουμε να αλλάξουμε την παραγγελία μετά την ολοκλήρωσή της:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;anotherProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;20m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Θα προκύψει exception.&lt;/p&gt;

&lt;p&gt;Ο επιχειρησιακός κανόνας προστατεύεται ανεξάρτητα από το αν η ενέργεια ξεκίνησε από REST API, background job ή message consumer.&lt;/p&gt;

&lt;p&gt;Αυτό είναι το πραγματικό πλεονέκτημα του Aggregate.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Aggregates και Entity Framework Core
&lt;/h2&gt;

&lt;p&gt;Ένα Aggregate δεν είναι το ίδιο πράγμα με ένα Database Table.&lt;/p&gt;

&lt;p&gt;Το Aggregate αποτελεί έννοια του Domain Model, ενώ το Entity Framework Core αναλαμβάνει το persistence.&lt;/p&gt;

&lt;p&gt;Μπορούμε να χαρτογραφήσουμε το Order και τα OrderItems χρησιμοποιώντας Fluent API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppDbContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DbContextOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DbSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Orders&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ModelBuilder&lt;/span&gt; &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasConversion&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ignore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithOne&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OrderId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeleteBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cascade&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Navigation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_items"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Navigation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UsePropertyAccessMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;PropertyAccessMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnitPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasPrecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ignore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalPrice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;OrderItem&lt;/code&gt; δεν χρειάζεται να εκτίθεται ως δημόσιο &lt;code&gt;DbSet&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Έτσι εκφράζουμε την πρόθεσή μας να χειριζόμαστε το Order ως μία ενιαία επιχειρησιακή μονάδα.&lt;/p&gt;

&lt;p&gt;Η φόρτωση του Aggregate μπορεί να γίνει ως εξής:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και στη συνέχεια:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;49.90m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το EF Core αναλαμβάνει το change tracking και το persistence.&lt;/p&gt;

&lt;p&gt;Σημαντικό: Το mapping δεν επιβάλλει από μόνο του τους κανόνες πρόσβασης του DDD. Αυτοί πρέπει να προστατεύονται από το Domain API και την αρχιτεκτονική της εφαρμογής.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Repository Pattern και Aggregates
&lt;/h2&gt;

&lt;p&gt;Στο Domain-Driven Design, τα Repositories συνήθως σχεδιάζονται γύρω από Aggregate Roots.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IOrderRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η υλοποίηση με EF Core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Δεν χρειάζεται να δημιουργήσουμε ξεχωριστό Repository για κάθε OrderItem.&lt;/p&gt;

&lt;p&gt;Το OrderItem ανήκει στο Order Aggregate.&lt;/p&gt;

&lt;p&gt;Επομένως, οι αλλαγές του γίνονται μέσω του Order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Χρειαζόμαστε πάντα Repository Pattern;
&lt;/h3&gt;

&lt;p&gt;Όχι απαραίτητα.&lt;/p&gt;

&lt;p&gt;Το EF Core DbContext ήδη προσφέρει χαρακτηριστικά Repository και Unit of Work.&lt;/p&gt;

&lt;p&gt;Ένα custom Repository είναι χρήσιμο όταν θέλουμε να απομονώσουμε το Domain/Application Layer από τις λεπτομέρειες του persistence ή να ορίσουμε συγκεκριμένες λειτουργίες ανάκτησης Aggregates.&lt;/p&gt;

&lt;p&gt;Δεν πρέπει να προσθέτουμε abstractions χωρίς ουσιαστικό όφελος.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Aggregates μέσα στην Clean Architecture
&lt;/h2&gt;

&lt;p&gt;Μια τυπική δομή έργου μπορεί να είναι:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
│
├── MyApp.Domain/
│   ├── Orders/
│   │   ├── Order.cs
│   │   ├── OrderItem.cs
│   │   └── OrderStatus.cs
│   │
│   └── Abstractions/
│       └── IOrderRepository.cs
│
├── MyApp.Application/
│   └── Orders/
│       └── CompleteOrder/
│           └── CompleteOrderHandler.cs
│
├── MyApp.Infrastructure/
│   ├── Persistence/
│   │   └── AppDbContext.cs
│   │
│   └── Repositories/
│       └── OrderRepository.cs
│
└── MyApp.Api/
    └── Controllers/
        └── OrdersController.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Domain δεν πρέπει να εξαρτάται από το Entity Framework Core ή από το ASP.NET Core.&lt;/p&gt;

&lt;p&gt;Οι επιχειρησιακοί κανόνες πρέπει να μπορούν να εκτελεστούν και να ελεγχθούν ανεξάρτητα από τις τεχνολογίες υποδομής.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Handler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompleteOrderHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IUnitOfWork&lt;/span&gt; &lt;span class="n"&gt;_unitOfWork&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CompleteOrderHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IUnitOfWork&lt;/span&gt; &lt;span class="n"&gt;unitOfWork&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_unitOfWork&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unitOfWork&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KeyNotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Order not found."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Εδώ το &lt;code&gt;IUnitOfWork&lt;/code&gt; είναι ένα interface του Application ή Domain abstraction layer, με υλοποίηση στο Infrastructure.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUnitOfWork&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο Handler δεν περιέχει τον κανόνα που καθορίζει αν μια παραγγελία μπορεί να ολοκληρωθεί.&lt;/p&gt;

&lt;p&gt;Απλώς καλεί:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η επιχειρησιακή απόφαση παραμένει στο Domain.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Aggregate = Consistency Boundary
&lt;/h2&gt;

&lt;p&gt;Ένα από τα σημαντικότερα χαρακτηριστικά των Aggregates είναι ότι ορίζουν τα όρια της επιχειρησιακής συνέπειας.&lt;/p&gt;

&lt;p&gt;Ας υποθέσουμε ότι έχουμε δύο Aggregates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Aggregate
├── Order
└── OrderItems

Inventory Aggregate
├── InventoryItem
└── StockReservations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Order είναι υπεύθυνο για τους κανόνες της παραγγελίας.&lt;/p&gt;

&lt;p&gt;Το Inventory είναι υπεύθυνο για τους κανόνες διαθεσιμότητας και δέσμευσης αποθέματος.&lt;/p&gt;

&lt;p&gt;Δεν πρέπει να δημιουργήσουμε ένα τεράστιο Aggregate που περιλαμβάνει όλα τα παραπάνω μόνο και μόνο επειδή σχετίζονται μεταξύ τους.&lt;/p&gt;

&lt;h3&gt;
  
  
  Πώς επικοινωνούν διαφορετικά Aggregates;
&lt;/h3&gt;

&lt;p&gt;Μέσω Application Services, Domain Events ή Integration Events, ανάλογα με τις απαιτήσεις.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Completed
       |
       v
OrderCompleted Event
       |
       v
Inventory Reservation Workflow
       |
       v
Inventory Aggregate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Μια πιθανή υλοποίηση Domain Event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderCompletedDomainEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Aggregate μπορεί να καταγράφει το event κατά την ολοκλήρωση.&lt;/p&gt;

&lt;p&gt;Ένας Application Handler μπορεί στη συνέχεια να συντονίζει την ενημέρωση του Inventory.&lt;/p&gt;

&lt;p&gt;Σε distributed architectures, μπορούμε να χρησιμοποιήσουμε:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional Outbox Pattern.&lt;/li&gt;
&lt;li&gt;Idempotent Message Consumers.&lt;/li&gt;
&lt;li&gt;Eventual Consistency.&lt;/li&gt;
&lt;li&gt;Saga / Process Manager.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Δεν είναι όμως υποχρεωτικό κάθε επικοινωνία μεταξύ Aggregates να γίνεται ασύγχρονα.&lt;/p&gt;

&lt;p&gt;Η επιλογή εξαρτάται από τις απαιτήσεις ατομικότητας, συνέπειας και διαθεσιμότητας.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Πώς αποφασίζουμε τα Aggregate Boundaries;
&lt;/h2&gt;

&lt;p&gt;Αυτό είναι ίσως το σημαντικότερο και δυσκολότερο μέρος του σχεδιασμού.&lt;/p&gt;

&lt;p&gt;Ένα συνηθισμένο λάθος είναι να θεωρούμε ότι κάθε σχέση μεταξύ Database Tables πρέπει να αντιστοιχεί σε ένα Aggregate.&lt;/p&gt;

&lt;p&gt;Αυτό δεν ισχύει.&lt;/p&gt;

&lt;p&gt;Τα Aggregate Boundaries καθορίζονται κυρίως από τους επιχειρησιακούς κανόνες και όχι από τα Foreign Keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ερωτήσεις που πρέπει να κάνουμε
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Ποιοι κανόνες πρέπει να ισχύουν άμεσα και ατομικά;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αν μια αλλαγή πρέπει να διατηρεί συγκεκριμένα invariants σε πολλά Entities, αυτά ενδέχεται να ανήκουν στο ίδιο Aggregate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ποια αντικείμενα έχουν ανεξάρτητο lifecycle;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αν ένα αντικείμενο μπορεί να δημιουργείται, να τροποποιείται και να διαγράφεται ανεξάρτητα, ίσως χρειάζεται δικό του Aggregate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Πόσο μεγάλο γίνεται το Aggregate;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένα υπερβολικά μεγάλο Aggregate μπορεί να δημιουργήσει προβλήματα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Concurrency conflicts.&lt;/li&gt;
&lt;li&gt;Performance overhead.&lt;/li&gt;
&lt;li&gt;Μεγάλα object graphs.&lt;/li&gt;
&lt;li&gt;Δυσκολότερη συντήρηση.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Μπορούμε να χρησιμοποιήσουμε eventual consistency;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αν δεν χρειάζεται άμεση συνέπεια μεταξύ δύο επιχειρησιακών λειτουργιών, ίσως είναι προτιμότερο να βρίσκονται σε διαφορετικά Aggregates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ένας πρακτικός κανόνας
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Σχεδίασε το μικρότερο δυνατό Aggregate που μπορεί να προστατεύσει σωστά τα επιχειρησιακά invariants του.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Aggregates και Concurrency
&lt;/h2&gt;

&lt;p&gt;Ακόμη και ένα σωστά σχεδιασμένο Aggregate μπορεί να αντιμετωπίσει προβλήματα όταν πολλοί χρήστες το τροποποιούν ταυτόχρονα.&lt;/p&gt;

&lt;p&gt;Ας υποθέσουμε ότι δύο requests φορτώνουν την ίδια παραγγελία.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request A                 Request B
    |                         |
Load Order               Load Order
    |                         |
Change Quantity          Complete Order
    |                         |
Save Changes             Save Changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Χωρίς concurrency protection, μπορεί να προκύψουν χαμένες ενημερώσεις ή μη έγκυρη τελική κατάσταση.&lt;/p&gt;

&lt;p&gt;Το Aggregate προστατεύει τους κανόνες στο επίπεδο του Domain Model, αλλά δεν αντικαθιστά τους μηχανισμούς concurrency του persistence layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimistic Concurrency με EF Core
&lt;/h3&gt;

&lt;p&gt;Μπορούμε να χρησιμοποιήσουμε ένα concurrency token.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, σε SQL Server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;RowVersion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και στο mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RowVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsRowVersion&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το EF Core μπορεί να εντοπίσει συγκρούσεις ενημέρωσης και να προκαλέσει &lt;code&gt;DbUpdateConcurrencyException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Για Aggregates με child entities χρειάζεται προσεκτικός σχεδιασμός ώστε οι αλλαγές των παιδιών να συμμετέχουν στην επιθυμητή στρατηγική concurrency. Ένα row version μόνο στον πίνακα του Root δεν προστατεύει αυτόματα όλες τις ενημερώσεις των child rows.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Unit Testing των Aggregates
&lt;/h2&gt;

&lt;p&gt;Ένα από τα μεγαλύτερα πλεονεκτήματα του συγκεκριμένου σχεδιασμού είναι ότι μπορούμε να κάνουμε unit testing χωρίς database.&lt;/p&gt;

&lt;p&gt;Με xUnit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Cannot_complete_empty_order&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Throws&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Cannot_modify_completed_order&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;50m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Throws&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="m"&gt;20m&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Total_amount_is_calculated_correctly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;130m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Cannot_add_duplicate_product&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Throws&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20m&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Τα tests αυτά εκτελούνται χωρίς:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL Server.&lt;/li&gt;
&lt;li&gt;Entity Framework Core.&lt;/li&gt;
&lt;li&gt;HTTP Requests.&lt;/li&gt;
&lt;li&gt;Dependency Injection.&lt;/li&gt;
&lt;li&gt;Mock Repositories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ελέγχουμε απευθείας την επιχειρησιακή συμπεριφορά.&lt;/p&gt;

&lt;p&gt;Αυτό κάνει το Domain Model πιο εύκολο να κατανοηθεί και να εξελιχθεί.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Συνηθισμένα λάθη στην εφαρμογή Aggregates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Λάθος 1: Public Setters παντού
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Έτσι επιτρέπουμε σε οποιοδήποτε σημείο της εφαρμογής να αλλάξει την κατάσταση.&lt;/p&gt;

&lt;p&gt;Προτιμούμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Validate business rules&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Λάθος 2: Ένα Aggregate για ολόκληρο το Domain
&lt;/h3&gt;

&lt;p&gt;Το Aggregate δεν είναι ένας μηχανισμός για να συνδέουμε όλα τα Entities μεταξύ τους.&lt;/p&gt;

&lt;p&gt;Μεγάλα Aggregates αυξάνουν το κόστος φόρτωσης και τον κίνδυνο concurrency conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 3: Business Logic στα Application Services
&lt;/h3&gt;

&lt;p&gt;Το Application Layer πρέπει κυρίως να συντονίζει τη λειτουργία.&lt;/p&gt;

&lt;p&gt;Δεν πρέπει να περιέχει κανόνες που ανήκουν στην εσωτερική συνέπεια του Aggregate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 4: Repository για κάθε Entity
&lt;/h3&gt;

&lt;p&gt;Δεν χρειάζεται υποχρεωτικά:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderRepository
OrderItemRepository
OrderStatusRepository
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αν το OrderItem ανήκει στο Order Aggregate, η διαχείρισή του γίνεται μέσω του Order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 5: Σύγχυση Aggregate και Database Transaction
&lt;/h3&gt;

&lt;p&gt;Ένα Aggregate ορίζει επιχειρησιακά όρια συνέπειας.&lt;/p&gt;

&lt;p&gt;Δεν είναι απλώς ένα σύνολο εγγραφών που αποθηκεύονται μαζί.&lt;/p&gt;

&lt;p&gt;Το database transaction είναι μηχανισμός υποδομής, ενώ το Aggregate είναι έννοια του Domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 6: Χρήση Aggregates χωρίς πραγματικούς κανόνες
&lt;/h3&gt;

&lt;p&gt;Δεν χρειάζεται κάθε Entity να αποκτήσει πολύπλοκο Aggregate Root.&lt;/p&gt;

&lt;p&gt;Σε ένα απλό CRUD σύστημα, η επιπλέον πολυπλοκότητα μπορεί να μην προσφέρει ουσιαστικό όφελος.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Πότε πρέπει να χρησιμοποιούμε Aggregates;
&lt;/h2&gt;

&lt;p&gt;Τα Aggregates είναι ιδιαίτερα χρήσιμα όταν αναπτύσσουμε εφαρμογές με σύνθετη επιχειρησιακή συμπεριφορά.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E-commerce:&lt;/strong&gt; Orders, Shopping Carts, Payments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Banking:&lt;/strong&gt; Accounts, Transfers, Loan Applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERP:&lt;/strong&gt; Invoices, Purchase Orders, Inventory Reservations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Booking Systems:&lt;/strong&gt; Reservations, Availability Allocations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insurance:&lt;/strong&gt; Policies, Claims, Coverage Rules.&lt;/p&gt;

&lt;p&gt;Σε αυτές τις περιπτώσεις, οι επιχειρησιακοί κανόνες είναι συχνά σημαντικότεροι από τις απλές CRUD λειτουργίες.&lt;/p&gt;

&lt;p&gt;Το Aggregate μάς επιτρέπει να εκφράσουμε αυτούς τους κανόνες με σαφή και ελεγχόμενο τρόπο.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Τελικές σκέψεις
&lt;/h2&gt;

&lt;p&gt;Το Aggregate είναι μία από τις πιο σημαντικές έννοιες του Domain-Driven Design.&lt;/p&gt;

&lt;p&gt;Δεν είναι απλώς ένα design pattern για την οργάνωση Entities.&lt;/p&gt;

&lt;p&gt;Είναι ένας τρόπος να σχεδιάζουμε το λογισμικό γύρω από την &lt;strong&gt;επιχειρησιακή συνέπεια και συμπεριφορά&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Όταν χρησιμοποιούμε σωστά Aggregates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Οι Business Rules βρίσκονται στο Domain.&lt;/li&gt;
&lt;li&gt;Τα Entities δεν τροποποιούνται ανεξέλεγκτα.&lt;/li&gt;
&lt;li&gt;Τα Application Services γίνονται πιο απλά.&lt;/li&gt;
&lt;li&gt;Τα Unit Tests εστιάζουν στην πραγματική επιχειρησιακή συμπεριφορά.&lt;/li&gt;
&lt;li&gt;Τα όρια των transactions και της συνέπειας γίνονται πιο ξεκάθαρα.&lt;/li&gt;
&lt;li&gt;Η εφαρμογή γίνεται ευκολότερη στη συντήρηση και στην εξέλιξη.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Το σημαντικότερο είναι να θυμόμαστε ότι &lt;strong&gt;δεν σχεδιάζουμε Aggregates με βάση τη δομή της βάσης δεδομένων, αλλά με βάση τους κανόνες του business&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An Aggregate is not just a collection of entities. It is a consistency boundary that protects business invariants.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Αυτή είναι η ουσία του Aggregate στο Domain-Driven Design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;Για περισσότερες πληροφορίες:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/" rel="noopener noreferrer"&gt;Microsoft — DDD and CQRS Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/seedwork-domain-model-base-classes-interfaces" rel="noopener noreferrer"&gt;Microsoft — Domain Model Base Classes and Interfaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/ef/core/modeling/relationships/navigations" rel="noopener noreferrer"&gt;Microsoft — EF Core Relationship Navigations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Εσείς χρησιμοποιείτε Aggregates στα .NET projects σας;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Προτιμάτε Rich Domain Models ή πιο απλές CRUD-based αρχιτεκτονικές;&lt;/p&gt;

&lt;p&gt;Μοιραστείτε την εμπειρία σας στα σχόλια!&lt;/p&gt;

&lt;h1&gt;
  
  
  dotnet #csharp #architecture #ddd
&lt;/h1&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>CQRS χωρίς MediatR στο .NET: Custom Implementation, Clean Architecture και Dependency Inversion</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Fri, 09 Oct 2026 19:32:49 +0000</pubDate>
      <link>https://dev.to/__b63657/cqrs-khoris-mediatr-sto-net-custom-implementation-clean-architecture-kai-dependency-inversion-pd5</link>
      <guid>https://dev.to/__b63657/cqrs-khoris-mediatr-sto-net-custom-implementation-clean-architecture-kai-dependency-inversion-pd5</guid>
      <description>&lt;h2&gt;
  
  
  Εισαγωγή
&lt;/h2&gt;

&lt;p&gt;Το CQRS είναι μία από τις αρχιτεκτονικές προσεγγίσεις που συναντάμε συχνά σε εφαρμογές .NET οι οποίες χρειάζονται σαφή διαχωρισμό ευθυνών, ελεγχόμενη επιχειρησιακή λογική και δυνατότητα εξέλιξης χωρίς να αυξάνεται ανεξέλεγκτα η πολυπλοκότητα του κώδικα.&lt;/p&gt;

&lt;p&gt;Στην πράξη, όμως, υπάρχει μια συνηθισμένη παρανόηση: πολλοί developers συνδέουν το CQRS με το MediatR σε τέτοιο βαθμό, ώστε θεωρούν ότι η χρήση της βιβλιοθήκης αποτελεί προϋπόθεση για την εφαρμογή του pattern.&lt;/p&gt;

&lt;p&gt;Αυτό δεν ισχύει.&lt;/p&gt;

&lt;p&gt;Το CQRS είναι αρχιτεκτονική προσέγγιση. Το MediatR είναι μία βιβλιοθήκη που μπορεί να χρησιμοποιηθεί για τη δρομολόγηση requests προς τους κατάλληλους handlers και για την εφαρμογή κοινών συμπεριφορών κατά την επεξεργασία τους.&lt;/p&gt;

&lt;p&gt;Μπορούμε να εφαρμόσουμε CQRS χωρίς MediatR, χωρίς εξωτερική βιβλιοθήκη και χωρίς να εγκαταστήσουμε κάποιο framework που αναλαμβάνει μεγάλο μέρος της υποδομής. Το .NET διαθέτει ήδη Dependency Injection, generics και abstractions που μας επιτρέπουν να σχεδιάσουμε τη δική μας υλοποίηση.&lt;/p&gt;

&lt;p&gt;Το ερώτημα, επομένως, δεν είναι αν μπορούμε να εφαρμόσουμε CQRS χωρίς MediatR. Μπορούμε.&lt;/p&gt;

&lt;p&gt;Το πραγματικό ερώτημα είναι:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ποια είναι η ελάχιστη custom υποδομή που χρειαζόμαστε ώστε η υλοποίηση να παραμένει καθαρή, επεκτάσιμη, ελέγξιμη και συνεπής με τις αρχές της Clean Architecture;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Σε αυτό το άρθρο θα σχεδιάσουμε μια ολοκληρωμένη προσέγγιση με C#, ASP.NET Core και Entity Framework Core, ξεκινώντας από τα βασικά contracts και φτάνοντας μέχρι το Dependency Injection και τα pipeline behaviors.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Τι ακριβώς προσφέρει το CQRS;
&lt;/h2&gt;

&lt;p&gt;Το CQRS σημαίνει Command Query Responsibility Segregation.&lt;/p&gt;

&lt;p&gt;Η βασική ιδέα είναι ότι διαχωρίζουμε τις ενέργειες που μεταβάλλουν την κατάσταση του συστήματος από εκείνες που διαβάζουν δεδομένα.&lt;/p&gt;

&lt;p&gt;Ένα Command εκφράζει την πρόθεση να πραγματοποιηθεί μια ενέργεια. Για παράδειγμα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CreateEmployeeCommand&lt;/li&gt;
&lt;li&gt;UpdateEmployeeCommand&lt;/li&gt;
&lt;li&gt;DeleteEmployeeCommand&lt;/li&gt;
&lt;li&gt;AssignRoleCommand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ένα Query εκφράζει την απαίτηση ανάκτησης δεδομένων:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GetEmployeeByIdQuery&lt;/li&gt;
&lt;li&gt;GetEmployeesQuery&lt;/li&gt;
&lt;li&gt;GetUserPermissionsQuery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Η διάκριση αυτή δεν είναι απλώς οργανωτική. Βοηθά να διαμορφώσουμε διαφορετικές συμβάσεις, διαφορετική λογική επεξεργασίας και διαφορετικές στρατηγικές πρόσβασης στα δεδομένα.&lt;/p&gt;

&lt;p&gt;Ένα Command μπορεί να φορτώσει μια οντότητα, να ελέγξει τους επιχειρησιακούς κανόνες της και να αποθηκεύσει τις αλλαγές της. Ένα Query μπορεί να επιστρέψει απευθείας ένα DTO μέσω projection, χωρίς να χρειάζεται να φορτώσει ολόκληρο το Domain Model.&lt;/p&gt;

&lt;p&gt;Σημαντικό: το CQRS δεν απαιτεί δύο διαφορετικές βάσεις δεδομένων. Μπορούμε να εφαρμόσουμε διαχωρισμό Commands και Queries χρησιμοποιώντας την ίδια SQL Server βάση και το ίδιο DbContext, όπου αυτό εξυπηρετεί τις ανάγκες της εφαρμογής.&lt;/p&gt;

&lt;p&gt;Ούτε απαιτεί υποχρεωτικά Mediator, message broker, event sourcing ή microservices.&lt;/p&gt;

&lt;p&gt;Αυτές είναι διαφορετικές σχεδιαστικές επιλογές.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. CQRS και Mediator: δύο διαφορετικές έννοιες
&lt;/h2&gt;

&lt;p&gt;Για να σχεδιάσουμε σωστά την υλοποίησή μας, πρέπει να διαχωρίσουμε τις ευθύνες τους.&lt;/p&gt;

&lt;p&gt;Το CQRS ορίζει τον διαχωρισμό μεταξύ των ενεργειών ανάγνωσης και μεταβολής δεδομένων.&lt;/p&gt;

&lt;p&gt;Το Mediator είναι ένα pattern επικοινωνίας. Επιτρέπει σε έναν caller να στέλνει ένα request χωρίς να χρειάζεται να γνωρίζει άμεσα τον συγκεκριμένο handler που θα το εκτελέσει.&lt;/p&gt;

&lt;p&gt;Μια εφαρμογή μπορεί να έχει CQRS με MediatR, CQRS με custom dispatcher ή CQRS με απευθείας injection των handlers.&lt;/p&gt;

&lt;p&gt;Και οι τρεις επιλογές είναι τεχνικά εφικτές. Δεν έχουν, όμως, τις ίδιες ιδιότητες.&lt;/p&gt;

&lt;p&gt;Με άμεσο injection, ο caller γνωρίζει το interface του handler που χρειάζεται. Με έναν dispatcher, ο caller στέλνει ένα request και η υποδομή εντοπίζει τον κατάλληλο handler. Με έναν mediator, αποκτάμε συνήθως επιπλέον έτοιμες δυνατότητες για routing και pipeline behaviors.&lt;/p&gt;

&lt;p&gt;Η επιλογή εξαρτάται από τις ανάγκες του συστήματος.&lt;/p&gt;

&lt;p&gt;Για μια εφαρμογή με συγκεκριμένα, strongly typed endpoints, το άμεσο injection μπορεί να είναι επαρκές. Για ένα μεγαλύτερο σύστημα με πολλές κοινές συμπεριφορές, ίσως αξίζει να σχεδιάσουμε έναν dispatcher και ένα pipeline.&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται να εισαγάγουμε περισσότερη έμμεση επικοινωνία από όση πραγματικά απαιτεί η εφαρμογή.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Η αρχιτεκτονική δομή της υλοποίησης
&lt;/h2&gt;

&lt;p&gt;Ας υποθέσουμε ότι αναπτύσσουμε μια εφαρμογή διαχείρισης εργαζομένων, με SQL Server και Entity Framework Core.&lt;/p&gt;

&lt;p&gt;Η δομή της λύσης μας θα μπορούσε να είναι η εξής:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── MyApplication.Domain/
│   ├── Entities/
│   │   └── Employee.cs
│   └── Exceptions/
│
├── MyApplication.Application/
│   ├── Abstractions/
│   │   ├── ICommand.cs
│   │   ├── IQuery.cs
│   │   ├── ICommandHandler.cs
│   │   └── IQueryHandler.cs
│   └── Employees/
│       ├── Commands/
│       │   └── CreateEmployee/
│       │       ├── CreateEmployeeCommand.cs
│       │       └── CreateEmployeeHandler.cs
│       └── Queries/
│           └── GetEmployeeById/
│               ├── GetEmployeeByIdQuery.cs
│               ├── EmployeeDto.cs
│               └── GetEmployeeByIdHandler.cs
│
├── MyApplication.Infrastructure/
│   ├── Persistence/
│   │   └── ApplicationDbContext.cs
│   └── DependencyInjection.cs
│
└── MyApplication.Api/
    ├── Controllers/
    │   └── EmployeesController.cs
    └── Program.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η συγκεκριμένη δομή είναι ενδεικτική και όχι υποχρεωτική. Μπορούμε να οργανώσουμε το Application ανά feature, ώστε κάθε επιχειρησιακή λειτουργία να συγκεντρώνει τα σχετικά requests, handlers, validators και DTOs.&lt;/p&gt;

&lt;p&gt;Η ουσία είναι τα όρια των εξαρτήσεων:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Το Domain περιέχει τις επιχειρησιακές οντότητες και τους κανόνες τους.&lt;/li&gt;
&lt;li&gt;Το Application περιγράφει τις περιπτώσεις χρήσης και συντονίζει την εκτέλεσή τους.&lt;/li&gt;
&lt;li&gt;Το Infrastructure υλοποιεί τις τεχνικές λεπτομέρειες, όπως η αποθήκευση δεδομένων.&lt;/li&gt;
&lt;li&gt;Το API εκθέτει τις λειτουργίες προς τους εξωτερικούς clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Το CQRS δεν επιβάλλει από μόνο του αυτή τη δομή. Η Clean Architecture είναι εκείνη που μας βοηθά να ορίσουμε τα αρχιτεκτονικά όρια και τις κατευθύνσεις των εξαρτήσεων.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Πρώτο βήμα: Ορίζουμε τα contracts του CQRS
&lt;/h2&gt;

&lt;p&gt;Το πρώτο πράγμα που χρειάζεται να αποφασίσουμε είναι ποια συμβόλαια θέλουμε να προσφέρει το Application layer.&lt;/p&gt;

&lt;p&gt;Δεν ξεκινάμε γράφοντας dispatcher, reflection ή assembly scanning. Ξεκινάμε ορίζοντας τι σημαίνει Command, τι σημαίνει Query και τι περιμένουμε από τους αντίστοιχους handlers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Command contracts
&lt;/h3&gt;

&lt;p&gt;Στο &lt;code&gt;ICommand.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;ICommand&amp;lt;TResult&amp;gt;&lt;/code&gt; είναι ένα marker interface. Δεν περιέχει μεθόδους. Δηλώνει ότι ένας συγκεκριμένος τύπος αντιπροσωπεύει ένα Command και προσδιορίζει τον τύπο του αποτελέσματος.&lt;/p&gt;

&lt;p&gt;Για ενέργειες που δεν χρειάζονται επιστρεφόμενη τιμή, θα μπορούσαμε να ορίσουμε επιπλέον ένα &lt;code&gt;ICommand&lt;/code&gt; χωρίς generic παράμετρο. Δεν είναι απαραίτητο, όμως, να υποστηρίζουμε και τις δύο περιπτώσεις από την πρώτη ημέρα.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Query contracts
&lt;/h3&gt;

&lt;p&gt;Στο &lt;code&gt;IQuery.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Query διαθέτει επίσης έναν τύπο αποτελέσματος.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, ένα Query μπορεί να επιστρέψει ένα DTO, μια συλλογή αποτελεσμάτων ή μια nullable τιμή όταν το ζητούμενο στοιχείο ενδέχεται να μην υπάρχει.&lt;/p&gt;

&lt;p&gt;Η διαφορά από το Command δεν είναι ο τύπος επιστροφής. Είναι η σημασιολογία της λειτουργίας: ένα Query δεν πρέπει να μεταβάλλει την επιχειρησιακή κατάσταση του συστήματος.&lt;/p&gt;

&lt;p&gt;Τα marker interfaces βοηθούν τον compiler και το Dependency Injection να αναγνωρίζουν τους τύπους που ανήκουν στην αντίστοιχη κατηγορία.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Command handler contract
&lt;/h3&gt;

&lt;p&gt;Στο &lt;code&gt;ICommandHandler.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό το interface περιγράφει τη σύμβαση ενός Command Handler.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;TCommand&lt;/code&gt; είναι ο τύπος της ενέργειας που θα εκτελεστεί και το &lt;code&gt;TResult&lt;/code&gt; ο τύπος του αποτελέσματος.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;CancellationToken&lt;/code&gt; είναι επίσης σημαντικό. Επιτρέπει στον caller να μεταφέρει το αίτημα ακύρωσης σε λειτουργίες όπως η πρόσβαση στη βάση δεδομένων.&lt;/p&gt;

&lt;p&gt;Δεν θα πρέπει να το παραλείπουμε από την υποδομή μας απλώς επειδή υλοποιούμε custom CQRS.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Query handler contract
&lt;/h3&gt;

&lt;p&gt;Στο &lt;code&gt;IQueryHandler.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TQuery&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TQuery&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Με αυτά τα τέσσερα contracts έχουμε ορίσει τον βασικό πυρήνα της υλοποίησης.&lt;/p&gt;

&lt;p&gt;Δεν έχουμε ακόμη δημιουργήσει dispatcher. Δεν έχουμε γράψει reflection. Δεν έχουμε εγκαταστήσει εξωτερική βιβλιοθήκη.&lt;/p&gt;

&lt;p&gt;Έχουμε απλώς ορίσει ένα ισχυρά τυποποιημένο συμβόλαιο για κάθε πλευρά του CQRS.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Υλοποίηση ενός πραγματικού Command
&lt;/h2&gt;

&lt;p&gt;Ας δημιουργήσουμε τώρα μια λειτουργία καταχώρισης εργαζομένου.&lt;/p&gt;

&lt;p&gt;Το Command θα βρίσκεται στο Application layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Employees.Commands.CreateEmployee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateEmployeeCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Command περιγράφει την πρόθεση του client. Δεν είναι η Domain Entity και δεν χρειάζεται να κληρονομεί από αυτήν.&lt;/p&gt;

&lt;p&gt;Αυτή η διάκριση μάς επιτρέπει να διατηρούμε το API contract ανεξάρτητο από το εσωτερικό μοντέλο της εφαρμογής.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Η Domain Entity
&lt;/h3&gt;

&lt;p&gt;Στο Domain layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Domain.Entities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Employee&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Used by Entity Framework Core.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Employee&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"First name is required."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Last name is required."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Email is required."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το παράδειγμα παρουσιάζει απλούς ελέγχους εισόδου. Σε πραγματική εφαρμογή, η πολιτική επικύρωσης email και οι κανόνες μοναδικότητας πρέπει να σχεδιαστούν ξεχωριστά, ενώ η μοναδικότητα στη βάση πρέπει να προστατεύεται με κατάλληλο unique constraint.&lt;/p&gt;

&lt;p&gt;Το σημαντικό αρχιτεκτονικό σημείο είναι ότι η δημιουργία της οντότητας πραγματοποιείται μέσα από μια ελεγχόμενη λειτουργία και όχι μέσω ανεξέλεγκτης εξωτερικής τροποποίησης των ιδιοτήτων της.&lt;/p&gt;

&lt;p&gt;Το Domain δεν γνωρίζει το CQRS, τον controller, το DbContext ή το Dependency Injection.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Η υλοποίηση του handler
&lt;/h3&gt;

&lt;p&gt;Για να αποθηκεύσουμε την οντότητα, χρειαζόμαστε έναν τρόπο πρόσβασης στα δεδομένα. Ας υποθέσουμε ότι η εφαρμογή διαθέτει ένα &lt;code&gt;IApplicationDbContext&lt;/code&gt; στο Application layer, το οποίο υλοποιείται από το Infrastructure με EF Core.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Domain.Entities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Employees.Commands.CreateEmployee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateEmployeeHandler&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateEmployeeCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IApplicationDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateEmployeeHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IApplicationDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CreateEmployeeCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Employees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;IApplicationDbContext&lt;/code&gt; είναι ένα ενδεικτικό abstraction που θα μπορούσε να οριστεί ως εξής:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Domain.Entities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IApplicationDbContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DbSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Employees&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Infrastructure υλοποιεί αυτό το interface με ένα πραγματικό &lt;code&gt;DbContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Έτσι, το Application εξαρτάται από το συμβόλαιο που χρειάζεται, όχι από τη συγκεκριμένη κλάση αποθήκευσης.&lt;/p&gt;

&lt;p&gt;Αυτό είναι εφαρμογή του Dependency Inversion Principle. Δεν χρειάζεται να εισαγάγουμε Mediator για να πετύχουμε αυτή την ανεξαρτησία.&lt;/p&gt;

&lt;p&gt;Σημειώνεται ότι ένα τέτοιο abstraction είναι επιλογή σχεδιασμού, όχι υποχρεωτικό βήμα. Σε ορισμένες εφαρμογές μπορεί να είναι καταλληλότερα repositories ή ένα διαφορετικό persistence contract. Το ζητούμενο είναι να αποφύγουμε την άσκοπη διαρροή υποδομών στο Domain και να επιλέξουμε τα abstractions βάσει πραγματικών αναγκών.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Υλοποίηση ενός Query
&lt;/h2&gt;

&lt;p&gt;Η πλευρά των Queries έχει διαφορετικό στόχο: την ανάκτηση δεδομένων με όσο το δυνατόν πιο κατάλληλη διαδρομή για την εκάστοτε περίπτωση χρήσης.&lt;/p&gt;

&lt;p&gt;Δημιουργούμε το Query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Employees.Queries.GetEmployeeById&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;GetEmployeeByIdQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;EmployeeId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και το DTO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Employees.Queries.GetEmployeeById&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApplication.Application.Employees.Queries.GetEmployeeById&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetEmployeeByIdHandler&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetEmployeeByIdQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IApplicationDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;GetEmployeeByIdHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IApplicationDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;GetEmployeeByIdQuery&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Employees&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EmployeeId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η χρήση του &lt;code&gt;AsNoTracking()&lt;/code&gt; είναι κατάλληλη εδώ, επειδή το Query δεν χρειάζεται να τροποποιήσει την οντότητα.&lt;/p&gt;

&lt;p&gt;Η projection δημιουργεί το DTO μέσα στο query, ώστε η βάση να επιστρέψει μόνο τα πεδία που χρειαζόμαστε.&lt;/p&gt;

&lt;p&gt;Δεν υπάρχει λόγος να φορτώσουμε ολόκληρο το Domain Model, εφόσον η συγκεκριμένη περίπτωση χρήσης χρειάζεται μόνο αυτά τα δεδομένα.&lt;/p&gt;

&lt;p&gt;Αυτό είναι ένα από τα πρακτικά πλεονεκτήματα του διαχωρισμού Commands και Queries: μπορούμε να σχεδιάζουμε την ανάγνωση και τη μεταβολή δεδομένων με διαφορετικές προτεραιότητες, ακόμη και όταν χρησιμοποιούμε την ίδια βάση.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Χρειαζόμαστε υποχρεωτικά Dispatcher;
&lt;/h2&gt;

&lt;p&gt;Όχι.&lt;/p&gt;

&lt;p&gt;Αυτό είναι ένα από τα σημαντικότερα σημεία της υλοποίησης.&lt;/p&gt;

&lt;p&gt;Εφόσον το API γνωρίζει ποιο handler χρειάζεται, μπορεί να κάνει inject απευθείας το αντίστοιχο interface.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/employees"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmployeesController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateEmployeeCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;_createHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetEmployeeByIdQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;_getHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;EmployeesController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateEmployeeCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetEmployeeByIdQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_createHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_getHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CreateEmployeeCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_createHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;CreatedAtAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id:guid}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_getHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GetEmployeeByIdQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο controller εξαρτάται από abstractions, όχι από concrete handlers. Αυτό είναι σαφώς προτιμότερο από το να κάνει inject απευθείας το &lt;code&gt;CreateEmployeeHandler&lt;/code&gt;, εφόσον θέλουμε να διατηρήσουμε ένα σταθερό συμβόλαιο μεταξύ Presentation και Application.&lt;/p&gt;

&lt;p&gt;Ωστόσο, υπάρχει μια σχεδιαστική παρατήρηση: ο controller εξακολουθεί να γνωρίζει τα συγκεκριμένα request types και τα αντίστοιχα handler interfaces. Σε ένα API με λίγες λειτουργίες αυτό μπορεί να είναι απολύτως αποδεκτό.&lt;/p&gt;

&lt;p&gt;Αν, αντίθετα, θέλουμε ένα ενιαίο σημείο εισόδου για όλα τα Commands και Queries, τότε μπορούμε να προσθέσουμε custom dispatchers.&lt;/p&gt;

&lt;p&gt;Η απόφαση αυτή πρέπει να λαμβάνεται βάσει αναγκών, όχι επειδή το CQRS υποχρεώνει τη χρήση τους.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Custom Command Dispatcher και Query Dispatcher
&lt;/h2&gt;

&lt;p&gt;Αν επιλέξουμε να υλοποιήσουμε έναν dispatcher, χρειαζόμαστε δύο ακόμη interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommandDispatcher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IQueryDispatcher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;QueryAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο dispatcher θα αναλαμβάνει να βρει τον κατάλληλο handler και να του παραδώσει το request.&lt;/p&gt;

&lt;p&gt;Εδώ, όμως, εμφανίζεται ένα τεχνικό ζήτημα: το &lt;code&gt;IServiceProvider&lt;/code&gt; του .NET δεν γνωρίζει αυτόματα ποιος handler αντιστοιχεί σε ένα συγκεκριμένο request. Χρειάζεται να κατασκευάσουμε τον κατάλληλο κλειστό generic τύπο, όπως &lt;code&gt;ICommandHandler&amp;lt;CreateEmployeeCommand, Guid&amp;gt;&lt;/code&gt;, και να τον επιλύσουμε από το DI container.&lt;/p&gt;

&lt;p&gt;Μια πιθανή υλοποίηση είναι η εξής:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandDispatcher&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommandDispatcher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CommandDispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_serviceProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handlerType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MakeGenericType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handlerType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handlerType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;))!;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;})!;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο παραπάνω κώδικας δείχνει τη βασική ιδέα της δρομολόγησης, αλλά δεν θα τον θεωρούσα έτοιμη production υλοποίηση.&lt;/p&gt;

&lt;p&gt;Η χρήση reflection εισάγει runtime αποτυχίες, δυσκολεύει την ανίχνευση σφαλμάτων κατά το compile time και απαιτεί ιδιαίτερη προσοχή στη διαχείριση των exceptions που προέρχονται από το &lt;code&gt;MethodInfo.Invoke&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Σε ένα πραγματικό σύστημα θα εξέταζα μια πιο ισχυρά τυποποιημένη υλοποίηση, με προσεκτικά σχεδιασμένα adapters, ή θα επέλεγα απευθείας injection, εφόσον δεν υπήρχε πραγματική ανάγκη για dispatcher.&lt;/p&gt;

&lt;p&gt;Αυτό είναι σημαντικό: &lt;strong&gt;δεν αξίζει να προσθέτουμε ένα custom abstraction εάν η υλοποίησή του δημιουργεί μεγαλύτερη πολυπλοκότητα από εκείνη που αφαιρεί.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ο ίδιος προβληματισμός ισχύει για το Query Dispatcher. Αν δεν έχουμε ανάγκη για κοινό σημείο δρομολόγησης, δεν χρειάζεται να τον δημιουργήσουμε.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Dependency Injection: Πώς συνδέονται τα contracts με τους handlers;
&lt;/h2&gt;

&lt;p&gt;Έχουμε ορίσει τα interfaces και τις υλοποιήσεις τους. Τώρα πρέπει να τις καταχωρίσουμε στο Dependency Injection container.&lt;/p&gt;

&lt;p&gt;Η απλούστερη προσέγγιση είναι η χειροκίνητη καταχώριση:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateEmployeeCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CreateEmployeeHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="n"&gt;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetEmployeeByIdQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GetEmployeeByIdHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό είναι πλήρως έγκυρο και έχει ένα σημαντικό πλεονέκτημα: οι καταχωρίσεις είναι ρητές και εύκολα κατανοητές.&lt;/p&gt;

&lt;p&gt;Το μειονέκτημα είναι ότι, όταν το Application layer περιλαμβάνει δεκάδες ή εκατοντάδες handlers, το &lt;code&gt;Program.cs&lt;/code&gt; ή το αρχείο καταχωρίσεων μπορεί να γίνει δυσανάλογα μεγάλο.&lt;/p&gt;

&lt;p&gt;Για αυτόν τον λόγο, σε μεγαλύτερα projects μπορούμε να δημιουργήσουμε μια επέκταση του &lt;code&gt;IServiceCollection&lt;/code&gt; που συγκεντρώνει τις καταχωρίσεις.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DependencyInjection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="nf"&gt;AddApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
            &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateEmployeeCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
            &lt;span class="n"&gt;CreateEmployeeHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
            &lt;span class="n"&gt;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetEmployeeByIdQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmployeeDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;,&lt;/span&gt;
            &lt;span class="n"&gt;GetEmployeeByIdHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και στο API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό δεν είναι αυτόματη ανακάλυψη handlers, αλλά είναι ήδη μια καθαρότερη οργάνωση των registrations.&lt;/p&gt;

&lt;p&gt;Αν ο αριθμός των handlers αυξηθεί σημαντικά, μπορούμε να εξετάσουμε assembly scanning με reflection ή τη βιβλιοθήκη Scrutor.&lt;/p&gt;

&lt;p&gt;Πρέπει, όμως, να θυμόμαστε ότι το assembly scanning είναι μηχανισμός καταχώρισης στο DI container. Δεν είναι απαραίτητο συστατικό του CQRS.&lt;/p&gt;

&lt;p&gt;Επίσης, οι handlers που χρησιμοποιούν &lt;code&gt;DbContext&lt;/code&gt; συνήθως πρέπει να έχουν scoped lifetime, συμβατό με το lifetime του context. Δεν θα πρέπει να καταχωρίζουμε τέτοιους handlers ως singletons.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Το πιο σημαντικό custom κομμάτι: Cross-cutting concerns
&lt;/h2&gt;

&lt;p&gt;Μέχρι αυτό το σημείο, έχουμε υλοποιήσει το βασικό CQRS.&lt;/p&gt;

&lt;p&gt;Στην πραγματικότητα, όμως, το δυσκολότερο ζήτημα σε ένα μεγαλύτερο project δεν είναι να δημιουργήσουμε handlers. Είναι να διαχειριστούμε τις συμπεριφορές που επαναλαμβάνονται σε πολλούς handlers.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Performance measurement&lt;/li&gt;
&lt;li&gt;Audit logging&lt;/li&gt;
&lt;li&gt;Transaction management&lt;/li&gt;
&lt;li&gt;Idempotency, όπου απαιτείται&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Αν κάθε handler πρέπει να υλοποιεί χειροκίνητα όλες αυτές τις λειτουργίες, θα δημιουργήσουμε duplication και θα δυσκολέψουμε τη συντήρηση.&lt;/p&gt;

&lt;p&gt;Εδώ χρειαζόμαστε μια συνειδητή σχεδιαστική επιλογή.&lt;/p&gt;

&lt;p&gt;Το MediatR προσφέρει pipeline behaviors. Χωρίς MediatR, μπορούμε να εφαρμόσουμε παρόμοιες ιδέες με decorators.&lt;/p&gt;

&lt;h3&gt;
  
  
  10.1 Παράδειγμα: Logging decorator
&lt;/h3&gt;

&lt;p&gt;Ας υποθέσουμε ότι θέλουμε να καταγράφουμε την εκτέλεση Commands.&lt;/p&gt;

&lt;p&gt;Μπορούμε να δημιουργήσουμε ένα decorator που υλοποιεί το ίδιο interface με τον πραγματικό handler και τον καλεί εσωτερικά:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggingCommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggingCommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LoggingCommandHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggingCommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_inner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stopwatch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Executing command {CommandType}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Command {CommandType} completed in {ElapsedMs} ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο decorator δεν χρειάζεται να γνωρίζει τι σημαίνει επιχειρησιακά η δημιουργία ενός εργαζομένου. Δεν χρειάζεται να γνωρίζει το SQL schema ή τους κανόνες του Domain.&lt;/p&gt;

&lt;p&gt;Αναλαμβάνει μόνο το logging της εκτέλεσης και στη συνέχεια μεταβιβάζει τον έλεγχο στον πραγματικό handler.&lt;/p&gt;

&lt;p&gt;Αυτή είναι η ουσία του Decorator Pattern: επεκτείνουμε τη συμπεριφορά ενός component χωρίς να αλλάζουμε την επιχειρησιακή υλοποίησή του.&lt;/p&gt;

&lt;p&gt;Σημειώνεται ότι το παραπάνω logging δεν καταγράφει το περιεχόμενο του Command. Αυτό είναι σκόπιμο, επειδή τα requests μπορεί να περιλαμβάνουν προσωπικά ή ευαίσθητα δεδομένα.&lt;/p&gt;

&lt;h3&gt;
  
  
  10.2 Πώς συνδέεται ο decorator με το DI;
&lt;/h3&gt;

&lt;p&gt;Η ύπαρξη του decorator δεν σημαίνει ότι το DI container θα τον εφαρμόσει αυτόματα.&lt;/p&gt;

&lt;p&gt;Χρειάζεται να οργανώσουμε τις καταχωρίσεις έτσι ώστε το interface να επιλύεται μέσω του decorator και όχι απευθείας μέσω του εσωτερικού handler.&lt;/p&gt;

&lt;p&gt;Για λίγους handlers, μπορούμε να κάνουμε χειροκίνητη σύνθεση. Για παράδειγμα, θα μπορούσαμε να χρησιμοποιήσουμε ένα factory registration που δημιουργεί πρώτα τον πραγματικό handler και στη συνέχεια τον τυλίγει με τον logging decorator.&lt;/p&gt;

&lt;p&gt;Σε μεγαλύτερο σύστημα, μια βιβλιοθήκη όπως το Scrutor μπορεί να διευκολύνει την εφαρμογή decorators σε καταχωρισμένες υπηρεσίες.&lt;/p&gt;

&lt;p&gt;Πρέπει να προσέχουμε τη σειρά των decorators, καθώς και να διασφαλίζουμε ότι το factory δεν επιλύει ξανά το ίδιο interface με αποτέλεσμα κυκλική εξάρτηση.&lt;/p&gt;

&lt;p&gt;Δεν αρκεί να γράψουμε μια κλάση &lt;code&gt;LoggingCommandHandler&lt;/code&gt;. Πρέπει να διασφαλίσουμε ότι η πραγματική διαδρομή εκτέλεσης περνάει μέσα από αυτήν.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Validation χωρίς Mediator
&lt;/h2&gt;

&lt;p&gt;Το validation είναι ένα άλλο χαρακτηριστικό που συχνά συνδέεται με το MediatR.&lt;/p&gt;

&lt;p&gt;Δεν απαιτεί, όμως, Mediator.&lt;/p&gt;

&lt;p&gt;Μπορούμε να χρησιμοποιήσουμε FluentValidation ή έναν δικό μας μηχανισμό επικύρωσης και να τον ενσωματώσουμε στη ροή εκτέλεσης.&lt;/p&gt;

&lt;p&gt;Υπάρχουν δύο βασικές προσεγγίσεις.&lt;/p&gt;

&lt;p&gt;Η πρώτη είναι να καλεί κάθε handler τον validator που χρειάζεται. Είναι απλή, αλλά μπορεί να δημιουργήσει επαναλαμβανόμενο κώδικα.&lt;/p&gt;

&lt;p&gt;Η δεύτερη είναι να χρησιμοποιήσουμε έναν decorator ή μια κοινή pipeline abstraction που εκτελεί τους validators πριν καλέσει τον πραγματικό handler.&lt;/p&gt;

&lt;p&gt;Η δεύτερη προσέγγιση είναι συνήθως πιο κατάλληλη όταν θέλουμε συνεπή συμπεριφορά σε μεγάλο αριθμό Commands.&lt;/p&gt;

&lt;p&gt;Πρέπει, ωστόσο, να ξεχωρίζουμε το validation των εισερχόμενων δεδομένων από τους επιχειρησιακούς κανόνες.&lt;/p&gt;

&lt;p&gt;Ένας validator μπορεί να ελέγξει αν ένα email έχει επιτρεπόμενη μορφή ή αν ένα απαιτούμενο πεδίο είναι κενό. Δεν πρέπει, όμως, να μεταφέρουμε κάθε επιχειρησιακό κανόνα σε ένα εξωτερικό validation layer.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, η απαγόρευση μιας μετάβασης κατάστασης σε έναν εργαζόμενο μπορεί να αποτελεί κανόνα του Domain και να πρέπει να προστατεύεται από την ίδια την οντότητα, ανεξάρτητα από το ποιος καλεί τη λειτουργία.&lt;/p&gt;

&lt;p&gt;Η αρχιτεκτονική μας πρέπει να διασφαλίζει ότι οι κανόνες δεν παρακάμπτονται όταν αλλάζει το API, προστίθεται background worker ή εισάγεται ένα νέο σημείο εισόδου.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Authorization: Πού πρέπει να εφαρμόζεται;
&lt;/h2&gt;

&lt;p&gt;Το authorization χρειάζεται ακόμη μεγαλύτερη προσοχή, επειδή αφορά την ασφάλεια του συστήματος.&lt;/p&gt;

&lt;p&gt;Μπορούμε να εφαρμόσουμε authorization στο ASP.NET Core API, μέσω policies και attributes, αλλά και σε επίπεδο Application όταν οι έλεγχοι εξαρτώνται από την επιχειρησιακή λειτουργία.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, το API μπορεί να απαιτεί authenticated χρήστη, ενώ το Application layer μπορεί να πρέπει να ελέγξει αν ο συγκεκριμένος χρήστης έχει δικαίωμα να τροποποιήσει έναν συγκεκριμένο εργαζόμενο.&lt;/p&gt;

&lt;p&gt;Ένας custom authorization decorator θα μπορούσε να εκτελεί έναν κοινό έλεγχο πριν από τον handler. Αυτό, όμως, απαιτεί προσεκτικό σχεδιασμό των contracts και της πληροφορίας που μεταφέρεται από το authenticated context.&lt;/p&gt;

&lt;p&gt;Δεν θα έβαζα αυθαίρετα ολόκληρη τη λογική authorization μέσα σε έναν γενικό dispatcher. Ο dispatcher πρέπει να παραμένει υπεύθυνος για τη δρομολόγηση και όχι να μετατραπεί σε κεντρική κλάση που γνωρίζει κάθε επιχειρησιακό κανόνα.&lt;/p&gt;

&lt;p&gt;Επίσης, το Domain δεν πρέπει να εξαρτάται από το ASP.NET Core &lt;code&gt;HttpContext&lt;/code&gt;. Αν ένας επιχειρησιακός κανόνας χρειάζεται πληροφορία για τον τρέχοντα χρήστη, αυτή πρέπει να μεταφέρεται μέσα από ένα κατάλληλο abstraction, όπου αυτό είναι αναγκαίο.&lt;/p&gt;

&lt;p&gt;Η επιλογή του μηχανισμού authorization εξαρτάται από το είδος του ελέγχου. Δεν είναι κάθε authorization rule κατάλληλο για ένα γενικό pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Transactions: Δεν τις αναλαμβάνει αυτόματα το CQRS
&lt;/h2&gt;

&lt;p&gt;Ένα από τα πιο συνηθισμένα λάθη είναι να θεωρούμε ότι, επειδή έχουμε έναν handler ανά Command, έχουμε αυτόματα και σωστή διαχείριση transactions.&lt;/p&gt;

&lt;p&gt;Δεν ισχύει.&lt;/p&gt;

&lt;p&gt;Το CQRS δεν ορίζει πώς θα διαχειριστούμε τις συναλλαγές στη βάση δεδομένων.&lt;/p&gt;

&lt;p&gt;Στο απλό παράδειγμά μας, το &lt;code&gt;SaveChangesAsync()&lt;/code&gt; του EF Core αποθηκεύει τις αλλαγές και, για τις συνηθισμένες σχετικές λειτουργίες που εκτελούνται σε μία κλήση, το EF Core παρέχει transaction behavior όταν υποστηρίζεται από τον provider.&lt;/p&gt;

&lt;p&gt;Αν όμως μια περίπτωση χρήσης περιλαμβάνει πολλές ξεχωριστές ενέργειες αποθήκευσης, πολλαπλά resources ή εξωτερικά συστήματα, πρέπει να σχεδιάσουμε συνειδητά τα όρια της συναλλαγής.&lt;/p&gt;

&lt;p&gt;Δεν θα πρόσθετα transaction decorator μόνο και μόνο επειδή υπάρχει CQRS.&lt;/p&gt;

&lt;p&gt;Θα τον πρόσθετα όταν το σύστημα έχει συγκεκριμένες ανάγκες διαχείρισης συναλλαγών που δεν καλύπτονται επαρκώς από το υπάρχον persistence layer.&lt;/p&gt;

&lt;p&gt;Αν μια λειτουργία ενημερώνει τη βάση και στη συνέχεια στέλνει μήνυμα σε εξωτερικό broker, πρέπει επίσης να σκεφτούμε τι θα συμβεί αν η μία ενέργεια επιτύχει και η άλλη αποτύχει. Σε τέτοιες περιπτώσεις, τεχνικές όπως το transactional outbox μπορεί να είναι καταλληλότερες από την προσπάθεια να δημιουργήσουμε μια γενική transaction abstraction για κάθε πιθανή περίπτωση.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Πότε χρειάζεται custom dispatcher και πότε όχι;
&lt;/h2&gt;

&lt;p&gt;Ας συνοψίσουμε τη σχεδιαστική απόφαση.&lt;/p&gt;

&lt;h3&gt;
  
  
  Επιλογή Α: Άμεσο injection των handlers
&lt;/h3&gt;

&lt;p&gt;Ο controller εξαρτάται από το αντίστοιχο handler interface.&lt;/p&gt;

&lt;p&gt;Πλεονεκτήματα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Λιγότερος κώδικας υποδομής.&lt;/li&gt;
&lt;li&gt;Strong typing σε compile time.&lt;/li&gt;
&lt;li&gt;Εύκολη μετάβαση στον πραγματικό handler μέσω του debugger.&lt;/li&gt;
&lt;li&gt;Δεν απαιτείται runtime routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Μειονεκτήματα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ο caller γνωρίζει τον τύπο του handler που χρειάζεται.&lt;/li&gt;
&lt;li&gt;Δεν υπάρχει αυτόματα ενιαίο σημείο δρομολόγησης για όλα τα requests.&lt;/li&gt;
&lt;li&gt;Η εφαρμογή κοινών behaviors χρειάζεται ξεχωριστό σχεδιασμό.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Επιλογή Β: Custom dispatcher
&lt;/h3&gt;

&lt;p&gt;Ο caller στέλνει το Command ή το Query σε ένα κοινό abstraction.&lt;/p&gt;

&lt;p&gt;Πλεονεκτήματα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ενιαίο σημείο εισόδου για την εκτέλεση requests.&lt;/li&gt;
&lt;li&gt;Δυνατότητα κεντρικής σύνθεσης της ροής εκτέλεσης.&lt;/li&gt;
&lt;li&gt;Μπορεί να είναι χρήσιμο σε generic infrastructure, background processing ή συστήματα που χρειάζονται runtime routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Μειονεκτήματα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Περισσότερος κώδικας υποδομής.&lt;/li&gt;
&lt;li&gt;Αν χρησιμοποιηθεί reflection, αυξάνεται η πολυπλοκότητα.&lt;/li&gt;
&lt;li&gt;Χρειάζεται προσεκτικός σχεδιασμός του error handling, των registrations και των pipeline behaviors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Για ένα τυπικό ASP.NET Core API, δεν θα θεωρούσα τον custom dispatcher υποχρεωτικό. Θα αξιολογούσα πρώτα αν η αφαίρεση που προσφέρει δικαιολογεί το κόστος της.&lt;/p&gt;

&lt;p&gt;Αν η εφαρμογή χρειάζεται dispatcher, θα φρόντιζα να είναι όσο το δυνατόν πιο απλός, χωρίς επιχειρησιακή λογική και χωρίς να μετατρέπεται σε service locator για ολόκληρη την εφαρμογή.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Unit testing: Ένα από τα πραγματικά πλεονεκτήματα
&lt;/h2&gt;

&lt;p&gt;Μια σωστά σχεδιασμένη υλοποίηση CQRS χωρίς Mediator μπορεί να είναι ιδιαίτερα εύκολη στη δοκιμή.&lt;/p&gt;

&lt;p&gt;Ο handler είναι μια απλή κλάση με σαφείς εξαρτήσεις και μία συγκεκριμένη ευθύνη.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, μπορούμε να γράψουμε ένα unit test για το &lt;code&gt;CreateEmployeeHandler&lt;/code&gt;, ελέγχοντας ότι μια έγκυρη ενέργεια δημιουργεί και αποθηκεύει τον εργαζόμενο.&lt;/p&gt;

&lt;p&gt;Το test μπορεί να χρησιμοποιήσει mock ή fake του persistence abstraction, ανάλογα με τη στρατηγική δοκιμών.&lt;/p&gt;

&lt;p&gt;Παράλληλα, χρειάζονται integration tests για να επαληθεύσουμε ότι το EF Core, οι mappings, τα constraints και η πραγματική βάση λειτουργούν σωστά.&lt;/p&gt;

&lt;p&gt;Δεν θα προσπαθούσα να αποδείξω τη σωστή λειτουργία της βάσης χρησιμοποιώντας αποκλειστικά mocks.&lt;/p&gt;

&lt;p&gt;Επίσης, αν έχουμε custom dispatcher, χρειάζονται tests που να επιβεβαιώνουν ότι κάθε request επιλύεται στον σωστό handler και ότι τα pipeline behaviors εκτελούνται με τη σωστή σειρά.&lt;/p&gt;

&lt;p&gt;Η απουσία MediatR δεν μειώνει από μόνη της την testability. Αυτή εξαρτάται κυρίως από τον σχεδιασμό των εξαρτήσεων και των συμβολαίων.&lt;/p&gt;




&lt;h2&gt;
  
  
  16. Συνηθισμένα αρχιτεκτονικά λάθη
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Λάθος 1: Θεωρούμε ότι CQRS σημαίνει δύο βάσεις δεδομένων
&lt;/h3&gt;

&lt;p&gt;Ο διαχωρισμός Commands και Queries μπορεί να υλοποιηθεί με μία κοινή βάση. Διαφορετικά read models ή ξεχωριστές βάσεις αποτελούν πρόσθετες επιλογές.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 2: Τοποθετούμε όλη την επιχειρησιακή λογική στους handlers
&lt;/h3&gt;

&lt;p&gt;Ο handler πρέπει κυρίως να συντονίζει τη συγκεκριμένη περίπτωση χρήσης. Οι ουσιαστικοί επιχειρησιακοί κανόνες πρέπει να βρίσκονται στο κατάλληλο Domain Model ή σε κατάλληλες domain services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 3: Δημιουργούμε dispatcher χωρίς πραγματική ανάγκη
&lt;/h3&gt;

&lt;p&gt;Αν ο caller γνωρίζει ήδη τον handler που χρειάζεται, το άμεσο injection μπορεί να είναι απλούστερο και επαρκές.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 4: Γράφουμε custom Mediator και τελικά ανακατασκευάζουμε ολόκληρο framework
&lt;/h3&gt;

&lt;p&gt;Ένας dispatcher, ένας μηχανισμός reflection, ένα σύστημα registration, decorators, validation pipelines, event publishing και transaction management μπορούν να εξελιχθούν σε σημαντική υποδομή.&lt;/p&gt;

&lt;p&gt;Αυτό δεν είναι απαραίτητα λάθος, αλλά πρέπει να δικαιολογείται από τις απαιτήσεις του συστήματος. Δεν χρειάζεται να υλοποιούμε κάθε πιθανή δυνατότητα πριν υπάρξει πραγματική ανάγκη.&lt;/p&gt;

&lt;h3&gt;
  
  
  Λάθος 5: Θεωρούμε ότι η απουσία MediatR εξασφαλίζει καλύτερη αρχιτεκτονική
&lt;/h3&gt;

&lt;p&gt;Η ποιότητα της αρχιτεκτονικής δεν καθορίζεται από το αν χρησιμοποιούμε εξωτερική βιβλιοθήκη.&lt;/p&gt;

&lt;p&gt;Καθορίζεται από τα όρια των εξαρτήσεων, τη σαφήνεια των contracts, τη διαχείριση των επιχειρησιακών κανόνων και τη δυνατότητα εξέλιξης του συστήματος.&lt;/p&gt;




&lt;h2&gt;
  
  
  17. Τι θα επέλεγα σε ένα πραγματικό production project;
&lt;/h2&gt;

&lt;p&gt;Για μια εφαρμογή που ακολουθεί Clean Architecture και έχει σαφή διαχωρισμό Commands και Queries, θα ξεκινούσα από μια μικρή, ρητή υλοποίηση.&lt;/p&gt;

&lt;p&gt;Θα δημιουργούσα τα contracts &lt;code&gt;ICommand&amp;lt;TResult&amp;gt;&lt;/code&gt;, &lt;code&gt;IQuery&amp;lt;TResult&amp;gt;&lt;/code&gt;, &lt;code&gt;ICommandHandler&amp;lt;TCommand, TResult&amp;gt;&lt;/code&gt; και &lt;code&gt;IQueryHandler&amp;lt;TQuery, TResult&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Θα διατηρούσα κάθε handler υπεύθυνο για μία συγκεκριμένη περίπτωση χρήσης και θα χρησιμοποιούσα Dependency Injection για να συνδέσω τις υλοποιήσεις με τα interfaces τους.&lt;/p&gt;

&lt;p&gt;Θα επέλεγα απευθείας injection όταν αυτό επαρκεί. Θα πρόσθετα custom dispatcher μόνο εφόσον υπήρχε σαφής ανάγκη για ενιαίο routing.&lt;/p&gt;

&lt;p&gt;Για τα cross-cutting concerns θα εξέταζα decorators, validation abstractions και κατάλληλες υπηρεσίες του ASP.NET Core. Δεν θα συγκέντρωνα όλες τις ευθύνες σε έναν κεντρικό dispatcher.&lt;/p&gt;

&lt;p&gt;Τέλος, θα διασφάλιζα ότι οι handlers παραμένουν testable, ότι τα transactions έχουν σαφή όρια και ότι το Domain δεν εξαρτάται από τεχνικές λεπτομέρειες του Infrastructure.&lt;/p&gt;

&lt;p&gt;Η προσέγγιση αυτή δεν είναι η μοναδική σωστή λύση. Είναι, όμως, μια συνειδητή αρχιτεκτονική επιλογή που μπορεί να προσφέρει έλεγχο και σαφήνεια χωρίς να εισάγει εξαρτήσεις ή πολυπλοκότητα χωρίς συγκεκριμένο όφελος.&lt;/p&gt;

&lt;h2&gt;
  
  
  Συμπέρασμα
&lt;/h2&gt;

&lt;p&gt;Το CQRS δεν είναι συνώνυμο του MediatR. Είναι μια προσέγγιση που διαχωρίζει τις λειτουργίες ανάγνωσης από τις λειτουργίες μεταβολής της κατάστασης του συστήματος.&lt;/p&gt;

&lt;p&gt;Μπορούμε να το υλοποιήσουμε με custom interfaces, handlers και το ενσωματωμένο Dependency Injection του .NET. Μπορούμε επίσης να προσθέσουμε dispatcher και pipeline behaviors όταν οι απαιτήσεις της εφαρμογής το δικαιολογούν.&lt;/p&gt;

&lt;p&gt;Το κρίσιμο σημείο είναι να γνωρίζουμε τι προσφέρει κάθε abstraction και ποιο πρόβλημα λύνει.&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται να δημιουργήσουμε έναν custom Mediator μόνο και μόνο για να πούμε ότι έχουμε CQRS. Χρειάζεται να σχεδιάσουμε σωστά τις εξαρτήσεις, να διαχωρίσουμε τις ευθύνες και να διασφαλίσουμε ότι το σύστημα παραμένει εύκολο στη συντήρηση και στην επέκταση.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Η αρχιτεκτονική ποιότητα δεν προκύπτει από τον αριθμό των abstractions ή των βιβλιοθηκών που χρησιμοποιούμε. Προκύπτει από το αν οι σχεδιαστικές επιλογές μας λύνουν πραγματικά προβλήματα, διατηρώντας τον κώδικα κατανοητό και ελεγχόμενο.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Πηγές και περαιτέρω μελέτη
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Microsoft Learn — &lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/microservice-application-layer-implementation-web-api" rel="noopener noreferrer"&gt;Implementing the microservice application layer using the Web API&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Εξηγεί την υλοποίηση Commands και Handlers, τη χρήση του Dependency Injection και τον ρόλο του Mediator και των pipeline behaviors.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Microsoft Learn — &lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice" rel="noopener noreferrer"&gt;Designing a DDD-oriented microservice&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Αναλύει τον διαχωρισμό Domain, Application και Infrastructure και τον ρόλο της επιχειρησιακής λογικής στο Domain Model.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Adrián Bailador — &lt;a href="https://dev.to/adrianbailador/cqrs-without-mediatr-hand-rolled-command-and-query-handlers-in-net-amm"&gt;CQRS Without MediatR: Hand-Rolled Command and Query Handlers in .NET&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Παρουσιάζει μια εναλλακτική προσέγγιση με custom contracts, dispatchers και pipeline behaviors.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitHub — &lt;a href="https://github.com/AdrianBailador/cqrs-without-mediatr-dotnet" rel="noopener noreferrer"&gt;CQRS Without MediatR: Sample implementation&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Παράδειγμα υλοποίησης σε .NET με custom handlers, Dependency Injection και decorators.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Repository Pattern στο .NET: Είναι πραγματικά ξεπερασμένο επειδή το Entity Framework Core το υλοποιεί ήδη;</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Fri, 09 Oct 2026 19:09:25 +0000</pubDate>
      <link>https://dev.to/__b63657/repository-pattern-sto-net-einai-pragmatika-xeperasmeno-epeide-to-entity-framework-core-to-406l</link>
      <guid>https://dev.to/__b63657/repository-pattern-sto-net-einai-pragmatika-xeperasmeno-epeide-to-entity-framework-core-to-406l</guid>
      <description>&lt;p&gt;&lt;strong&gt;Υπότιτλος:&lt;/strong&gt; Clean Architecture, Dependency Inversion, αρχιτεκτονικά όρια και οι πραγματικοί συμβιβασμοί πίσω από τα custom repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Εισαγωγή
&lt;/h2&gt;

&lt;p&gt;Κάθε τόσο επανέρχεται στις κοινότητες των .NET developers η ίδια συζήτηση:&lt;/p&gt;

&lt;p&gt;«Γιατί χρησιμοποιείς Repository Pattern; Το Entity Framework Core υλοποιεί ήδη Repository και Unit of Work. Η επιπλέον αφαίρεση είναι περιττή και ξεπερασμένη».&lt;/p&gt;

&lt;p&gt;Με την πρώτη ματιά, το επιχείρημα ακούγεται λογικό. Το Entity Framework Core παρέχει το &lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt; για την εργασία με οντότητες και το &lt;code&gt;DbContext&lt;/code&gt; για την παρακολούθηση αλλαγών και την αποθήκευσή τους. Γιατί, λοιπόν, να προσθέσουμε επιπλέον interfaces και κλάσεις που φαίνεται να κάνουν ακριβώς το ίδιο πράγμα;&lt;/p&gt;

&lt;p&gt;Ωστόσο, αυτή η συζήτηση συχνά συγχέει δύο διαφορετικά ερωτήματα:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Παρέχει το Entity Framework Core λειτουργίες αντίστοιχες με Repository και Unit of Work;&lt;/li&gt;
&lt;li&gt;Σημαίνει αυτό ότι ένα custom Repository δεν έχει αρχιτεκτονική αξία;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Στο πρώτο ερώτημα η απάντηση είναι ναι. Στο δεύτερο, όχι.&lt;/p&gt;

&lt;p&gt;Η ουσιαστική συζήτηση δεν αφορά το αν ένα pattern είναι παλιό ή σύγχρονο. Αφορά την κατεύθυνση των εξαρτήσεων, τα αρχιτεκτονικά όρια, τη συντηρησιμότητα, τη δυνατότητα δοκιμών και τους συμβιβασμούς που αποδεχόμαστε συνειδητά όταν σχεδιάζουμε μια εφαρμογή.&lt;/p&gt;

&lt;p&gt;Αν επιλέγουμε Clean Architecture, πρέπει να γνωρίζουμε γιατί την επιλέγουμε εξαρχής.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Η Clean Architecture αφορά την κατεύθυνση των εξαρτήσεων
&lt;/h2&gt;

&lt;p&gt;Η Clean Architecture δεν είναι απλώς μια δομή φακέλων με &lt;code&gt;Domain&lt;/code&gt;, &lt;code&gt;Application&lt;/code&gt;, &lt;code&gt;Infrastructure&lt;/code&gt; και &lt;code&gt;Presentation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Σκοπός της είναι να διατηρεί τους επιχειρησιακούς κανόνες ανεξάρτητους από τις εξωτερικές τεχνικές λεπτομέρειες, όπως οι βάσεις δεδομένων, τα frameworks, τα user interfaces και οι υπηρεσίες υποδομής.&lt;/p&gt;

&lt;p&gt;Ο Robert C. Martin περιγράφει αυτή την προσέγγιση στο άρθρο του &lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;The Clean Architecture&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ένας από τους σημαντικότερους κανόνες είναι ο &lt;strong&gt;Dependency Rule&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Οι εξαρτήσεις του πηγαίου κώδικα πρέπει να κατευθύνονται προς τα μέσα, προς τις πολιτικές υψηλότερου επιπέδου.&lt;/p&gt;

&lt;p&gt;Σε μια τυπική λύση .NET με Clean Architecture, οι εξαρτήσεις των projects μπορούν να αποτυπωθούν ως εξής:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    Presentation --&amp;gt; Application
    Application --&amp;gt; Domain
    Infrastructure --&amp;gt; Application
    Infrastructure --&amp;gt; Domain&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Το διάγραμμα παρουσιάζει τις εξαρτήσεις των projects και του πηγαίου κώδικα, όχι την κατεύθυνση των κλήσεων κατά την εκτέλεση.&lt;/p&gt;

&lt;p&gt;Οι ευθύνες των layers είναι οι εξής:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain:&lt;/strong&gt; Entities, Value Objects, domain services και επιχειρησιακοί κανόνες.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application:&lt;/strong&gt; Use cases, commands, queries, handlers και συμβόλαια που απαιτούν οι περιπτώσεις χρήσης.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; Entity Framework Core, πρόσβαση στη βάση δεδομένων, εξωτερικές υπηρεσίες και υλοποιήσεις των persistence contracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation:&lt;/strong&gt; Controllers, endpoints και το σημείο εισόδου της εφαρμογής.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Το Domain δεν πρέπει να εξαρτάται από το Application ή το Infrastructure. Το Application δεν πρέπει να εξαρτάται από τις υλοποιήσεις του Infrastructure. Αντίθετα, το Infrastructure μπορεί να εξαρτάται από abstractions που ορίζονται στα εσωτερικά layers.&lt;/p&gt;

&lt;p&gt;Εδώ ακριβώς αποκτά σημασία το Repository Pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dependency Inversion Principle: Το σημαντικότερο σημείο
&lt;/h2&gt;

&lt;p&gt;Το Dependency Inversion Principle (DIP) είναι μία από τις αρχές SOLID.&lt;/p&gt;

&lt;p&gt;Σύμφωνα με αυτή, τα high-level modules δεν πρέπει να εξαρτώνται άμεσα από low-level modules. Και τα δύο πρέπει να εξαρτώνται από abstractions. Οι αφαιρέσεις δεν πρέπει να εξαρτώνται από τις λεπτομέρειες υλοποίησης· οι υλοποιήσεις πρέπει να εξαρτώνται από τις αφαιρέσεις.&lt;/p&gt;

&lt;p&gt;Ας εξετάσουμε έναν handler που εξαρτάται άμεσα από το Entity Framework Core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateUserHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο κώδικας είναι λειτουργικός. Δεν είναι εγγενώς λανθασμένος.&lt;/p&gt;

&lt;p&gt;Ωστόσο, το application use case εξαρτάται άμεσα από το &lt;code&gt;AppDbContext&lt;/code&gt;, το οποίο αποτελεί μέρος της υλοποίησης του persistence layer.&lt;/p&gt;

&lt;p&gt;Επομένως, το Application project χρειάζεται αναφορά στο project ή assembly που περιέχει το συγκεκριμένο context και συνδέεται με την προσέγγιση persistence του EF Core.&lt;/p&gt;

&lt;p&gt;Αν ο αρχιτεκτονικός μας στόχος είναι να διατηρήσουμε το Application ανεξάρτητο από την τεχνολογία αποθήκευσης, αυτή η εξάρτηση αποτελεί έναν σημαντικό συμβιβασμό.&lt;/p&gt;

&lt;p&gt;Μπορούμε να εισαγάγουμε μια αφαίρεση:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το interface βρίσκεται στο Application, επειδή εκεί βρίσκεται το use case που το χρειάζεται.&lt;/p&gt;

&lt;p&gt;Η υλοποίηση βρίσκεται στο Infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο handler εξαρτάται πλέον από την αφαίρεση:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateUserHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IUserRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το παράδειγμα παραλείπει σκόπιμα τον συντονισμό της αποθήκευσης. Αυτός πρέπει να αντιμετωπίζεται από την επιλεγμένη στρατηγική Unit of Work ή transactions.&lt;/p&gt;

&lt;p&gt;Το σημαντικό σημείο είναι η σχέση των εξαρτήσεων:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;classDiagram
    class CreateUserHandler {
        +HandleAsync()
    }

    class IUserRepository {
        &amp;lt;&amp;lt;interface&amp;gt;&amp;gt;
        +Add(User)
    }

    class UserRepository {
        +Add(User)
    }

    CreateUserHandler --&amp;gt; IUserRepository : εξαρτάται από
    UserRepository ..|&amp;gt; IUserRepository : υλοποιεί&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Το Application ορίζει το συμβόλαιο που χρειάζεται. Το Infrastructure το υλοποιεί χρησιμοποιώντας EF Core.&lt;/p&gt;

&lt;p&gt;Αυτή είναι εφαρμογή του Dependency Inversion στο persistence layer.&lt;/p&gt;

&lt;p&gt;Το Repository Pattern δεν είναι ο μοναδικός τρόπος εφαρμογής του DIP. Είναι, όμως, ένας καθιερωμένος τρόπος δημιουργίας ενός σαφούς ορίου μεταξύ της εφαρμογής και της πρόσβασης στα δεδομένα.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. «Μα το Entity Framework Core υλοποιεί ήδη Repository και Unit of Work»
&lt;/h2&gt;

&lt;p&gt;Αυτό είναι το ισχυρότερο επιχείρημα εναντίον των custom repositories και αξίζει να εξεταστεί δίκαια.&lt;/p&gt;

&lt;p&gt;Το Entity Framework Core παρέχει ήδη abstractions με συμπεριφορά αντίστοιχη του Repository και του Unit of Work.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το &lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt; παρέχει πρόσβαση σε σύνολα οντοτήτων και υποστηρίζει την ανάκτηση και τροποποίησή τους.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;DbContext&lt;/code&gt; παρακολουθεί τις αλλαγές και συντονίζει την αποθήκευσή τους μέσω του &lt;code&gt;SaveChangesAsync()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Για πολλές εφαρμογές, αυτό είναι απολύτως επαρκές.&lt;/p&gt;

&lt;p&gt;Ένα custom Repository που απλώς περιτυλίγει κάθε μέθοδο του &lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt; μπορεί να προσθέτει boilerplate χωρίς ουσιαστικό διαχωρισμό.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό το interface δεν είναι αυτομάτως κακό. Αν, όμως, απλώς αναπαράγει τις λειτουργίες του EF Core και εκθέτει παντού &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt;, ενδέχεται να προσφέρει ελάχιστη προστασία από την εξάρτηση της εφαρμογής από το persistence layer.&lt;/p&gt;

&lt;p&gt;Το σωστό συμπέρασμα δεν είναι ότι τα repositories έχουν ξεπεραστεί.&lt;/p&gt;

&lt;p&gt;Είναι ότι &lt;strong&gt;ένα Repository πρέπει να δικαιολογεί την ύπαρξή του μέσω του αρχιτεκτονικού ορίου ή της εξειδικευμένης συμπεριφοράς που προσφέρει&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Η Microsoft εξετάζει τόσο την προσέγγιση με repositories όσο και την απευθείας χρήση του &lt;code&gt;DbContext&lt;/code&gt; στην τεκμηρίωση &lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core" rel="noopener noreferrer"&gt;Implementing the persistence layer with Entity Framework Core&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Η τεκμηρίωση αναγνωρίζει ότι το EF Core παρέχει ήδη συμπεριφορά Repository και Unit of Work, ενώ παράλληλα εξηγεί γιατί τα custom repositories μπορούν να παραμένουν χρήσιμα, ιδιαίτερα σε πιο σύνθετες εφαρμογές.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generic Repository ή Domain-Specific Repository;
&lt;/h2&gt;

&lt;p&gt;Δεν προσφέρουν όλα τα repositories τον ίδιο βαθμό αφαίρεσης.&lt;/p&gt;

&lt;p&gt;Ένα generic repository μπορεί να παρέχει τυπικές λειτουργίες CRUD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό μπορεί να είναι χρήσιμο όταν οι λειτουργίες είναι πράγματι κοινές μεταξύ των οντοτήτων.&lt;/p&gt;

&lt;p&gt;Ωστόσο, τα επιχειρησιακά domains σπάνια περιορίζονται σε απλές CRUD λειτουργίες.&lt;/p&gt;

&lt;p&gt;Ας εξετάσουμε ένα σύστημα διαχείρισης χρηστών.&lt;/p&gt;

&lt;p&gt;Μια λειτουργία μπορεί να χρειάζεται να ελέγξει αν υπάρχει ήδη ένα email, να ανακτήσει έναν χρήστη μαζί με τα σχετικά permissions ή να φορτώσει ένα aggregate για μια επιχειρησιακή ενέργεια.&lt;/p&gt;

&lt;p&gt;Ένα domain-specific repository μπορεί να εκφράζει απευθείας αυτές τις ανάγκες:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EmailExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η υλοποίηση μπορεί να χρησιμοποιεί τα καταλληλότερα queries του EF Core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EmailExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Application εκφράζει τι χρειάζεται, χωρίς να γνωρίζει πώς υλοποιείται το query στη βάση δεδομένων.&lt;/p&gt;

&lt;p&gt;Υπάρχει και μια δεύτερη σημαντική διάκριση: δεν είναι απαραίτητο να δημιουργούμε διαφορετικό Repository για κάθε use case.&lt;/p&gt;

&lt;p&gt;Τα &lt;code&gt;CreateUser&lt;/code&gt;, &lt;code&gt;UpdateUser&lt;/code&gt; και &lt;code&gt;GetUser&lt;/code&gt; μπορούν να χρησιμοποιούν το ίδιο &lt;code&gt;IUserRepository&lt;/code&gt;, εφόσον εξυπηρετεί τις ανάγκες τους.&lt;/p&gt;

&lt;p&gt;Σε εφαρμογές CQRS, οι command handlers μπορούν να χρησιμοποιούν repositories για την ανάκτηση και τροποποίηση aggregates, ενώ οι read handlers μπορούν να χρησιμοποιούν απευθείας projections του EF Core σε DTOs, εφόσον η αρχιτεκτονική επιτρέπει συνειδητά αυτή την εξάρτηση.&lt;/p&gt;

&lt;p&gt;Στόχος δεν είναι να μεγιστοποιήσουμε τον αριθμό των repositories. Στόχος είναι να δημιουργήσουμε χρήσιμα και σαφή όρια.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Πού πρέπει να βρίσκονται τα Repository Interfaces;
&lt;/h2&gt;

&lt;p&gt;Αυτό το ερώτημα συζητείται συχνά στις υλοποιήσεις Clean Architecture.&lt;/p&gt;

&lt;p&gt;Δεν υπάρχει καθολικός κανόνας ότι κάθε repository interface πρέπει να ανήκει στο Domain project.&lt;/p&gt;

&lt;p&gt;Η σωστή θέση εξαρτάται από την ευθύνη που εκφράζει το interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Επιλογή Α: Interfaces στο Application
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application/
    Users/
        IUserRepository.cs
        CreateUser/
            CreateUserCommand.cs
            CreateUserHandler.cs

Infrastructure/
    Persistence/
        UserRepository.cs
        AppDbContext.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτός είναι ένας έγκυρος σχεδιασμός όταν το repository εκφράζει ένα συμβόλαιο πρόσβασης στα δεδομένα που απαιτούν τα application use cases.&lt;/p&gt;

&lt;p&gt;Το Application ορίζει το συμβόλαιο και το Infrastructure παρέχει την υλοποίησή του.&lt;/p&gt;

&lt;p&gt;Η προσέγγιση αυτή είναι ιδιαίτερα πρακτική σε εφαρμογές που οργανώνουν τη λειτουργικότητά τους ανά use case ή feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Επιλογή Β: Interfaces στο Domain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Domain/
    Users/
        User.cs
        IUserRepository.cs

Infrastructure/
    Persistence/
        UserRepository.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Και αυτή είναι έγκυρη επιλογή όταν το συμβόλαιο του repository θεωρείται μέρος του domain model, συχνά σε μια προσέγγιση Domain-Driven Design που οργανώνεται γύρω από aggregate roots.&lt;/p&gt;

&lt;p&gt;Η ουσιαστική διάκριση αφορά την ιδιοκτησία του συμβολαίου: ποιο layer χρειάζεται την αφαίρεση και ποιο πρέπει να την ορίζει;&lt;/p&gt;

&lt;p&gt;Η θέση του interface πρέπει να αντικατοπτρίζει αυτή την απόφαση, αντί να ακολουθεί έναν κανόνα σύμφωνα με τον οποίο όλα τα repositories πρέπει να βρίσκονται στο ίδιο project ανεξάρτητα από τον σκοπό τους.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Επιβάλλει η Clean Architecture τη χρήση custom repositories;
&lt;/h2&gt;

&lt;p&gt;Όχι. Η Clean Architecture απαιτεί να τηρούνται τα όρια των εξαρτήσεων· δεν επιβάλλει μία συγκεκριμένη υλοποίηση Repository.&lt;/p&gt;

&lt;p&gt;Αυτή η διάκριση είναι σημαντική.&lt;/p&gt;

&lt;p&gt;Ας εξετάσουμε δύο διαφορετικές αρχιτεκτονικές.&lt;/p&gt;

&lt;h3&gt;
  
  
  Αρχιτεκτονική Α: Απευθείας χρήση EF Core
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    └── CreateUserHandler
            └── AppDbContext

Infrastructure
    └── EF Core configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτή μπορεί να είναι λογική επιλογή για μια σχετικά απλή εφαρμογή, ιδιαίτερα όταν η ομάδα αποδέχεται συνειδητά την εξάρτηση του Application από το EF Core.&lt;/p&gt;

&lt;p&gt;Μειώνει τον αριθμό των abstractions και των κλάσεων.&lt;/p&gt;

&lt;p&gt;Ωστόσο, το Application συνδέεται πλέον με το persistence framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Αρχιτεκτονική Β: Custom Repository abstraction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ├── CreateUserHandler
    └── IUserRepository

Infrastructure
    ├── UserRepository
    └── AppDbContext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτός ο σχεδιασμός εισάγει επιπλέον κώδικα και ένα επίπεδο έμμεσης πρόσβασης.&lt;/p&gt;

&lt;p&gt;Ανταλλάσσει αυτή την πολυπλοκότητα με ένα συγκεκριμένο όφελος: το Application εξαρτάται από ένα συμβόλαιο που μπορεί να υλοποιηθεί ανεξάρτητα από το EF Core.&lt;/p&gt;

&lt;p&gt;Το αν αυτή η επιπλέον απομόνωση αξίζει το κόστος εξαρτάται από τις απαιτήσεις του project.&lt;/p&gt;

&lt;p&gt;Αν ο δηλωμένος στόχος είναι να παραμείνει το Application ανεξάρτητο από την τεχνολογία persistence, η δεύτερη αρχιτεκτονική εκφράζει πιο άμεσα αυτόν τον στόχο.&lt;/p&gt;

&lt;p&gt;Αν ο στόχος είναι η ελαχιστοποίηση των abstractions σε μια μικρή εφαρμογή, η πρώτη επιλογή μπορεί να είναι προτιμότερη.&lt;/p&gt;

&lt;p&gt;Πρόκειται για διαφορετικούς συμβιβασμούς, όχι για απόδειξη ότι ένα pattern έχει ξεπεραστεί.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Testing: Αρχιτεκτονικό όφελος, όχι μαγική λύση
&lt;/h2&gt;

&lt;p&gt;Τα custom repositories μπορούν να διευκολύνουν τις δοκιμές των application use cases χωρίς σύνδεση σε production database.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, ένας handler που εξαρτάται από το &lt;code&gt;IUserRepository&lt;/code&gt; μπορεί να δοκιμαστεί με μια fake υλοποίηση ή ένα mock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeUserRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_users&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EmailExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Έτσι, μπορούμε να ελέγξουμε τη συμπεριφορά του Application ανεξάρτητα από την υποδομή της βάσης δεδομένων.&lt;/p&gt;

&lt;p&gt;Ωστόσο, ένα fake repository δεν επιβεβαιώνει ότι η πραγματική υλοποίηση EF Core παράγει το σωστό query ή συμπεριφέρεται σωστά με τον επιλεγμένο database provider.&lt;/p&gt;

&lt;p&gt;Για αυτές τις περιπτώσεις εξακολουθούν να απαιτούνται integration tests.&lt;/p&gt;

&lt;p&gt;Η Microsoft εξηγεί αυτούς τους συμβιβασμούς στην τεκμηρίωσή της για &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/choosing-a-testing-strategy" rel="noopener noreferrer"&gt;την επιλογή στρατηγικής testing για το EF Core&lt;/a&gt; και για &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/testing-without-the-database" rel="noopener noreferrer"&gt;testing χωρίς την production database&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Το συμπέρασμα είναι απλό: τα repositories μπορούν να βελτιώσουν την απομόνωση των tests, αλλά δεν καταργούν την ανάγκη ελέγχου της συμπεριφοράς του persistence layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Τι γίνεται με το Unit of Work και τα transactions;
&lt;/h2&gt;

&lt;p&gt;Οι συζητήσεις για τα repositories συχνά οδηγούν σε μια ακόμη παρανόηση: ότι η προσθήκη ενός Repository λύνει αυτομάτως τη διαχείριση συναλλαγών.&lt;/p&gt;

&lt;p&gt;Δεν ισχύει κάτι τέτοιο.&lt;/p&gt;

&lt;p&gt;Το &lt;code&gt;DbContext&lt;/code&gt; του EF Core παρέχει ήδη συμπεριφορά Unit of Work. Το &lt;code&gt;SaveChangesAsync()&lt;/code&gt; αποθηκεύει τις αλλαγές που παρακολουθεί το context και, στις συνήθεις περιπτώσεις όπου υποστηρίζονται συναλλαγές από τον provider, τις εκτελεί ατομικά μέσα σε transaction.&lt;/p&gt;

&lt;p&gt;Ένα custom repository μπορεί να προσθέτει οντότητες στο context χωρίς να κάνει άμεση αποθήκευση:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό μπορεί να κάνει πιο σαφή τη διαχείριση της αποθήκευσης και να συντονίσει πολλαπλά repositories στο πλαίσιο ενός Unit of Work.&lt;/p&gt;

&lt;p&gt;Ωστόσο, ένα ξεχωριστό &lt;code&gt;IUnitOfWork&lt;/code&gt; δεν είναι υποχρεωτικό απλώς και μόνο επειδή υπάρχουν repositories. Η αξία του εξαρτάται από το αν εκφράζει ένα χρήσιμο συμβόλαιο και από τον τρόπο που η εφαρμογή διαχειρίζεται την αποθήκευση και τα transactions.&lt;/p&gt;

&lt;p&gt;Το σημαντικό είναι να γνωρίζουμε πού γίνεται το commit των αλλαγών και αν το συγκεκριμένο use case απαιτεί ατομικότητα.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Η ευθύνη του Architect: Να εξηγεί τους συμβιβασμούς
&lt;/h2&gt;

&lt;p&gt;Ένας senior developer ή software architect πρέπει να μπορεί να εξηγήσει τις συνέπειες μιας σχεδιαστικής απόφασης και όχι απλώς να χαρακτηρίζει ένα pattern παλιό ή σύγχρονο.&lt;/p&gt;

&lt;p&gt;Μια ουσιαστική αρχιτεκτονική αξιολόγηση πρέπει να εξετάζει:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Εξαρτάται το Application από υλοποιήσεις του Infrastructure;&lt;/li&gt;
&lt;li&gt;Εκφράζουν τα interfaces πραγματικές ανάγκες του Application ή του Domain;&lt;/li&gt;
&lt;li&gt;Κρύβουν τα repositories τις λεπτομέρειες του persistence ή απλώς αναπαράγουν το &lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Είναι σαφείς οι ευθύνες των queries;&lt;/li&gt;
&lt;li&gt;Μπορεί η ομάδα να δοκιμάσει τη συμπεριφορά του Application χωρίς να συνδέει αναγκαστικά κάθε test με τη βάση δεδομένων;&lt;/li&gt;
&lt;li&gt;Δικαιολογείται το πρόσθετο κόστος συντήρησης των abstractions;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Αυτά τα ερωτήματα οδηγούν σε μια πολύ πιο χρήσιμη τεχνική συζήτηση από το αν το Repository Pattern είναι ακόμη στη μόδα.&lt;/p&gt;

&lt;p&gt;Η ίδια αρχή ισχύει για πολλές αρχιτεκτονικές αποφάσεις. Ένα pattern δεν είναι πολύτιμο απλώς επειδή έχει συγκεκριμένο όνομα. Ούτε είναι άχρηστο επειδή ένα framework παρέχει παρόμοια λειτουργικότητα.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Συμπεράσματα
&lt;/h2&gt;

&lt;p&gt;Το Entity Framework Core παρέχει ήδη λειτουργίες αντίστοιχες με Repository και Unit of Work. Αυτό το γεγονός πρέπει να επηρεάζει τις σχεδιαστικές μας αποφάσεις.&lt;/p&gt;

&lt;p&gt;Δεν πρέπει, όμως, να τερματίζει τη συζήτηση.&lt;/p&gt;

&lt;p&gt;Όταν επιλέγουμε Clean Architecture, πρέπει να κατανοούμε τα αρχιτεκτονικά όρια που θέλουμε να διατηρήσουμε. Η κατεύθυνση των εξαρτήσεων είναι θεμελιώδης για αυτόν τον σχεδιασμό και το Dependency Inversion είναι μία από τις αρχές που βοηθούν στη διατήρησή του.&lt;/p&gt;

&lt;p&gt;Ένα custom Repository δεν είναι υποχρεωτικό σε κάθε .NET εφαρμογή. Ένα generic repository που δεν προσφέρει ουσιαστική απομόνωση μπορεί να μετατραπεί σε περιττό boilerplate.&lt;/p&gt;

&lt;p&gt;Ένα προσεκτικά σχεδιασμένο Repository, όμως, μπορεί να παρέχει σαφές συμβόλαιο πρόσβασης στα δεδομένα, να προστατεύει το Application από εξαρτήσεις στο EF Core, να υποστηρίζει στοχευμένα tests και να διευκολύνει τη μελλοντική εξέλιξη του συστήματος.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Το ερώτημα δεν είναι αν το Repository Pattern έχει ξεπεραστεί. Το ερώτημα είναι αν η αφαίρεση εξυπηρετεί έναν σαφή αρχιτεκτονικό σκοπό στο σύστημα που κατασκευάζουμε.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αυτή είναι η διαφορά ανάμεσα στην τυφλή εφαρμογή ενός pattern και στην τεκμηριωμένη αρχιτεκτονική απόφαση.&lt;/p&gt;

&lt;h3&gt;
  
  
  Πηγές
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Robert C. Martin, &lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;The Clean Architecture&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Microsoft, &lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core" rel="noopener noreferrer"&gt;Implementing the persistence layer with Entity Framework Core&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Microsoft, &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/choosing-a-testing-strategy" rel="noopener noreferrer"&gt;Choosing a testing strategy&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Microsoft, &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/testing-without-the-database" rel="noopener noreferrer"&gt;Testing without your production database system&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Repository Pattern in .NET: Is It Really Obsolete Because Entity Framework Core Already Implements It?</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Fri, 09 Oct 2026 18:55:46 +0000</pubDate>
      <link>https://dev.to/__b63657/repository-pattern-in-net-is-it-really-obsolete-because-entity-framework-core-already-implements-1g8l</link>
      <guid>https://dev.to/__b63657/repository-pattern-in-net-is-it-really-obsolete-because-entity-framework-core-already-implements-1g8l</guid>
      <description>&lt;p&gt;Understanding Clean Architecture, Dependency Inversion, persistence boundaries, and the real trade-offs behind custom repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every few years, the same discussion appears in .NET development communities:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Why are you using the Repository Pattern? Entity Framework Core already implements a Repository and Unit of Work. Your abstraction is unnecessary and outdated."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first glance, this argument sounds reasonable. Entity Framework Core provides &lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt; for working with entities and &lt;code&gt;DbContext&lt;/code&gt; for tracking changes and persisting them. Why introduce additional interfaces and classes that appear to do the same thing?&lt;/p&gt;

&lt;p&gt;However, this discussion often mixes two different questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does Entity Framework Core provide repository-like and unit-of-work capabilities?&lt;/li&gt;
&lt;li&gt;Does that mean a custom repository has no architectural value?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answer to the first question is yes. The answer to the second is no.&lt;/p&gt;

&lt;p&gt;The real discussion is not about whether a pattern is old or new. It is about dependency direction, architectural boundaries, maintainability, testability, and the trade-offs we consciously accept when designing an application.&lt;/p&gt;

&lt;p&gt;If we choose Clean Architecture, we should understand why we chose it in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Clean Architecture Is About Dependency Direction
&lt;/h2&gt;

&lt;p&gt;Clean Architecture is not simply a folder structure containing &lt;code&gt;Domain&lt;/code&gt;, &lt;code&gt;Application&lt;/code&gt;, &lt;code&gt;Infrastructure&lt;/code&gt;, and &lt;code&gt;Presentation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its purpose is to keep business rules independent of external technical details, such as databases, frameworks, user interfaces, and infrastructure services.&lt;/p&gt;

&lt;p&gt;Robert C. Martin describes this principle in his article, &lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;The Clean Architecture&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One of the most important rules is the &lt;strong&gt;Dependency Rule&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Source code dependencies must point inward, toward higher-level policies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a typical .NET Clean Architecture solution, the dependency relationships look like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    Presentation --&amp;gt; Application
    Application --&amp;gt; Domain
    Infrastructure --&amp;gt; Application
    Infrastructure --&amp;gt; Domain&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This diagram represents project and source-code dependencies, not the direction of runtime calls.&lt;/p&gt;

&lt;p&gt;The responsibilities are straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain:&lt;/strong&gt; Entities, value objects, domain services, and business rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application:&lt;/strong&gt; Use cases, commands, queries, handlers, and application-level contracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; Entity Framework Core, database access, external services, and implementations of persistence contracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation:&lt;/strong&gt; Controllers, endpoints, and the user-facing entry point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Domain should not depend on the Application or Infrastructure. The Application should not depend on Infrastructure implementations. Infrastructure can depend on the abstractions defined by the inner layers.&lt;/p&gt;

&lt;p&gt;This is where the Repository Pattern becomes relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dependency Inversion Principle: The Important Part
&lt;/h2&gt;

&lt;p&gt;The Dependency Inversion Principle (DIP) is one of the SOLID principles.&lt;/p&gt;

&lt;p&gt;It states that high-level modules should not depend directly on low-level modules. Both should depend on abstractions. Those abstractions should not depend on implementation details; implementations should depend on abstractions.&lt;/p&gt;

&lt;p&gt;Consider an application handler that directly depends on Entity Framework Core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateUserHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is perfectly functional. It is not inherently incorrect C#.&lt;/p&gt;

&lt;p&gt;However, the application use case directly depends on &lt;code&gt;AppDbContext&lt;/code&gt;, which belongs to the persistence implementation.&lt;/p&gt;

&lt;p&gt;Consequently, the Application project needs a reference to the project or assembly containing that context, and it becomes coupled to the EF Core persistence approach.&lt;/p&gt;

&lt;p&gt;If our architectural goal is to keep Application independent of the persistence technology, this is an important trade-off.&lt;/p&gt;

&lt;p&gt;We can introduce an abstraction instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interface belongs to the Application layer, where the use case needs it.&lt;/p&gt;

&lt;p&gt;The implementation belongs to Infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler now depends on the abstraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateUserHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IUserRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example intentionally omits persistence coordination, which must be handled by the application's chosen unit-of-work or transaction strategy.&lt;/p&gt;

&lt;p&gt;The important point is the dependency relationship:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;classDiagram
    class CreateUserHandler {
        +HandleAsync()
    }

    class IUserRepository {
        &amp;lt;&amp;lt;interface&amp;gt;&amp;gt;
        +Add(User)
    }

    class UserRepository {
        +Add(User)
    }

    CreateUserHandler --&amp;gt; IUserRepository : depends on
    UserRepository ..|&amp;gt; IUserRepository : implements&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The Application layer defines the contract it requires. Infrastructure implements that contract using EF Core.&lt;/p&gt;

&lt;p&gt;This is dependency inversion applied to persistence.&lt;/p&gt;

&lt;p&gt;The Repository Pattern is not the only way to achieve dependency inversion. However, it is a well-established way to create a persistence boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "But Entity Framework Core Already Implements Repository and Unit of Work"
&lt;/h2&gt;

&lt;p&gt;This is the strongest argument against introducing custom repositories, and it deserves a fair explanation.&lt;/p&gt;

&lt;p&gt;Entity Framework Core already provides abstractions with repository-like and unit-of-work behavior.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt; provides access to entity sets and supports querying and modifying entities.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DbContext&lt;/code&gt; tracks changes and coordinates their persistence through &lt;code&gt;SaveChangesAsync()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For many applications, this is entirely sufficient.&lt;/p&gt;

&lt;p&gt;A custom repository that merely wraps every &lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt; method may add boilerplate without providing meaningful separation.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This interface is not automatically bad. However, if it simply reproduces EF Core operations and exposes &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt; everywhere, the abstraction may provide little protection against persistence coupling.&lt;/p&gt;

&lt;p&gt;The correct conclusion is not that repositories are obsolete.&lt;/p&gt;

&lt;p&gt;It is that &lt;strong&gt;a repository should justify its existence by the architectural boundary or domain-specific behavior it provides&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Microsoft discusses both the repository approach and direct use of &lt;code&gt;DbContext&lt;/code&gt; in its documentation on &lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core" rel="noopener noreferrer"&gt;implementing the persistence layer with Entity Framework Core&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The documentation recognizes that EF Core already implements repository and unit-of-work capabilities while explaining why custom repositories can still be valuable, particularly in more complex applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generic Repositories Versus Domain-Specific Repositories
&lt;/h2&gt;

&lt;p&gt;Not all repositories provide the same level of abstraction.&lt;/p&gt;

&lt;p&gt;A generic repository might expose standard CRUD operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may be useful when the operations are genuinely common across entities.&lt;/p&gt;

&lt;p&gt;However, business domains rarely remain limited to simple CRUD operations.&lt;/p&gt;

&lt;p&gt;Consider a user management system.&lt;/p&gt;

&lt;p&gt;A user-related operation might need to determine whether an email address already exists, retrieve a user together with relevant permissions, or load an aggregate for a business operation.&lt;/p&gt;

&lt;p&gt;A domain-specific repository can express these requirements directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EmailExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation can use the most appropriate EF Core queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EmailExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Application layer expresses what it needs without knowing how the database query is implemented.&lt;/p&gt;

&lt;p&gt;There is another important distinction: a repository should not necessarily be created for every use case.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CreateUser&lt;/code&gt;, &lt;code&gt;UpdateUser&lt;/code&gt;, and &lt;code&gt;GetUser&lt;/code&gt; can use the same &lt;code&gt;IUserRepository&lt;/code&gt; if they need the same persistence boundary.&lt;/p&gt;

&lt;p&gt;For CQRS applications, command handlers may use repositories to load and modify aggregates, while read handlers may use direct EF Core projections into DTOs if the architecture deliberately allows that dependency.&lt;/p&gt;

&lt;p&gt;The objective is not to maximize the number of repositories. It is to establish useful and explicit boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Where Should Repository Interfaces Live?
&lt;/h2&gt;

&lt;p&gt;This question is frequently debated in Clean Architecture implementations.&lt;/p&gt;

&lt;p&gt;There is no universal rule that every repository interface must belong to the Domain project.&lt;/p&gt;

&lt;p&gt;The correct location depends on the responsibility represented by the interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Interfaces in Application
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application/
    Users/
        IUserRepository.cs
        CreateUser/
            CreateUserCommand.cs
            CreateUserHandler.cs

Infrastructure/
    Persistence/
        UserRepository.cs
        AppDbContext.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a valid design when the repository represents an application-level persistence contract required by use cases.&lt;/p&gt;

&lt;p&gt;The Application layer defines the contract, and Infrastructure provides its implementation.&lt;/p&gt;

&lt;p&gt;This approach is particularly straightforward in applications that organize functionality by use case or feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Interfaces in Domain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Domain/
    Users/
        User.cs
        IUserRepository.cs

Infrastructure/
    Persistence/
        UserRepository.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also a valid design when the repository contract is considered part of the domain model, often in a Domain-Driven Design approach involving aggregate roots.&lt;/p&gt;

&lt;p&gt;The key distinction is semantic ownership: which layer needs the abstraction, and which layer should own its contract?&lt;/p&gt;

&lt;p&gt;The interface's location should reflect that decision rather than follow a rule that every repository must live in the same project regardless of its purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Does Clean Architecture Require Custom Repositories?
&lt;/h2&gt;

&lt;p&gt;No. Clean Architecture requires the dependency boundaries to be respected; it does not mandate a particular repository implementation.&lt;/p&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;Consider two possible architectures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture A: Direct EF Core access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    └── CreateUserHandler
            └── AppDbContext

Infrastructure
    └── EF Core configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be a reasonable choice for a relatively simple application, especially when the team deliberately accepts EF Core as an Application dependency.&lt;/p&gt;

&lt;p&gt;It reduces the number of abstractions and classes.&lt;/p&gt;

&lt;p&gt;However, the Application layer is now coupled to the persistence framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture B: Custom repository abstraction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ├── CreateUserHandler
    └── IUserRepository

Infrastructure
    ├── UserRepository
    └── AppDbContext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design introduces additional code and indirection.&lt;/p&gt;

&lt;p&gt;In return, the Application layer depends on a contract that can be implemented independently of EF Core.&lt;/p&gt;

&lt;p&gt;Whether that additional separation is worth its cost depends on the project's requirements.&lt;/p&gt;

&lt;p&gt;If the stated goal is to keep the Application layer independent of persistence technology, Architecture B expresses that goal more directly.&lt;/p&gt;

&lt;p&gt;If the goal is to minimize abstractions in a small application, Architecture A may be preferable.&lt;/p&gt;

&lt;p&gt;These are different trade-offs, not proof that one pattern has become obsolete.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Testing: An Architectural Benefit, Not a Magic Solution
&lt;/h2&gt;

&lt;p&gt;Custom repositories can make application use cases easier to test without connecting to a production database.&lt;/p&gt;

&lt;p&gt;For example, a handler that depends on &lt;code&gt;IUserRepository&lt;/code&gt; can be tested with a fake implementation or a mock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeUserRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_users&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EmailExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the application behavior to be tested independently of database infrastructure.&lt;/p&gt;

&lt;p&gt;However, a fake repository does not verify that the actual EF Core implementation generates the correct query or behaves correctly against the selected database provider.&lt;/p&gt;

&lt;p&gt;Those concerns still require integration testing.&lt;/p&gt;

&lt;p&gt;Microsoft explains these trade-offs in its guidance on &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/choosing-a-testing-strategy" rel="noopener noreferrer"&gt;choosing a testing strategy for EF Core&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/testing-without-the-database" rel="noopener noreferrer"&gt;testing without your production database&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The lesson is straightforward: repositories can improve test isolation, but they do not eliminate the need to test persistence behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What About Unit of Work and Transactions?
&lt;/h2&gt;

&lt;p&gt;Repository discussions often introduce another misconception: that adding a repository automatically solves transaction management.&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;EF Core's &lt;code&gt;DbContext&lt;/code&gt; already provides unit-of-work behavior. Calling &lt;code&gt;SaveChangesAsync()&lt;/code&gt; persists the tracked changes, normally within a transaction when the provider supports transactions.&lt;/p&gt;

&lt;p&gt;A custom repository may add entities to the context without saving immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can make the persistence boundary explicit and coordinate multiple repositories within one unit of work.&lt;/p&gt;

&lt;p&gt;However, adding a separate &lt;code&gt;IUnitOfWork&lt;/code&gt; interface is not mandatory merely because repositories exist. Its value depends on whether it expresses a useful application-level contract and how the application manages persistence and transactions.&lt;/p&gt;

&lt;p&gt;The important thing is to understand where changes are committed and whether the use case requires atomicity.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. The Architect's Responsibility: Explain the Trade-Off
&lt;/h2&gt;

&lt;p&gt;A senior developer or software architect should be able to explain the consequences of a design decision, not simply label a pattern as old or modern.&lt;/p&gt;

&lt;p&gt;A useful review should ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the Application layer depend on infrastructure-specific implementations?&lt;/li&gt;
&lt;li&gt;Do the interfaces express meaningful application or domain requirements?&lt;/li&gt;
&lt;li&gt;Are repositories hiding persistence details or merely duplicating &lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Are query responsibilities clear?&lt;/li&gt;
&lt;li&gt;Can the team test application behavior without unnecessarily coupling tests to the database?&lt;/li&gt;
&lt;li&gt;Does the additional abstraction justify its maintenance cost?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions lead to a more useful technical discussion than asking whether the Repository Pattern is still fashionable.&lt;/p&gt;

&lt;p&gt;The same principle applies to many architectural decisions. A pattern is not valuable simply because it has a name, and it is not useless simply because a framework already provides similar functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Entity Framework Core already provides repository-like and unit-of-work capabilities. That fact should influence our design decisions.&lt;/p&gt;

&lt;p&gt;It should not, however, end the discussion.&lt;/p&gt;

&lt;p&gt;When we choose Clean Architecture, we should understand the architectural boundaries we want to preserve. Dependency direction is fundamental to that design, and Dependency Inversion is one of the principles that helps maintain it.&lt;/p&gt;

&lt;p&gt;A custom repository is not mandatory in every .NET application. A generic repository that adds no meaningful separation can become unnecessary boilerplate.&lt;/p&gt;

&lt;p&gt;But a carefully designed repository can provide a clear persistence contract, protect the Application layer from EF Core dependencies, support focused testing, and make the system easier to evolve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question is not whether the Repository Pattern is obsolete. The question is whether the abstraction serves a clear architectural purpose in the system you are building.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the difference between applying a pattern mechanically and making an informed architectural decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Robert C. Martin, &lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;The Clean Architecture&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Microsoft, &lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core" rel="noopener noreferrer"&gt;Implementing the persistence layer with Entity Framework Core&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Microsoft, &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/choosing-a-testing-strategy" rel="noopener noreferrer"&gt;Choosing a testing strategy&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Microsoft, &lt;a href="https://learn.microsoft.com/en-us/ef/core/testing/testing-without-the-database" rel="noopener noreferrer"&gt;Testing without your production database system&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Result Pattern στο ASP.NET Core: Πώς να διαχειριζόμαστε επιτυχίες και αποτυχίες χωρίς να βασιζόμαστε στα Exceptions</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:29:36 +0000</pubDate>
      <link>https://dev.to/__b63657/result-pattern-sto-aspnet-core-pos-na-diakheirizomaste-epitukhies-kai-apotukhies-khoris-na-9l0</link>
      <guid>https://dev.to/__b63657/result-pattern-sto-aspnet-core-pos-na-diakheirizomaste-epitukhies-kai-apotukhies-khoris-na-9l0</guid>
      <description>&lt;p&gt;Εισαγωγή&lt;/p&gt;

&lt;p&gt;Ένα από τα σημαντικότερα ζητήματα στον σχεδιασμό μιας σύγχρονης εφαρμογής είναι ο τρόπος με τον οποίο διαχειριζόμαστε τις αποτυχίες.&lt;/p&gt;

&lt;p&gt;Στην καθημερινότητα ενός .NET developer, αρκετές λειτουργίες μπορούν να αποτύχουν για απολύτως αναμενόμενους λόγους.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;Ένας χρήστης προσπαθεί να εγγραφεί με email που υπάρχει ήδη.&lt;/p&gt;

&lt;p&gt;Ένα προϊόν δεν βρίσκεται στη βάση δεδομένων.&lt;/p&gt;

&lt;p&gt;Μια παραγγελία δεν μπορεί να ολοκληρωθεί επειδή δεν υπάρχει διαθέσιμο απόθεμα.&lt;/p&gt;

&lt;p&gt;Ένας χρήστης προσπαθεί να εκτελέσει μια ενέργεια για την οποία δεν έχει δικαιώματα.&lt;/p&gt;

&lt;p&gt;Μια πληρωμή απορρίπτεται από τον πάροχο πληρωμών.&lt;/p&gt;

&lt;p&gt;Σε όλες αυτές τις περιπτώσεις, η εφαρμογή δεν αντιμετωπίζει απαραίτητα κάποιο τεχνικό πρόβλημα. Αντίθετα, πρόκειται για αναμενόμενα αποτελέσματα της επιχειρησιακής λογικής.&lt;/p&gt;

&lt;p&gt;Παρόλα αυτά, σε πολλές εφαρμογές χρησιμοποιούμε exceptions για να εκφράσουμε ακόμη και αυτές τις αναμενόμενες καταστάσεις.&lt;/p&gt;

&lt;p&gt;Είναι όμως αυτή η καλύτερη επιλογή;&lt;/p&gt;

&lt;p&gt;Εδώ έρχεται το Result Pattern.&lt;/p&gt;

&lt;p&gt;Το Result Pattern είναι μια προσέγγιση σχεδιασμού που επιτρέπει σε μια μέθοδο να επιστρέφει ρητά είτε ένα επιτυχημένο αποτέλεσμα είτε μια αποτυχία, χωρίς να χρησιμοποιεί exceptions ως τον συνηθισμένο μηχανισμό ελέγχου ροής.&lt;/p&gt;

&lt;p&gt;Στο άρθρο αυτό θα εξετάσουμε:&lt;/p&gt;

&lt;p&gt;Ποιο πρόβλημα προσπαθεί να λύσει το Result Pattern.&lt;/p&gt;

&lt;p&gt;Πώς λειτουργεί και ποιες αρχές κρύβονται πίσω του.&lt;/p&gt;

&lt;p&gt;Πώς υλοποιούμε μια δική μας, strongly typed έκδοση σε C#.&lt;/p&gt;

&lt;p&gt;Πώς το ενσωματώνουμε σε ASP.NET Core Web APIs.&lt;/p&gt;

&lt;p&gt;Πώς συνδέεται με Clean Architecture, CQRS και Domain-Driven Design.&lt;/p&gt;

&lt;p&gt;Πώς διαχειριζόμαστε validation, errors και HTTP status codes.&lt;/p&gt;

&lt;p&gt;Πώς γράφουμε unit tests.&lt;/p&gt;

&lt;p&gt;Πότε αξίζει να το χρησιμοποιούμε και πότε όχι.&lt;/p&gt;

&lt;p&gt;Στόχος δεν είναι απλώς να δημιουργήσουμε μια κλάση Result, αλλά να κατανοήσουμε πώς αυτή η επιλογή επηρεάζει την αρχιτεκτονική και τη συντηρησιμότητα μιας εφαρμογής.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Το πρόβλημα: Exceptions για αναμενόμενες αποτυχίες&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ας ξεκινήσουμε με ένα απλό παράδειγμα.&lt;/p&gt;

&lt;p&gt;Έχουμε μια εφαρμογή στην οποία οι χρήστες μπορούν να δημιουργήσουν λογαριασμό.&lt;/p&gt;

&lt;p&gt;Ένας βασικός κανόνας είναι ότι δεν επιτρέπονται δύο χρήστες με το ίδιο email.&lt;/p&gt;

&lt;p&gt;Μια συνηθισμένη υλοποίηση θα μπορούσε να είναι η εξής:&lt;/p&gt;

&lt;p&gt;public async Task RegisterAsync(&lt;br&gt;
    string email,&lt;br&gt;
    CancellationToken cancellationToken)&lt;br&gt;
{&lt;br&gt;
    var existingUser = await _userRepository&lt;br&gt;
        .GetByEmailAsync(email, cancellationToken);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (existingUser is not null)
{
    throw new Exception("Email already exists");
}

var user = new User(email);

await _userRepository.AddAsync(
    user,
    cancellationToken);

return user;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Ο κώδικας είναι απλός και λειτουργικός.&lt;/p&gt;

&lt;p&gt;Ωστόσο, κρύβει ορισμένα σημαντικά προβλήματα.&lt;/p&gt;

&lt;p&gt;1.1 Η υπογραφή της μεθόδου δεν αποκαλύπτει όλες τις πιθανές εκβάσεις&lt;/p&gt;

&lt;p&gt;Παρατηρούμε την υπογραφή:&lt;/p&gt;

&lt;p&gt;Task RegisterAsync(...)&lt;/p&gt;

&lt;p&gt;Από αυτήν καταλαβαίνουμε ότι η μέθοδος επιστρέφει έναν User.&lt;/p&gt;

&lt;p&gt;Δεν γνωρίζουμε όμως ότι μπορεί να αποτύχει επειδή το email χρησιμοποιείται ήδη.&lt;/p&gt;

&lt;p&gt;Για να το ανακαλύψουμε, πρέπει να διαβάσουμε την υλοποίηση ή την τεκμηρίωση.&lt;/p&gt;

&lt;p&gt;Αυτό γίνεται ιδιαίτερα προβληματικό σε μεγάλα συστήματα, όπου μια μέθοδος μπορεί να καλείται από πολλαπλά σημεία.&lt;/p&gt;

&lt;p&gt;1.2 Τα exceptions γίνονται μέρος της κανονικής ροής&lt;/p&gt;

&lt;p&gt;Το γεγονός ότι ένα email υπάρχει ήδη δεν είναι κάτι απρόβλεπτο.&lt;/p&gt;

&lt;p&gt;Είναι ένας επιχειρησιακός κανόνας που γνωρίζουμε εκ των προτέρων.&lt;/p&gt;

&lt;p&gt;Επομένως, η αποτυχία της εγγραφής αποτελεί φυσιολογική πιθανή έκβαση.&lt;/p&gt;

&lt;p&gt;Αν χρησιμοποιούμε exceptions για τέτοιες περιπτώσεις, καταλήγουμε να αντιμετωπίζουμε αναμενόμενα αποτελέσματα σαν εξαιρετικές καταστάσεις.&lt;/p&gt;

&lt;p&gt;1.3 Ο κώδικας που καλεί τη μέθοδο γίνεται πιο περίπλοκος&lt;/p&gt;

&lt;p&gt;Ας δούμε πώς μπορεί να χρησιμοποιηθεί η προηγούμενη μέθοδος μέσα σε έναν controller:&lt;/p&gt;

&lt;p&gt;[HttpPost]&lt;br&gt;
public async Task Register(&lt;br&gt;
    RegisterRequest request,&lt;br&gt;
    CancellationToken cancellationToken)&lt;br&gt;
{&lt;br&gt;
    try&lt;br&gt;
    {&lt;br&gt;
        var user = await _userService.RegisterAsync(&lt;br&gt;
            request.Email,&lt;br&gt;
            cancellationToken);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    return Ok(user);
}
catch (Exception)
{
    return BadRequest();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Η συγκεκριμένη υλοποίηση έχει ένα σοβαρό μειονέκτημα.&lt;/p&gt;

&lt;p&gt;Όλα τα exceptions μετατρέπονται σε HTTP 400.&lt;/p&gt;

&lt;p&gt;Αυτό σημαίνει ότι ακόμη και ένα database connection failure ή ένα απροσδόκητο NullReferenceException μπορεί να εμφανιστεί στον client ως λανθασμένο αίτημα.&lt;/p&gt;

&lt;p&gt;Η εφαρμογή χάνει τη δυνατότητα να διαχωρίζει μια επιχειρησιακή αποτυχία από ένα τεχνικό σφάλμα.&lt;/p&gt;

&lt;p&gt;1.4 Δεν έχουν όλες οι αποτυχίες την ίδια σημασία&lt;/p&gt;

&lt;p&gt;Είναι σημαντικό να ξεχωρίσουμε δύο κατηγορίες:&lt;/p&gt;

&lt;p&gt;Αναμενόμενες αποτυχίες&lt;/p&gt;

&lt;p&gt;Το email υπάρχει ήδη.&lt;/p&gt;

&lt;p&gt;Το προϊόν δεν βρέθηκε.&lt;/p&gt;

&lt;p&gt;Το υπόλοιπο δεν επαρκεί.&lt;/p&gt;

&lt;p&gt;Η παραγγελία βρίσκεται σε κατάσταση που δεν επιτρέπει ακύρωση.&lt;/p&gt;

&lt;p&gt;Απροσδόκητες αποτυχίες&lt;/p&gt;

&lt;p&gt;Η βάση δεδομένων δεν είναι διαθέσιμη.&lt;/p&gt;

&lt;p&gt;Παρουσιάστηκε σφάλμα προγραμματισμού.&lt;/p&gt;

&lt;p&gt;Ένα εξωτερικό dependency επιστρέφει μη αναμενόμενη απάντηση.&lt;/p&gt;

&lt;p&gt;Παραβιάστηκε μια εσωτερική τεχνική υπόθεση.&lt;/p&gt;

&lt;p&gt;Το Result Pattern είναι ιδιαίτερα χρήσιμο για την πρώτη κατηγορία.&lt;/p&gt;

&lt;p&gt;Δεν σημαίνει ότι πρέπει να καταργήσουμε τα exceptions από την εφαρμογή μας.&lt;/p&gt;

&lt;p&gt;Σημαίνει ότι πρέπει να χρησιμοποιούμε τον κατάλληλο μηχανισμό για κάθε κατηγορία αποτυχίας.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Τι είναι το Result Pattern;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Το Result Pattern είναι ένα μοτίβο σχεδιασμού σύμφωνα με το οποίο μια λειτουργία επιστρέφει ένα αντικείμενο που αναπαριστά το αποτέλεσμά της.&lt;/p&gt;

&lt;p&gt;Αυτό το αντικείμενο μπορεί να βρίσκεται σε μία από δύο καταστάσεις:&lt;/p&gt;

&lt;p&gt;Success: Η λειτουργία ολοκληρώθηκε επιτυχώς.&lt;/p&gt;

&lt;p&gt;Failure: Η λειτουργία δεν ολοκληρώθηκε και επιστρέφεται πληροφορία για τον λόγο αποτυχίας.&lt;/p&gt;

&lt;p&gt;Μια τυπική χρήση θα μπορούσε να είναι:&lt;/p&gt;

&lt;p&gt;Result result =&lt;br&gt;
    await userService.RegisterAsync(&lt;br&gt;
        email,&lt;br&gt;
        cancellationToken);&lt;/p&gt;

&lt;p&gt;if (result.IsFailure)&lt;br&gt;
{&lt;br&gt;
    Console.WriteLine(result.Error.Description);&lt;br&gt;
    return;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;User user = result.Value;&lt;/p&gt;

&lt;p&gt;Το σημαντικό είναι ότι η πιθανότητα αποτυχίας εμφανίζεται πλέον στην υπογραφή:&lt;/p&gt;

&lt;p&gt;Task&amp;gt; RegisterAsync(...)&lt;/p&gt;

&lt;p&gt;Ο caller γνωρίζει ότι δεν λαμβάνει απλώς έναν User, αλλά ένα αποτέλεσμα που πρέπει να αξιολογήσει.&lt;/p&gt;

&lt;p&gt;Αυτή η ιδέα συνδέεται με τις έννοιες Either και Result που συναντάμε στον functional programming κόσμο.&lt;/p&gt;

&lt;p&gt;Στη C#, όπου δεν υπάρχει ενσωματωμένος γενικός discriminated union τύπος με πλήρη exhaustiveness checking, μπορούμε να προσεγγίσουμε αυτή τη συμπεριφορά με δικούς μας τύπους.&lt;/p&gt;

&lt;p&gt;Μια σημαντική διευκρίνιση&lt;/p&gt;

&lt;p&gt;Το Result Pattern δεν εξαναγκάζει από μόνο του τον caller να ελέγξει την αποτυχία.&lt;/p&gt;

&lt;p&gt;Η C# εξακολουθεί να επιτρέπει σε κάποιον να αγνοήσει ένα επιστρεφόμενο αποτέλεσμα.&lt;/p&gt;

&lt;p&gt;Επιπλέον, ένα χειροποίητο Result δεν παρέχει αυτόματα compile-time exhaustiveness checking.&lt;/p&gt;

&lt;p&gt;Το βασικό πλεονέκτημα είναι ότι η αποτυχία γίνεται ρητό μέρος του συμβολαίου της μεθόδου, αντί να παραμένει κρυμμένη σε κάποιο exception path.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Σχεδιάζοντας το Error Model&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Πριν δημιουργήσουμε το Result, πρέπει να αποφασίσουμε πώς θα αναπαριστούμε τα errors.&lt;/p&gt;

&lt;p&gt;Μια πρώτη προσέγγιση θα ήταν να επιστρέφουμε απλώς ένα string:&lt;/p&gt;

&lt;p&gt;return Result.Failure(&lt;br&gt;
    "Email already exists");&lt;/p&gt;

&lt;p&gt;Αυτό λειτουργεί, αλλά έχει περιορισμούς.&lt;/p&gt;

&lt;p&gt;Ένα string δεν περιγράφει με δομημένο τρόπο το είδος του σφάλματος.&lt;/p&gt;

&lt;p&gt;Επίσης, αν κάποιος αλλάξει το μήνυμα, μπορεί να επηρεάσει κώδικα που βασίζεται στο συγκεκριμένο κείμενο.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, δεν θα θέλαμε να γράφουμε:&lt;/p&gt;

&lt;p&gt;if (result.Error == "Email already exists")&lt;br&gt;
{&lt;br&gt;
    // Handle duplicate email&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Πρόκειται για εύθραυστο συμβόλαιο.&lt;/p&gt;

&lt;p&gt;Αντίθετα, θα δημιουργήσουμε ένα strongly typed error model.&lt;/p&gt;

&lt;p&gt;3.1 ErrorType&lt;/p&gt;

&lt;p&gt;Αρχικά ορίζουμε τις βασικές κατηγορίες σφαλμάτων.&lt;/p&gt;

&lt;p&gt;public enum ErrorType&lt;br&gt;
{&lt;br&gt;
    Failure,&lt;br&gt;
    Validation,&lt;br&gt;
    NotFound,&lt;br&gt;
    Conflict,&lt;br&gt;
    Forbidden,&lt;br&gt;
    Unauthorized&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Κάθε κατηγορία εκφράζει μια διαφορετική σημασιολογία.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, το NotFound σημαίνει ότι ο πόρος που αναζητούμε δεν υπάρχει, ενώ το Conflict σημαίνει ότι η λειτουργία συγκρούεται με την τρέχουσα κατάσταση του συστήματος.&lt;/p&gt;

&lt;p&gt;3.2 Error&lt;/p&gt;

&lt;p&gt;Στη συνέχεια δημιουργούμε τον τύπο Error.&lt;/p&gt;

&lt;p&gt;public sealed record Error(&lt;br&gt;
    string Code,&lt;br&gt;
    string Description,&lt;br&gt;
    ErrorType Type)&lt;br&gt;
{&lt;br&gt;
    public static Error Failure(&lt;br&gt;
        string code,&lt;br&gt;
        string description) =&amp;gt;&lt;br&gt;
        new(code, description, ErrorType.Failure);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static Error Validation(
    string code,
    string description) =&amp;gt;
    new(code, description, ErrorType.Validation);

public static Error NotFound(
    string code,
    string description) =&amp;gt;
    new(code, description, ErrorType.NotFound);

public static Error Conflict(
    string code,
    string description) =&amp;gt;
    new(code, description, ErrorType.Conflict);

public static Error Forbidden(
    string code,
    string description) =&amp;gt;
    new(code, description, ErrorType.Forbidden);

public static Error Unauthorized(
    string code,
    string description) =&amp;gt;
    new(code, description, ErrorType.Unauthorized);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Χρησιμοποιούμε record επειδή θέλουμε value-based equality.&lt;/p&gt;

&lt;p&gt;Δύο errors με τα ίδια δεδομένα θεωρούνται ισοδύναμα.&lt;/p&gt;

&lt;p&gt;Επίσης, το Error είναι immutable, γεγονός που περιορίζει την πιθανότητα ανεπιθύμητων αλλαγών μετά τη δημιουργία του.&lt;/p&gt;

&lt;p&gt;3.3 Γιατί χρειαζόμαστε Code και Description;&lt;/p&gt;

&lt;p&gt;Ας εξετάσουμε το ακόλουθο error:&lt;/p&gt;

&lt;p&gt;public static readonly Error EmailAlreadyExists =&lt;br&gt;
    Error.Conflict(&lt;br&gt;
        "Users.EmailAlreadyExists",&lt;br&gt;
        "A user with this email already exists.");&lt;/p&gt;

&lt;p&gt;Το Code είναι ένα σταθερό αναγνωριστικό.&lt;/p&gt;

&lt;p&gt;Μπορεί να χρησιμοποιηθεί για:&lt;/p&gt;

&lt;p&gt;Error handling.&lt;/p&gt;

&lt;p&gt;Client-side translations.&lt;/p&gt;

&lt;p&gt;Monitoring.&lt;/p&gt;

&lt;p&gt;Structured logging.&lt;/p&gt;

&lt;p&gt;Automated tests.&lt;/p&gt;

&lt;p&gt;Το Description είναι το μήνυμα που περιγράφει το πρόβλημα.&lt;/p&gt;

&lt;p&gt;Ο διαχωρισμός είναι σημαντικός επειδή το κείμενο ενός μηνύματος μπορεί να αλλάξει, ενώ το error code πρέπει ιδανικά να παραμένει σταθερό.&lt;/p&gt;

&lt;p&gt;Σε ένα production API, τα codes πρέπει να θεωρούνται μέρος του συμβολαίου προς τους clients.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Υλοποιώντας το Result Pattern σε C#&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ας περάσουμε στην κεντρική υλοποίηση.&lt;/p&gt;

&lt;p&gt;Θα δημιουργήσουμε δύο τύπους:&lt;/p&gt;

&lt;p&gt;Result για λειτουργίες που δεν επιστρέφουν τιμή.&lt;/p&gt;

&lt;p&gt;Result για λειτουργίες που επιστρέφουν δεδομένα.&lt;/p&gt;

&lt;p&gt;Η διάκριση είναι χρήσιμη.&lt;/p&gt;

&lt;p&gt;Μια λειτουργία διαγραφής, για παράδειγμα, μπορεί να χρειάζεται μόνο να μας ενημερώσει αν ολοκληρώθηκε.&lt;/p&gt;

&lt;p&gt;Αντίθετα, μια λειτουργία δημιουργίας χρήστη χρειάζεται να επιστρέψει και το αναγνωριστικό του νέου χρήστη.&lt;/p&gt;

&lt;p&gt;4.1 Η βασική κλάση Result&lt;/p&gt;

&lt;p&gt;public class Result&lt;br&gt;
{&lt;br&gt;
    public bool IsSuccess { get; }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public bool IsFailure =&amp;gt; !IsSuccess;

public Error? Error { get; }

protected Result(bool isSuccess, Error? error)
{
    if (isSuccess &amp;amp;&amp;amp; error is not null)
    {
        throw new ArgumentException(
            "A successful result cannot contain an error.");
    }

    if (!isSuccess &amp;amp;&amp;amp; error is null)
    {
        throw new ArgumentException(
            "A failed result must contain an error.");
    }

    IsSuccess = isSuccess;
    Error = error;
}

public static Result Success() =&amp;gt;
    new(true, null);

public static Result Failure(Error error) =&amp;gt;
    new(false, error);

public static Result&amp;lt;T&amp;gt; Success&amp;lt;T&amp;gt;(T value) =&amp;gt;
    Result&amp;lt;T&amp;gt;.Success(value);

public static Result&amp;lt;T&amp;gt; Failure&amp;lt;T&amp;gt;(Error error) =&amp;gt;
    Result&amp;lt;T&amp;gt;.Failure(error);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Η σημαντικότερη λεπτομέρεια βρίσκεται στον constructor.&lt;/p&gt;

&lt;p&gt;Δεν επιτρέπουμε να δημιουργηθεί ένα επιτυχημένο αποτέλεσμα που περιέχει error.&lt;/p&gt;

&lt;p&gt;Ούτε επιτρέπουμε να δημιουργηθεί αποτυχημένο αποτέλεσμα χωρίς error.&lt;/p&gt;

&lt;p&gt;Αυτές οι συνθήκες είναι τα invariants του τύπου μας.&lt;/p&gt;

&lt;p&gt;Με άλλα λόγια, ορίζουμε ποιες καταστάσεις θεωρούνται έγκυρες και αποτρέπουμε τη δημιουργία μη έγκυρων αντικειμένων.&lt;/p&gt;

&lt;p&gt;4.2 Το generic Result&lt;/p&gt;

&lt;p&gt;public sealed class Result : Result&lt;br&gt;
{&lt;br&gt;
    private readonly T? _value;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private Result(
    bool isSuccess,
    T? value,
    Error? error)
    : base(isSuccess, error)
{
    _value = value;
}

public T Value =&amp;gt;
    IsSuccess
        ? _value!
        : throw new InvalidOperationException(
            "Cannot access the value of a failed result.");

public static Result&amp;lt;T&amp;gt; Success(T value) =&amp;gt;
    new(true, value, null);

public static new Result&amp;lt;T&amp;gt; Failure(Error error) =&amp;gt;
    new(false, default, error);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Εδώ εισάγουμε μια σημαντική αρχή.&lt;/p&gt;

&lt;p&gt;Το Value είναι διαθέσιμο μόνο όταν το αποτέλεσμα είναι επιτυχημένο.&lt;/p&gt;

&lt;p&gt;Αν προσπαθήσουμε να το διαβάσουμε σε αποτυχημένο αποτέλεσμα, δημιουργείται InvalidOperationException.&lt;/p&gt;

&lt;p&gt;Μπορεί αυτό να φαίνεται αντιφατικό;&lt;/p&gt;

&lt;p&gt;Αφού προσπαθούμε να αποφύγουμε τα exceptions, γιατί χρησιμοποιούμε ένα μέσα στο Result;&lt;/p&gt;

&lt;p&gt;Η απάντηση βρίσκεται στη διάκριση ανάμεσα στις αναμενόμενες επιχειρησιακές αποτυχίες και στη λανθασμένη χρήση ενός API.&lt;/p&gt;

&lt;p&gt;Το να μην υπάρχει ένας χρήστης στη βάση δεδομένων είναι αναμενόμενο.&lt;/p&gt;

&lt;p&gt;Το να προσπαθήσει ένας developer να διαβάσει το Value από ένα αποτυχημένο Result παραβιάζει το συμβόλαιο του τύπου.&lt;/p&gt;

&lt;p&gt;Το δεύτερο αποτελεί programming error.&lt;/p&gt;

&lt;p&gt;Senior-level σημείωση: Η παραπάνω υλοποίηση είναι σκόπιμα μικρή για εκπαιδευτικούς λόγους. Το T? και ο τελεστής ! δεν επιβάλλουν πραγματικό non-null invariant για reference types. Αν το συμβόλαιό μας απαγορεύει το Success(null), πρέπει να προσθέσουμε ρητό έλεγχο ή να επιλέξουμε διαφορετικό μοντέλο nullability. Επίσης, η ιδιότητα Error είναι nullable, οπότε ο compiler δεν μπορεί να συμπεράνει ότι μετά από IsFailure είναι μη-null.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ορίζοντας Domain Errors&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Μια καλή πρακτική είναι να ορίζουμε τα errors κοντά στο domain στο οποίο ανήκουν.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;public static class UserErrors&lt;br&gt;
{&lt;br&gt;
    public static readonly Error EmailAlreadyExists =&lt;br&gt;
        Error.Conflict(&lt;br&gt;
            "Users.EmailAlreadyExists",&lt;br&gt;
            "A user with this email already exists.");&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static Error NotFound(Guid userId) =&amp;gt;
    Error.NotFound(
        "Users.NotFound",
        $"User with id '{userId}' was not found.");

public static readonly Error InvalidEmail =
    Error.Validation(
        "Users.InvalidEmail",
        "The provided email is invalid.");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Αυτό προσφέρει δύο σημαντικά οφέλη.&lt;/p&gt;

&lt;p&gt;Πρώτον, τα errors είναι επαναχρησιμοποιήσιμα.&lt;/p&gt;

&lt;p&gt;Δεύτερον, οι υπηρεσίες δεν χρειάζεται να γνωρίζουν πώς κατασκευάζεται κάθε error.&lt;/p&gt;

&lt;p&gt;Μπορούν απλώς να επιστρέφουν ένα γνωστό, καλά ορισμένο domain error.&lt;/p&gt;

&lt;p&gt;Παρατηρούμε επίσης ότι το NotFound είναι μέθοδος και όχι στατικό πεδίο, επειδή το description περιλαμβάνει δυναμική πληροφορία.&lt;/p&gt;

&lt;p&gt;Σε πραγματικές εφαρμογές πρέπει να προσέχουμε ώστε τέτοια μηνύματα να μην αποκαλύπτουν προσωπικά δεδομένα ή εσωτερικές πληροφορίες.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Πρακτικό παράδειγμα: User Registration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ας εφαρμόσουμε τώρα το Result Pattern σε ένα πιο ρεαλιστικό σενάριο.&lt;/p&gt;

&lt;p&gt;Θέλουμε να δημιουργήσουμε μια λειτουργία εγγραφής χρήστη.&lt;/p&gt;

&lt;p&gt;Η διαδικασία περιλαμβάνει:&lt;/p&gt;

&lt;p&gt;Έλεγχο εγκυρότητας του email.&lt;/p&gt;

&lt;p&gt;Έλεγχο αν υπάρχει ήδη χρήστης με το ίδιο email.&lt;/p&gt;

&lt;p&gt;Δημιουργία του χρήστη.&lt;/p&gt;

&lt;p&gt;Αποθήκευση στη βάση δεδομένων.&lt;/p&gt;

&lt;p&gt;Επιστροφή του αναγνωριστικού.&lt;/p&gt;

&lt;p&gt;6.1 Το Request&lt;/p&gt;

&lt;p&gt;public sealed record RegisterUserRequest(&lt;br&gt;
    string Email);&lt;/p&gt;

&lt;p&gt;6.2 Το Response&lt;/p&gt;

&lt;p&gt;public sealed record RegisterUserResponse(&lt;br&gt;
    Guid UserId,&lt;br&gt;
    string Email);&lt;/p&gt;

&lt;p&gt;6.3 Το Repository Contract&lt;/p&gt;

&lt;p&gt;public interface IUserRepository&lt;br&gt;
{&lt;br&gt;
    Task ExistsByEmailAsync(&lt;br&gt;
        string email,&lt;br&gt;
        CancellationToken cancellationToken);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task AddAsync(
    User user,
    CancellationToken cancellationToken);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Για το παράδειγμα υποθέτουμε ότι το repository αποθηκεύει τον χρήστη κατά την κλήση AddAsync.&lt;/p&gt;

&lt;p&gt;Σε μια εφαρμογή που χρησιμοποιεί Unit of Work, η αποθήκευση μπορεί να πραγματοποιείται αργότερα μέσω SaveChangesAsync.&lt;/p&gt;

&lt;p&gt;6.4 Το Domain Entity&lt;/p&gt;

&lt;p&gt;public sealed class User&lt;br&gt;
{&lt;br&gt;
    public Guid Id { get; private set; }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public string Email { get; private set; }

private User(Guid id, string email)
{
    Id = id;
    Email = email;
}

public static User Create(string email)
{
    return new User(
        Guid.NewGuid(),
        email);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Για να παραμείνει το παράδειγμα εστιασμένο στο Result Pattern, κρατάμε την οντότητα απλή.&lt;/p&gt;

&lt;p&gt;Σε ένα πιο αυστηρό domain model θα μπορούσαμε να χρησιμοποιήσουμε Email value object, ώστε να επιβάλλουμε την εγκυρότητα και την κανονικοποίηση της διεύθυνσης στο ίδιο το domain.&lt;/p&gt;

&lt;p&gt;6.5 Το Application Service&lt;/p&gt;

&lt;p&gt;public sealed class UserService&lt;br&gt;
{&lt;br&gt;
    private readonly IUserRepository _userRepository;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public UserService(IUserRepository userRepository)
{
    _userRepository = userRepository;
}

public async Task&amp;lt;Result&amp;lt;RegisterUserResponse&amp;gt;&amp;gt; RegisterAsync(
    RegisterUserRequest request,
    CancellationToken cancellationToken)
{
    if (string.IsNullOrWhiteSpace(request.Email))
    {
        return Result.Failure&amp;lt;RegisterUserResponse&amp;gt;(
            UserErrors.InvalidEmail);
    }

    var normalizedEmail = request.Email
        .Trim()
        .ToLowerInvariant();

    var emailExists = await _userRepository
        .ExistsByEmailAsync(
            normalizedEmail,
            cancellationToken);

    if (emailExists)
    {
        return Result.Failure&amp;lt;RegisterUserResponse&amp;gt;(
            UserErrors.EmailAlreadyExists);
    }

    var user = User.Create(normalizedEmail);

    await _userRepository.AddAsync(
        user,
        cancellationToken);

    var response = new RegisterUserResponse(
        user.Id,
        user.Email);

    return Result.Success(response);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Ας εξετάσουμε προσεκτικά τι συμβαίνει.&lt;/p&gt;

&lt;p&gt;Πρώτο βήμα: Validation&lt;/p&gt;

&lt;p&gt;if (string.IsNullOrWhiteSpace(request.Email))&lt;br&gt;
{&lt;br&gt;
    return Result.Failure(&lt;br&gt;
        UserErrors.InvalidEmail);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Αν το email είναι κενό, η μέθοδος επιστρέφει ένα αποτυχημένο Result.&lt;/p&gt;

&lt;p&gt;Δεν δημιουργείται exception.&lt;/p&gt;

&lt;p&gt;Ο caller μπορεί να ελέγξει το error και να αποφασίσει πώς θα αντιδράσει.&lt;/p&gt;

&lt;p&gt;Ο έλεγχος αυτός δεν αποτελεί πλήρη email validation. Είναι απλώς ένας αρχικός έλεγχος για τις ανάγκες του παραδείγματος.&lt;/p&gt;

&lt;p&gt;Δεύτερο βήμα: Business Rule&lt;/p&gt;

&lt;p&gt;if (emailExists)&lt;br&gt;
{&lt;br&gt;
    return Result.Failure(&lt;br&gt;
        UserErrors.EmailAlreadyExists);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Εδώ εφαρμόζουμε τον κανόνα ότι δεν επιτρέπονται διπλές εγγραφές με το ίδιο email.&lt;/p&gt;

&lt;p&gt;Η αποτυχία είναι αναμενόμενη.&lt;/p&gt;

&lt;p&gt;Επομένως, επιστρέφουμε Result αντί να κάνουμε throw.&lt;/p&gt;

&lt;p&gt;Τρίτο βήμα: Success&lt;/p&gt;

&lt;p&gt;return Result.Success(response);&lt;/p&gt;

&lt;p&gt;Η μέθοδος ολοκληρώνεται με ένα επιτυχημένο Result που περιέχει το response.&lt;/p&gt;

&lt;p&gt;Ο caller γνωρίζει ότι το Value είναι διαθέσιμο όταν το IsSuccess είναι true.&lt;/p&gt;

&lt;p&gt;6.6 Ένα κρίσιμο production ζήτημα: Concurrency&lt;/p&gt;

&lt;p&gt;Η προηγούμενη υλοποίηση περιέχει έναν κλασικό κίνδυνο.&lt;/p&gt;

&lt;p&gt;Φανταστείτε δύο ταυτόχρονα requests με το ίδιο email.&lt;/p&gt;

&lt;p&gt;Και τα δύο μπορούν να εκτελέσουν τον έλεγχο:&lt;/p&gt;

&lt;p&gt;ExistsByEmailAsync(email)&lt;/p&gt;

&lt;p&gt;Και τα δύο μπορεί να λάβουν false.&lt;/p&gt;

&lt;p&gt;Στη συνέχεια, και τα δύο προσπαθούν να αποθηκεύσουν τον χρήστη.&lt;/p&gt;

&lt;p&gt;Αυτό είναι ένα race condition.&lt;/p&gt;

&lt;p&gt;Το Result Pattern δεν λύνει το συγκεκριμένο πρόβλημα.&lt;/p&gt;

&lt;p&gt;Η πραγματική προστασία πρέπει να βρίσκεται και στη βάση δεδομένων, μέσω unique constraint ή unique index στο κανονικοποιημένο email.&lt;/p&gt;

&lt;p&gt;Όταν προκύψει η συγκεκριμένη παραβίαση, το persistence layer μπορεί να την αναγνωρίσει και να τη μετατρέψει σε ένα γνωστό EmailAlreadyExists Result, εφόσον αυτό αποτελεί μέρος του συμβολαίου του.&lt;/p&gt;

&lt;p&gt;Δεν πρέπει όμως να μετατρέπουμε κάθε DbUpdateException σε conflict.&lt;/p&gt;

&lt;p&gt;Ένα DbUpdateException μπορεί να προκύψει για πολλούς διαφορετικούς λόγους.&lt;/p&gt;

&lt;p&gt;Χρειάζεται provider-specific αναγνώριση της συγκεκριμένης παραβίασης και προσεκτικός χειρισμός.&lt;/p&gt;

&lt;p&gt;Αυτή είναι μια σημαντική διάκριση ανάμεσα σε ένα εκπαιδευτικό παράδειγμα και μια production-ready υλοποίηση.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ενσωμάτωση σε ASP.NET Core Web API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Μέχρι τώρα το Application Service επιστρέφει ένα Result.&lt;/p&gt;

&lt;p&gt;Το επόμενο ερώτημα είναι πώς μετατρέπουμε αυτό το αποτέλεσμα σε HTTP response.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;HTTP Status&lt;/p&gt;

&lt;p&gt;Success&lt;/p&gt;

&lt;p&gt;200 OK ή 201 Created&lt;/p&gt;

&lt;p&gt;Validation&lt;/p&gt;

&lt;p&gt;400 Bad Request&lt;/p&gt;

&lt;p&gt;NotFound&lt;/p&gt;

&lt;p&gt;404 Not Found&lt;/p&gt;

&lt;p&gt;Conflict&lt;/p&gt;

&lt;p&gt;409 Conflict&lt;/p&gt;

&lt;p&gt;Unauthorized&lt;/p&gt;

&lt;p&gt;401 Unauthorized&lt;/p&gt;

&lt;p&gt;Forbidden&lt;/p&gt;

&lt;p&gt;403 Forbidden&lt;/p&gt;

&lt;p&gt;Ο πίνακας είναι μια ενδεικτική πολιτική mapping και όχι ένας απόλυτος κανόνας.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, σε ένα API που χρησιμοποιεί model validation μπορεί να επιλεγεί HTTP 422 για συγκεκριμένες semantic validation failures.&lt;/p&gt;

&lt;p&gt;Επίσης, η πραγματική authentication και authorization συμπεριφορά πρέπει να παραμένει συμβατή με το security pipeline του ASP.NET Core.&lt;/p&gt;

&lt;p&gt;7.1 Γιατί δεν πρέπει να επιστρέφουμε IActionResult από το Application Layer;&lt;/p&gt;

&lt;p&gt;Θα μπορούσαμε να γράψουμε:&lt;/p&gt;

&lt;p&gt;public async Task RegisterAsync(...)&lt;/p&gt;

&lt;p&gt;Ωστόσο, αυτό θα δημιουργούσε εξάρτηση του application layer από το ASP.NET Core.&lt;/p&gt;

&lt;p&gt;Η επιχειρησιακή λογική θα γνώριζε πλέον για HTTP status codes και MVC abstractions.&lt;/p&gt;

&lt;p&gt;Σε μια Clean Architecture προσέγγιση θέλουμε να αποφύγουμε αυτή την εξάρτηση.&lt;/p&gt;

&lt;p&gt;Το application layer πρέπει να εκφράζει το αποτέλεσμα της λειτουργίας.&lt;/p&gt;

&lt;p&gt;Το presentation layer πρέπει να αποφασίζει πώς θα το παρουσιάσει στον εξωτερικό κόσμο.&lt;/p&gt;

&lt;p&gt;7.2 Mapping σε ProblemDetails&lt;/p&gt;

&lt;p&gt;Το ASP.NET Core υποστηρίζει το ProblemDetails, ένα καθιερωμένο format για την αναπαράσταση HTTP API errors.&lt;/p&gt;

&lt;p&gt;Μπορούμε να δημιουργήσουμε έναν κεντρικό mapper:&lt;/p&gt;

&lt;p&gt;using Microsoft.AspNetCore.Mvc;&lt;/p&gt;

&lt;p&gt;public static class ResultExtensions&lt;br&gt;
{&lt;br&gt;
    public static IActionResult ToActionResult(&lt;br&gt;
        this Result result,&lt;br&gt;
        ControllerBase controller)&lt;br&gt;
    {&lt;br&gt;
        if (result.IsSuccess)&lt;br&gt;
        {&lt;br&gt;
            return controller.Ok(result.Value);&lt;br&gt;
        }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    var error = result.Error!;

    var statusCode = error.Type switch
    {
        ErrorType.Validation =&amp;gt;
            StatusCodes.Status400BadRequest,

        ErrorType.NotFound =&amp;gt;
            StatusCodes.Status404NotFound,

        ErrorType.Conflict =&amp;gt;
            StatusCodes.Status409Conflict,

        ErrorType.Unauthorized =&amp;gt;
            StatusCodes.Status401Unauthorized,

        ErrorType.Forbidden =&amp;gt;
            StatusCodes.Status403Forbidden,

        _ =&amp;gt;
            StatusCodes.Status400BadRequest
    };

    var problem = new ProblemDetails
    {
        Status = statusCode,
        Title = error.Code,
        Detail = error.Description
    };

    return new ObjectResult(problem)
    {
        StatusCode = statusCode,
        ContentTypes =
        {
            "application/problem+json"
        }
    };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Το ! χρησιμοποιείται επειδή ο compiler δεν μπορεί να συμπεράνει από το IsFailure ότι το Error είναι μη-null.&lt;/p&gt;

&lt;p&gt;Η εγγύηση προκύπτει από τον constructor του Result.&lt;/p&gt;

&lt;p&gt;Σε μεγαλύτερη υλοποίηση θα μπορούσαμε να χρησιμοποιήσουμε TryGetError, pattern matching ή ένα διαφορετικό union-like API ώστε να αποφύγουμε αυτόν τον τελεστή.&lt;/p&gt;

&lt;p&gt;7.3 Ο Controller&lt;/p&gt;

&lt;p&gt;Ο controller γίνεται πλέον αρκετά απλός.&lt;/p&gt;

&lt;p&gt;[ApiController]&lt;br&gt;
[Route("api/users")]&lt;br&gt;
public sealed class UsersController : ControllerBase&lt;br&gt;
{&lt;br&gt;
    private readonly UserService _userService;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public UsersController(UserService userService)
{
    _userService = userService;
}

[HttpPost]
public async Task&amp;lt;IActionResult&amp;gt; Register(
    RegisterUserRequest request,
    CancellationToken cancellationToken)
{
    var result = await _userService.RegisterAsync(
        request,
        cancellationToken);

    if (result.IsFailure)
    {
        return result.ToActionResult(this);
    }

    return Created(
        $"/api/users/{result.Value.UserId}",
        result.Value);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Παρατηρούμε ότι ο controller δεν γνωρίζει τις λεπτομέρειες των business rules.&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται να ξέρει πώς γίνεται ο έλεγχος του email.&lt;/p&gt;

&lt;p&gt;Ούτε χρειάζεται να κάνει catch ένα exception για κάθε αναμενόμενη αποτυχία.&lt;/p&gt;

&lt;p&gt;Απλώς μετατρέπει το αποτέλεσμα της εφαρμογής σε HTTP response.&lt;/p&gt;

&lt;p&gt;Για τη συγκεκριμένη λειτουργία δημιουργίας χρησιμοποιούμε 201 Created.&lt;/p&gt;

&lt;p&gt;Αν το API διαθέτει endpoint ανάκτησης χρήστη, μπορούμε να χρησιμοποιήσουμε και CreatedAtAction ή CreatedAtRoute, ώστε να παράγουμε το resource location με βάση το routing configuration.&lt;/p&gt;

&lt;p&gt;7.4 Παράδειγμα HTTP Response&lt;/p&gt;

&lt;p&gt;Αν το email υπάρχει ήδη, το API μπορεί να επιστρέψει:&lt;/p&gt;

&lt;p&gt;HTTP/1.1 409 Conflict&lt;br&gt;
Content-Type: application/problem+json&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "type": "about:blank",&lt;br&gt;
  "title": "Users.EmailAlreadyExists",&lt;br&gt;
  "status": 409,&lt;br&gt;
  "detail": "A user with this email already exists."&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Αυτό το response είναι προβλέψιμο και μπορεί να καταναλωθεί από διαφορετικούς clients.&lt;/p&gt;

&lt;p&gt;Το frontend μπορεί να βασίζεται στο error code αντί να συγκρίνει μηνύματα.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;if (problem.title === "Users.EmailAlreadyExists") {&lt;br&gt;
    showEmailAlreadyExistsMessage();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Σε μεγαλύτερα APIs θα ήταν προτιμότερο το error code να βρίσκεται σε ξεχωριστό extension property, όπως code, και το title να χρησιμοποιείται για μια σύντομη, ανθρώπινα αναγνώσιμη περιγραφή.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Result Pattern και Clean Architecture&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Το Result Pattern ταιριάζει ιδιαίτερα καλά σε εφαρμογές που ακολουθούν Clean Architecture.&lt;/p&gt;

&lt;p&gt;Μια τυπική δομή θα μπορούσε να είναι:&lt;/p&gt;

&lt;p&gt;src/&lt;br&gt;
├── MyApp.Domain/&lt;br&gt;
│   ├── Entities/&lt;br&gt;
│   └── Errors/&lt;br&gt;
│&lt;br&gt;
├── MyApp.Application/&lt;br&gt;
│   ├── Abstractions/&lt;br&gt;
│   ├── Users/&lt;br&gt;
│   └── Common/&lt;br&gt;
│       └── Results/&lt;br&gt;
│&lt;br&gt;
├── MyApp.Infrastructure/&lt;br&gt;
│   ├── Persistence/&lt;br&gt;
│   └── Repositories/&lt;br&gt;
│&lt;br&gt;
└── MyApp.Api/&lt;br&gt;
    ├── Controllers/&lt;br&gt;
    └── Extensions/&lt;/p&gt;

&lt;p&gt;Σε αυτή τη δομή, το Result βρίσκεται στο Application layer ή σε ένα μικρό κοινό abstraction assembly, εφόσον υπάρχει πραγματική ανάγκη να χρησιμοποιείται από περισσότερα layers.&lt;/p&gt;

&lt;p&gt;Τα domain errors μπορούν να ορίζονται στο Domain layer όταν αντιπροσωπεύουν καθαρά domain concepts.&lt;/p&gt;

&lt;p&gt;Ο HTTP mapper βρίσκεται στο API layer.&lt;/p&gt;

&lt;p&gt;Το βασικό πλεονέκτημα είναι ότι διαχωρίζουμε τις ευθύνες.&lt;/p&gt;

&lt;p&gt;Domain: Ποιοι επιχειρησιακοί κανόνες ισχύουν;&lt;/p&gt;

&lt;p&gt;Application: Πώς εκτελείται ένα use case και ποια αποτελέσματα μπορεί να επιστρέψει;&lt;/p&gt;

&lt;p&gt;Infrastructure: Πώς επικοινωνούμε με τη βάση δεδομένων και τα εξωτερικά συστήματα;&lt;/p&gt;

&lt;p&gt;Presentation: Πώς μετατρέπουμε τα αποτελέσματα σε HTTP responses;&lt;/p&gt;

&lt;p&gt;Το Result Pattern δεν επιβάλλει από μόνο του Clean Architecture, αλλά μπορεί να υποστηρίξει αποτελεσματικά τον διαχωρισμό των layers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Result Pattern και CQRS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Αν χρησιμοποιούμε CQRS, μπορούμε να εφαρμόσουμε την ίδια προσέγγιση σε commands και queries.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, ένα command για δημιουργία χρήστη:&lt;/p&gt;

&lt;p&gt;public sealed record CreateUserCommand(&lt;br&gt;
    string Email);&lt;/p&gt;

&lt;p&gt;Και ένας handler:&lt;/p&gt;

&lt;p&gt;public sealed class CreateUserHandler&lt;br&gt;
{&lt;br&gt;
    private readonly IUserRepository _repository;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public CreateUserHandler(IUserRepository repository)
{
    _repository = repository;
}

public async Task&amp;lt;Result&amp;lt;Guid&amp;gt;&amp;gt; Handle(
    CreateUserCommand command,
    CancellationToken cancellationToken)
{
    var exists = await _repository.ExistsByEmailAsync(
        command.Email,
        cancellationToken);

    if (exists)
    {
        return Result.Failure&amp;lt;Guid&amp;gt;(
            UserErrors.EmailAlreadyExists);
    }

    var user = User.Create(command.Email);

    await _repository.AddAsync(
        user,
        cancellationToken);

    return Result.Success(user.Id);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Ο handler επιστρέφει ένα Result.&lt;/p&gt;

&lt;p&gt;Δεν επιστρέφει IActionResult.&lt;/p&gt;

&lt;p&gt;Δεν γνωρίζει HTTP status codes.&lt;/p&gt;

&lt;p&gt;Επομένως, ο ίδιος handler μπορεί να χρησιμοποιηθεί από ένα Web API, ένα background job ή έναν message consumer.&lt;/p&gt;

&lt;p&gt;Το Result Pattern κάνει το συμβόλαιο του use case ανεξάρτητο από το transport mechanism.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Συνδυάζοντας πολλαπλά Results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Σε πιο σύνθετα workflows, μπορεί να χρειαστεί να εκτελέσουμε διαδοχικές λειτουργίες.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;Εντοπισμός χρήστη.&lt;/p&gt;

&lt;p&gt;Έλεγχος δικαιωμάτων.&lt;/p&gt;

&lt;p&gt;Δημιουργία παραγγελίας.&lt;/p&gt;

&lt;p&gt;Αποθήκευση παραγγελίας.&lt;/p&gt;

&lt;p&gt;Κάθε βήμα μπορεί να επιστρέφει Result.&lt;/p&gt;

&lt;p&gt;Μια απλή υλοποίηση θα μπορούσε να είναι:&lt;/p&gt;

&lt;p&gt;public async Task&amp;gt; CreateOrderAsync(&lt;br&gt;
    Guid userId,&lt;br&gt;
    CancellationToken cancellationToken)&lt;br&gt;
{&lt;br&gt;
    var userResult = await GetUserAsync(&lt;br&gt;
        userId,&lt;br&gt;
        cancellationToken);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (userResult.IsFailure)
{
    return Result.Failure&amp;lt;Guid&amp;gt;(
        userResult.Error!);
}

var permissionResult = await CheckPermissionAsync(
    userResult.Value,
    cancellationToken);

if (permissionResult.IsFailure)
{
    return Result.Failure&amp;lt;Guid&amp;gt;(
        permissionResult.Error!);
}

var order = new Order(userId);

await _orderRepository.AddAsync(
    order,
    cancellationToken);

return Result.Success(order.Id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Ο κώδικας είναι σαφής, αλλά σε μεγάλα workflows μπορεί να εμφανιστεί αρκετό επαναλαμβανόμενο error handling.&lt;/p&gt;

&lt;p&gt;Εδώ μπορούν να βοηθήσουν functional composition τεχνικές όπως Bind και Map.&lt;/p&gt;

&lt;p&gt;10.1 Map&lt;/p&gt;

&lt;p&gt;Το Map μετασχηματίζει την τιμή ενός επιτυχημένου Result.&lt;/p&gt;

&lt;p&gt;Αν το Result είναι αποτυχημένο, διατηρεί το error.&lt;/p&gt;

&lt;p&gt;public static Result Map(&lt;br&gt;
    this Result result,&lt;br&gt;
    Func mapper)&lt;br&gt;
{&lt;br&gt;
    if (result.IsFailure)&lt;br&gt;
    {&lt;br&gt;
        return Result.Failure(&lt;br&gt;
            result.Error!);&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return Result.Success(
    mapper(result.Value));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Παράδειγμα:&lt;/p&gt;

&lt;p&gt;Result userResult = GetUser();&lt;/p&gt;

&lt;p&gt;Result emailResult = userResult.Map(&lt;br&gt;
    user =&amp;gt; user.Email);&lt;/p&gt;

&lt;p&gt;Αν ο χρήστης βρέθηκε, το αποτέλεσμα περιέχει το email.&lt;/p&gt;

&lt;p&gt;Αν ο χρήστης δεν βρέθηκε, το αποτέλεσμα διατηρεί το αρχικό error.&lt;/p&gt;

&lt;p&gt;10.2 Bind&lt;/p&gt;

&lt;p&gt;Το Bind χρησιμοποιείται όταν η επόμενη λειτουργία επιστρέφει και αυτή Result.&lt;/p&gt;

&lt;p&gt;public static Result Bind(&lt;br&gt;
    this Result result,&lt;br&gt;
    Func&amp;gt; binder)&lt;br&gt;
{&lt;br&gt;
    if (result.IsFailure)&lt;br&gt;
    {&lt;br&gt;
        return Result.Failure(&lt;br&gt;
            result.Error!);&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return binder(result.Value);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Παράδειγμα:&lt;/p&gt;

&lt;p&gt;Result userResult = GetUser();&lt;/p&gt;

&lt;p&gt;Result orderResult = userResult.Bind(&lt;br&gt;
    user =&amp;gt; CreateOrder(user));&lt;/p&gt;

&lt;p&gt;Αν το πρώτο Result αποτύχει, η δεύτερη λειτουργία δεν εκτελείται.&lt;/p&gt;

&lt;p&gt;Αν το πρώτο Result επιτύχει, η τιμή του περνά στη δεύτερη λειτουργία.&lt;/p&gt;

&lt;p&gt;Αυτή η συμπεριφορά επιτρέπει να δημιουργούμε αλυσίδες λειτουργιών χωρίς να επαναλαμβάνουμε συνεχώς τον ίδιο έλεγχο.&lt;/p&gt;

&lt;p&gt;Ωστόσο, χρειάζεται προσοχή.&lt;/p&gt;

&lt;p&gt;Η υπερβολική χρήση functional abstractions μπορεί να κάνει έναν απλό επιχειρησιακό αλγόριθμο δυσκολότερο στην ανάγνωση.&lt;/p&gt;

&lt;p&gt;Η αναγνωσιμότητα πρέπει να παραμένει σημαντικότερη από την προσπάθεια να εξαφανίσουμε κάθε if.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validation: Fail Fast ή Error Aggregation;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Μέχρι τώρα κάθε Result περιέχει ένα μόνο error.&lt;/p&gt;

&lt;p&gt;Αυτό είναι αρκετό για πολλά business rules.&lt;/p&gt;

&lt;p&gt;Ωστόσο, υπάρχουν περιπτώσεις όπου θέλουμε να επιστρέψουμε πολλαπλά validation errors.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, σε μια φόρμα εγγραφής μπορεί να υπάρχουν ταυτόχρονα τα εξής προβλήματα:&lt;/p&gt;

&lt;p&gt;Το email δεν είναι έγκυρο.&lt;/p&gt;

&lt;p&gt;Ο κωδικός είναι πολύ μικρός.&lt;/p&gt;

&lt;p&gt;Το όνομα είναι υποχρεωτικό.&lt;/p&gt;

&lt;p&gt;Αν επιστρέψουμε μόνο το πρώτο error, ο χρήστης θα χρειαστεί να διορθώνει ένα πρόβλημα κάθε φορά.&lt;/p&gt;

&lt;p&gt;Για validation scenarios μπορούμε να επιλέξουμε ένα πιο σύνθετο αποτέλεσμα:&lt;/p&gt;

&lt;p&gt;public sealed record ValidationError(&lt;br&gt;
    string PropertyName,&lt;br&gt;
    string Code,&lt;br&gt;
    string Message);&lt;/p&gt;

&lt;p&gt;Και να χρησιμοποιήσουμε ένα collection:&lt;/p&gt;

&lt;p&gt;public sealed record ValidationFailure(&lt;br&gt;
    IReadOnlyList Errors);&lt;/p&gt;

&lt;p&gt;Ένα τέτοιο μοντέλο μπορεί να συνδυαστεί με ValidationProblemDetails, ώστε το API να επιστρέφει σφάλματα ανά πεδίο.&lt;/p&gt;

&lt;p&gt;Το σημαντικό είναι να διαχωρίσουμε δύο διαφορετικές στρατηγικές.&lt;/p&gt;

&lt;p&gt;Fail Fast: Σταματάμε στην πρώτη αποτυχία. Είναι κατάλληλο όταν η συνέχεια της διαδικασίας δεν έχει νόημα χωρίς την επιτυχία του προηγούμενου βήματος.&lt;/p&gt;

&lt;p&gt;Error Aggregation: Συγκεντρώνουμε ανεξάρτητα validation errors. Είναι κατάλληλο όταν θέλουμε να ενημερώσουμε τον χρήστη για όλα τα προβλήματα του request.&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται να χρησιμοποιούμε την ίδια στρατηγική σε κάθε σημείο της εφαρμογής.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Πότε χρησιμοποιούμε Exceptions αντί για Result;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ένα από τα πιο συχνά λάθη είναι η προσπάθεια να μετατρέψουμε κάθε exception σε Result.&lt;/p&gt;

&lt;p&gt;Αυτό δεν είναι απαραίτητα σωστό.&lt;/p&gt;

&lt;p&gt;Ας εξετάσουμε μερικές περιπτώσεις.&lt;/p&gt;

&lt;p&gt;Περίπτωση&lt;/p&gt;

&lt;p&gt;Προτεινόμενος χειρισμός&lt;/p&gt;

&lt;p&gt;Email ήδη καταχωρημένο&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;Προϊόν δεν βρέθηκε&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;Ανεπαρκές υπόλοιπο&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;Απαγορευμένη αλλαγή κατάστασης&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;Database connection failure&lt;/p&gt;

&lt;p&gt;Exception&lt;/p&gt;

&lt;p&gt;Απρόβλεπτο programming bug&lt;/p&gt;

&lt;p&gt;Exception&lt;/p&gt;

&lt;p&gt;CancellationToken cancellation&lt;/p&gt;

&lt;p&gt;Συνήθως propagation της cancellation&lt;/p&gt;

&lt;p&gt;Αναμενόμενη απόρριψη πληρωμής&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;Απρόβλεπτο failure του payment provider&lt;/p&gt;

&lt;p&gt;Exception ή ρητά μοντελοποιημένη αποτυχία, ανάλογα με το συμβόλαιο&lt;/p&gt;

&lt;p&gt;Η τελευταία περίπτωση είναι ιδιαίτερα ενδιαφέρουσα.&lt;/p&gt;

&lt;p&gt;Μια εξωτερική υπηρεσία μπορεί να αποτύχει για διαφορετικούς λόγους.&lt;/p&gt;

&lt;p&gt;Μια απόρριψη πληρωμής λόγω ανεπαρκών χρημάτων είναι επιχειρησιακό αποτέλεσμα.&lt;/p&gt;

&lt;p&gt;Αντίθετα, ένα network timeout μπορεί να αποτελεί προσωρινό τεχνικό πρόβλημα.&lt;/p&gt;

&lt;p&gt;Σε ένα σύστημα με υψηλές απαιτήσεις αξιοπιστίας, ορισμένα infrastructure failures μπορούν επίσης να μοντελοποιηθούν ως Result, ειδικά όταν αποτελούν αναμενόμενο και ανακτήσιμο μέρος του συμβολαίου.&lt;/p&gt;

&lt;p&gt;Το κριτήριο δεν είναι απλώς αν το πρόβλημα είναι τεχνικό ή επιχειρησιακό.&lt;/p&gt;

&lt;p&gt;Το βασικό ερώτημα είναι:&lt;/p&gt;

&lt;p&gt;Πρόκειται για μια αναμενόμενη κατάσταση που ο caller μπορεί και οφείλει να χειριστεί ως μέρος της κανονικής ροής;&lt;/p&gt;

&lt;p&gt;Αν ναι, το Result μπορεί να είναι κατάλληλο.&lt;/p&gt;

&lt;p&gt;Αν πρόκειται για παραβίαση εσωτερικής υπόθεσης ή μη αναμενόμενο failure, ένα exception συνήθως είναι πιο σωστό.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Global Exception Handling και Result Pattern&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Σε μια σωστά σχεδιασμένη εφαρμογή, το Result Pattern και το global exception handling λειτουργούν συμπληρωματικά.&lt;/p&gt;

&lt;p&gt;Το Result Pattern χειρίζεται τις αναμενόμενες αποτυχίες.&lt;/p&gt;

&lt;p&gt;Το global exception handling αναλαμβάνει τις απροσδόκητες αποτυχίες.&lt;/p&gt;

&lt;p&gt;Στο ASP.NET Core μπορούμε να χρησιμοποιήσουμε το IExceptionHandler.&lt;/p&gt;

&lt;p&gt;using Microsoft.AspNetCore.Diagnostics;&lt;br&gt;
using Microsoft.AspNetCore.Mvc;&lt;/p&gt;

&lt;p&gt;public sealed class GlobalExceptionHandler&lt;br&gt;
    : IExceptionHandler&lt;br&gt;
{&lt;br&gt;
    private readonly ILogger _logger;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public GlobalExceptionHandler(
    ILogger&amp;lt;GlobalExceptionHandler&amp;gt; logger)
{
    _logger = logger;
}

public async ValueTask&amp;lt;bool&amp;gt; TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken)
{
    _logger.LogError(
        exception,
        "An unexpected error occurred.");

    var problem = new ProblemDetails
    {
        Status = StatusCodes.Status500InternalServerError,
        Title = "Internal Server Error",
        Detail = "An unexpected error occurred."
    };

    httpContext.Response.StatusCode =
        StatusCodes.Status500InternalServerError;

    await httpContext.Response.WriteAsJsonAsync(
        problem,
        options: null,
        contentType: "application/problem+json",
        cancellationToken: cancellationToken);

    return true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Στο Program.cs:&lt;/p&gt;

&lt;p&gt;builder.Services.AddProblemDetails();&lt;/p&gt;

&lt;p&gt;builder.Services.AddExceptionHandler&amp;lt;&lt;br&gt;
    GlobalExceptionHandler&amp;gt;();&lt;/p&gt;

&lt;p&gt;var app = builder.Build();&lt;/p&gt;

&lt;p&gt;app.UseExceptionHandler();&lt;/p&gt;

&lt;p&gt;app.MapControllers();&lt;/p&gt;

&lt;p&gt;app.Run();&lt;/p&gt;

&lt;p&gt;Η συγκεκριμένη υλοποίηση είναι απλοποιημένη.&lt;/p&gt;

&lt;p&gt;Σε production περιβάλλον θα πρέπει να διατηρούμε τη σωστή συμπεριφορά για cancellations, να προσέχουμε αν έχει ήδη ξεκινήσει η HTTP response και να μην αποκαλύπτουμε εσωτερικές πληροφορίες στους clients.&lt;/p&gt;

&lt;p&gt;Επίσης, για τα unexpected failures συνήθως θέλουμε structured logging και correlation identifiers.&lt;/p&gt;

&lt;p&gt;Το βασικό αρχιτεκτονικό συμπέρασμα παραμένει το ίδιο:&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται να επιλέξουμε ανάμεσα σε Result Pattern και exception handling.&lt;/p&gt;

&lt;p&gt;Χρειαζόμαστε και τα δύο, αλλά για διαφορετικές ευθύνες.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unit Testing με Result Pattern&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ένα σημαντικό πλεονέκτημα του Result Pattern είναι ότι οι αναμενόμενες αποτυχίες μπορούν να ελεγχθούν χωρίς να βασιζόμαστε σε exception assertions.&lt;/p&gt;

&lt;p&gt;Ας δούμε ένα παράδειγμα με xUnit και Moq.&lt;/p&gt;

&lt;p&gt;[Fact]&lt;br&gt;
public async Task RegisterAsync_ShouldReturnConflict_WhenEmailExists()&lt;br&gt;
{&lt;br&gt;
    // Arrange&lt;br&gt;
    var repository = new Mock();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repository
    .Setup(x =&amp;gt; x.ExistsByEmailAsync(
        It.IsAny&amp;lt;string&amp;gt;(),
        It.IsAny&amp;lt;CancellationToken&amp;gt;()))
    .ReturnsAsync(true);

var service = new UserService(repository.Object);

var request = new RegisterUserRequest(
    "test@example.com");

// Act
var result = await service.RegisterAsync(
    request,
    CancellationToken.None);

// Assert
Assert.True(result.IsFailure);

Assert.Equal(
    "Users.EmailAlreadyExists",
    result.Error!.Code);

Assert.Equal(
    ErrorType.Conflict,
    result.Error.Type);

repository.Verify(
    x =&amp;gt; x.AddAsync(
        It.IsAny&amp;lt;User&amp;gt;(),
        It.IsAny&amp;lt;CancellationToken&amp;gt;()),
    Times.Never);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Το test ελέγχει όχι μόνο ότι η λειτουργία απέτυχε, αλλά και ότι απέτυχε για τον σωστό λόγο.&lt;/p&gt;

&lt;p&gt;Επίσης, επιβεβαιώνει ότι δεν επιχειρήθηκε αποθήκευση χρήστη.&lt;/p&gt;

&lt;p&gt;Testing του Success Path&lt;/p&gt;

&lt;p&gt;[Fact]&lt;br&gt;
public async Task RegisterAsync_ShouldReturnUser_WhenEmailIsAvailable()&lt;br&gt;
{&lt;br&gt;
    // Arrange&lt;br&gt;
    var repository = new Mock();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repository
    .Setup(x =&amp;gt; x.ExistsByEmailAsync(
        It.IsAny&amp;lt;string&amp;gt;(),
        It.IsAny&amp;lt;CancellationToken&amp;gt;()))
    .ReturnsAsync(false);

var service = new UserService(repository.Object);

var request = new RegisterUserRequest(
    "test@example.com");

// Act
var result = await service.RegisterAsync(
    request,
    CancellationToken.None);

// Assert
Assert.True(result.IsSuccess);

Assert.NotEqual(
    Guid.Empty,
    result.Value.UserId);

Assert.Equal(
    "test@example.com",
    result.Value.Email);

repository.Verify(
    x =&amp;gt; x.AddAsync(
        It.IsAny&amp;lt;User&amp;gt;(),
        It.IsAny&amp;lt;CancellationToken&amp;gt;()),
    Times.Once);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Με αυτόν τον τρόπο, τα tests περιγράφουν καθαρά το αναμενόμενο συμβόλαιο του use case.&lt;/p&gt;

&lt;p&gt;Φυσικά, εξακολουθούμε να χρειαζόμαστε integration tests για πραγματικά database constraints, transaction behavior και HTTP mappings.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Result Pattern και Performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ένα επιχείρημα που συχνά αναφέρεται υπέρ του Result Pattern είναι η απόδοση.&lt;/p&gt;

&lt;p&gt;Η δημιουργία και το throwing ενός exception έχουν κόστος, ειδικά επειδή περιλαμβάνουν stack trace και μηχανισμό exception propagation.&lt;/p&gt;

&lt;p&gt;Αν χρησιμοποιούμε exceptions για συχνές, αναμενόμενες αποτυχίες, αυτό το κόστος μπορεί να γίνει σημαντικό.&lt;/p&gt;

&lt;p&gt;Ένα Result αντικείμενο συνήθως έχει πιο προβλέψιμο κόστος.&lt;/p&gt;

&lt;p&gt;Ωστόσο, χρειάζεται προσοχή.&lt;/p&gt;

&lt;p&gt;Το Result Pattern δεν είναι αυτόματα ταχύτερο σε κάθε σενάριο.&lt;/p&gt;

&lt;p&gt;Μια class-based υλοποίηση όπως η δική μας δημιουργεί allocations.&lt;/p&gt;

&lt;p&gt;Μια struct-based υλοποίηση μπορεί να μειώσει allocations, αλλά εισάγει άλλες προκλήσεις, όπως default struct states και μεγαλύτερο κόστος αντιγραφής.&lt;/p&gt;

&lt;p&gt;Επίσης, αν μια αποτυχία συμβαίνει εξαιρετικά σπάνια, η διαφορά μπορεί να είναι πρακτικά αμελητέα.&lt;/p&gt;

&lt;p&gt;Σε εφαρμογές με σημαντικές απαιτήσεις απόδοσης, η σωστή προσέγγιση είναι να χρησιμοποιούμε benchmarks με αντιπροσωπευτικά workloads.&lt;/p&gt;

&lt;p&gt;Το βασικό όφελος του Result Pattern είναι πρωτίστως η σαφήνεια του συμβολαίου και η αρχιτεκτονική συνέπεια.&lt;/p&gt;

&lt;p&gt;Η απόδοση μπορεί να αποτελέσει επιπλέον πλεονέκτημα, αλλά δεν πρέπει να είναι το μοναδικό κριτήριο επιλογής.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Συνηθισμένα λάθη&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;16.1 Μετατροπή κάθε exception σε Result&lt;/p&gt;

&lt;p&gt;Αν κάθε catch καταλήγει σε Result.Failure, υπάρχει κίνδυνος να κρύβουμε πραγματικά bugs.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;try&lt;br&gt;
{&lt;br&gt;
    await SaveAsync();&lt;br&gt;
}&lt;br&gt;
catch (Exception)&lt;br&gt;
{&lt;br&gt;
    return Result.Failure(&lt;br&gt;
        Error.Failure(&lt;br&gt;
            "General.Error",&lt;br&gt;
            "Something went wrong."));&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Ο κώδικας χάνει σημαντική πληροφορία για την πραγματική αιτία της αποτυχίας.&lt;/p&gt;

&lt;p&gt;Η σωστή στρατηγική είναι να μετατρέπουμε σε Result μόνο τις αποτυχίες που γνωρίζουμε πώς να αναγνωρίσουμε και να χειριστούμε.&lt;/p&gt;

&lt;p&gt;16.2 Επιστροφή HTTP errors από το Domain&lt;/p&gt;

&lt;p&gt;Το domain δεν πρέπει να γνωρίζει ότι ένα error αντιστοιχεί σε HTTP 404 ή HTTP 409.&lt;/p&gt;

&lt;p&gt;Το HTTP mapping ανήκει στο presentation layer.&lt;/p&gt;

&lt;p&gt;16.3 Χρήση ενός γενικού error για τα πάντα&lt;/p&gt;

&lt;p&gt;Αν όλα τα failures επιστρέφουν:&lt;/p&gt;

&lt;p&gt;Error.Failure(&lt;br&gt;
    "Error",&lt;br&gt;
    "Operation failed");&lt;/p&gt;

&lt;p&gt;χάνουμε μεγάλο μέρος της αξίας του Result Pattern.&lt;/p&gt;

&lt;p&gt;Τα errors πρέπει να έχουν συγκεκριμένη σημασιολογία.&lt;/p&gt;

&lt;p&gt;16.4 Υπερβολικά πολύπλοκη υλοποίηση&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται κάθε εφαρμογή δεκάδες extensions, operators και functional abstractions.&lt;/p&gt;

&lt;p&gt;Μια μικρή εφαρμογή μπορεί να καλυφθεί από ένα απλό Result και έναν mapper.&lt;/p&gt;

&lt;p&gt;Η πολυπλοκότητα πρέπει να προστίθεται όταν υπάρχει πραγματική ανάγκη.&lt;/p&gt;

&lt;p&gt;16.5 Αγνόηση των failed Results&lt;/p&gt;

&lt;p&gt;Ένα Result μπορεί να επιστραφεί και να αγνοηθεί:&lt;/p&gt;

&lt;p&gt;await ProcessOrderAsync(orderId);&lt;/p&gt;

&lt;p&gt;Αν η μέθοδος επιστρέφει Task, ο compiler δεν απαιτεί να ελεγχθεί το αποτέλεσμα.&lt;/p&gt;

&lt;p&gt;Αυτό σημαίνει ότι το Result Pattern απαιτεί και πειθαρχία από την ομάδα.&lt;/p&gt;

&lt;p&gt;Code reviews, conventions και κατάλληλοι analyzers μπορούν να βοηθήσουν στον εντοπισμό τέτοιων περιπτώσεων.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Πότε αξίζει να χρησιμοποιήσουμε το Result Pattern;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Το Result Pattern είναι ιδιαίτερα χρήσιμο όταν:&lt;/p&gt;

&lt;p&gt;Έχουμε αρκετούς επιχειρησιακούς κανόνες που μπορούν να οδηγήσουν σε αναμενόμενες αποτυχίες.&lt;/p&gt;

&lt;p&gt;Θέλουμε να διαχωρίσουμε την επιχειρησιακή λογική από το HTTP layer.&lt;/p&gt;

&lt;p&gt;Χρησιμοποιούμε Clean Architecture ή CQRS.&lt;/p&gt;

&lt;p&gt;Θέλουμε συνεπή error contracts.&lt;/p&gt;

&lt;p&gt;Χρειαζόμαστε προβλέψιμο error handling ανάμεσα σε διαφορετικά application layers.&lt;/p&gt;

&lt;p&gt;Θέλουμε να γράφουμε tests που ελέγχουν ρητά τις διαφορετικές εκβάσεις ενός use case.&lt;/p&gt;

&lt;p&gt;Δεν είναι όμως απαραίτητο να χρησιμοποιείται παντού.&lt;/p&gt;

&lt;p&gt;Σε μια πολύ απλή εφαρμογή CRUD, με ελάχιστους επιχειρησιακούς κανόνες, μπορεί να εισάγει περισσότερη πολυπλοκότητα από όση αφαιρεί.&lt;/p&gt;

&lt;p&gt;Επίσης, δεν χρειάζεται κάθε private helper method να επιστρέφει Result.&lt;/p&gt;

&lt;p&gt;Μια συνάρτηση που εκτελεί έναν απλό μαθηματικό υπολογισμό ή έναν εγγυημένα έγκυρο μετασχηματισμό μπορεί να συνεχίσει να επιστρέφει απευθείας την τιμή της.&lt;/p&gt;

&lt;p&gt;Το Result Pattern έχει μεγαλύτερη αξία στα σημεία όπου η αποτυχία αποτελεί ουσιαστικό μέρος του συμβολαίου.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Custom Result ή έτοιμη βιβλιοθήκη;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Μέχρι τώρα υλοποιήσαμε μια δική μας έκδοση.&lt;/p&gt;

&lt;p&gt;Αυτό είναι ιδιαίτερα χρήσιμο για να κατανοήσουμε τις αρχές του pattern.&lt;/p&gt;

&lt;p&gt;Σε πραγματικά projects, όμως, υπάρχουν και ώριμες βιβλιοθήκες που προσφέρουν παρόμοια abstractions.&lt;/p&gt;

&lt;p&gt;Ενδεικτικά:&lt;/p&gt;

&lt;p&gt;FluentResults&lt;/p&gt;

&lt;p&gt;ErrorOr&lt;/p&gt;

&lt;p&gt;Ardalis.Result&lt;/p&gt;

&lt;p&gt;CSharpFunctionalExtensions&lt;/p&gt;

&lt;p&gt;Η επιλογή εξαρτάται από τις ανάγκες της εφαρμογής.&lt;/p&gt;

&lt;p&gt;Μια έτοιμη βιβλιοθήκη μπορεί να προσφέρει richer error models, composition operators, validation support και integration helpers.&lt;/p&gt;

&lt;p&gt;Από την άλλη πλευρά, ένα μικρό custom implementation μπορεί να είναι ευκολότερο στην κατανόηση και να προσφέρει πλήρη έλεγχο του συμβολαίου.&lt;/p&gt;

&lt;p&gt;Δεν υπάρχει μία σωστή επιλογή για όλες τις ομάδες.&lt;/p&gt;

&lt;p&gt;Πριν επιλέξουμε βιβλιοθήκη, αξίζει να εξετάσουμε την πολυπλοκότητα του API της, τη συμβατότητα με το architecture μας, τη συντήρηση, τις εξαρτήσεις και το πόσο εύκολα μπορούμε να τη χρησιμοποιήσουμε με συνέπεια.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Τελικές σκέψεις&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Το Result Pattern δεν είναι απλώς ένας διαφορετικός τρόπος να επιστρέφουμε errors.&lt;/p&gt;

&lt;p&gt;Είναι ένας τρόπος να κάνουμε τα συμβόλαια των λειτουργιών μας πιο ξεκάθαρα.&lt;/p&gt;

&lt;p&gt;Αντί μια μέθοδος να φαίνεται ότι επιστρέφει μόνο μια τιμή, ενώ στην πραγματικότητα μπορεί να αποτύχει για πολλούς αναμενόμενους λόγους, επιστρέφει ένα αντικείμενο που αναπαριστά ρητά την επιτυχία ή την αποτυχία.&lt;/p&gt;

&lt;p&gt;Αυτό μας επιτρέπει να σχεδιάζουμε εφαρμογές στις οποίες:&lt;/p&gt;

&lt;p&gt;Οι επιχειρησιακές αποτυχίες είναι σαφώς ορισμένες.&lt;/p&gt;

&lt;p&gt;Τα exceptions διατηρούν τον ρόλο τους για μη αναμενόμενες καταστάσεις.&lt;/p&gt;

&lt;p&gt;Το application layer παραμένει ανεξάρτητο από το HTTP.&lt;/p&gt;

&lt;p&gt;Οι controllers είναι απλούστεροι.&lt;/p&gt;

&lt;p&gt;Τα error contracts είναι πιο συνεπή.&lt;/p&gt;

&lt;p&gt;Τα unit tests περιγράφουν καθαρά τις διαφορετικές εκβάσεις.&lt;/p&gt;

&lt;p&gt;Ωστόσο, το Result Pattern δεν αντικαθιστά το σωστό domain modeling, το validation, τα database constraints, τα transactions ή το exception handling.&lt;/p&gt;

&lt;p&gt;Ούτε αποτελεί λόγο να προσθέτουμε abstractions σε κάθε σημείο της εφαρμογής.&lt;/p&gt;

&lt;p&gt;Η αξία του βρίσκεται στη σωστή χρήση του.&lt;/p&gt;

&lt;p&gt;Ένας καλός σχεδιασμός δεν προσπαθεί να εξαφανίσει κάθε πιθανότητα αποτυχίας. Προσπαθεί να κάνει τις αναμενόμενες αποτυχίες σαφείς, προβλέψιμες και εύκολες στον χειρισμό.&lt;/p&gt;

&lt;p&gt;Και αυτό είναι τελικά το σημαντικότερο που μας προσφέρει το Result Pattern σε μια σύγχρονη εφαρμογή ASP.NET Core.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Πώς λειτουργεί το Authentication με Microsoft Entra ID στο ASP.NET Core</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Fri, 02 Oct 2026 20:58:16 +0000</pubDate>
      <link>https://dev.to/__b63657/authentication-me-microsoft-entra-id-se-aspnet-core-2hpd</link>
      <guid>https://dev.to/__b63657/authentication-me-microsoft-entra-id-se-aspnet-core-2hpd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1xvl8kpedc7969wnak2j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1xvl8kpedc7969wnak2j.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Στις σύγχρονες εφαρμογές δεν είναι απαραίτητο η ίδια η εφαρμογή να αποθηκεύει και να διαχειρίζεται τους κωδικούς πρόσβασης των χρηστών. Αντίθετα, μπορούμε να χρησιμοποιήσουμε έναν Identity Provider, όπως το &lt;strong&gt;Microsoft Entra ID&lt;/strong&gt;, ο οποίος αναλαμβάνει να επιβεβαιώσει την ταυτότητα του χρήστη.&lt;/p&gt;

&lt;p&gt;Με απλά λόγια, η εφαρμογή μας δεν χρειάζεται να γνωρίζει τον κωδικό του χρήστη. Ρωτά το Entra ID: &lt;strong&gt;«Έχεις επιβεβαιώσει ότι αυτός ο χρήστης είναι πράγματι αυτός που δηλώνει ότι είναι;»&lt;/strong&gt;. Εφόσον η απάντηση είναι θετική, η εφαρμογή μπορεί να συνεχίσει.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication και Authorization
&lt;/h2&gt;

&lt;p&gt;Πριν δούμε τη διαδικασία, πρέπει να ξεχωρίσουμε δύο έννοιες.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; σημαίνει «ποιος είσαι;».&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt; σημαίνει «τι επιτρέπεται να κάνεις;».&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, το Entra ID μπορεί να επιβεβαιώσει ότι ο χρήστης είναι ο &lt;code&gt;nikos@company.com&lt;/code&gt;. Αυτό είναι authentication. Το αν ο συγκεκριμένος χρήστης μπορεί να διαγράψει έναν πελάτη είναι authorization και αποτελεί ευθύνη της εφαρμογής.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ο ρόλος του Microsoft Entra ID
&lt;/h2&gt;

&lt;p&gt;Το Entra ID λειτουργεί ως &lt;strong&gt;Identity Provider&lt;/strong&gt;. Η εφαρμογή μας είναι καταχωρημένη στο Entra ID μέσω ενός &lt;strong&gt;App Registration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Στο App Registration υπάρχουν βασικές πληροφορίες όπως το:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client ID
Tenant ID
Redirect URI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Client ID αναγνωρίζει την εφαρμογή. Δεν είναι password ούτε secret.&lt;/p&gt;

&lt;p&gt;Μια ASP.NET Core εφαρμογή μπορεί, για παράδειγμα, να έχει configuration όπως:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AzureAd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"TenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ClientId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Με αυτόν τον τρόπο η εφαρμογή γνωρίζει σε ποιο Entra tenant ανήκει και ποια εφαρμογή εκπροσωπεί.&lt;/p&gt;




&lt;h2&gt;
  
  
  Τι γίνεται όταν ο χρήστης κάνει Login;
&lt;/h2&gt;

&lt;p&gt;Ας υποθέσουμε ότι έχουμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο χρήστης επισκέπτεται το &lt;code&gt;/Dashboard&lt;/code&gt;, αλλά δεν έχει κάνει login.&lt;/p&gt;

&lt;p&gt;Το ASP.NET Core αντιλαμβάνεται ότι απαιτείται authentication και οδηγεί τον χρήστη στο Microsoft Entra ID.&lt;/p&gt;

&lt;p&gt;Η απλοποιημένη ροή είναι:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   ↓
ASP.NET Core
   ↓
Microsoft Entra ID
   ↓
User Login
   ↓
ASP.NET Core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ο χρήστης κάνει login στο Entra ID και, ανάλογα με τις ρυθμίσεις του οργανισμού, μπορεί να χρειαστεί password, MFA ή άλλους ελέγχους ασφαλείας.&lt;/p&gt;

&lt;p&gt;Σημαντικό είναι ότι &lt;strong&gt;ο κωδικός του χρήστη δεν περνάει από τη δική μας εφαρμογή&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Τι επιστρέφει το Entra ID;
&lt;/h2&gt;

&lt;p&gt;Μετά την επιτυχημένη authentication διαδικασία, το Entra ID επιστρέφει πληροφορίες που επιτρέπουν στην εφαρμογή να γνωρίζει την ταυτότητα του χρήστη.&lt;/p&gt;

&lt;p&gt;Στο OpenID Connect χρησιμοποιείται το &lt;strong&gt;ID Token&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Το ID Token είναι ένα JWT και περιέχει claims, δηλαδή πληροφορίες σχετικά με τον χρήστη.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Nikos Stavropoulos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferred_username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nikos@company.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η ASP.NET Core εφαρμογή χρησιμοποιεί αυτές τις πληροφορίες για να δημιουργήσει το authenticated user.&lt;/p&gt;




&lt;p&gt;Πού βρίσκεται το OpenID Connect;&lt;/p&gt;

&lt;p&gt;Για να καταλάβουμε πώς λειτουργεί το authentication με το Microsoft Entra ID, πρέπει πρώτα να γνωρίζουμε ότι το Entra ID δεν χρησιμοποιεί έναν δικό του, απομονωμένο μηχανισμό επικοινωνίας με κάθε εφαρμογή. Βασίζεται σε διεθνή πρωτόκολλα, κυρίως το OAuth 2.0 και το OpenID Connect (OIDC).&lt;/p&gt;

&lt;p&gt;Το OAuth 2.0 αφορά κυρίως το authorization, δηλαδή την πρόσβαση μιας εφαρμογής σε έναν προστατευμένο πόρο. Το OpenID Connect επεκτείνει το OAuth 2.0 και προσθέτει τη δυνατότητα του authentication, δηλαδή την επιβεβαίωση της ταυτότητας του χρήστη.&lt;/p&gt;

&lt;p&gt;Μπορούμε επομένως να το δούμε απλά ως εξής:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Microsoft Entra ID
                    │
                    │ χρησιμοποιεί
                    ▼
           ┌───────────────────┐
           │    OAuth 2.0      │
           │  Authorization    │
           └─────────┬─────────┘
                     │
                     │ επεκτείνεται από
                     ▼
           ┌───────────────────┐
           │ OpenID Connect    │
           │  Authentication   │
           └───────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Άρα, όταν ένας χρήστης κάνει login σε μια ASP.NET Core εφαρμογή μέσω Entra ID, το OpenID Connect είναι το πρωτόκολλο που χρησιμοποιείται για να μεταφερθεί στην εφαρμογή η πληροφορία ότι ο χρήστης ταυτοποιήθηκε επιτυχώς.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Τι συμβαίνει στην πράξη;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Η εφαρμογή μας ζητά από το Entra ID να κάνει authentication για τον χρήστη.&lt;/p&gt;

&lt;p&gt;Η ροή είναι περίπου:&lt;/p&gt;

&lt;p&gt;Browser&lt;br&gt;
   │&lt;br&gt;
   │ Login&lt;br&gt;
   ▼&lt;br&gt;
ASP.NET Core Application&lt;br&gt;
   │&lt;br&gt;
   │ Authentication request&lt;br&gt;
   ▼&lt;br&gt;
Microsoft Entra ID&lt;br&gt;
   │&lt;br&gt;
   │ OpenID Connect&lt;br&gt;
   │&lt;br&gt;
   │ User authenticates&lt;br&gt;
   ▼&lt;br&gt;
Microsoft Entra ID&lt;br&gt;
   │&lt;br&gt;
   │ Authentication result&lt;br&gt;
   ▼&lt;br&gt;
ASP.NET Core Application&lt;/p&gt;

&lt;p&gt;Το Entra ID, αφού ολοκληρώσει την ταυτοποίηση, επιστρέφει τα απαραίτητα tokens. Στο OpenID Connect το σημαντικό token για την ταυτότητα του χρήστη είναι το ID Token.&lt;/p&gt;

&lt;p&gt;Το ID Token περιέχει claims, δηλαδή πληροφορίες που περιγράφουν την ταυτότητα του χρήστη, όπως το όνομα ή το username.&lt;/p&gt;

&lt;p&gt;Επομένως η σχέση είναι:&lt;/p&gt;

&lt;p&gt;OpenID Connect&lt;br&gt;
       │&lt;br&gt;
       ▼&lt;br&gt;
   Authentication&lt;br&gt;
       │&lt;br&gt;
       ▼&lt;br&gt;
    ID Token&lt;br&gt;
       │&lt;br&gt;
       ▼&lt;br&gt;
ASP.NET Core&lt;br&gt;
       │&lt;br&gt;
       ▼&lt;br&gt;
ClaimsPrincipal&lt;br&gt;
       │&lt;br&gt;
       ▼&lt;br&gt;
HttpContext.User&lt;/p&gt;

&lt;p&gt;Από την άλλη πλευρά, όταν η εφαρμογή χρειάζεται να αποκτήσει πρόσβαση σε ένα προστατευμένο API, τότε μπαίνει στο παιχνίδι το OAuth 2.0 authorization και χρησιμοποιείται ένα Access Token.&lt;/p&gt;

&lt;p&gt;OAuth 2.0&lt;br&gt;
    │&lt;br&gt;
    ▼&lt;br&gt;
Authorization&lt;br&gt;
    │&lt;br&gt;
    ▼&lt;br&gt;
Access Token&lt;br&gt;
    │&lt;br&gt;
    ▼&lt;br&gt;
Protected API&lt;/p&gt;

&lt;p&gt;Άρα δεν πρέπει να βλέπουμε το OpenID Connect ως ένα ακόμη framework που προσθέτουμε στην εφαρμογή. Είναι το πρωτόκολλο authentication που χρησιμοποιείται από το Microsoft identity platform για να επιβεβαιωθεί η ταυτότητα του χρήστη.&lt;/p&gt;

&lt;p&gt;Στο ASP.NET Core, ο developer συνήθως δεν υλοποιεί χειροκίνητα το OpenID Connect. Η σύνδεση με το Entra ID γίνεται μέσω των authentication libraries και, για εφαρμογές ASP.NET Core, μπορεί να χρησιμοποιηθεί το Microsoft.Identity.Web, το οποίο αναλαμβάνει μεγάλο μέρος της απαιτούμενης integration.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Τι γίνεται μέσα στο ASP.NET Core;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Εδώ βρίσκεται το σημαντικό σημείο για έναν .NET developer.&lt;/p&gt;

&lt;p&gt;Το authentication middleware επεξεργάζεται το αποτέλεσμα του authentication και δημιουργεί ένα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ClaimsPrincipal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό είναι που τελικά βλέπει η εφαρμογή μέσω:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;IsAuthenticated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;μπορεί να επιστρέψει:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;και μπορούμε να διαβάσουμε τα claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claims&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Άρα η διαδικασία είναι:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft Entra ID
        ↓
      Token
        ↓
ASP.NET Core Authentication
        ↓
   ClaimsPrincipal
        ↓
   HttpContext.User
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Και τι κάνει το &lt;code&gt;[Authorize]&lt;/code&gt;;
&lt;/h2&gt;

&lt;p&gt;Το &lt;code&gt;[Authorize]&lt;/code&gt; δεν κάνει authentication από μόνο του. Λέει στο ASP.NET Core ότι το συγκεκριμένο endpoint απαιτεί authenticated user.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αν ο χρήστης είναι authenticated, μπορεί να συνεχίσει.&lt;/p&gt;

&lt;p&gt;Αν δεν είναι authenticated, ενεργοποιείται η διαδικασία login.&lt;/p&gt;

&lt;p&gt;Μπορούμε επίσης να έχουμε authorization με roles ή policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Roles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Admin"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;DeleteUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Εδώ πλέον δεν μας ενδιαφέρει μόνο αν ο χρήστης είναι authenticated. Θέλουμε να έχει και τον κατάλληλο ρόλο.&lt;/p&gt;




&lt;h2&gt;
  
  
  ID Token και Access Token
&lt;/h2&gt;

&lt;p&gt;Εδώ υπάρχει μια πολύ σημαντική διάκριση.&lt;/p&gt;

&lt;p&gt;Το &lt;strong&gt;ID Token&lt;/strong&gt; χρησιμοποιείται κυρίως για να πληροφορήσει την εφαρμογή σχετικά με την ταυτότητα του χρήστη.&lt;/p&gt;

&lt;p&gt;Το &lt;strong&gt;Access Token&lt;/strong&gt; χρησιμοποιείται όταν μια εφαρμογή θέλει να καλέσει ένα προστατευμένο API.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Web Application
 ↓
Orders API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η Web Application μπορεί να χρειαστεί ένα Access Token για να καλέσει το Orders API.&lt;/p&gt;

&lt;p&gt;Το API ελέγχει το token και αποφασίζει αν ο caller έχει την απαιτούμενη πρόσβαση.&lt;/p&gt;

&lt;p&gt;Επομένως:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ID Token    → Ποιος είναι ο χρήστης;
Access Token → Τι επιτρέπεται να χρησιμοποιήσει;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Πού βρίσκεται το Microsoft.Identity.Web;
&lt;/h2&gt;

&lt;p&gt;Στο ASP.NET Core δεν χρειάζεται να υλοποιήσουμε μόνοι μας όλη αυτή τη διαδικασία.&lt;/p&gt;

&lt;p&gt;Η Microsoft παρέχει το &lt;strong&gt;Microsoft.Identity.Web&lt;/strong&gt;, το οποίο διευκολύνει την ενσωμάτωση του Microsoft Entra ID σε ASP.NET Core εφαρμογές.&lt;/p&gt;

&lt;p&gt;Ενδεικτικά:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMicrosoftIdentityWebApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AzureAd"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η βιβλιοθήκη αναλαμβάνει μεγάλο μέρος της integration διαδικασίας με το Microsoft identity platform.&lt;/p&gt;

&lt;p&gt;Σε ένα Web API χρησιμοποιούμε αντίστοιχα το κατάλληλο Microsoft Identity Web API integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Η συνολική εικόνα
&lt;/h2&gt;

&lt;p&gt;Τελικά, η διαδικασία μπορεί να αποτυπωθεί πολύ απλά:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Microsoft Entra ID
                     │
                     │ Login
                     ▼
                  User
                     │
                     │ Token
                     ▼
              ASP.NET Core
                     │
                     ▼
              Authentication
                     │
                     ▼
             ClaimsPrincipal
                     │
                     ▼
              Authorization
                     │
                     ▼
                Controller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το βασικό που πρέπει να καταλάβουμε είναι ότι &lt;strong&gt;το Entra ID αναλαμβάνει την ταυτοποίηση&lt;/strong&gt;, ενώ η εφαρμογή μας χρησιμοποιεί την ταυτότητα που προκύπτει για να εφαρμόσει τους δικούς της κανόνες πρόσβασης.&lt;/p&gt;

&lt;p&gt;Αυτός ο διαχωρισμός είναι ιδιαίτερα σημαντικός στις enterprise εφαρμογές. Η εφαρμογή δεν χρειάζεται να διαχειρίζεται passwords και authentication μηχανισμούς από την αρχή. Αντίθετα, βασίζεται σε έναν κεντρικό Identity Provider και επικεντρώνεται στο authorization και στους business κανόνες της.&lt;/p&gt;

&lt;p&gt;Με αυτόν τον τρόπο το authentication γίνεται ένα ξεχωριστό κομμάτι της αρχιτεκτονικής και μπορεί να χρησιμοποιηθεί με συνέπεια σε πολλές εφαρμογές και APIs.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>dotnet</category>
      <category>microsoft</category>
      <category>security</category>
    </item>
    <item>
      <title>Κατανοώντας τα .NET Assemblies στην Clean Architecture: Από τα Compilation Boundaries στα Architecture Tests</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Mon, 24 Aug 2026 10:16:10 +0000</pubDate>
      <link>https://dev.to/__b63657/katanoontas-ta-net-assemblies-sten-clean-architecture-apo-ta-compilation-boundaries-sta-3c5e</link>
      <guid>https://dev.to/__b63657/katanoontas-ta-net-assemblies-sten-clean-architecture-apo-ta-compilation-boundaries-sta-3c5e</guid>
      <description>&lt;p&gt;Όταν συζητάμε για Clean Architecture στο .NET, η προσοχή συνήθως στρέφεται σε έννοιες όπως το Domain Layer, το Application Layer, το Infrastructure Layer, το Dependency Inversion Principle, τα repositories και τα use cases.&lt;/p&gt;

&lt;p&gt;Όλες αυτές οι έννοιες είναι θεμελιώδεις. Ωστόσο, πίσω από αυτές υπάρχει ένας μηχανισμός του .NET που δίνει στις αρχιτεκτονικές μας αποφάσεις μια πραγματική τεχνική υπόσταση: το Assembly.&lt;/p&gt;

&lt;p&gt;Η κατανόηση ενός assembly δεν περιορίζεται στο ότι «το build παράγει ένα .dll αρχείο». Σε μία σωστά σχεδιασμένη εφαρμογή, ένα assembly μπορεί να αποτελέσει ένα πραγματικό architectural boundary, ένα όριο μεταγλώττισης που διαχωρίζει διαφορετικές ευθύνες του συστήματος.&lt;/p&gt;

&lt;p&gt;Από τη στιγμή που αντιληφθούμε αυτή τη σχέση, τα Architecture Tests αποκτούν πολύ μεγαλύτερη σημασία. Δεν αποτελούν απλώς tests που ελέγχουν naming conventions ή namespaces. Μπορούν να επαληθεύσουν ότι η πραγματική, compiled δομή του συστήματος εξακολουθεί να ακολουθεί το dependency graph που σχεδιάσαμε.&lt;/p&gt;

&lt;p&gt;Σε αυτό το άρθρο θα εξετάσουμε τη σχέση μεταξύ Projects, Assemblies, Reflection, Clean Architecture και Architecture Tests, χρησιμοποιώντας ως παράδειγμα μία εφαρμογή ASP.NET Core.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Από τον C# κώδικα στο Assembly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ας θεωρήσουμε μία τυπική λύση Clean Architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TestMyApp&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  TestMyApp.Domain &lt;/li&gt;
&lt;li&gt;  TestMyApp.Application &lt;/li&gt;
&lt;li&gt;  TestMyApp.Infrastructure &lt;/li&gt;
&lt;li&gt;  TestMyApp.Api&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Μέσα στο Visual Studio βλέπουμε τέσσερα διαφορετικά projects.&lt;/p&gt;

&lt;p&gt;Σε πρώτη ανάγνωση θα μπορούσε κανείς να θεωρήσει ότι πρόκειται απλώς για έναν τρόπο καλύτερης οργάνωσης του source code.&lt;/p&gt;

&lt;p&gt;Η πραγματικότητα όμως είναι βαθύτερη.&lt;/p&gt;

&lt;p&gt;Όταν πραγματοποιούμε build τη solution, κάθε project μεταγλωττίζεται και παράγει συνήθως το δικό του assembly.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;TestMyApp.Domain&lt;br&gt;
              │&lt;br&gt;
              │ Build&lt;br&gt;
              ▼&lt;br&gt;
TestMyApp.Domain.dll&lt;/p&gt;

&lt;p&gt;Αντίστοιχα:&lt;/p&gt;

&lt;p&gt;TestMyApp.Application&lt;br&gt;
              │&lt;br&gt;
              │ Build&lt;br&gt;
              ▼&lt;br&gt;
TestMyApp.Application.dll&lt;/p&gt;

&lt;p&gt;και:&lt;/p&gt;

&lt;p&gt;TestMyApp.Infrastructure&lt;br&gt;
              │&lt;br&gt;
              │ Build&lt;br&gt;
              ▼&lt;br&gt;
TestMyApp.Infrastructure.dll&lt;/p&gt;

&lt;p&gt;Επομένως, το assembly δεν αποτελεί απλώς μία σύμβαση οργάνωσης που εισάγει το Visual Studio.&lt;/p&gt;

&lt;p&gt;Είναι ένα πραγματικό compilation artifact.&lt;/p&gt;

&lt;p&gt;Σε υψηλό επίπεδο, η διαδικασία μπορεί να απεικονιστεί ως:&lt;/p&gt;

&lt;p&gt;C# Source Code  --&amp;gt; C# Compiler&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      Assembly 
         IL  
      Metadata
        Types 
       Members
      Assembly 
     References 
         │ 
         ▼ 
   .NET Runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Ένα .NET assembly περιλαμβάνει compiled Intermediate Language, metadata για τους τύπους που περιέχει, πληροφορίες σχετικά με το ίδιο το assembly και references προς άλλα assemblies.&lt;/p&gt;

&lt;p&gt;Αυτό ακριβώς το χαρακτηριστικό είναι που το κάνει ιδιαίτερα σημαντικό από αρχιτεκτονικής πλευράς.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Assembly και Namespace δεν είναι το ίδιο πράγμα&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένα από τα σημεία που δημιουργούν συχνά σύγχυση είναι η διαφορά μεταξύ &lt;strong&gt;namespace&lt;/strong&gt; και &lt;strong&gt;assembly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ας υποθέσουμε ότι το Domain project έχει την ακόλουθη δομή:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqeofug4c4nvmqurrjn3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqeofug4c4nvmqurrjn3.png" alt=" " width="236" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Οι παραπάνω classes μπορεί να ανήκουν στα namespaces:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TestMyApp.Domain.Users &lt;br&gt;
TestMyApp.Domain.Orders &lt;br&gt;
TestMyApp.Domain.Products&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Όμως όλες μπορούν να βρίσκονται μέσα στο ίδιο assembly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TestMyApp.Domain.dll&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Το &lt;strong&gt;namespace&lt;/strong&gt; αποτελεί κυρίως έναν λογικό μηχανισμό ονομασίας και οργάνωσης των types.&lt;/p&gt;

&lt;p&gt;Το &lt;strong&gt;assembly&lt;/strong&gt;, αντίθετα, αποτελεί πραγματική μονάδα compilation και deployment του .NET.&lt;/p&gt;

&lt;p&gt;Η διαφορά αυτή έχει ιδιαίτερη αρχιτεκτονική σημασία.&lt;/p&gt;

&lt;p&gt;Θα μπορούσαμε να έχουμε ένα μόνο project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7909oixa26487c88hvgt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7909oixa26487c88hvgt.png" alt=" " width="223" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;και να ισχυριστούμε ότι έχουμε Clean Architecture επειδή δημιουργήσαμε τέσσερα folders.&lt;/p&gt;

&lt;p&gt;Ωστόσο, μετά το build θα μπορούσαμε να καταλήξουμε σε:&lt;/p&gt;

&lt;p&gt;MyApplication.dll&lt;/p&gt;

&lt;p&gt;Όλα τα layers θα βρίσκονταν μέσα στο ίδιο assembly.&lt;/p&gt;

&lt;p&gt;Τα directories θα μας έδιναν λογική οργάνωση, αλλά όχι πραγματικά compilation boundaries.&lt;/p&gt;

&lt;p&gt;Αντίθετα, όταν δημιουργούμε:&lt;/p&gt;

&lt;p&gt;Domain.csproj&lt;br&gt;
Application.csproj&lt;br&gt;
Infrastructure.csproj&lt;br&gt;
Api.csproj&lt;/p&gt;

&lt;p&gt;δημιουργούμε ξεχωριστές μονάδες compilation.&lt;/p&gt;

&lt;p&gt;Μετά το build έχουμε:&lt;/p&gt;

&lt;p&gt;Domain.dll&lt;br&gt;
Application.dll&lt;br&gt;
Infrastructure.dll&lt;br&gt;
Api.dll&lt;/p&gt;

&lt;p&gt;Επομένως, η αρχιτεκτονική παύει να αποτελεί απλώς μία οργάνωση directories και αρχίζει να αποτυπώνεται στην πραγματική compiled δομή του συστήματος.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Το Assembly ως Architectural Boundary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Μία από τις σημαντικότερες αρχές της Clean Architecture αφορά την κατεύθυνση των dependencies.&lt;/p&gt;

&lt;p&gt;Σε μία απλοποιημένη μορφή μπορούμε να έχουμε:&lt;/p&gt;

&lt;p&gt;API&lt;br&gt;
              │&lt;br&gt;
              ▼&lt;br&gt;
       Infrastructure&lt;br&gt;
              │&lt;br&gt;
              ▼&lt;br&gt;
        Application&lt;br&gt;
              │&lt;br&gt;
              ▼&lt;br&gt;
           Domain&lt;/p&gt;

&lt;p&gt;Η ουσία δεν βρίσκεται απλώς στο ότι έχουμε τέσσερα κουτιά.&lt;/p&gt;

&lt;p&gt;Η ουσία βρίσκεται στο ποιος επιτρέπεται να γνωρίζει ποιον.&lt;/p&gt;

&lt;p&gt;Το Domain αποτελεί τον πυρήνα της εφαρμογής.&lt;/p&gt;

&lt;p&gt;Δεν θα πρέπει να γνωρίζει πώς αποθηκεύονται τα δεδομένα.&lt;/p&gt;

&lt;p&gt;Δεν θα πρέπει να γνωρίζει τι είναι HTTP request.&lt;/p&gt;

&lt;p&gt;Δεν θα πρέπει να γνωρίζει τι είναι ASP.NET Core Controller.&lt;/p&gt;

&lt;p&gt;Δεν θα πρέπει να γνωρίζει τι είναι Microsoft Entra ID.&lt;/p&gt;

&lt;p&gt;Δεν θα πρέπει να γνωρίζει τι είναι Entity Framework Core.&lt;/p&gt;

&lt;p&gt;Ένα Domain Entity θα πρέπει να μπορεί να είναι κάτι τόσο καθαρό όσο:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Confirm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// Business rule } &lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Δεν θα θέλαμε μέσα στο Domain να εμφανίζονται dependencies όπως:&lt;/p&gt;

&lt;p&gt;Microsoft.AspNetCore.*&lt;br&gt;
Microsoft.EntityFrameworkCore.*&lt;br&gt;
Microsoft.Identity.*&lt;/p&gt;

&lt;p&gt;Αυτό σημαίνει ότι αρχιτεκτονικά θέλουμε:&lt;/p&gt;

&lt;p&gt;Domain  X ───&amp;gt; Infrastructure&lt;br&gt;
Domain  X ───&amp;gt; API&lt;/p&gt;

&lt;p&gt;Το Domain δεν πρέπει να εξαρτάται από αυτά.&lt;/p&gt;

&lt;p&gt;Όταν κάθε layer βρίσκεται σε διαφορετικό project, αυτή η αρχιτεκτονική αρχίζει να αποκτά φυσική υπόσταση μέσα από τα assemblies.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Project References και Assembly Dependencies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ας υποθέσουμε ότι στο Visual Studio προσθέτουμε:&lt;/p&gt;

&lt;p&gt;Application&lt;br&gt;
      │&lt;br&gt;
      ▼&lt;br&gt;
    Domain&lt;/p&gt;

&lt;p&gt;ως Project Reference.&lt;/p&gt;

&lt;p&gt;Αυτό δεν είναι απλώς μία ρύθμιση του Visual Studio.&lt;/p&gt;

&lt;p&gt;Δηλώνουμε ότι το Application έχει δικαίωμα να χρησιμοποιεί types του Domain.&lt;/p&gt;

&lt;p&gt;Μετά το compilation, η σχέση αυτή αποτυπώνεται στα assemblies:&lt;/p&gt;

&lt;p&gt;Application.dll&lt;br&gt;
      │&lt;br&gt;
      ▼&lt;br&gt;
Domain.dll&lt;/p&gt;

&lt;p&gt;Έτσι το Application μπορεί να χρησιμοποιήσει:&lt;/p&gt;

&lt;p&gt;using TestMyApp.Domain.Users;&lt;/p&gt;

&lt;p&gt;και στη συνέχεια:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserHandler&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// ... } &lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Η αντίστροφη σχέση όμως δεν πρέπει να υπάρχει.&lt;/p&gt;

&lt;p&gt;Δεν θέλουμε το Domain να περιέχει:&lt;/p&gt;

&lt;p&gt;using TestMyApp.Application;&lt;/p&gt;

&lt;p&gt;Η κατεύθυνση:&lt;/p&gt;

&lt;p&gt;Application → Domain&lt;/p&gt;

&lt;p&gt;είναι επιτρεπτή.&lt;/p&gt;

&lt;p&gt;Η κατεύθυνση:&lt;/p&gt;

&lt;p&gt;Domain → Application&lt;/p&gt;

&lt;p&gt;παραβιάζει το dependency direction που έχουμε επιλέξει.&lt;/p&gt;

&lt;p&gt;Εδώ γίνεται εμφανής μία σημαντική αρχή:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ένα Project Reference δεν είναι απλώς configuration του IDE. Συμμετέχει άμεσα στη dependency topology του compiled συστήματος.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;Τι περιέχει ένα Assembly;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένα assembly δεν περιέχει απλώς τον C# source code όπως τον γράψαμε.&lt;/p&gt;

&lt;p&gt;Σε υψηλό επίπεδο μπορούμε να το αντιληφθούμε ως:&lt;/p&gt;

&lt;p&gt;TestMyApp.Domain.dll &lt;/p&gt;

&lt;p&gt;Intermediate Language &lt;br&gt;
  Metadata &lt;br&gt;
  Types &lt;br&gt;
  User&lt;br&gt;&lt;br&gt;
  Order&lt;br&gt;&lt;br&gt;
  Role&lt;br&gt;&lt;br&gt;
  Permission &lt;/p&gt;

&lt;p&gt;Members &lt;br&gt;
  Methods &lt;br&gt;
  Properties &lt;br&gt;
  Fields &lt;/p&gt;

&lt;p&gt;Assembly References&lt;/p&gt;

&lt;p&gt;Τα metadata είναι εξαιρετικά σημαντικά.&lt;/p&gt;

&lt;p&gt;Επιτρέπουν στο .NET Runtime αλλά και στον δικό μας κώδικα να ανακαλύψει πληροφορίες σχετικά με τη compiled εφαρμογή.&lt;/p&gt;

&lt;p&gt;Αυτό μας οδηγεί σε μία από τις σημαντικότερες δυνατότητες του .NET: το Reflection.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Reflection: Παρατηρώντας τη δομή της εφαρμογής&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ας θεωρήσουμε ότι έχουμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Μπορούμε να γράψουμε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var type = typeof(User);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Το typeof(User) δεν δημιουργεί ένα User object.&lt;/p&gt;

&lt;p&gt;Δεν είναι:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;new User();&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Αντίθετα, ζητάμε από το .NET πληροφορίες σχετικά με τον type User.&lt;/p&gt;

&lt;p&gt;Μπορούμε για παράδειγμα να εξετάσουμε:&lt;/p&gt;

&lt;p&gt;type.Name&lt;/p&gt;

&lt;p&gt;ή:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;type.Namespace&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;ή ακόμη πιο ενδιαφέρον:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;type.Assembly&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Άρα:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var assembly = typeof(User).Assembly;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;σημαίνει ουσιαστικά:&lt;/p&gt;

&lt;p&gt;Σε ποιο assembly βρίσκεται ο συγκεκριμένος type;&lt;/p&gt;

&lt;p&gt;Αν ο User βρίσκεται στο Domain project, τότε το αποτέλεσμα αντιστοιχεί στο:&lt;/p&gt;

&lt;p&gt;TestMyApp.Domain.dll&lt;/p&gt;

&lt;p&gt;Έτσι δημιουργείται μία πολύ ενδιαφέρουσα σύνδεση:&lt;/p&gt;

&lt;p&gt;Type &lt;br&gt;
    │ &lt;br&gt;
    ▼ &lt;br&gt;
Reflection &lt;br&gt;
    │ &lt;br&gt;
    ▼ &lt;br&gt;
 Assembly &lt;br&gt;
    │ &lt;br&gt;
    ▼ &lt;br&gt;
Compiled Architecture&lt;/p&gt;

&lt;p&gt;Μπορούμε πλέον να εξετάσουμε προγραμματιστικά τη δομή της εφαρμογής μας.&lt;/p&gt;




&lt;p&gt;Το AssemblyReference Pattern&lt;/p&gt;

&lt;p&gt;Αν θέλουμε να πάρουμε το Domain assembly, θα μπορούσαμε να χρησιμοποιήσουμε:&lt;/p&gt;

&lt;p&gt;typeof(User).Assembly&lt;/p&gt;

&lt;p&gt;Τεχνικά αυτό λειτουργεί.&lt;/p&gt;

&lt;p&gt;Δεν είναι όμως η καλύτερη δυνατή επιλογή.&lt;/p&gt;

&lt;p&gt;Ο User αποτελεί business concept.&lt;/p&gt;

&lt;p&gt;Μπορεί κάποια στιγμή να μετονομαστεί, να μετακινηθεί ή ακόμη και να αφαιρεθεί.&lt;/p&gt;

&lt;p&gt;Για αυτό χρησιμοποιείται συχνά ένα απλό marker type:&lt;/p&gt;

&lt;p&gt;namespace TestMyApp.Domain;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;public sealed class AssemblyReference&lt;br&gt;
{&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Η συγκεκριμένη class φαίνεται αρχικά περίεργη.&lt;/p&gt;

&lt;p&gt;Δεν έχει properties.&lt;/p&gt;

&lt;p&gt;Δεν έχει methods.&lt;/p&gt;

&lt;p&gt;Δεν έχει business logic.&lt;/p&gt;

&lt;p&gt;Δεν προορίζεται για:&lt;/p&gt;

&lt;p&gt;new AssemblyReference();&lt;/p&gt;

&lt;p&gt;Ο ρόλος της είναι διαφορετικός.&lt;/p&gt;

&lt;p&gt;Αποτελεί ένα σταθερό σημείο αναφοράς προς το assembly.&lt;/p&gt;

&lt;p&gt;Έτσι μπορούμε να γράψουμε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var assembly = typeof(AssemblyReference).Assembly;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Η λογική είναι:&lt;/p&gt;

&lt;p&gt;AssemblyReference&lt;br&gt;
       │&lt;br&gt;
       │ βρίσκεται στο&lt;br&gt;
       ▼&lt;br&gt;
Domain Project&lt;br&gt;
       │&lt;br&gt;
       │ γίνεται compile σε&lt;br&gt;
       ▼&lt;br&gt;
TestMyApp.Domain.dll&lt;/p&gt;

&lt;p&gt;Άρα:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;typeof(AssemblyReference).Assembly&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;μας δίνει με σαφή και type-safe τρόπο το Domain assembly.&lt;/p&gt;

&lt;p&gt;Η συγκεκριμένη class δεν μοντελοποιεί κάποιο business concept.&lt;/p&gt;

&lt;p&gt;Αποτελεί έναν architectural marker.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Γιατί να μην χρησιμοποιήσουμε απλώς ένα string;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Θα μπορούσαμε θεωρητικά να προσπαθήσουμε να φορτώσουμε:&lt;/p&gt;

&lt;p&gt;"TestMyApp.Domain"&lt;/p&gt;

&lt;p&gt;μέσω Reflection APIs.&lt;/p&gt;

&lt;p&gt;Ωστόσο, η χρήση:&lt;/p&gt;

&lt;p&gt;typeof(AssemblyReference).Assembly&lt;/p&gt;

&lt;p&gt;είναι ιδιαίτερα απλή και ασφαλής.&lt;/p&gt;

&lt;p&gt;Δεν βασιζόμαστε σε κάποιο αυθαίρετο business type και δεν χρειάζεται να διαχειριστούμε χειροκίνητα assembly names.&lt;/p&gt;

&lt;p&gt;Το ίδιο pattern μπορεί να χρησιμοποιηθεί σε όλα τα layers:&lt;/p&gt;

&lt;p&gt;Domain&lt;br&gt;
└── AssemblyReference.cs&lt;/p&gt;

&lt;p&gt;Application&lt;br&gt;
└── AssemblyReference.cs&lt;/p&gt;

&lt;p&gt;Infrastructure&lt;br&gt;
└── AssemblyReference.cs&lt;/p&gt;

&lt;p&gt;Api&lt;br&gt;
└── AssemblyReference.cs&lt;/p&gt;

&lt;p&gt;Έτσι κάθε assembly αποκτά ένα σαφές entry point για reflection-based operations.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Από την αρχιτεκτονική σύμβαση στο Architecture Test&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ας υποθέσουμε ότι έχουμε αποφασίσει:&lt;/p&gt;

&lt;p&gt;Το Domain δεν επιτρέπεται να εξαρτάται από Application, Infrastructure ή API.&lt;/p&gt;

&lt;p&gt;Το να γράψουμε αυτή την πρόταση σε ένα README είναι χρήσιμο.&lt;/p&gt;

&lt;p&gt;Το να τη σχεδιάσουμε σε ένα architecture diagram είναι επίσης χρήσιμο.&lt;/p&gt;

&lt;p&gt;Όμως τίποτα από αυτά δεν εμποδίζει έναν developer να παραβιάσει τον κανόνα έξι μήνες αργότερα.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, κάποιος μπορεί να εισαγάγει στο Domain μία εξάρτηση προς Infrastructure.&lt;/p&gt;

&lt;p&gt;Η εφαρμογή μπορεί να συνεχίσει να λειτουργεί.&lt;/p&gt;

&lt;p&gt;Τα business unit tests μπορεί να συνεχίσουν να περνούν.&lt;/p&gt;

&lt;p&gt;Το API μπορεί να επιστρέφει κανονικά 200 OK.&lt;/p&gt;

&lt;p&gt;Παρόλα αυτά, η αρχιτεκτονική έχει υποβαθμιστεί.&lt;/p&gt;

&lt;p&gt;Αυτό μας οδηγεί στη διάκριση μεταξύ δύο διαφορετικών μορφών correctness:&lt;/p&gt;

&lt;p&gt;Behavioural Correctness&lt;br&gt;
        και&lt;br&gt;
Structural Correctness&lt;/p&gt;

&lt;p&gt;Ένα κλασικό Unit Test εξετάζει συνήθως:&lt;/p&gt;

&lt;p&gt;Για συγκεκριμένο input,&lt;br&gt;
παράγει η μέθοδος το σωστό output;&lt;/p&gt;

&lt;p&gt;Ένα Architecture Test εξετάζει κάτι διαφορετικό:&lt;/p&gt;

&lt;p&gt;Η compiled δομή του συστήματος&lt;br&gt;
εξακολουθεί να ακολουθεί&lt;br&gt;
τους αρχιτεκτονικούς κανόνες;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Architecture Testing με NetArchTest&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένα χαρακτηριστικό architecture test μπορεί να είναι:&lt;/p&gt;

&lt;p&gt;`using NetArchTest.Rules;&lt;br&gt;
using TestMyApp.Domain;&lt;/p&gt;

&lt;p&gt;namespace TestMyApp.ArchitectureTests;&lt;/p&gt;

&lt;p&gt;public class DomainArchitectureTests&lt;br&gt;
{&lt;br&gt;
    [Fact]&lt;br&gt;
    public void Domain_Should_Not_Have_Dependency_On_Other_Layers()&lt;br&gt;
    {&lt;br&gt;
        var assembly = typeof(AssemblyReference).Assembly;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    var result = Types
        .InAssembly(assembly)
        .ShouldNot()
        .HaveDependencyOnAny(
            "TestAILoginAuthoAuthed.Application",
            "TestAILoginAuthoAuthed.Infrastructure",
            "TestAILoginAuthoAuthed.Api")
        .GetResult();

    Assert.True(result.IsSuccessful);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;Παρότι πρόκειται για ελάχιστο κώδικα, το test εκφράζει μία σημαντική αρχιτεκτονική πολιτική.&lt;/p&gt;

&lt;p&gt;Ας εξετάσουμε τι πραγματικά συμβαίνει.&lt;/p&gt;

&lt;p&gt;Αρχικά:&lt;/p&gt;

&lt;p&gt;var assembly = typeof(AssemblyReference).Assembly;&lt;/p&gt;

&lt;p&gt;εντοπίζουμε το Domain assembly.&lt;/p&gt;

&lt;p&gt;Στη συνέχεια:&lt;/p&gt;

&lt;p&gt;Types.InAssembly(assembly)&lt;/p&gt;

&lt;p&gt;ζητάμε από το NetArchTest να εξετάσει τους types που υπάρχουν μέσα στο συγκεκριμένο assembly.&lt;/p&gt;

&lt;p&gt;Εννοιολογικά:&lt;/p&gt;

&lt;p&gt;TestMyApp.Domain.dll&lt;br&gt;
              │&lt;br&gt;
              ▼&lt;br&gt;
       Examine Types&lt;br&gt;
              │&lt;br&gt;
      ┌───────┼────────┐&lt;br&gt;
      ▼       ▼        ▼&lt;br&gt;
    User    Order     Role&lt;/p&gt;

&lt;p&gt;Στη συνέχεια:&lt;/p&gt;

&lt;p&gt;.ShouldNot()&lt;br&gt;
.HaveDependencyOnAny(...)&lt;/p&gt;

&lt;p&gt;εκφράζει τον αρχιτεκτονικό περιορισμό.&lt;/p&gt;

&lt;p&gt;Ουσιαστικά το test ρωτά:&lt;/p&gt;

&lt;p&gt;Για τους types του Domain assembly:&lt;/p&gt;

&lt;p&gt;Υπάρχει dependency προς Application;&lt;br&gt;
Υπάρχει dependency προς Infrastructure;&lt;br&gt;
Υπάρχει dependency προς API?&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     │
┌────┴────┐
│         │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;ΝΑΙ       ΟΧΙ&lt;br&gt;
    │         │&lt;br&gt;
    ▼         ▼&lt;br&gt;
  FAIL       PASS&lt;/p&gt;

&lt;p&gt;Έτσι ένας κανόνας που προηγουμένως υπήρχε μόνο ως αρχιτεκτονική πρόθεση μετατρέπεται σε εκτελέσιμο specification.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Unit Tests και Architecture Tests προστατεύουν διαφορετικά πράγματα&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ας θεωρήσουμε ένα Unit Test:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[Fact]&lt;br&gt;
public void Order_Should_Calculate_Total()&lt;br&gt;
{&lt;br&gt;
    // ...&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Το συγκεκριμένο test προστατεύει μία επιχειρησιακή συμπεριφορά.&lt;/p&gt;

&lt;p&gt;Θέλουμε να γνωρίζουμε ότι το Order υπολογίζει σωστά το total.&lt;/p&gt;

&lt;p&gt;Αντίθετα:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[Fact]&lt;br&gt;
public void Domain_Should_Not_Depend_On_Infrastructure()&lt;br&gt;
{&lt;br&gt;
    // ...&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;δεν ενδιαφέρεται για το αποτέλεσμα κάποιου business calculation.&lt;/p&gt;

&lt;p&gt;Προστατεύει τη δομή του συστήματος.&lt;/p&gt;

&lt;p&gt;Έχουμε επομένως:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Software Correctness
                      │
           ┌──────────┴──────────┐
           │                     │
           ▼                     ▼
   Behavioural Rules       Structural Rules
           │                     │
           ▼                     ▼
      Unit Tests          Architecture Tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Αυτές οι δύο κατηγορίες tests είναι συμπληρωματικές.&lt;/p&gt;

&lt;p&gt;Το γεγονός ότι μία εφαρμογή παράγει σωστά αποτελέσματα δεν σημαίνει ότι διαθέτει σωστή αρχιτεκτονική.&lt;/p&gt;

&lt;p&gt;Και αντίστροφα, μία άψογη dependency structure δεν σημαίνει ότι το business logic λειτουργεί σωστά.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Η έννοια του Architectural Fitness Function&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένας ιδιαίτερα χρήσιμος τρόπος να αντιληφθούμε τα Architecture Tests είναι ως Architectural Fitness Functions.&lt;/p&gt;

&lt;p&gt;Κατά τον σχεδιασμό του συστήματος αποφασίζουμε ότι ορισμένες ιδιότητες πρέπει να παραμένουν αληθείς όσο εξελίσσεται η εφαρμογή.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;Το Domain πρέπει να παραμένει ανεξάρτητο.&lt;/p&gt;

&lt;p&gt;Το Application δεν πρέπει να εξαρτάται από Infrastructure.&lt;/p&gt;

&lt;p&gt;Οι Controllers δεν πρέπει να χρησιμοποιούν απευθείας DbContext.&lt;/p&gt;

&lt;p&gt;Το Domain δεν πρέπει να γνωρίζει το Entity Framework Core.&lt;/p&gt;

&lt;p&gt;Το Domain δεν πρέπει να γνωρίζει το ASP.NET Core.&lt;/p&gt;

&lt;p&gt;Το persistence implementation πρέπει να παραμένει εκτός Domain.&lt;/p&gt;

&lt;p&gt;Τα Architecture Tests μπορούν να αξιολογούν συνεχώς αυτές τις ιδιότητες.&lt;/p&gt;

&lt;p&gt;Έτσι μετακινούμαστε από ένα μοντέλο:&lt;/p&gt;

&lt;p&gt;Architecture&lt;br&gt;
    =&lt;br&gt;
Documentation&lt;br&gt;
    +&lt;br&gt;
Developer Discipline&lt;/p&gt;

&lt;p&gt;σε ένα ισχυρότερο μοντέλο:&lt;/p&gt;

&lt;p&gt;Architecture&lt;br&gt;
    =&lt;br&gt;
Documentation&lt;br&gt;
    +&lt;br&gt;
Compilation Boundaries&lt;br&gt;
    +&lt;br&gt;
Automated Verification&lt;/p&gt;

&lt;p&gt;Αυτό είναι ιδιαίτερα σημαντικό σε συστήματα που πρόκειται να συντηρηθούν για χρόνια.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Το πραγματικό νόημα ενός failing Architecture Test&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένα Architecture Test γίνεται ακόμη πιο κατανοητό όταν το δούμε να αποτυγχάνει.&lt;/p&gt;

&lt;p&gt;Ας υποθέσουμε ότι κάποιος δημιουργεί dependency:&lt;/p&gt;

&lt;p&gt;Domain&lt;br&gt;
   │&lt;br&gt;
   └────────────► Infrastructure&lt;/p&gt;

&lt;p&gt;ενώ η αρχιτεκτονική απαιτεί:&lt;/p&gt;

&lt;p&gt;Domain&lt;br&gt;
   X────────────► Infrastructure&lt;/p&gt;

&lt;p&gt;Το application μπορεί να εξακολουθεί να κάνει compile.&lt;/p&gt;

&lt;p&gt;Ένα endpoint μπορεί να συνεχίζει να επιστρέφει:&lt;/p&gt;

&lt;p&gt;200 OK&lt;/p&gt;

&lt;p&gt;Όμως το architecture test θα αποτύχει.&lt;/p&gt;

&lt;p&gt;Αυτό δεν σημαίνει απαραίτητα ότι έχουμε functional bug.&lt;/p&gt;

&lt;p&gt;Σημαίνει ότι έχουμε architectural regression.&lt;/p&gt;

&lt;p&gt;Η διάκριση είναι σημαντική.&lt;/p&gt;

&lt;p&gt;Ένα failing business test μπορεί να μας πει:&lt;/p&gt;

&lt;p&gt;Η εφαρμογή δεν συμπεριφέρεται όπως πρέπει.&lt;/p&gt;

&lt;p&gt;Ένα failing architecture test μπορεί να μας πει:&lt;/p&gt;

&lt;p&gt;Η εφαρμογή εξακολουθεί ίσως να συμπεριφέρεται σωστά, αλλά η εσωτερική της δομή έχει αρχίσει να αποκλίνει από την αρχιτεκτονική που συμφωνήσαμε.&lt;/p&gt;

&lt;p&gt;Σε ένα CI/CD pipeline αυτό είναι εξαιρετικά ισχυρό.&lt;/p&gt;

&lt;p&gt;Ένα Pull Request που εισάγει απαγορευμένο dependency μπορεί να αποτύχει πριν γίνει merge.&lt;/p&gt;

&lt;p&gt;Η αρχιτεκτονική αποκτά επομένως έναν βαθμό αυτοπροστασίας.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Γιατί έχει μεγαλύτερη σημασία σε μεγάλα συστήματα&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Σε μία εφαρμογή δύο χιλιάδων γραμμών κώδικα, ένας developer μπορεί ίσως να γνωρίζει ολόκληρο το dependency graph.&lt;/p&gt;

&lt;p&gt;Σε ένα production σύστημα εκατοντάδων χιλιάδων γραμμών αυτό δεν είναι ρεαλιστικό.&lt;/p&gt;

&lt;p&gt;Οι ομάδες αλλάζουν.&lt;/p&gt;

&lt;p&gt;Νέοι developers προστίθενται.&lt;/p&gt;

&lt;p&gt;Τα requirements μεταβάλλονται.&lt;/p&gt;

&lt;p&gt;Νέα integrations εμφανίζονται.&lt;/p&gt;

&lt;p&gt;Τα deadlines δημιουργούν πίεση.&lt;/p&gt;

&lt;p&gt;Το Infrastructure μεγαλώνει.&lt;/p&gt;

&lt;p&gt;Χωρίς μηχανισμούς ελέγχου, μία αρχικά καθαρή αρχιτεκτονική μπορεί σταδιακά να μετατραπεί σε:&lt;/p&gt;

&lt;p&gt;Domain ─────────► Infrastructure&lt;br&gt;
   ▲                   │&lt;br&gt;
   │                   ▼&lt;br&gt;
Application ◄──────── API&lt;br&gt;
   │                   ▲&lt;br&gt;
   └───────────────────┘&lt;/p&gt;

&lt;p&gt;Το σύστημα μπορεί να εξακολουθεί να λειτουργεί, αλλά η δυνατότητα ανεξάρτητης εξέλιξης των components έχει περιοριστεί σημαντικά.&lt;/p&gt;

&lt;p&gt;Αυτό το φαινόμενο συχνά περιγράφεται ως architectural erosion.&lt;/p&gt;

&lt;p&gt;Τα compilation boundaries και τα architecture tests δεν εξαφανίζουν το πρόβλημα, αλλά δημιουργούν έναν σημαντικό μηχανισμό αντίστασης απέναντί του.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Τα Assemblies δεν δημιουργούν από μόνα τους Clean Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Υπάρχει, ωστόσο, μία σημαντική παγίδα.&lt;/p&gt;

&lt;p&gt;Το γεγονός ότι δημιουργήσαμε:&lt;/p&gt;

&lt;p&gt;Domain&lt;br&gt;
Application&lt;br&gt;
Infrastructure&lt;br&gt;
Api&lt;/p&gt;

&lt;p&gt;ως τέσσερα διαφορετικά projects δεν σημαίνει αυτομάτως ότι έχουμε Clean Architecture.&lt;/p&gt;

&lt;p&gt;Μπορούμε κάλλιστα να δημιουργήσουμε τέσσερα assemblies με λανθασμένα dependencies.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;Domain ───────► Infrastructure&lt;br&gt;
Application ──► API&lt;br&gt;
Infrastructure ► Application&lt;br&gt;
API ──────────► Domain&lt;/p&gt;

&lt;p&gt;Τα projects από μόνα τους δεν λύνουν το πρόβλημα.&lt;/p&gt;

&lt;p&gt;Παρέχουν όμως έναν μηχανισμό πάνω στον οποίο μπορούμε να επιβάλουμε πραγματικά boundaries.&lt;/p&gt;

&lt;p&gt;Η αρχιτεκτονική απαιτεί πρώτα συνειδητές αποφάσεις σχετικά με τα dependencies.&lt;/p&gt;

&lt;p&gt;Στη συνέχεια τα assemblies και τα tests μπορούν να βοηθήσουν στην επιβολή αυτών των αποφάσεων.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Architecture Tests χωρίς αρχιτεκτονικό λόγο είναι απλώς ceremony&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το ίδιο ισχύει και για τα Architecture Tests.&lt;/p&gt;

&lt;p&gt;Δεν έχει ιδιαίτερη αξία να δημιουργούμε δεκάδες tests μόνο και μόνο επειδή μπορούμε.&lt;/p&gt;

&lt;p&gt;Κάθε architectural rule θα πρέπει να απαντά στην ερώτηση:&lt;/p&gt;

&lt;p&gt;Ποια σημαντική ιδιότητα του συστήματος προστατεύει αυτός ο κανόνας;&lt;/p&gt;

&lt;p&gt;Για παράδειγμα:&lt;/p&gt;

&lt;p&gt;Domain must not depend on Infrastructure&lt;/p&gt;

&lt;p&gt;έχει ξεκάθαρη αιτιολόγηση.&lt;/p&gt;

&lt;p&gt;Προστατεύει την ανεξαρτησία των business rules από τεχνολογικές λεπτομέρειες.&lt;/p&gt;

&lt;p&gt;Αντίθετα, ένας αυθαίρετος κανόνας naming χωρίς πραγματικό architectural impact μπορεί να δημιουργήσει περισσότερο θόρυβο παρά αξία.&lt;/p&gt;

&lt;p&gt;Τα Architecture Tests είναι περισσότερο χρήσιμα όταν προστατεύουν:&lt;/p&gt;

&lt;p&gt;Dependency Direction&lt;br&gt;
Coupling&lt;br&gt;
Separation of Concerns&lt;br&gt;
Replaceability&lt;br&gt;
Testability&lt;br&gt;
Technology Independence&lt;/p&gt;

&lt;p&gt;και όχι όταν χρησιμοποιούνται απλώς ως μηχανισμός επιβολής προσωπικών προτιμήσεων.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Η συνολική σχέση&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Μπορούμε πλέον να ενώσουμε όλες τις έννοιες σε μία ακολουθία:&lt;/p&gt;

&lt;p&gt;Architectural Decision&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
Project Boundary&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
Compilation Boundary&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
     Assembly&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
Assembly Metadata / Types&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
    Reflection&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
    NetArchTest&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
Automated Architecture Test&lt;br&gt;
        │&lt;br&gt;
        ▼&lt;br&gt;
CI/CD Validation&lt;/p&gt;

&lt;p&gt;Αυτή είναι ίσως η σημαντικότερη ιδέα.&lt;/p&gt;

&lt;p&gt;Ξεκινάμε από μία αφηρημένη αρχιτεκτονική απόφαση:&lt;/p&gt;

&lt;p&gt;Το Domain πρέπει να είναι ανεξάρτητο από το Infrastructure.&lt;/p&gt;

&lt;p&gt;Τη μετατρέπουμε σε project boundaries.&lt;/p&gt;

&lt;p&gt;Τα projects μετατρέπονται σε assemblies.&lt;/p&gt;

&lt;p&gt;Τα assemblies διαθέτουν metadata.&lt;/p&gt;

&lt;p&gt;Το Reflection μας επιτρέπει να εξετάσουμε αυτά τα metadata.&lt;/p&gt;

&lt;p&gt;Το NetArchTest μπορεί να εφαρμόσει κανόνες πάνω στους types των assemblies.&lt;/p&gt;

&lt;p&gt;Το xUnit μπορεί να εκτελέσει αυτούς τους κανόνες ως tests.&lt;/p&gt;

&lt;p&gt;Και τελικά ένα CI/CD pipeline μπορεί να εμποδίσει την εισαγωγή ενός architectural regression στην κύρια έκδοση του συστήματος.&lt;/p&gt;

&lt;p&gt;Έτσι μία αφηρημένη αρχιτεκτονική αρχή καταλήγει να είναι machine-verifiable.&lt;/p&gt;




&lt;p&gt;Να θυμάσαι..&lt;/p&gt;

&lt;p&gt;Η κατανόηση των assemblies αλλάζει σημαντικά τον τρόπο με τον οποίο μπορούμε να αντιληφθούμε την Clean Architecture στο .NET.&lt;/p&gt;

&lt;p&gt;Η αρχιτεκτονική δεν είναι πλέον απλώς:&lt;/p&gt;

&lt;p&gt;Domain&lt;br&gt;
Application&lt;br&gt;
Infrastructure&lt;br&gt;
API&lt;/p&gt;

&lt;p&gt;σχεδιασμένη ως τέσσερα κουτιά σε ένα διάγραμμα.&lt;/p&gt;

&lt;p&gt;Τα κουτιά αυτά μπορούν να αντιστοιχούν σε πραγματικές compilation units:&lt;/p&gt;

&lt;p&gt;Domain.dll&lt;br&gt;
Application.dll&lt;br&gt;
Infrastructure.dll&lt;br&gt;
Api.dll&lt;/p&gt;

&lt;p&gt;με σαφείς dependency relationships μεταξύ τους.&lt;/p&gt;

&lt;p&gt;Το assembly αποτελεί επομένως κάτι περισσότερο από το αποτέλεσμα του compiler. Σε μία συνειδητά σχεδιασμένη εφαρμογή μπορεί να αποτελέσει τεχνική αναπαράσταση ενός architectural boundary.&lt;/p&gt;

&lt;p&gt;Το Reflection μας επιτρέπει να εξετάσουμε αυτή την αναπαράσταση.&lt;/p&gt;

&lt;p&gt;Ένας απλός marker type:&lt;/p&gt;

&lt;p&gt;public sealed class AssemblyReference&lt;br&gt;
{&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;μας παρέχει ένα σταθερό σημείο πρόσβασης:&lt;/p&gt;

&lt;p&gt;typeof(AssemblyReference).Assembly&lt;/p&gt;

&lt;p&gt;και από εκεί μπορούμε να εξετάσουμε το compiled Domain:&lt;/p&gt;

&lt;p&gt;Assembly&lt;br&gt;
   ↓&lt;br&gt;
Types&lt;br&gt;
   ↓&lt;br&gt;
Dependencies&lt;br&gt;
   ↓&lt;br&gt;
Architectural Rules&lt;/p&gt;

&lt;p&gt;Με εργαλεία όπως το NetArchTest μπορούμε στη συνέχεια να μετατρέψουμε μία αρχιτεκτονική αρχή όπως:&lt;/p&gt;

&lt;p&gt;Domain must not depend on Infrastructure&lt;/p&gt;

&lt;p&gt;σε executable test.&lt;/p&gt;

&lt;p&gt;Αυτό αποτελεί μία ουσιαστική μετάβαση από τη documented architecture προς την executable architecture.&lt;/p&gt;

&lt;p&gt;Σε ένα ώριμο software system, η αρχιτεκτονική δεν θα πρέπει να εξαρτάται αποκλειστικά από τη μνήμη των developers, από ένα παλιό διάγραμμα ή από την προσοχή του reviewer σε κάθε Pull Request.&lt;/p&gt;

&lt;p&gt;Οι σημαντικότεροι κανόνες της μπορούν, όπου αυτό είναι τεχνικά εφικτό και αρχιτεκτονικά χρήσιμο, να αποτελούν μέρος του ίδιου του μηχανισμού επαλήθευσης του συστήματος.&lt;/p&gt;

&lt;p&gt;Και εκεί βρίσκεται ίσως η μεγαλύτερη χρησιμότητα των assemblies στο συγκεκριμένο πλαίσιο:&lt;/p&gt;

&lt;p&gt;μετατρέπουν τα αρχιτεκτονικά όρια από αφηρημένες γραμμές ενός διαγράμματος σε πραγματικά όρια του compiled software — όρια που μπορούμε να παρατηρήσουμε, να εξετάσουμε και τελικά να ελέγξουμε αυτοματοποιημένα.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Πλήρης αρχιτεκτονική JWT, Middleware, Authorization, Permissions και Cache σε .NET</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Fri, 12 Jun 2026 13:34:24 +0000</pubDate>
      <link>https://dev.to/__b63657/pleres-arkhitektonike-jwt-middleware-authorization-permissions-kai-cache-se-net-1c05</link>
      <guid>https://dev.to/__b63657/pleres-arkhitektonike-jwt-middleware-authorization-permissions-kai-cache-se-net-1c05</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj56csfqt9r1u2kvdmdha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj56csfqt9r1u2kvdmdha.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Σε ένα σύγχρονο ASP.NET Core backend, το authentication και το authorization δεν είναι μία ενιαία διαδικασία, ούτε ένα σημείο κώδικα που “ανεβάζεις και τελείωσες”. Είναι μια ροή από στάδια, όπου κάθε στάδιο έχει διαφορετική ευθύνη και κανένα δεν πρέπει να μπερδεύεται με το άλλο.&lt;/p&gt;

&lt;p&gt;Όταν αυτά τα στάδια ανακατεύονται, το αποτέλεσμα είναι σύστημα δύσκολο να συντηρηθεί, δύσκολο να επεκταθεί και συνήθως αργό. Όταν όμως είναι σωστά διαχωρισμένα, το σύστημα γίνεται προβλέψιμο, γρήγορο και scalable.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Πώς ενεργοποιείται ο Authorization Handler;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ίσως αναρωτιέστε πώς καλείται ο AuthorizationHandler, αφού δεν τον καλούμε ποτέ εμείς μέσα από τον Controller.&lt;/p&gt;

&lt;p&gt;Η απάντηση είναι ότι αυτό γίνεται αυτόματα από το ASP.NET Core.&lt;/p&gt;

&lt;p&gt;Όταν ένα endpoint προστατεύεται με μια πολιτική, για παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Orders.Read"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetOrders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;το Authorization Middleware εντοπίζει ότι το endpoint απαιτεί την πολιτική Orders.Read. Πριν επιτρέψει την εκτέλεση του Controller, ενεργοποιεί αυτόματα τον αντίστοιχο AuthorizationHandler.&lt;/p&gt;

&lt;p&gt;Ο AuthorizationHandler χρησιμοποιεί το PermissionService για να ελέγξει αν ο χρήστης διαθέτει το απαιτούμενο permission. Αν ο έλεγχος είναι επιτυχής, το request συνεχίζει στον Controller. Διαφορετικά επιστρέφεται 403 Forbidden και ο Controller δεν εκτελείται ποτέ.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Το πρώτο σημείο: Middleware και JWT validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Κάθε request που έρχεται στο API δεν φτάνει ποτέ απευθείας στον controller. Περνάει πρώτα από το middleware pipeline της ASP.NET Core.&lt;/p&gt;

&lt;p&gt;Εκεί βρίσκεται το authentication middleware, το οποίο είναι υπεύθυνο να ελέγξει το JWT token πριν συμβεί οτιδήποτε άλλο.&lt;/p&gt;

&lt;p&gt;Σε αυτό το σημείο γίνεται ο έλεγχος της υπογραφής του token, του issuer, του audience και κυρίως του χρόνου ζωής του. Αν το token έχει λήξει, το request απορρίπτεται άμεσα και επιστρέφεται 401 Unauthorized χωρίς να εκτελεστεί τίποτα άλλο.&lt;/p&gt;

&lt;p&gt;Αυτό σημαίνει πρακτικά ότι ο χρήστης “κόβεται” πριν φτάσει σε business logic, πριν φτάσει σε authorization, πριν γίνει οποιοδήποτε database call. Το middleware λειτουργεί σαν απόλυτο security gate.&lt;/p&gt;

&lt;p&gt;Αν το token είναι έγκυρο, τότε δημιουργείται το HttpContext.User και το request συνεχίζει τη διαδρομή του μέσα στο pipeline.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Τι σημαίνει στην πράξη η λήξη του JWT&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Η λήξη του JWT δεν είναι κάτι που το χειρίζεται η εφαρμογή ως business logic. Είναι καθαρά security rule.&lt;/p&gt;

&lt;p&gt;Όταν ο χρόνος exp περάσει, το middleware απλά απορρίπτει το request. Δεν υπάρχει session, δεν υπάρχει “logout server side”, δεν υπάρχει κάποια επιπλέον διαδικασία. Το token θεωρείται άκυρο και τελείωσε εκεί η διαδρομή.&lt;/p&gt;

&lt;p&gt;Ο client είναι αυτός που πρέπει να αντιδράσει σε αυτή την κατάσταση, είτε κάνοντας refresh token call είτε ζητώντας login ξανά.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Μετά το middleware: αρχίζει το authorization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Μόλις το JWT περάσει τον έλεγχο, το σύστημα γνωρίζει πλέον ποιος είναι ο χρήστης. Αυτό όμως δεν σημαίνει ότι ξέρει τι επιτρέπεται να κάνει.&lt;/p&gt;

&lt;p&gt;Εδώ ξεκινά το authorization κομμάτι, το οποίο δεν είναι ένα απλό if statement, ούτε ένα δεύτερο middleware που “τρέχει μόνο του”. Είναι ένα policy-based σύστημα που ενεργοποιείται όταν ένα endpoint απαιτεί συγκεκριμένο δικαίωμα.&lt;/p&gt;

&lt;p&gt;Για παράδειγμα, όταν ένα endpoint έχει attribute όπως “Orders.Read”, δεν σημαίνει ότι το framework απλά κοιτάει roles. Σημαίνει ότι ενεργοποιείται μια διαδικασία ελέγχου που θα αποφασίσει δυναμικά αν ο συγκεκριμένος χρήστης μπορεί να εκτελέσει αυτή την ενέργεια.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Πώς υλοποιείται στην πράξη το authorization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Στην πράξη, το authorization εκφράζεται μέσω policies και handlers. Το endpoint δηλώνει τι απαιτείται, και ο handler αποφασίζει αν ο χρήστης το πληροί.&lt;/p&gt;

&lt;p&gt;Όταν ένα request φτάνει σε ένα endpoint με policy, η ASP.NET ενεργοποιεί τον αντίστοιχο authorization handler. Εκεί γίνεται η πραγματική αξιολόγηση.&lt;/p&gt;

&lt;p&gt;Ο handler δεν παίρνει την απόφαση από μόνος του. Ζητάει δεδομένα από ένα service, το PermissionService, το οποίο είναι υπεύθυνο να βρει τα δικαιώματα του χρήστη.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Πού αποθηκεύονται τα permissions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Τα permissions δεν πρέπει να βρίσκονται μέσα στο JWT. Ο λόγος είναι ότι αλλάζουν συχνά, πρέπει να μπορούν να ανακληθούν και δεν πρέπει να “παγώνουν” μέσα σε ένα token.&lt;/p&gt;

&lt;p&gt;Αντίθετα, αποθηκεύονται σε βάση δεδομένων σε κανονικό relational μοντέλο.&lt;/p&gt;

&lt;p&gt;Υπάρχουν users, roles και permissions. Οι χρήστες συνδέονται με ρόλους και οι ρόλοι συνδέονται με permissions. Έτσι το σύστημα γίνεται δυναμικό και μπορεί να αλλάξει ανά πάσα στιγμή χωρίς να χρειάζεται reissue όλων των tokens.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;PermissionService: το σημείο που διαβάζονται τα δικαιώματα&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το PermissionService είναι το μοναδικό κομμάτι της εφαρμογής που γνωρίζει πώς να πάρει τα permissions ενός χρήστη από τη βάση δεδομένων.&lt;/p&gt;

&lt;p&gt;Όταν καλείται, κάνει join μεταξύ users, roles και permissions και επιστρέφει τη λίστα των δικαιωμάτων.&lt;/p&gt;

&lt;p&gt;Αυτό το κομμάτι είναι business data access layer και όχι security logic. Η απόφαση για το αν επιτρέπεται μια ενέργεια γίνεται αλλού, όχι εδώ.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Cache γιατί υπάρχει και πού μπαίνει&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αν κάθε request χτυπάει τη βάση για permissions, το σύστημα θα αρχίσει πολύ γρήγορα να υποφέρει σε load.&lt;/p&gt;

&lt;p&gt;Γι’ αυτό μπαίνει cache.&lt;/p&gt;

&lt;p&gt;Η cache δεν αλλάζει τη λογική του συστήματος. Δεν αποφασίζει τίποτα. Απλώς αποθηκεύει προσωρινά το αποτέλεσμα ενός προηγούμενου query ώστε να μην χρειάζεται να επαναληφθεί.&lt;/p&gt;

&lt;p&gt;Το PermissionService πρώτα κοιτάει αν υπάρχουν ήδη τα permissions του χρήστη στην cache. Αν υπάρχουν, τα επιστρέφει αμέσως. Αν δεν υπάρχουν, τότε πηγαίνει στη βάση, τα φέρνει και τα αποθηκεύει για επόμενες κλήσεις.&lt;/p&gt;

&lt;p&gt;Έτσι το πρώτο request είναι πιο “βαρύ”, αλλά όλα τα επόμενα είναι σχεδόν άμεσα.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Όλη η ροή μαζί&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Όταν όλα τα κομμάτια συνδεθούν μεταξύ τους, η ροή του request είναι καθαρή.&lt;/p&gt;

&lt;p&gt;Το request έρχεται στο σύστημα και περνάει πρώτα από το middleware. Εκεί γίνεται ο έλεγχος του JWT. Αν το token έχει λήξει, το request σταματάει αμέσως. Αν είναι valid, δημιουργείται το user context και το request συνεχίζει.&lt;/p&gt;

&lt;p&gt;Στη συνέχεια ενεργοποιείται το authorization layer, το οποίο βλέπει ότι το συγκεκριμένο endpoint απαιτεί συγκεκριμένα permissions. Ο authorization handler καλείται και ζητάει τα permissions του χρήστη από το PermissionService.&lt;/p&gt;

&lt;p&gt;Το PermissionService πρώτα κοιτάει στην cache. Αν τα δεδομένα υπάρχουν εκεί, τα επιστρέφει άμεσα. Αν όχι, κάνει query στη βάση δεδομένων, τα αποθηκεύει στην cache και τα επιστρέφει.&lt;/p&gt;

&lt;p&gt;Αν ο χρήστης έχει το απαιτούμενο permission, το request συνεχίζει στον controller και από εκεί μπορεί να καλέσει άλλα microservices ή να εκτελέσει business logic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Γιατί αυτό δεν γίνεται bottleneck&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το κρίσιμο σημείο είναι το PermissionService και η βάση δεδομένων. Θεωρητικά αυτό θα μπορούσε να γίνει bottleneck, αλλά στην πράξη δεν γίνεται, γιατί σχεδόν ποτέ δεν χτυπάει τη βάση.&lt;/p&gt;

&lt;p&gt;Η cache απορροφά τη συντριπτική πλειοψηφία των requests, οπότε η βάση χρησιμοποιείται μόνο σε cache miss ή όταν αλλάξουν τα permissions.&lt;/p&gt;

&lt;p&gt;Αυτό σημαίνει ότι το authorization δεν είναι hot path προς τη βάση. Είναι hot path προς τη cache, η οποία είναι εξαιρετικά γρήγορη.&lt;/p&gt;

&lt;p&gt;Έτσι το σύστημα παραμένει σταθερό ακόμα και υπό μεγάλο φορτίο.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Τι συμβαίνει όταν λήξει το JWT;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Μέχρι τώρα είδαμε πώς το middleware επικυρώνει το JWT και αποφασίζει αν ο χρήστης έχει πρόσβαση σε έναν πόρο. Τι γίνεται όμως όταν το Access Token λήξει;&lt;/p&gt;

&lt;p&gt;Όταν η API λάβει ένα ληγμένο Access Token, η επικύρωση αποτυγχάνει και επιστρέφεται:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Σε αυτό το σημείο ο χρήστης δεν χρειάζεται να συνδεθεί ξανά ούτε να πατήσει κάποιο κουμπί. Ο client (React, Angular, Blazor, Mobile App κ.λπ.) λαμβάνει την απάντηση 401 Unauthorized και χρησιμοποιεί το Refresh Token που έχει αποθηκεύσει για να ζητήσει ένα νέο Access Token από τον Authentication Server.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /refresh-token&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Η ροή είναι η εξής:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   │
   │ GET /api/orders
   │ Access Token
   ▼
API
   │
   └── 401 Unauthorized
            │
            ▼
Client
   │
   │ POST /refresh-token
   │ Refresh Token
   ▼
Authentication Server
   │
   └── Νέο Access Token
            │
            ▼
Client
   │
   │ Επαναλαμβάνει το αρχικό request
   ▼
API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Είναι σημαντικό να τονίσουμε ότι η API και το JWT Middleware δεν ανανεώνουν ποτέ το Access Token. Ο μοναδικός τους ρόλος είναι να ελέγχουν αν το token που έλαβαν είναι έγκυρο. Η ανανέωση πραγματοποιείται αποκλειστικά από τον Authentication Server μέσω του Refresh Token, ενώ ο client είναι υπεύθυνος να ξεκινήσει αυτή τη διαδικασία όταν χρειαστεί.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Σημείωση: Αν έχει λήξει ή έχει ανακληθεί και το Refresh Token, τότε ο Authentication Server δεν μπορεί να εκδώσει νέο Access Token. Σε αυτή την περίπτωση ο χρήστης πρέπει να πραγματοποιήσει ξανά login.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Διάγραμμα&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────────────────────────────┐
│                       Client / SPA                        │
└───────────────────────┬───────────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────────┐
│                     HTTP Request                          │
│                  Authorization: Bearer JWT                │
└───────────────────────┬───────────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────────┐
│                 Authentication Middleware                 │
│                                                           │
│  Validate JWT Signature                                   │
│  Validate Issuer / Audience                               │
│  Validate Expiration (exp)                                │
└───────────────────────┬───────────────────────────────────┘
                        │
          ┌─────────────┴─────────────┐
          │                           │
          ▼                           ▼
     Token Invalid               Token Valid
          │                           │
          ▼                           ▼
   401 Unauthorized          HttpContext.User Created
          │                           │
          │                           ▼
          │             ┌──────────────────────────────┐
          │             │    Token Revocation Check    │
          │             │                              │
          │             │ Redis / Cache / DB Lookup    │
          │             └──────────────┬───────────────┘
          │                            │
          │              ┌─────────────┴────────────┐
          │              │                          │
          │              ▼                          ▼
          │        Revoked                    Active
          │              │                          │
          │              ▼                          ▼
          │       401 Unauthorized        Authorization Layer
          │                                         │
          │                                         ▼
          │                     ┌────────────────────────────┐
          │                     │    Policy / Handler        │
          │                     │  Orders.Read Permission    │
          │                     └─────────────┬──────────────┘
          │                                   │
          │                                   ▼
          │                     ┌────────────────────────────┐
          │                     │     PermissionService      │
          │                     └─────────────┬──────────────┘
          │                                   │
          │                                   ▼
          │                     ┌────────────────────────────┐
          │                     │         Cache              │
          │                     │ (MemoryCache / Redis)      │
          │                     └─────────────┬──────────────┘
          │                                   │
          │                 ┌─────────────────┴────────────┐
          │                 │                              │
          │                 ▼                              ▼
          │           Cache Hit                      Cache Miss
          │                 │                              │
          │                 │                              ▼
          │                 │              ┌────────────────────────┐
          │                 │              │      Database          │
          │                 │              │ Users / Roles / Perms  │
          │                 │              └───────────┬────────────┘
          │                 │                          │
          │                 └──────────────┬───────────┘
          │                                │
          │                                ▼
          │                    Permission Decision
          │                                │
          │                    ┌───────────┴───────────┐
          │                    │                       │
          │                    ▼                       ▼
          │                 Allow                   Deny
          │                    │                       │
          │                    ▼                       ▼
          │             Controller              403 Forbidden
          │                    │
          │                    ▼
          │        Orders API / Customers API
          │
          │
          └─────────────────────────────────────────────────


              ACCESS TOKEN EXPIRES
                        │
                        ▼
                Client receives 401
                        │
                        ▼
                POST /auth/refresh
                        │
                        ▼
              Refresh Token Validation
                        │
                        ▼
                Refresh Token Store
                  (DB / Redis)
                        │
          ┌─────────────┴─────────────┐
          │                           │
          ▼                           ▼
      Invalid                    Valid
          │                           │
          ▼                           ▼
   Login Required          New JWT Issued
                                   │
                                   ▼
                          User Continues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Να θυμάσαι!&lt;/p&gt;

&lt;p&gt;Ένα σωστό authentication και authorization σύστημα δεν βασίζεται σε ένα μόνο layer, αλλά σε καθαρό διαχωρισμό ευθυνών.&lt;/p&gt;

&lt;p&gt;Το middleware είναι το security gate που απορρίπτει invalid tokens πριν μπουν στο σύστημα.&lt;/p&gt;

&lt;p&gt;Το authorization layer είναι το σημείο που αποφασίζει αν ένας χρήστης έχει πρόσβαση σε ένα συγκεκριμένο endpoint.&lt;/p&gt;

&lt;p&gt;Το PermissionService είναι το σημείο που διαβάζει την πραγματική κατάσταση από τη βάση δεδομένων.&lt;/p&gt;

&lt;p&gt;Και η cache είναι απλώς ένα performance layer που μειώνει το load χωρίς να επηρεάζει τη λογική του συστήματος.&lt;/p&gt;

&lt;p&gt;Όταν όλα αυτά λειτουργούν μαζί με καθαρό διαχωρισμό, έχεις ένα σύστημα που είναι ταυτόχρονα ασφαλές, γρήγορο και έτοιμο για production scale χωρίς bottlenecks.&lt;/p&gt;




&lt;p&gt;&lt;a href="mailto:nikosstit@gmail.com"&gt;nikosstit@gmail.com&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI Development Tools για Σύγχρονους Software Engineers: Πότε να χρησιμοποιείς Agent, Ask, Plan, GitHub Copilot, Debugger, Git και Profiler</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Wed, 10 Jun 2026 14:41:33 +0000</pubDate>
      <link>https://dev.to/__b63657/ai-development-tools-gia-sugkhronous-software-engineers-pote-na-khresimopoieis-agent-ask-plan-a0b</link>
      <guid>https://dev.to/__b63657/ai-development-tools-gia-sugkhronous-software-engineers-pote-na-khresimopoieis-agent-ask-plan-a0b</guid>
      <description>&lt;p&gt;Η ανάπτυξη λογισμικού το 2026 έχει αλλάξει δραματικά. Ο σύγχρονος προγραμματιστής δεν γράφει απλώς κώδικα· συνεργάζεται καθημερινά με &lt;strong&gt;AI Agents&lt;/strong&gt;, &lt;strong&gt;εργαλεία αυτόματης δημιουργίας κώδικα&lt;/strong&gt;, &lt;strong&gt;συστήματα debugging&lt;/strong&gt;, &lt;strong&gt;profiling&lt;/strong&gt; και &lt;strong&gt;πλατφόρμες διαχείρισης εκδόσεων&lt;/strong&gt;. Η πρόκληση πλέον δεν είναι μόνο να γνωρίζει κάποιος προγραμματισμό, αλλά να ξέρει ποιο εργαλείο πρέπει να χρησιμοποιήσει σε κάθε στάδιο του Software Development Lifecycle.&lt;/p&gt;

&lt;p&gt;Ένας senior developer ή software architect οφείλει να επιλέγει το κατάλληλο εργαλείο ανάλογα με το πρόβλημα που αντιμετωπίζει. Η λανθασμένη χρήση ενός AI Agent για απλό debugging μπορεί να οδηγήσει σε χαμένο χρόνο, ενώ η χρήση ενός profiler σε πρόβλημα λογικής δεν θα προσφέρει καμία αξία. Η πραγματική παραγωγικότητα προκύπτει όταν κάθε εργαλείο χρησιμοποιείται στο σωστό σημείο της διαδικασίας ανάπτυξης.&lt;/p&gt;

&lt;p&gt;Στο άρθρο αυτό αναλύουμε τα σημαντικότερα εργαλεία της σύγχρονης ανάπτυξης λογισμικού, τον ρόλο τους, τις βέλτιστες πρακτικές χρήσης τους και το κόστος των δημοφιλέστερων μοντέλων της αγοράς.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Ask Mode – Όταν χρειάζεσαι γνώση και καθοδήγηση&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το &lt;strong&gt;Ask Mode&lt;/strong&gt; είναι η πιο απλή μορφή αλληλεπίδρασης με ένα AI σύστημα. Ο προγραμματιστής θέτει ερωτήσεις και λαμβάνει απαντήσεις, εξηγήσεις ή παραδείγματα.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ το όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Θέλεις να κατανοήσεις μια βιβλιοθήκη ή framework.&lt;/li&gt;
&lt;li&gt;Αναζητάς βέλτιστες πρακτικές.&lt;/li&gt;
&lt;li&gt;Θέλεις επεξήγηση ενός error message.&lt;/li&gt;
&lt;li&gt;Χρειάζεσαι παραδείγματα κώδικα.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Μην το χρησιμοποιείς όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Απαιτείται αλλαγή σε πολλά αρχεία.&lt;/li&gt;
&lt;li&gt;Χρειάζεται ανάλυση ολόκληρου project.&lt;/li&gt;
&lt;li&gt;Πρέπει να γίνει refactoring μεγάλης κλίμακας.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Το Ask λειτουργεί σαν ένας πολύ έμπειρος συνάδελφος που απαντά σε ερωτήσεις, αλλά δεν αναλαμβάνει εργασία.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;2. Plan Mode – Πριν γράψεις γραμμή κώδικα&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το μεγαλύτερο λάθος των περισσότερων ομάδων είναι ότι ξεκινούν να υλοποιούν χωρίς σχέδιο.&lt;/p&gt;

&lt;p&gt;Το Plan Mode δημιουργήθηκε ακριβώς για αυτόν τον λόγο.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ το όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Σχεδιάζεις νέο feature.&lt;/li&gt;
&lt;li&gt;Θέλεις να σπάσεις ένα έργο σε tasks.&lt;/li&gt;
&lt;li&gt;Κάνεις migration συστήματος.&lt;/li&gt;
&lt;li&gt;Προετοιμάζεις αρχιτεκτονική λύση.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Παράδειγμα&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Αντί να ζητήσεις:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"Γράψε authentication"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;ζήτησε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"Σχεδίασε authentication architecture για .NET microservices με JWT και refresh tokens."&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Το αποτέλεσμα θα είναι σαφώς καλύτερο.&lt;/p&gt;

&lt;p&gt;Οι senior engineers επενδύουν περισσότερο χρόνο στον σχεδιασμό παρά στην υλοποίηση.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;3. Agent Mode – Ο ψηφιακός συνεργάτης&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το Agent Mode αποτελεί τη μεγαλύτερη εξέλιξη των τελευταίων ετών.&lt;/p&gt;

&lt;p&gt;Ένας Agent δεν απαντά απλώς σε ερωτήσεις. Μπορεί να:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;διαβάσει ολόκληρο repository,&lt;/li&gt;
&lt;li&gt;αναλύσει requirements,&lt;/li&gt;
&lt;li&gt;τροποποιήσει πολλαπλά αρχεία,&lt;/li&gt;
&lt;li&gt;δημιουργήσει tests,&lt;/li&gt;
&lt;li&gt;κάνει refactoring,&lt;/li&gt;
&lt;li&gt;ανοίξει Pull Requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ το όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Υπάρχει μεγάλο codebase.&lt;/li&gt;
&lt;li&gt;Θέλεις αυτοματοποίηση εργασιών.&lt;/li&gt;
&lt;li&gt;Κάνεις modernization legacy εφαρμογών.&lt;/li&gt;
&lt;li&gt;Χρειάζεσαι end-to-end υλοποίηση.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Μην το χρησιμοποιείς όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Έχεις μια απλή απορία.&lt;br&gt;
Χρειάζεται μόνο ένα μικρό snippet.&lt;/p&gt;

&lt;p&gt;Ο Agent λειτουργεί περισσότερο σαν junior-to-mid developer παρά σαν chatbot.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;4. GitHub Copilot – Ο καθημερινός βοηθός προγραμματισμού&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το GitHub Copilot είναι το πιο διαδεδομένο AI εργαλείο ανάπτυξης λογισμικού.&lt;/p&gt;

&lt;p&gt;Βρίσκεται μέσα στο IDE και προσφέρει:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;autocomplete,&lt;/li&gt;
&lt;li&gt;code generation,&lt;/li&gt;
&lt;li&gt;chat assistance,&lt;/li&gt;
&lt;li&gt;code reviews,&lt;/li&gt;
&lt;li&gt;agent workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ το όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Γράφεις καθημερινό κώδικα.&lt;/li&gt;
&lt;li&gt;Δημιουργείς APIs.&lt;/li&gt;
&lt;li&gt;Υλοποιείς CRUD λειτουργίες.&lt;/li&gt;
&lt;li&gt;Γράφεις unit tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Πλεονεκτήματα&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Μικρό context switching.&lt;/li&gt;
&lt;li&gt;Εξαιρετική ενσωμάτωση σε VS Code.&lt;/li&gt;
&lt;li&gt;Υψηλή παραγωγικότητα.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Περιορισμοί&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Δεν κατανοεί πάντα ολόκληρη την αρχιτεκτονική.&lt;/li&gt;
&lt;li&gt;Μπορεί να παράγει λανθασμένο κώδικα.&lt;/li&gt;
&lt;li&gt;Απαιτεί code review.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;5. Debugger – Το εργαλείο που δεν αντικαθίσταται&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Παρά την πρόοδο της τεχνητής νοημοσύνης, κανένα εργαλείο δεν αντικαθιστά έναν σωστά χρησιμοποιημένο debugger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ τον όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Υπάρχει runtime σφάλμα.&lt;br&gt;
Οι τιμές μεταβλητών είναι λανθασμένες.&lt;br&gt;
Το σύστημα συμπεριφέρεται διαφορετικά από το αναμενόμενο.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Βέλτιστη πρακτική&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce.&lt;/li&gt;
&lt;li&gt;Breakpoint.&lt;/li&gt;
&lt;li&gt;Step Through.&lt;/li&gt;
&lt;li&gt;Root Cause Analysis.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Οι καλύτεροι developers δεν μαντεύουν. Μετρούν.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;6. Git – Η ασφάλεια του project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το Git είναι το θεμέλιο κάθε σύγχρονης ομάδας ανάπτυξης.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ το για:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version control.&lt;/li&gt;
&lt;li&gt;Feature branching.&lt;/li&gt;
&lt;li&gt;Pull Requests.&lt;/li&gt;
&lt;li&gt;Code Reviews.&lt;/li&gt;
&lt;li&gt;Rollback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Senior πρακτικές&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature branches.&lt;/li&gt;
&lt;li&gt;Squash merges.&lt;/li&gt;
&lt;li&gt;Atomic commits.&lt;/li&gt;
&lt;li&gt;Protected main branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Χωρίς Git δεν υπάρχει ασφαλής ανάπτυξη λογισμικού.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;7. Profiler – Όταν η εφαρμογή είναι αργή&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ένα από τα συνηθέστερα λάθη είναι η βελτιστοποίηση χωρίς μετρήσεις.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ο profiler δείχνει:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU bottlenecks,&lt;/li&gt;
&lt;li&gt;memory leaks,&lt;/li&gt;
&lt;li&gt;database latency,&lt;/li&gt;
&lt;li&gt;inefficient algorithms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ τον όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Υπάρχουν προβλήματα απόδοσης.&lt;/li&gt;
&lt;li&gt;Αυξάνεται η κατανάλωση μνήμης.&lt;/li&gt;
&lt;li&gt;Υπάρχουν μεγάλοι χρόνοι απόκρισης.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Μην τον χρησιμοποιείς όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Υπάρχει λογικό bug.&lt;br&gt;
Το πρόβλημα είναι business logic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;8. Test Generation – Η ασπίδα της ποιότητας&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Τα σύγχρονα AI εργαλεία μπορούν να δημιουργούν:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit Tests&lt;/li&gt;
&lt;li&gt;Integration Tests&lt;/li&gt;
&lt;li&gt;API Tests&lt;/li&gt;
&lt;li&gt;Regression Tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Χρησιμοποίησέ τα όταν:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Προσθέτεις νέα features.&lt;/li&gt;
&lt;li&gt;Κάνεις refactoring.&lt;/li&gt;
&lt;li&gt;Κάνεις modernization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ένα καλό σύστημα testing μειώνει δραματικά το τεχνικό χρέος.&lt;/p&gt;




&lt;p&gt;Να θυμάσαι!&lt;/p&gt;

&lt;p&gt;Ο σύγχρονος προγραμματιστής δεν ανταγωνίζεται πλέον την τεχνητή νοημοσύνη· συνεργάζεται μαζί της. Η πραγματική αξία δεν βρίσκεται στο ποιο εργαλείο είναι «καλύτερο», αλλά στο πότε χρησιμοποιείται το καθένα.&lt;/p&gt;

&lt;p&gt;Για καθημερινό development, το GitHub Copilot παραμένει η πιο αποδοτική επιλογή. Για σύνθετες αλλαγές σε μεγάλα codebases, οι AI Agents και το Codex προσφέρουν τεράστια επιτάχυνση. Για σχεδιασμό αρχιτεκτονικής, το Plan Mode μειώνει τα λάθη πριν γραφτεί κώδικας. Για κατανόηση και εκπαίδευση, το Ask Mode είναι ιδανικό. Όταν εμφανίζονται σφάλματα, ο Debugger παραμένει αναντικατάστατος, ενώ ο Profiler είναι το μοναδικό εργαλείο που μπορεί να αποδείξει με δεδομένα πού πραγματικά χάνεται η απόδοση.&lt;/p&gt;

&lt;p&gt;Οι κορυφαίοι software engineers του 2026 δεν είναι αυτοί που γράφουν περισσότερο κώδικα. Είναι εκείνοι που ξέρουν να συνδυάζουν σωστά ανθρώπινη εμπειρία, αρχιτεκτονική σκέψη και AI εργαλεία, δημιουργώντας λογισμικό ταχύτερα, ασφαλέστερα και με υψηλότερη ποιότητα.&lt;/p&gt;




&lt;p&gt;&lt;a href="mailto:nikosstit@gmail.com"&gt;nikosstit@gmail.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>softwareengineering</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Local Time, UTC, Offset και Epoch: Ο απόλυτος οδηγός για developers</title>
      <dc:creator>nikosst</dc:creator>
      <pubDate>Tue, 09 Jun 2026 21:29:38 +0000</pubDate>
      <link>https://dev.to/__b63657/local-time-utc-offset-kai-epoch-o-apolutos-odegos-gia-developers-3h0f</link>
      <guid>https://dev.to/__b63657/local-time-utc-offset-kai-epoch-o-apolutos-odegos-gia-developers-3h0f</guid>
      <description>&lt;p&gt;&lt;strong&gt;Το πρόβλημα της ώρας&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Η ώρα είναι από τα πιο ύπουλα προβλήματα στην ανάπτυξη λογισμικού.&lt;/p&gt;

&lt;p&gt;Αν ένας χρήστης στην Αθήνα δημιουργήσει μια παραγγελία στις 20:00 και ένας άλλος στη Νέα Υόρκη τη δει στις 13:00, ποια είναι η "σωστή" ώρα;&lt;/p&gt;

&lt;p&gt;Αν μια εφαρμογή αποθηκεύσει μόνο το 20:00, χωρίς να γνωρίζει τη ζώνη ώρας, τότε η πληροφορία είναι πρακτικά άχρηστη.&lt;/p&gt;

&lt;p&gt;Αυτός είναι ο λόγος που υπάρχουν έννοιες όπως:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local Time&lt;/li&gt;
&lt;li&gt;UTC&lt;/li&gt;
&lt;li&gt;UTC Offset&lt;/li&gt;
&lt;li&gt;Epoch / Unix Timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Δεν δημιουργήθηκαν για να μας μπερδεύουν. Δημιουργήθηκαν για να λύνουν το πρόβλημα της παγκόσμιας διαχείρισης χρόνου.&lt;/p&gt;




&lt;p&gt;Local Time&lt;/p&gt;

&lt;p&gt;Το Local Time είναι η ώρα που βλέπει ο χρήστης στη χώρα του.&lt;/p&gt;

&lt;p&gt;Παραδείγματα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Αθήνα:    2026-06-09 20:00
Λονδίνο:  2026-06-09 18:00
Νέα Υόρκη:2026-06-09 13:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Όλες οι παραπάνω ώρες μπορεί να αντιστοιχούν στην ίδια ακριβώς χρονική στιγμή. Συνέβει ένα γεγονός μία ενέργεια στον πλανίτη γη ακριβώς αυτή την στιγμή που όμως για διαφορετικές γεωγραφικές περιοχές αντιστοιχεί σε διαφορετικές ώρες.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Πότε χρησιμοποιούμε Local Time;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Μόνο για εμφάνιση στον χρήστη.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Παραδείγματα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ημερομηνία παραγγελίας&lt;/li&gt;
&lt;li&gt;Ώρα δημιουργίας post&lt;/li&gt;
&lt;li&gt;Ημερολόγιο συναντήσεων&lt;/li&gt;
&lt;li&gt;Reports προς τον χρήστη&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Πότε ΔΕΝ το αποθηκεύουμε;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Σχεδόν ποτέ ως μοναδική πηγή αλήθειας.&lt;/p&gt;

&lt;p&gt;Αν αποθηκεύσεις:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-06-09 20:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;δεν γνωρίζεις:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Σε ποια χώρα δημιουργήθηκε&lt;/li&gt;
&lt;li&gt;Σε ποια ζώνη ώρας ανήκει&lt;/li&gt;
&lt;li&gt;Αν ίσχυε θερινή ώρα (DST)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;UTC (Coordinated Universal Time)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το UTC είναι η παγκόσμια αναφορά χρόνου.&lt;/p&gt;

&lt;p&gt;Όλες οι ζώνες ώρας υπολογίζονται σε σχέση με αυτό. &lt;/p&gt;

&lt;p&gt;Παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UTC: 2026-06-09 17:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Την ίδια στιγμή με βάση την UTC ώρα μπορούμε να έχουμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;στην Αθήνα   UTC+3 -&amp;gt; 20:00
στο Λονδίνο UTC+1 -&amp;gt; 18:00
στη Νέα Υόρκη UTC-4 -&amp;gt; 13:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Πότε χρησιμοποιούμε UTC;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Σχεδόν πάντα στο backend.&lt;/p&gt;

&lt;p&gt;Αποθηκεύουμε:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;2026-06-09&lt;/span&gt;&lt;span class="err"&gt;T&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;Z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Το Z σημαίνει UTC.&lt;/p&gt;

&lt;p&gt;Γιατί;&lt;/p&gt;

&lt;p&gt;Επειδή:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Δεν αλλάζει με DST&lt;/li&gt;
&lt;li&gt;Δεν εξαρτάται από χώρα&lt;/li&gt;
&lt;li&gt;Είναι παγκόσμιο σημείο αναφοράς&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ένας κανόνας που ακολουθούν σχεδόν όλες οι μεγάλες εταιρείες:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Store in UTC, display in Local Time.&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;UTC Offset&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Παραδείγματα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UTC+3
UTC+2
UTC-5
UTC+9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Για την Αθήνα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Χειμώνας -&amp;gt; UTC+2
Καλοκαίρι -&amp;gt; UTC+3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Χειμώνας -&amp;gt; UTC+2
Καλοκαίρι -&amp;gt; UTC+3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Παράδειγμα&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;2026-06-09&lt;/span&gt;&lt;span class="err"&gt;T&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;σημαίνει:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Τοπική ώρα: 20:00
Offset: +03:00
UTC: 17:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Πότε αποθηκεύουμε Offset;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Όταν θέλουμε να γνωρίζουμε ακριβώς τι έβλεπε ο χρήστης τη στιγμή της καταχώρησης.&lt;/p&gt;

&lt;p&gt;Παραδείγματα:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Αεροπορικά εισιτήρια&lt;/li&gt;
&lt;li&gt;Νομικά έγγραφα&lt;/li&gt;
&lt;li&gt;Ιατρικά συστήματα&lt;/li&gt;
&lt;li&gt;Financial systems&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Epoch / Unix Timestamp&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το Epoch είναι η πιο "καθαρή" αναπαράσταση χρόνου.&lt;/p&gt;

&lt;p&gt;Μετράει πόσα δευτερόλεπτα έχουν περάσει από:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1970-01-01 00:00:00 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό ονομάζεται Unix Epoch. &lt;a href="https://www.epochconverter.com/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;epochconverter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1780968655
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτός είναι απλά ένας αριθμός.&lt;/p&gt;

&lt;p&gt;Δεν περιέχει:&lt;/p&gt;

&lt;p&gt;Timezone&lt;br&gt;
Offset&lt;br&gt;
DST&lt;/p&gt;

&lt;p&gt;Μόνο μια απόλυτη χρονική στιγμή.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Γιατί οι developers αγαπούν τα Epochs;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Επειδή οι συγκρίσεις είναι πανεύκολες.&lt;/p&gt;

&lt;p&gt;Αν έχεις:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1780968655
1780968755
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ξέρεις αμέσως ότι το δεύτερο συνέβη αργότερα.&lt;/p&gt;

&lt;p&gt;Δεν χρειάζεται:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;μετατροπή timezone&lt;/li&gt;
&lt;li&gt;parsing&lt;/li&gt;
&lt;li&gt;date calculations&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Timestamp ως Long&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Στη .NET και στις περισσότερες γλώσσες το Epoch αποθηκεύεται ως:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;long timestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1780968655
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ή&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1780968655000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(σε milliseconds)&lt;/p&gt;

&lt;p&gt;Πότε χρησιμοποιείται;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;Event streams&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;li&gt;Message queues&lt;/li&gt;
&lt;li&gt;Logging systems&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Timestamp ως String&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;2026-06-09&lt;/span&gt;&lt;span class="err"&gt;T&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;Z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ή&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;2026-06-09&lt;/span&gt;&lt;span class="err"&gt;T&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Αυτό είναι συνήθως ISO 8601 format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Πλεονεκτήματα&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Αναγνώσιμο από άνθρωπο&lt;/li&gt;
&lt;li&gt;Περιέχει timezone πληροφορία&lt;/li&gt;
&lt;li&gt;Ιδανικό για APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Μειονεκτήματα&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Μεγαλύτερο μέγεθος&lt;/li&gt;
&lt;li&gt;Πιο αργό parsing&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Timestamp ως DateTime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Στη .NET:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DateTime&lt;br&gt;
DateTimeOffset&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Σήμερα οι περισσότεροι senior developers προτιμούν:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DateTimeOffset&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;γιατί περιέχει και το offset.&lt;/p&gt;

&lt;p&gt;Παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-06-09 20:00 +03:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Τι να χρησιμοποιήσω τελικά;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Για αποθήκευση στη βάση&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Προτίμησε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UTC&lt;/code&gt; ή &lt;code&gt;DateTimeOffset&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Για APIs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Προτίμησε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ISO 8601&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Παράδειγμα:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-09T17:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Για logs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Προτίμησε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UTC&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;ή Epoch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Για sorting και performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Προτίμησε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Epoch Timestamp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Για εμφάνιση στον χρήστη&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Προτίμησε:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Local Time&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;με βάση τη ζώνη ώρας του χρήστη.&lt;/p&gt;




&lt;p&gt;Τι είναι το Epoch Converter;&lt;/p&gt;

&lt;p&gt;Το &lt;a href="https://www.epochconverter.com/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Epoch Converter&lt;/a&gt; είναι ένα πολύ γνωστό εργαλείο για developers που μετατρέπει:&lt;/p&gt;

&lt;p&gt;Epoch → Human Readable Date&lt;br&gt;
Date → Epoch&lt;br&gt;
UTC ↔ Local Time&lt;br&gt;
Seconds ↔ Milliseconds ↔ Microseconds&lt;/p&gt;

&lt;p&gt;και υποστηρίζει διάφορες μορφές timestamps.&lt;/p&gt;

&lt;p&gt;Είναι εξαιρετικά χρήσιμο όταν κάνεις debugging logs, APIs ή βάσεις δεδομένων.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Συμπέρασμα&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Το μεγαλύτερο λάθος που κάνουν οι developers είναι να θεωρούν ότι η ημερομηνία είναι απλώς ένα πεδίο στη βάση.&lt;/p&gt;

&lt;p&gt;Στην πραγματικότητα, ο χρόνος είναι ένα παγκόσμιο πρόβλημα συγχρονισμού. Το Local Time εξυπηρετεί τον χρήστη, το UTC εξυπηρετεί το σύστημα, το Offset διατηρεί το πραγματικό τοπικό πλαίσιο μιας στιγμής και το Epoch παρέχει την πιο απλή και απόλυτη αναπαράσταση του χρόνου.&lt;/p&gt;

&lt;p&gt;Όταν σχεδιάζεις ένα σύστημα, η σωστή ερώτηση δεν είναι «ποιο type να χρησιμοποιήσω;». Η σωστή ερώτηση είναι:&lt;/p&gt;

&lt;p&gt;Θέλω να αποθηκεύσω πώς είδε ο χρήστης την ώρα ή ποια ακριβώς στιγμή συνέβη το γεγονός;&lt;/p&gt;

&lt;p&gt;Αν απαντήσεις σωστά σε αυτή την ερώτηση, τότε η επιλογή μεταξύ Local Time, UTC, Offset και Epoch γίνεται σχεδόν αυτονόητη. Οι καλύτερες εφαρμογές δεν είναι αυτές που χειρίζονται σωστά τις ημερομηνίες στις εύκολες περιπτώσεις· είναι αυτές που συνεχίζουν να λειτουργούν σωστά όταν οι χρήστες βρίσκονται σε διαφορετικές ηπείρους, αλλάζουν ζώνες ώρας ή όταν η θερινή ώρα αποφασίζει να σου χαλάσει το βράδυ παραγωγής.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.epochconverter.com" rel="noopener noreferrer"&gt;Το πιο γνωστό και ευρέως χρησιμοποιούμενο εργαλείο μετατροπής Unix timestamps για developers.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.epochconverter.it" rel="noopener noreferrer"&gt;Online εργαλείο μετατροπής Unix Epoch timestamps σε αναγνώσιμες ημερομηνίες και αντίστροφα.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://epochconverter.dev" rel="noopener noreferrer"&gt;Developer-focused εργαλείο για μετατροπές Epoch, UTC και ISO 8601 timestamps.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.epochconverter.io" rel="noopener noreferrer"&gt;Web εφαρμογή για μετατροπές Unix time, debugging και date calculations.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://epoch.to" rel="noopener noreferrer"&gt;Minimalistic converter για γρήγορη μετατροπή Epoch timestamps και ημερομηνιών.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://epochconverter24.com/" rel="noopener noreferrer"&gt;Online converter με υποστήριξη Epoch, UTC, timezone conversions και date utilities.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://epoch-timestamp.com/" rel="noopener noreferrer"&gt;Εργαλείο μετατροπής Unix timestamps με έμφαση σε προγραμματιστικές χρήσεις και APIs.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://unixtime.org/" rel="noopener noreferrer"&gt;Πληροφοριακό site για Unix Time με converter και επεξήγηση του Epoch format.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.unixcalculator.com/" rel="noopener noreferrer"&gt;Unix timestamp calculator με δυνατότητες date arithmetic και μετατροπών χρόνου.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="mailto:nikosstit@gmail.com"&gt;nikosstit@gmail.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
