DbContext and Entities
EF Core maps entity classes to tables, matching names by convention. A DbContext class lists a DbSet for each table and holds the connection. In ASP.NET Core, register it with AddDbContext and UseSqlServer, and controllers ask for it like any other service.
Journeyman · C# · Entity Framework Core · Updated
After this lesson you can
- Write entity classes and a DbContext with a DbSet for each table of an existing database
- Register a DbContext with AddDbContext and UseSqlServer, and ask for it in a controller
Key takeaways
- An entity is a class EF Core maps to a table: one object per row, and one property per column with the same name.
- By convention, a property named
Id, or the class name plusIdsuch asProductId, is the primary key. - Property types must match the columns: a nullable column needs a nullable property such as
int?, or anullthrows an exception. - A
DbContextstands for the database, with aDbSet<T>property for each table, such asDbSet<Product> Products. - Register the context with
AddDbContextandUseSqlServer, and a controller asks for it in its constructor, as it asked for a DAO.
EF Core needs two things from you: classes that describe the tables, and a class that describes the database.
The package
Add the Microsoft.EntityFrameworkCore.SqlServer package (it brings EF Core and the SQL Server provider). As with Microsoft.Data.SqlClient, also add a current System.Security.Cryptography.Pkcs, or the build warns about an older version with a known vulnerability:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.14" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.14" />
</ItemGroup>
Entities
An entity is a class EF Core maps to a table: one object per row, one property per column. They're the same plain classes you used with Dapper:
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public int? CategoryId { get; set; }
[Column(TypeName = "money")]
public decimal? UnitPrice { get; set; }
public short? UnitsInStock { get; set; }
public bool Discontinued { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; } = "";
public string? Description { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
}
EF Core works most things out by convention:
- The key: a property named
Id, or the class name plusId(ProductId), is the primary key. - The columns: each property maps to the column with the same name. Columns you leave out, such as
QuantityPerUnit, are simply ignored. - The types: C# types match SQL types:
int?for a nullableintcolumn,short?for a nullablesmallint,boolforbit. Get these right: anullin a column whose property can't hold one throws an exception. - Relationships:
Category.ProductsandProduct.CategoryIdtell EF Core that a category has many products, joined onCategoryId. A property likeProductsis called a navigation property.
Where a convention isn't enough, an attribute says more. [Column(TypeName = "money")] tells EF Core the column is SQL Server's money type, so it stores prices correctly. ([Table("Products")] and [Key] exist too, for names that don't follow the conventions.)
The DbContext
A DbContext stands for the database. Derive your own, with a DbSet<T> property for each table:
using Microsoft.EntityFrameworkCore;
public class NorthwindContext : DbContext
{
public NorthwindContext(DbContextOptions<NorthwindContext> options) : base(options)
{
}
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
}
- The
DbSet<Product>namedProductsmaps to theProductstable. Everything you do with products goes through it: queries, adds, removes. - The constructor takes the options, which say which database to use and how to reach it, and passes them to
DbContext.
Registering it in ASP.NET Core
In Program.cs, AddDbContext registers the context, and UseSqlServer gives it the connection string:
string connectionString = builder.Configuration.GetConnectionString("Northwind")!;
builder.Services.AddDbContext<NorthwindContext>(options => options.UseSqlServer(connectionString));
The context is registered scoped, like your DAOs: one per request, with its own connection, thrown away when the request ends.
A controller asks for it in its constructor, as it asked for a DAO:
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly NorthwindContext db;
public ProductsController(NorthwindContext db)
{
this.db = db;
}
[HttpGet("count")]
public async Task<int> Count()
{
return await db.Products.CountAsync();
}
}
GET /api/products/count answers 77. In fact, the DbContext plays the DAO's part: it's the class that talks to the database.
Trying it in a console app
Outside ASP.NET Core, build the options yourself and create the context with new. It's handy for experiments:
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
string connectionString = "Server=localhost;Database=Northwind;Integrated Security=true;TrustServerCertificate=true;";
DbContextOptions<NorthwindContext> options = new DbContextOptionsBuilder<NorthwindContext>()
.UseSqlServer(connectionString)
.Options;
using NorthwindContext db = new NorthwindContext(options);
Console.WriteLine( quot;{await db.Products.CountAsync()} products");
Console.WriteLine( quot;{await db.Categories.CountAsync()} categories");
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public int? CategoryId { get; set; }
[Column(TypeName = "money")]
public decimal? UnitPrice { get; set; }
public short? UnitsInStock { get; set; }
public bool Discontinued { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; } = "";
public string? Description { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
}
public class NorthwindContext : DbContext
{
public NorthwindContext(DbContextOptions<NorthwindContext> options) : base(options)
{
}
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
}
77 products
8 categories
using disposes the context when the program ends, closing its connection, just like a SqlConnection.
Check yourself
2 questions to try on the lesson page.
So what
Entities describe the rows, the DbContext describes the database, and AddDbContext hands it to whoever asks. With those three pieces in place, every table is a DbSet you can query with LINQ.
Key terms
- Entity
- DbContext
- DbSet
- Primary Key Convention
- Navigation Property