Skip to content

10. Entity Framework

orm
Entity Framework[^1]

"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.

ef
EF Core architecture

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.

  1. Create a Console Application `dotnet new console -o ProjectName
  2. Navigate to the project folder and add the ef sqlite package dotnet add package Microsoft.EntityFrameworkCore.Sqlite
  3. Define your model classes and DBContext.
  4. Build the application -- if you have build errors, the following will not work!
  5. Create the database
    dotnet tool install --global dotnet-ef
    dotnet add package Microsoft.EntityFrameworkCore.Design
    dotnet ef migrations add InitialCreate
    dotnet ef database update
    
    Check the created folder Migrations.
  6. Add code to add rows to your database.
  7. 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

  1. 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.

  2. 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);

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


  1. 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/

  2. 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).