EF Core Relationships, Mixed Approach and Choosing One
Author: Oğuzhan KARAGÜZEL ·
In the earlier articles of the series we examined Database First and Code First separately. In this final article we first see how to insert and read related data with our Code First model, then explain the Mixed approach, compare all three approaches in terms of security and data consistency, and summarize which one to choose and when.
Inserting related data in one go
We use the Musteri, Siparis, Urun and Fatura model from the Code First article. Many-to-many is the most comprehensive relationship type, so let's work through that model. (Make sure you removed the IsDeleted field and the Calisan class that you added for demonstration earlier; otherwise you will get build or migration errors.)
We define a customer, an invoice and a product and attach them to the order's navigation properties. Finally we add only the order to the database and save:
using var context = new MarketDbContext();
// Existing customer: read from the same context, so EF Core is tracking it.
var customer = context.Musteriler.First(x => x.Email == "ahmet@example.com");
var order = new Siparis
{
SiparisTarihi = DateTime.UtcNow,
Musteri = customer,
Fatura = new Fatura { FaturaTarihi = DateTime.UtcNow.AddDays(4) },
Urunler = { new Urun { UrunAdi = "Anchovy", Fiyat = 150 } }
};
// We add only the order; EF Core discovers the related new objects itself.
context.Siparisler.Add(order);
context.SaveChanges();What happens behind the scenes?
EF Core inspects the order you added and its navigation properties. Objects that do not yet exist in the database (the new product and invoice) are queued for insertion, and when saving, EF Core inserts them first and obtains their Id values. It then writes those values into the foreign key columns of the other tables. Remember the SiparisUrun join table between orders and products: we never wrote it, but once both the order and the product have an Id, EF Core inserts the required row into the join table itself.
The customer already exists in the database and was read through the same context, so EF Core is tracking it. It is therefore not inserted again; only its Id is written to the order's MusteriId foreign key. Everything happens in a single SaveChanges call inside a single transaction; if any step fails, nothing is saved.
After running the code and inspecting the tables in DBeaver you will see the invoice row created in Faturalar with its SiparisId, the new product in Urunler, the order–product pairing in SiparisUrun, and the customer Id written into Siparisler.
Reading related data: Include
Let's read the same data starting from the order only. Thanks to EF Core we can include related data with Include without writing joins:
using var context = new MarketDbContext();
var order = context.Siparisler
.AsNoTracking()
.Include(x => x.Fatura)
.Include(x => x.Urunler)
.Include(x => x.Musteri)
.OrderBy(x => x.Id)
.First();
Console.WriteLine($"Order date : {order.SiparisTarihi}");
Console.WriteLine($"Invoice date : {order.Fatura?.FaturaTarihi}");
foreach (var product in order.Urunler)
Console.WriteLine($"Product : {product.UrunAdi} Price : {product.Fiyat}");
Console.WriteLine($"Customer : {order.Musteri.MusteriAdi}");A few notes: in read-only queries AsNoTracking() removes the change-tracking cost and makes the query faster. When you include several collections the query can produce a very large result set; in that case AsSplitQuery() or a Select projection that picks only the fields you need is more efficient. Also, related data is not loaded unless you write Include (unless you deliberately enabled lazy loading), so stating every relationship explicitly, as in the example, is the safest option.
What is the Mixed approach?
The Mixed approach combines Database First and Code First: you pull the existing database into code with Scaffold-DbContext and then manage later database changes with migrations. It lets you keep the existing structure while adding new entities.
In practice it works like this:
- Bring the existing database into the project with scaffolding (the command from the Database First article).
- Because the database is already populated, create the first migration as a baseline:
Add-Migration Baseline. Empty the bodies of the generated migration'sUpandDownmethods so it does not try to recreate the existing tables. - Run
Update-Database. This changes nothing in the database; it only writes the baseline record into the migration history table. - From then on, change your entities as in Code First and update the database with
Add-MigrationandUpdate-Database.
An important warning: if you run scaffolding again with -Force after the baseline, your manual changes are overwritten. If you choose the Mixed approach, settle the direction: from now on, who owns the schema, the code or the database team? Both sides changing the schema at the same time is the biggest cause of maintenance trouble.
Advantages and disadvantages
- Advantage: full compatibility with the existing database structure.
- Advantage: flexibility to add new entities while preserving the existing structure.
- Disadvantage: the complexity of using two different approaches together.
- Disadvantage: management and maintenance difficulties, especially if it stays unclear which side may change the schema.
DB First vs Code First vs Mixed
| Feature | DB First | Code First | Mixed |
|---|---|---|---|
| Ease of use | Easy for existing database structures | Ideal for new projects and full control | Complex, needs careful management |
| Flexibility | Tied to the database structure | Full flexibility from code | Both database and code flexibility |
| Management | Bring database changes into code with scaffolding | Easy management with migrations and updates | Both scaffold and migration management |
| Security and data consistency | Database administration comes first | Code-based checks and change tracking with Git | Combines the advantages of both |
Security and data consistency
Whichever approach you choose, a few rules do not change:
- Manage connection strings securely. Keep passwords out of source code and Git; use User Secrets or environment variables during development and a secrets manager (for example Azure Key Vault) in production.
- Apply least privilege. The database user your application uses must not be
sa; it should have only the permissions it needs. Running schema-changing migrations with a separate, more privileged account is a good practice. - Take backups. To avoid data loss during migrations and scaffolding, back up a live database before touching it and review the generated SQL script.
Who manages the database also shapes the choice. If only developers will deal with the database, Code First is usually the safer choice: every migration is traceable in Git, can be rolled back when needed, and the database history can be followed through the migration classes. If a separate team of database specialists runs it, Database First fits better: monitoring and data consistency sit with that team, so developers do not need to touch the schema and changes are simply reflected into code with scaffolding.
Conclusion: which one should you choose?
In short: if the database is ready or managed by a separate team, choose Database First; if there is no database and developers will own the schema, choose Code First; if you want to bring an existing database into code and manage it with code from then on, choose Mixed. Because migration classes make database changes traceable and safe, Code First is the most comfortable choice for most new projects.
One more note: the "Model First" approach of the older Entity Framework (drawing the model in a visual designer) is not supported in EF Core, which is why this series does not cover it. In EF Core the practical options are the three approaches above.
The series ends here, but the topic doesn't: what SQL your queries turn into behind the scenes and how to speed them up is covered in EF Core Query Performance, and indexes on the database side in SQL Server Indexes.
Sources
For official documentation see the Microsoft EF Core docs. Many articles on EF Core and .NET can also be found on the sites of my mentors Gençay Yıldız (gencayyildiz.com), Burak Selim Şenyurt (buraksenyurt.com) and Bora Kaşmer (borakasmer.com), whose writing I have learned from.

