10. Entity Framework
"Entity Framework Core (EF Core) is an object-relational mapper (ORM). An ORM provides a layer between the domain model that you implement in code and a database. EF Core is a data access API that allows you to interact with the database by using .NET plain old Common Runtime Language (CLR) objects (POCOs) and strongly typed Language Integrated Query (LINQ) syntax.
DbContext is a special class that represents a unit of work. DbContext provides methods that you can use to configure options, connection strings, logging, and the model that's used to map your domain to the database.
Classes that derive from DbContext:
- Represent an active session with the database.
- Save and query instances of entities.
- Include properties of type DbSet
that represent tables in the database."2 - It is the primary class responsible for interacting with the database.
Entity Framework supports
- database first (start with your existing database)
- code first (start with your classes etc, use this, whenever you can)
- EF models are built using a combination of three mechanisms: conventions, mapping attributes, and the model builder API.
EF Core is the part that runs on all platforms. EF supports many DBMS, see Database Providers. For MAUI I recommend to use SQLite.
Follow the instructions of Getting Started with EF Core and create your first console application using EF, i.e.
- Create a Console Application
`dotnet new console -o ProjectName - Navigate to the project folder and add the ef sqlite package
dotnet add package Microsoft.EntityFrameworkCore.Sqlite - Define your model classes and DBContext.
- Build the application -- if you have build errors, the following will not work!
- Create the database
Check the created folder
dotnet tool install --global dotnet-ef dotnet add package Microsoft.EntityFrameworkCore.Design dotnet ef migrations add InitialCreate dotnet ef database updateMigrations. - Add code to add rows to your database.
- Run the app
Rider & EF
If you follow the tutorial, the commands like Add-Migration will not work in a Rider terminal. Follow the instructions for Rider Running Entity Framework (Core) commands in Rider.
Database-creation
-
CLI Method: When using the CLI command
dotnet ef database update, the database is created in the directory where you execute this command. However, when you run the application without specifying a database path, it will look for the database in the bin/Debug/... folder by default. -
Programmatic Method: Alternatively, create the database programmatically using
context.Database.EnsureCreated();, where context is an instance of your DbContext-derived class. This method creates the database and necessary tables if they do not already exist, without requiring CLI commands (step 5).
Sqlite-viewer
Use the VS Code Extension SQLite to explore your database.
10.1 Model
The tutorial and example provided work well for simple databases with one table only or simple modelling, were each table matches one class of your application.
The fundamental principle of using Entity Framework (EF) is to focus on object-oriented design and let EF handle the relational aspects. This means you should model your classes in a way that makes sense for your application, and then use annotations and configuration to allow EF to map these classes effectively to the database.
public class Blog
{
public int Id { get; set; }
[Required]
[StringLength(60, MinimumLength = 3)]
public string? Title { get; set; }
[MaxLength(500)]
[Required]
public string Url { get; set; }
[Precision(3)]
public DateTime LastUpdated { get; set; }
[StringLength(30)]
[RegularExpression(@"^[A-Z]+[a-zA-Z()\s-]*$")]
public string? Topic { get; set; }
[NotMapped]
public DateTime LoadedFromDatabase { get; set; }
}
There are many attributes for your model classes, e.g. you can make a field required or define a field with a different name than id or StudentId or similar to be a key -- by convention, a property named Id or TypeNameId, e.g. StudentId, will be configured as the primary key of an entity.
Relationships may be implemented in the model classes by adding the foreign key id field and/or by embedding the object.
10.1.1 Many-to-many relationship
In the following you will find three equivalent methods to define a Many-to-many relationship.
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
Is equivalent to
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts);
}
Is equivalent to
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
"PostTag",
l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
j => j.HasKey("PostsId", "TagsId"));
}
The first method is preferred. Below you will find the resulting SQL schema
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "PostTag" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
10.1.2 Load Related Data
Eager Loading: You can use the Include method to specify related data to be included in query results. In the following example, the blogs that are returned in the results will have their Posts property populated with the related posts.
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ToList();
}
Explicit Loading: Related data is explicitly loaded using the Load method.
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
context.Entry(blog)
.Collection(b => b.Posts)
.Load();
context.Entry(blog)
.Reference(b => b.Owner)
.Load();
}
Lazy Loading: Related data is loaded on demand when the navigation property is accessed. It requires installing the Microsoft.EntityFrameworkCore.Proxies package and configuring the proxy in OnConfiguring. Do not use it in web applications!
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseLazyLoadingProxies()
.UseSqlServer(myConnectionString);
10.2 Further Reading
- Connect to and query Azure SQL Database using .NET and Entity Framework Core
- Building an App with Repositories and Document DB
- LINQ Queries
-
Jason Hales, Almantas Karpavicius, and Mateus Viegas. The C# workshop: Kickstart your career as a software developer with C#. Packt Publishing, Birmingham, UK, [first edition] edition, 2022. ISBN 9781800566491. URL: https://learning.oreilly.com/library/view/the-c-workshop/9781800566491/. ↩
-
Microsoft. Persist and retrieve relational data by using entity framework core. 2023. URL: https://learn.microsoft.com/en-us/training/modules/persist-data-ef-core/2-understanding-ef-core (visited on 30.07.2024). ↩