Entity Framework Core#
1. Navigation Properties#
// A |->* B
public class A{
//...
public List<B> Bs{get;set;}
}
public class B{
// ...
public B B{set;get;}
}
Navigation properties can be any list type.
2. Database Context ⚠️#
A management class used to coordinate EF and the database.#
- A Context is a non-abstract class that inherits from DbContext. The primary function of this class is to expose collections of all entities, allowing operations on these collections to read or save data in storage. Additionally, the Context holds the model for saving data. Each entity's model and other necessary configurations must be correctly set up before use.
- Another important function of the Context is to track changes to entities so that it knows how to handle them when saving changes.
- Each entity tracked by the Context is in one of the following states: Unchanged/Added, Modified/Modified, Added/Added, Deleted/Deleted, or Detached/Detached. You can think of the context as a sandbox where we can make changes to the entity collections and then apply those changes with a single save operation.
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
// Entity sets DbSet correspond to database tables
// Since the Courses table includes the two tables below, EF will automatically include them even if not explicitly written.
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
// Override default behavior to set table names
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}
DbContext Hook Methods#
- OnConfiguring Method: This method is called when the context needs to configure its provider and connection string, providing an entry point for developers to intervene.
- OnModelCreating Method: This method is automatically called when EF Core assembles the data model, and you will add code to build the model here.
- SaveChanges Method: This method is called when we want to save modifications to the database. It returns the number of records affected by the save operation.
Database Provider#
- EF Core is database-agnostic. Therefore, different databases require specific interfaces to be provided.
- Introduce the relevant database provider packages and add the corresponding configuration method (UseSqlServer) in the Context's OnConfiguring.
3. Service Registration#
// Startup.cs
/* Using */
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
/* Start up */
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolContext>(
options=>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllersWithViews();
// Exception filtering feature for development environment
services.AddDatabaseDeveloperPageExceptionFilter();
}
// appsetting.json
{
"ConnectionStrings": {
"DefaultConnection": "Database connection string"
},
}
4. Creating Data Models#
-
Creating a data model requires Entity types. Entity types are a series of C# classes that typically correspond to database tables.
-
Configure the relationship between Entity types and Database entities. There are two ways: "Fluent API" and "Data Annotations".
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; // Configure corresponding table name and corresponding database schema name [Table("blogs",Schema="dbo")] public class Blog{ [Key] // Primary key, usually efcore will automatically recognize public int BlogId{set;get;} [MaxLength(400)] // Maximum length public string Url{set;get;} [NotMapped] // Exclude mapping public DataTime LoadedFromDatabase {set;get;} [Column("rating",TypeName="decimal(5,2)")] // Map column name and type public decimal Rating {set;get;} } // Use fluent API to configure default schema name protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.HasDefaultSchema("dbo"); }
"Entity Reference/Reference Navigation" Configuration One-to-One#
// One Blog corresponds to one Customer
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogImage> BlogImages { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Set up one-to-one relationship between Blog and BlogImage
modelBuilder.Entity<Blog>()
.HasOne(b => b.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
}
public class Blog
{
public Blog()
{
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public int? ImageId { get; set; }
// Reference navigation efcore can automatically configure, see official documentation for writing
public BlogImage BlogImage { get; set; }
public HashSet<Post> Posts { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public string ImageUrl { get; set; }
public string Caption { get; set; }
}
"Entity Collection/Collection Navigation" Configuration: One-to-Many#
// Configuration of entity collection properties
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure the one-to-many relationship of Blog
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId);
}
}
public class Blog
{
public Blog()
{
// Because there is a HashSet navigation, the constructor needs to add this content
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public int? ImageId { get; set; }
public BlogImage BlogImage { get; set; }
// Collection navigation, indicating a one-to-many relationship, requires HashSet assistance
public HashSet<Post> Posts { get; set; }
}
public class Post
{
[Key]
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Summary#
The role of the data model is not only to describe the data structure but also to explain the data relationships. Relationships are also core to databases.
EF Core supports various ways to describe model relationships, using annotations is the most intuitive and straightforward method, while special relationships require the use of FluentAPI for assistance.
Both methods configure related behaviors in the ModelBuilder.
5. Migrations from Model to Database#
Migrations are used to create and manage databases from entity models using the EF Core migration tools.
Migration Process#
-
Follow the steps above to build entity models and relationships, resulting in a class. This class typically has the intended database name + Context, inheriting from DbContext. EF Core generates the database based on the entity models and relationship descriptions in this file.
-
Open the terminal.
-
First, ensure that the .NET Core CLI tools are installed.
# Install the dotnet-ef tool globally dotnet tool install --global dotnet-ef # Update dotnet tool update --global dotnet-ef # Add EFCoreDesign package to the project dotnet add package Microsoft.EntityFrameworkCore.Design # You can also use the NuGet package manager # Check tool installation status dotnet ef
⚠️ EF Core Tools Reference (.NET CLI) - EF Core | Microsoft Learn for detailed usage of the tools.
-
Open the terminal in the project directory:
# Create a migration dotnet ef migrations add InitialCreate ## This command will generate a migration folder to record migrations, and you can also modify its contents directly. # Create the database schema dotnet ef database update
⚠️ For validation issues, you can add
TrustServerCertificate=true;Encrypt=true
to the connection string. -
Done.
Evolving Models#
When your entity model changes, we need to update the database again.
It is important to clarify that the migrations/ series of files generated by the migration tool are migration records, providing a complete description of the evolution of the database structure. They contain a snapshot of the database structure and a migration file.
Therefore, when the model changes, we can manually modify the migrations/ files and execute dotnet ef database update
again to apply the migration.
To retain records of database evolution, we use the following process:
-
Create a new migration.
# It is best to use meaningful migration names here dotnet ef migrations add AddSomeTable
-
Apply the migration
dotnet ef database update
.
Explanation#
Reading the migration/ folder, there are a series of .cs
files with timestamps and migration names indicating the actions executed during each migration. It can be seen that when the entity model changes, EF Core automatically compares and performs incremental updates instead of regenerating the database.
Another file is the .cs
file for the database snapshot, which contains the actual database table entity model. It is the database snapshot from the last update.
Both types of files are C# files that can be included in version control to record the evolution of the database during the development process.
6. Others#
Excluding Types#
If the current Context references types from other Contexts, conflicts may occur during migration (the referenced objects do not exist in the current Context, and errors are normal). In this case, you need to exclude that type from the Context.
// This is the referenced Context class
// In the model creation hook, exclude the non-existent type.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUser>()
.ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}
Renaming Columns#
EF Core cannot distinguish between renaming or deleting columns, so when you rename a column, EF Core will generate Delete-Create operations. This may lead to data loss and needs to be manually changed.
// EF Core generated migration actions
migrationBuilder.DropColumn(
name: "Name",
table: "Customers");
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customers",
nullable: true);
// It needs to be modified to
migrationBuilder.RenameColumn(
name: "Name",
table: "Customers",
newName: "FullName");
Introducing SQL Behavior in Migrations#
Suppose you want to merge the original Firstname and LastName in the entity into FullName for re-storage and delete the first two columns.
EF Core will not automatically recognize this behavior, so you need to manually introduce SQL statements for migration.
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customer",
nullable: true);
migrationBuilder.Sql(
@"
UPDATE Customer
SET FullName = FirstName + ' ' + LastName;
");
migrationBuilder.DropColumn(
name: "FirstName",
table: "Customer");
migrationBuilder.DropColumn(
name: "LastName",
table: "Customer");
⚠️ This process is sequential.
When migrating using old data, SQL can be used.
Custom SQL Migrations#
You can generate an empty migration without changing the model and manually fill in SQL statements.
SQL can operate on database objects that the model does not know about.
migrationBuilder.Sql(
@"
EXEC ('CREATE PROCEDURE getFullName
@LastName nvarchar(50),
@FirstName nvarchar(50)
AS
RETURN @LastName + @FirstName;')");
⚠️ When a statement must be the first or only statement in a SQL batch, use EXEC
. It can also be used to resolve parser errors in idempotent migration scripts when referenced columns do not currently exist in the table.
⚠️ EF Core typically executes migration operations within a transaction. When certain databases do not support this operation, you can pass the parameter suppressTransaction: true
to migrationBuilder.Sql
to choose not to use a transaction.
7. Migration Management#
Deleting Migrations#
Use dotnet ef migrations remove
to remove the last migration.
⚠️ Do not remove applied migrations!
Listing Migrations#
dotnet ef migrations list
lists all migrations.
Resetting All#
You can delete the Migration folder directly to remove all migration records. For details:
Managing Migrations - EF Core | Microsoft Learn
Applying Migrations#
-
Generate SQL scripts to create the database from scratch. This is the best practice for deploying the project model to a production environment.
dotnet ef migrations script [from] [to]
From and to are the migration version names. To defaults to the latest. You can use the above code to roll back.
-
Automatically compare SQL scripts. This is useful when applying migrations to an existing database but not knowing its current version. EF Core will retrieve the migration history and update automatically.
dotnet ef migrations script --idempotent
-
Use command-line tools to upgrade the database, which is the best practice for development and testing environments.
dotnet ef database update [to]
-
Execute migrations at runtime, which is the best practice for local development testing, saving the step of manually migrating each time you start.
// Add in the .net main function var host = CreateHostBuilder(args).Build(); using(var scope = host.Services.CreateScope()){ var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); younameContext.Database.Migrate(); // This line is key. } host.Run();
-
Create bundles.
Team Management#
⏰