This application was built in stages to demonstrate how EF Core works. It was built specifically for the "EF Core for Modern Data Access" presentation at DEVintersection Orlando Spring 2021.
To get the final application, simply clone this repository:
git clone https://github.com/JeremyLikness/EFCoreModernDataAccess.git
Set your current working directory to the root of the EFCoreModernDataAccess
project.
cd EFCoreModernDataAccess/EFCoreModernDataAccess
Run the project. It will automatically create the SQLite database, seed the data, and run the examples.
dotnet run
The database is written as conference.sqlite
.
The project features tagged commits to show the stages of development.
git checkout step00
This is a bare-bones console application. For a friendly message, build and run it:
dotnet run
git checkout step01
Explore the new Domain
subfolder and see how the domain is defined. Speakers and attendees are modeled as specialized forms of participants. Speakers and sessions can both have tags (categories).
git checkout step02
Run the seed logic:
dotnet run
Put a breakpoint after the sessions are created.
git checkout step03
Create the database:
dotnet run
Use the SQLite extension in Visual Studio Code to view the generated database: conference.sqlite
.
Note the pivot or many-to-many tables are implicitly created as "shadow entities." The speakers and attendees have their own tables (table-per-type or TPT). To see what it looks like as table-per-hierarchy (TPH) simply add this to the model configuration:
modelBuilder.Entity<Participant>();
Re-run and examine the new table structure.
git checkout step04
Run the queries:
dotnet run
The questions include:
- What tags are there?
- How many sessions and speakers use each tag?
- What speakers use the "Azure" tag?
- What sessions are being presented by speakers using the “Azure” tag?
- What sessions tagged with "Azure" are being presented by speakers using the “Azure” tag?
The answers:
Query Name | LINQ | Description |
---|---|---|
GetTagsAsync |
context.Tags.OrderBy(t => t.Name); |
A simple query showing explicit loading (related entities are not loaded by default). |
GetTagsWithRelatedEntitiesAsync |
var query = context.Tags .Include(t => t.Speakers) .Include(t => t.Sessions) .OrderBy(t => t.Name); |
Explicit loading using the Includes extension. |
GetTagsWithProjectionAsync |
context.Tags .Include(t => t.Speakers) .Include(t => t.Sessions) .Select(t => new { t.Name, Speakers = t.Speakers.Count, Sessions = t.Sessions.Count }) .OrderBy(t => t.Name); |
Same query using projection to limit to just the properties needed to satisfy the query. |
GetSpeakersWithTagAsync |
context.Speakers .Where(s => s.Tags.Any(t => t.Name == "Azure")) .Select(s => new { s.LastName, s.FirstName }) .OrderBy(s => s.LastName) .ThenBy(s => s.FirstName); |
Simple filter on a related entity. |
GetSessionsForSpeakersWithTagAsync |
context.Speakers .Include(s => s.Presentations) .Where(s => s.Tags.Any(t => t.Name == "Azure")) .Select(s => new { s.LastName, s.FirstName, Presentations = s.Presentations.Select(p => p.Name) }) .OrderBy(s => s.LastName) .ThenBy(s => s.FirstName); |
Filter with explicit loading of related entity. |
GetSessionsWithTagForSpeakersWithTagAsync |
context.Speakers .Include(s => s.Presentations) .ThenInclude(p => p.Tags) .Where(s => s.Tags.Any(t => t.Name == "Azure")) .Select(s => new { s.LastName, s.FirstName, Presentations = s.Presentations .Where(p => p.Tags.Any(t => t.Name == "Azure")) .Select(p => p.Name) }) .OrderBy(s => s.LastName) .ThenBy(s => s.FirstName); |
Example of filtered entity with filtered related entities and explicit loading. |
...and what EF Core generates:
Query Name | Generated SQL | Description |
---|---|---|
GetTagsAsync |
SELECT "t"."Id", "t"."Name" FROM "Tags" AS "t" ORDER BY "t"."Name" |
A simple query showing explicit loading (related entities are not loaded by default). |
GetTagsWithRelatedEntitiesAsync |
SELECT "t"."Id", "t"."Name", "t0"."SpeakersId", "t0"."TagsId", "t0"."Id", "t0"."Bio", "t0"."FirstName", "t0"."LastName", "t1"."SessionsId", "t1"."TagsId", "t1"."Id", "t1"."Description", "t1"."Name", "t1"."SessionEnd", "t1"."SessionStart" FROM "Tags" AS "t" LEFT JOIN ( SELECT "s"."SpeakersId", "s"."TagsId", "s0"."Id", "s0"."Bio", "s0"."FirstName", "s0"."LastName" FROM "SpeakerTag" AS "s" INNER JOIN "Speakers" AS "s0" ON "s"."SpeakersId" = "s0"."Id" ) AS "t0" ON "t"."Id" = "t0"."TagsId" LEFT JOIN ( SELECT "s1"."SessionsId", "s1"."TagsId", "s2"."Id", "s2"."Description", "s2"."Name", "s2"."SessionEnd", "s2"."SessionStart" FROM "SessionTag" AS "s1" INNER JOIN "Sessions" AS "s2" ON "s1"."SessionsId" = "s2"."Id" ) AS "t1" ON "t"."Id" = "t1"."TagsId" ORDER BY "t"."Name", "t"."Id", "t0"."SpeakersId", "t0"."TagsId", "t0"."Id", "t1"."SessionsId", "t1"."TagsId", "t1"."Id" |
Explicit loading using the Includes extension. |
GetTagsWithProjectionAsync |
SELECT "t"."Name", ( SELECT COUNT(*) FROM "SpeakerTag" AS "s" INNER JOIN "Speakers" AS "s0" ON "s"."SpeakersId" = "s0"."Id" WHERE "t"."Id" = "s"."TagsId") AS "Speakers", ( SELECT COUNT(*) FROM "SessionTag" AS "s1" INNER JOIN "Sessions" AS "s2" ON "s1"."SessionsId" = "s2"."Id" WHERE "t"."Id" = "s1"."TagsId") AS "Sessions" FROM "Tags" AS "t" ORDER BY "t"."Name" |
Same query using projection to limit to just the properties needed to satisfy the query. |
GetSpeakersWithTagAsync |
SELECT "s"."LastName", "s"."FirstName" FROM "Speakers" AS "s" WHERE EXISTS ( SELECT 1 FROM "SpeakerTag" AS "s0" INNER JOIN "Tags" AS "t" ON "s0"."TagsId" = "t"."Id" WHERE ("s"."Id" = "s0"."SpeakersId") AND ("t"."Name" = 'Azure')) ORDER BY "s"."LastName", "s"."FirstName" |
Simple filter on a related entity. |
GetSessionsForSpeakersWithTagAsync |
SELECT "s"."LastName", "s"."FirstName", "s"."Id", "t"."Name", "t"."PresentationsId", "t"."SpeakersId", "t"."Id" FROM "Speakers" AS "s" LEFT JOIN ( SELECT "s1"."Name", "s0"."PresentationsId", "s0"."SpeakersId", "s1"."Id" FROM "SessionSpeaker" AS "s0" INNER JOIN "Sessions" AS "s1" ON "s0"."PresentationsId" = "s1"."Id" ) AS "t" ON "s"."Id" = "t"."SpeakersId" WHERE EXISTS ( SELECT 1 FROM "SpeakerTag" AS "s2" INNER JOIN "Tags" AS "t0" ON "s2"."TagsId" = "t0"."Id" WHERE ("s"."Id" = "s2"."SpeakersId") AND ("t0"."Name" = 'Azure')) ORDER BY "s"."LastName", "s"."FirstName", "s"."Id", "t"."PresentationsId", "t"."SpeakersId", "t"."Id" |
Filter with explicit loading of related entity. |
GetSessionsWithTagForSpeakersWithTagAsync |
SELECT "s"."LastName", "s"."FirstName", "s"."Id", "t0"."Name", "t0"."PresentationsId", "t0"."SpeakersId", "t0"."Id" FROM "Speakers" AS "s" LEFT JOIN ( SELECT "s1"."Name", "s0"."PresentationsId", "s0"."SpeakersId", "s1"."Id" FROM "SessionSpeaker" AS "s0" INNER JOIN "Sessions" AS "s1" ON "s0"."PresentationsId" = "s1"."Id" WHERE EXISTS ( SELECT 1 FROM "SessionTag" AS "s2" INNER JOIN "Tags" AS "t" ON "s2"."TagsId" = "t"."Id" WHERE ("s1"."Id" = "s2"."SessionsId") AND ("t"."Name" = 'Azure')) ) AS "t0" ON "s"."Id" = "t0"."SpeakersId" WHERE EXISTS ( SELECT 1 FROM "SpeakerTag" AS "s3" INNER JOIN "Tags" AS "t1" ON "s3"."TagsId" = "t1"."Id" WHERE ("s"."Id" = "s3"."SpeakersId") AND ("t1"."Name" = 'Azure')) ORDER BY "s"."LastName", "s"."FirstName", "s"."Id", "t0"."PresentationsId", "t0"."SpeakersId", "t0"."Id" |
Example of filtered entity with filtered related entities and explicit loading. |
git checkout step05
Run the queries:
dotnet run
The questions include:
- How many possible combinations of speakers and attendees are there?
- Which attendees share the most sessions together?
- Which attendees share the least sessions together?
Look at AdvancedQueries.cs
for details.
git checkout step06
Run the updates:
dotnet run
Examples include:
- Using the simple logging
LogTo
directive in options configuration - Using the
ChangeTracker
to show before/after snapshots - Simple load, manipulate, update
- Graph update (modify properties on related entities and save)
- Disconnected entity (common scenario in Web API implementations) update
- Concurrency detection
This app is designed to show how to map a domain to the backend database using EF Core. Please provide any feedback, comments, suggestions, or questions you may have.
Regards,