🧩 1. Παράδειγμα Μοντέλων
📘 Παράδειγμα Domain:
Στο παρόν παράδειγμα θα κάνουμε χρήση ενός σύστηματος πανεπιστημίου με τις ακόλουθες οντότητες:
_- Student
- StudentProfile → σχέση 1-1
- Course → σχέση Ν-Ν (Students ↔ Courses)
- Department → σχέση 1-Ν (Department ↔ Students)_
Μοντέλα
// Domain/Entities/Student.cs
public class Student
{
public int Id { get; set; }
public string FullName { get; set; } = null!;
public int DepartmentId { get; set; }
// Relationships
public Department Department { get; set; } = null!;
public StudentProfile Profile { get; set; } = null!;
public ICollection<Course> Courses { get; set; } = new List<Course>();
}
Εξήγηση
Στην κλάση Student εκτός των ιδιωτήτων properties που αφορούν τα χαρακτηριστικά της κλάσης, έχουμε επιπλέον ένα property τύπου Department το οποίο σηματοδοτεί την σχέση του Φοιτητή με το τμήμα, δλδ σε ποιο τμήμα ανήκει ο φοιτητής καθός ένα τμήμα μπορεί να έχει πολλούς φοιτητές αλλά ένας φοιτητής ανήκει σε ένα και μόνο τμήμα.
Το StudentProfile δίχνει την 1 προς 1 σχέση του Φοιτητή με τα εκτεταμένα στοιχεία του.
Και τέλος η συλλογή Courses δίχνει τα μαθήματα που ο φοιτητής παρακολουθεί. Η σχέση αυτή είναι (N-N) πολλά προς πολλά καθώς ένας φοιτητής μπορεί να έχει πολλά μαθήματα και ένα μάθημα μπορεί να το επιλέξουν πολλοί φοιτητές.
// Domain/Entities/StudentProfile.cs
public class StudentProfile
{
public int Id { get; set; }
public DateTime BirthDate { get; set; }
public string Address { get; set; } = null!;
// 1-1 with Student
public int StudentId { get; set; }
public Student Student { get; set; } = null!;
}
Εξήγηση
Το StudentId είναι ξένο κλειδί συσχέτησης με την οντότητα Student.
Για να γίνει η συσχέτηση χρειάζεται και το property τύπου Student. Με αυτά τα δύο ολοκληρώνεται η συσχέτηση των δυο οντοτήτων.
// Domain/Entities/Department.cs
public class Department
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public ICollection<Student> Students { get; set; } = new List<Student>();
}
Εξήγηση
Η Συλλογή Students ικανοποιεί την παραδοχή ότι ένα τμήμα μπορεί να έχει πολλούς μαθητές αλλά ένας μαθητής έχει κάνει εγγραφή και ανήκει σε ένα και μόνο τμήμα. (1-Ν)
// Domain/Entities/Course.cs
public class Course
{
public int Id { get; set; }
public string Title { get; set; } = null!;
public ICollection<Student> Students { get; set; } = new List<Student>();
}
Εξήγηση
Η Συλλογή Students ικανοποιεί την παραδοχή ότι ένα μάθημα μπορεί να το έχουν πολλοί μαθητές και έτσι γνωρίζουμε αυτό το μάθημα ποιοί μαθητές το έχουν πάρει.
Πώς θα στήσουμε το DbContext για την επικοινωνία με τις 4 οντότητες.
// Infrastructure/Persistence/UniversityDbContext.cs
using Microsoft.EntityFrameworkCore;
using Domain.Entities;
public class UniversityDbContext : DbContext
{
public DbSet<Student> Students => Set<Student>();
public DbSet<StudentProfile> StudentProfiles => Set<StudentProfile>();
public DbSet<Department> Departments => Set<Department>();
public DbSet<Course> Courses => Set<Course>();
public UniversityDbContext(DbContextOptions<UniversityDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 1-1 Student ↔ Profile
modelBuilder.Entity<Student>()
.HasOne(s => s.Profile)
.WithOne(p => p.Student)
.HasForeignKey<StudentProfile>(p => p.StudentId);
// 1-N Department ↔ Students
modelBuilder.Entity<Department>()
.HasMany(d => d.Students)
.WithOne(s => s.Department)
.HasForeignKey(s => s.DepartmentId);
// N-N Student ↔ Course
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses"));
}
}
Εξήγηση
// 1-1 Student ↔ Profile
modelBuilder.Entity<Student>()
.HasOne(s => s.Profile)
.WithOne(p => p.Student)
.HasForeignKey<StudentProfile>(p => p.StudentId);
Ένας φοιτητής έχει ένα προφίλ με τα αναλυτικά του στοιχεία και ένα προφίλ ανήκει σε έναν και μόνο φοιτητή.
// 1-N Department ↔ Students
modelBuilder.Entity<Department>()
.HasMany(d => d.Students)
.WithOne(s => s.Department)
.HasForeignKey(s => s.DepartmentId);
Ένα τμήμα έχει πολούς μαθητές και ένας μαθητής φοιτεί σε ένα και μόνον τμήμα.
// N-N Student ↔ Course
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses"));
Ένα μάθημα μπορεί να το παρακολουθήσουν πολλοί γοιτητές και ένας φοιτητής μπορεί να παρακολουθεί πολλά μαθήματα. Για την σχέση αυτή βλεπουμε πως δημιουργείτε ένας ανδιάμεσος πίνακας ο οποίος κρατά τα ξένα κλειδιά για γνωρίζουμε ποιός φοιτητής παρακολουθεί ποιά μαθήματα.
1️⃣ Τι είναι το ModelBuilder και πώς δουλεύει
🔹 Ρόλος:
Το ModelBuilder είναι το αντικείμενο που σου δίνει το EF Core μέσα στη μέθοδο OnModelCreating του DbContext.
- Μέσω αυτού μπορείς να διαμορφώσεις (configure) το μοντέλο σου, δηλαδή:
- Πώς χαρτογραφούνται τα classes → σε tables
- Ποιες είναι οι σχέσεις (1-1, 1-Ν, Ν-Ν)
- Ποιο είναι το primary key / foreign key
- Πώς λέγονται οι πίνακες και οι στήλες
- Indexes, constraints, default values, precision κ.λπ.
🔹 Πώς δουλεύει στην πράξη
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 1️⃣ Πίνακας & Primary key
modelBuilder.Entity<Student>()
.ToTable("Students")
.HasKey(s => s.Id);
// 2️⃣ Required property & column name
modelBuilder.Entity<Student>()
.Property(s => s.FullName)
.IsRequired()
.HasColumnName("Full_Name");
// 3️⃣ One-to-One
modelBuilder.Entity<Student>()
.HasOne(s => s.Profile)
.WithOne(p => p.Student)
.HasForeignKey<StudentProfile>(p => p.StudentId);
// 4️⃣ One-to-Many
modelBuilder.Entity<Department>()
.HasMany(d => d.Students)
.WithOne(s => s.Department)
.HasForeignKey(s => s.DepartmentId);
// 5️⃣ Many-to-Many
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses"));
}
💡 Ο ModelBuilder χρησιμοποιεί Fluent API, δηλαδή αλυσιδωτές μεθόδους (chain methods), ώστε να διαμορφώνεις δηλωτικά τις σχέσεις και τα mappings.
Αν δεν τον χρησιμοποιήσεις, το EF κάνει convention-based mapping (βασισμένο στα property names).
Εξήγηση
// 1️⃣ Πίνακας & Primary key
modelBuilder.Entity<Student>()
.ToTable("Students")
.HasKey(s => s.Id);
Δήλωση ονόματος πίνακα και πίνακα καθώς και Primary key
// 2️⃣ Required property & column name
modelBuilder.Entity()
.Property(s => s.FullName)
.IsRequired()
.HasColumnName("Full_Name");
Δήλωση ότι απαιτείται το FullName και έχει το όνομα Full_Name
// 3️⃣ One-to-One
modelBuilder.Entity<Student>()
.HasOne(s => s.Profile)
.WithOne(p => p.Student)
.HasForeignKey<StudentProfile>(p => p.StudentId);
Δήλωση σχέσης πικάνων και ξένο κλειδί
// 4️⃣ One-to-Many
modelBuilder.Entity()
.HasMany(d => d.Students)
.WithOne(s => s.Department)
.HasForeignKey(s => s.DepartmentId);
Δήλωση σχέσης πικάνων και ξένο κλειδί
// 5️⃣ Many-to-Many
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses"));
_Δήλωση σχέσης πικάνων και πίνακα > StudentCourses για την διατήρηση των ξάνων κλειδιών στη σχέση (Ν-Ν) _
Migration (ενδεικτικά)
Στο terminal:
dotnet ef migrations add InitialCreate
dotnet ef database update
🧱 2️⃣ CRUD Ενέργειες (Create, Read, Update, Delete)
Ακολουθούν παραδείγματα για κάθε entity χρησιμοποιώντας DbContext και async/await.
🔹 CREATE (Εισαγωγή)
var department = new Department { Name = "Computer Science" };
context.Departments.Add(department);
await context.SaveChangesAsync();
var student = new Student
{
FullName = "John Doe",
DepartmentId = department.Id,
Profile = new StudentProfile
{
BirthDate = new DateTime(2000, 1, 1),
Address = "Athens"
}
};
context.Students.Add(student);
await context.SaveChangesAsync();
var course = new Course { Title = "Databases" };
context.Courses.Add(course);
await context.SaveChangesAsync();
// Many-to-Many association
student.Courses.Add(course);
await context.SaveChangesAsync();
🔹 READ (Ανάγνωση)
// Όλοι οι φοιτητές
var students = await context.Students.ToListAsync();
// Με Include (φόρτωμα σχέσεων)
var studentsWithDepartment = await context.Students
.Include(s => s.Department)
.Include(s => s.Profile)
.Include(s => s.Courses)
.ToListAsync();
// Εύρεση φοιτητή με ID
var student = await context.Students
.Include(s => s.Profile)
.Include(s => s.Department)
.FirstOrDefaultAsync(s => s.Id == 1);
🔹 UPDATE (Ενημέρωση)
var student = await context.Students.FirstAsync();
student.FullName = "Johnathan Doe";
student.Profile.Address = "Thessaloniki";
await context.SaveChangesAsync();
🔹 DELETE (Διαγραφή)
var student = await context.Students.FirstAsync();
context.Students.Remove(student);
await context.SaveChangesAsync();
3️⃣ Παραδείγματα Ερωτημάτων (LINQ / SQL-Style)
3.1 — Όλοι οι φοιτητές και το τμήμα τους
var query = await context.Students
.Include(s => s.Department)
.Select(s => new
{
StudentName = s.FullName,
Department = s.Department.Name
})
.ToListAsync();
3.2 — Όλα τα μαθήματα που παρακολουθεί ένας φοιτητής
int studentId = 1;
var courses = await context.Students
.Where(s => s.Id == studentId)
.SelectMany(s => s.Courses)
.Select(c => c.Title)
.ToListAsync();
ή
var studentWithCourses = await context.Students
.Include(s => s.Courses)
.FirstOrDefaultAsync(s => s.Id == studentId);
foreach (var course in studentWithCourses.Courses)
Console.WriteLine(course.Title);
3.3 — Ποιοι φοιτητές είναι σε κάθε τμήμα
var departments = await context.Departments
.Include(d => d.Students)
.Select(d => new
{
Department = d.Name,
Students = d.Students.Select(s => s.FullName)
})
.ToListAsync();
3.4 — Όλοι οι φοιτητές με πληροφορίες από προφίλ και μαθήματα
var data = await context.Students
.Include(s => s.Profile)
.Include(s => s.Department)
.Include(s => s.Courses)
.Select(s => new
{
s.FullName,
s.Profile.Address,
s.Profile.BirthDate,
Department = s.Department.Name,
Courses = s.Courses.Select(c => c.Title)
})
.ToListAsync();
3.5 — Raw SQL (αν θέλεις να γράψεις SQL απευθείας)
var students = await context.Students
.FromSqlRaw("SELECT * FROM Students WHERE DepartmentId = {0}", 1)
.ToListAsync();
4️⃣ Παράδειγμα Σύνθετου Σεναρίου
Βρες τον φοιτητή, τα μαθήματα του και το τμήμα του σε μία LINQ ερώτηση
int studentId = 2;
var studentDetails = await context.Students
.Where(s => s.Id == studentId)
.Select(s => new
{
s.FullName,
Department = s.Department.Name,
Courses = s.Courses.Select(c => c.Title)
})
.FirstOrDefaultAsync();
Console.WriteLine($"Φοιτητής: {studentDetails.FullName}");
Console.WriteLine($"Τμήμα: {studentDetails.Department}");
Console.WriteLine("Μαθήματα:");
foreach (var course in studentDetails.Courses)
Console.WriteLine($" - {course}");
⚠️ Προσοχή: το EF κάνει parameter binding, μην βάζεις raw string concatenation (SQL injection).
5️⃣ Χρήσιμες Εντολές EF CLI
# Δημιουργία migration
dotnet ef migrations add InitialCreate
# Ενημέρωση βάσης
dotnet ef database update
# Διαγραφή βάσης (όταν θες restart)
dotnet ef database drop
# Δημιουργία SQL script
dotnet ef migrations script
Ας δούμε κάποια επιπλέον ερωτήματα
6️⃣ Include / ThenInclude (Eager Loading)
👉 Φέρε όλους τους φοιτητές με το προφίλ και το τμήμα τους
var students = await context.Students
.Include(s => s.Profile)
.Include(s => s.Department)
.ToListAsync();
👉 Φέρε όλα τα τμήματα με τους φοιτητές τους και τα προφίλ των φοιτητών
var departments = await context.Departments
.Include(d => d.Students)
.ThenInclude(s => s.Profile)
.ToListAsync();
👉 Φέρε όλους τους φοιτητές με τα μαθήματα και το προφίλ
var studentsWithCourses = await context.Students
.Include(s => s.Courses)
.Include(s => s.Profile)
.ToListAsync();
👉 Φέρε φοιτητές ενός συγκεκριμένου τμήματος και τα μαθήματα τους
int departmentId = 1;
var students = await context.Students
.Where(s => s.DepartmentId == departmentId)
.Include(s => s.Courses)
.ToListAsync();
👉 Φέρε πόσοι φοιτητές υπάρχουν ανά τμήμα
var studentCounts = await context.Students
.GroupBy(s => s.Department.Name)
.Select(g => new
{
Department = g.Key,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.ToListAsync();
👉 Ποιοι φοιτητές έχουν πάνω από 3 μαθήματα
var students = await context.Students
.Where(s => s.Courses.Count > 3)
.Include(s => s.Courses)
.ToListAsync();
👉 Μέσος όρος ηλικίας φοιτητών ανά τμήμα
var averageAges = await context.Students
.Include(s => s.Profile)
.GroupBy(s => s.Department.Name)
.Select(g => new
{
Department = g.Key,
AverageAge = g.Average(s => EF.Functions.DateDiffYear(s.Profile.BirthDate, DateTime.Now))
})
.ToListAsync();
👉 Φέρε πλήρη καρτέλα φοιτητή
int studentId = 2;
var studentCard = await context.Students
.Where(s => s.Id == studentId)
.Include(s => s.Profile)
.Include(s => s.Department)
.Include(s => s.Courses)
.Select(s => new
{
s.FullName,
Department = s.Department.Name,
Address = s.Profile.Address,
BirthDate = s.Profile.BirthDate,
Courses = s.Courses.Select(c => c.Title)
})
.FirstOrDefaultAsync();
👉 Λίστα με φοιτητές και τον αριθμό μαθημάτων τους
var studentCourses = await context.Students
.Select(s => new
{
s.FullName,
CourseCount = s.Courses.Count
})
.OrderByDescending(x => x.CourseCount)
.ToListAsync();
👉 Όλα τα μαθήματα με φοιτητές που τα παρακολουθούν
var courseList = await context.Courses
.Include(c => c.Students)
.Select(c => new
{
c.Title,
Students = c.Students.Select(s => s.FullName)
})
.ToListAsync();
👉 Όλοι οι φοιτητές ενός μαθήματος
string courseTitle = "Databases";
var students = await context.Courses
.Where(c => c.Title == courseTitle)
.SelectMany(c => c.Students)
.Include(s => s.Department)
.ToListAsync();
👉 Join φοιτητών και τμημάτων (χωρίς navigation property)
var joinResult = await (from s in context.Students
join d in context.Departments on s.DepartmentId equals d.Id
select new
{
StudentName = s.FullName,
DepartmentName = d.Name
}).ToListAsync();
👉 Join φοιτητών και μαθημάτων μέσω πίνακα συνδέσμου
var studentCourses = await (from s in context.Students
from c in s.Courses
select new
{
Student = s.FullName,
Course = c.Title
}).ToListAsync();
👉 Soft delete φοιτητών
// Entity
public class Student
{
public int Id { get; set; }
public string FullName { get; set; } = null!;
public bool IsDeleted { get; set; } = false;
}
// Global Filter
modelBuilder.Entity<Student>()
.HasQueryFilter(s => !s.IsDeleted);
👉 Εκτέλεση stored procedure ή raw SQL query
var result = await context.Students
.FromSqlRaw("EXEC GetStudentsByDepartment @p0", 1)
.ToListAsync();
student.IsDeleted = true;
await context.SaveChangesAsync();
// Αυτόματα θα φιλτραριστεί από τα queries
➕ Φέρε όλους τους φοιτητές που παρακολουθούν ΜΑΘΗΜΑ στο οποίο είναι πάνω από 5 φοιτητές
var busyCoursesStudents = await context.Students
.Where(s => s.Courses.Any(c => c.Students.Count > 5))
.ToListAsync();
➕ Φέρε φοιτητές που είναι στο ίδιο τμήμα με κάποιον συγκεκριμένο φοιτητή
int studentId = 3;
var departmentId = await context.Students
.Where(s => s.Id == studentId)
.Select(s => s.DepartmentId)
.FirstAsync();
var classmates = await context.Students
.Where(s => s.DepartmentId == departmentId && s.Id != studentId)
.ToListAsync();
➕ Φέρε τμήματα που δεν έχουν φοιτητές
var emptyDepartments = await context.Departments
.Where(d => !d.Students.Any())
.ToListAsync();
➕ Φέρε φοιτητές χωρίς προφίλ
var studentsWithoutProfile = await context.Students
.Where(s => s.Profile == null)
.ToListAsync();
➕ Top 5 φοιτητές με τα περισσότερα μαθήματα
var topStudents = await context.Students
.OrderByDescending(s => s.Courses.Count)
.Take(5)
.Select(s => new { s.FullName, CourseCount = s.Courses.Count })
.ToListAsync();
7️⃣ Advanced Eager Loading & Performance Tips
Σενάριο | Παράδειγμα | Σχόλιο |
---|---|---|
Επίπεδο include | .Include(s => s.Courses) |
Φέρνει N:N σχέσεις |
Πολλαπλό επίπεδο | .Include(s => s.Courses).ThenInclude(c => c.Students) |
Nested includes |
Select για optimization | .Select(s => new { s.FullName, Courses = s.Courses.Select(c => c.Title) }) |
Επιστρέφεις μόνο τα πεδία που χρειάζεσαι |
No Tracking | .AsNoTracking() |
Για read-only queries (βελτιώνει performance) |
Split Queries | .AsSplitQuery() |
Αποφεύγει Cartesian explosion στα includes |
Κατέβασε από το git το solution Clean Architecture .NET 9 με SQL Server και Swagger. Download the solution (UniversitySolution.zip)
Τι περιλαμβάνει το πακέτο
- src/Domain — Entities (Student, StudentProfile, Department, Course)
- src/Application — Interfaces & StudentService
- src/Infrastructure — UniversityDbContext, repository, EF Core packages (SqlServer)
- src/WebApi — Web API με StudentsController, Program.cs, appsettings.json και Swagger
- Seed data στο OnModelCreating (μερικοί initial records)
- README με οδηγίες
Τι να κάνεις μετά (βήμα-βήμα)
Αποσυμπίεσε / άνοιξε το zip.
Άλλαξε τη σύνδεση στη src/WebApi/appsettings.json στο DefaultConnection για να δείχνει τη SQL Server instance σου.
Παράδειγμα:
"Server=localhost;Database=UniversityDb;Trusted_Connection=True;"-
Από το root του project:
- dotnet restore
- dotnet build
Δημιούργησε το migration και ενημέρωσε τη βάση:
- dotnet ef migrations add InitialCreate --project src/Infrastructure --startup-project src/WebApi
- dotnet ef database update --project src/Infrastructure --startup-project src/WebApi
-
Τρέξε το API:
- dotnet run --project src/WebApi
Άνοιξε Swagger UI (σε Development): https://localhost:5001/swagger ή κατάλληλο URL από το console.
Σημειώσεις & περιορισμοί
Χρησιμοποίησα EF Core 8 package versions that match .NET 9 usage; μπορείς να αναβαθμίσεις αν χρειάζεσαι.
Η seeding των many-to-many rows σε implicit join table έχει placeholder — αν θέλεις να περάσω και συγκεκριμένα seed entries για StudentCourses, μπορώ να προσθέσω ρητό join entity (π.χ. StudentCourse) και να σεeding-άρω πλήρως.
Αν θες, προσθέσε επίσης:
- DTOs + AutoMapper
- Repository pattern με generic base
- Integration tests / xUnit project
- Example Postman collection
Πηγές Entity Framework Tutorial
Δείτε συμπληρωματικά Εισαγωγή στα SQL JOINs με Παραδείγματα
Top comments (0)