Overview of Entity Framework Core
EF Core is the official .NET data access technology. It’s an ORM (Object Relational Mapper), providing some framework for how to work with the data layer. It allows accessing Tables, Stored Procedures, Views, and more.
NuGet
Microsoft.EntityFrameworkCore
- just the core logic, no providers;Microsoft.EntityFrameworkCore.SqlServer
- SQL Server provider, installing it will also pull in the above package as its dependency.Microsoft.EntityFrameworkCore.Design
- a package that is needed for invoking migrations
DbContext
The tables names are inferred from the DbSet<>
names.
SaveChanges
The DbContext
tracks changes so that they can be applied when we call
SaveChanges()
on it.
A SaveChanges()
call applies all the requests to the database wrapped in a
transaction, so failure of some requests will not corrupt the database.
NoTracking
In some cases, we’re not interested in DbContext
’s tracking capabilities,
especially when building web APIs where we often fire some query to the DB and
dispose of the connection. In such cases we can enable NoTracking to improve
performance.
We can do that in a few ways:
-
in queries - useful when we normally want tracking, but for some specific query we don’t:
-
on
DbContext
- makes all queries NoTracking by default: -
in Dependency Injection registration:
Connection
In EF Core we need to explicitly provide info on which provider to use and what is the connection string. Here’s the simplest way to do it:
In a real scenario, we’d obviously not hardcode the connection string.
Conventions
EF Core has a bunch of conventions that are applied by default. Here’re some of them:
-
Property called
Id
or<Class>Id
will become a Primary Key -
for every Foreign Key, an index is created
-
a
string
type is turned intonvarchar(MAX)
in the DB (that’s a default from SQL Server provider, not the EF Core itself) -
When C#‘s Nullable feature is enabled, reference types are “not nullable” by default - the same applies to DB table’s columns
-
DB tables names come from
DbSet
’s name or, ifDbSet
was not defined, from a class’s name.
The conventions can be tweaked with:
- Data Annotations
- Fluent API (more powerful)
Migrations
Every change of data model is a prompt to do a migration. This way the database’s shape corresponds to our code model. Migrations are files and they are supposed to land in the VCS.
Migrations history is kept in the DB itself, in the “__EFMigrationsHistory” table.
Creating Migration
Command: dotnet ef migrations add <NAME>
. It should be invoked from a
directory of a .NET project containig DbContext
.
When adding a migration, the file Migrations/*ModelSnapshot.cs
is loaded and
compared with the current DbContext
. Based on the difference between these, a
new migration file is generated.
A migration file’s name contains a timestamp and migrations’s name. It is a class with 2 methods:
Up
- work to do when the migration is appliedDown
- work to do when the particular migration is revoked
Applying Migration
The EF Tool can either execute the migration (good in DEV), or it can generate an SQL file with all the migration steps (good for PROD) - we can then execute it later on.
Generating a script: dotnet ef migrations script <FROM> <TO>
- the SQL is
printed to stdout. We can specify the migration to start from and the one we
want to get to. By default it would generate SQL for all the migrations.
Tools
The migration tools can be installed as a NuGet package, or as a dotnet CLI tool
(dotnet tool install --global dotnet-ef
). The CLI tool is invoked with dotnet ef
.
Additionally, we’d need to install the Microsoft.EntityFrameworkCore.Design
NuGet package to access the Migration APIs from code.
Scaffolding
The “Code First” approach is recommended, but it is also possible to kind of
reverse engineer a DB into models. The EF Tool has the scaffold
subcommand for
such a usecase.
Relations
One-to-Many
If an entity contains a collection of another entity, EF Core treats it as a one-to-many relationship.
The child entity can optionally have a property of a type of a parent - it will be a reference. The child may also have an ID property that would be a Foreign Key to the parent. EF Core will initialize these properties for us.
Many-to-Many
To create many-to-many, we just need two entities referring to each other via collections:
EF Core has a convention that understands it and it will create three tables:
- Samurais
- Battles
- BattleSamurai - association of BattleIds with SamuraiIds (the PK will be a combination of these two FKs)
It’s called Skip Navigation, because in code we can forget about the intermediary table and just work with the two entities being related.
Join Class
For simplest cases, the above is enough. For some more advanced scenarios, we need to create additional class that joins our entities together.
Here’s an example of a class joining two entites (with some metadata):
This class represents the connection between a Samurai and a Battle. It’s
similar to the SQL table that EF Core creates behind the sceness. We’ve added a
DateJoined
parameter as some metadata about the connection (often called
“Payload”).
Additionally, we have to update the DbContext
to explicitly specify the
relationship:
For the simplest cases we wouldn’t have to do such configuration.
One-to-One
By default, the “principal” does not have to have any “dependant”, while the “dependant” has to have a “principal”.
In the DB, the “dependant” will have FK to the “principal”. The “principal” will not have FK to the “dependant”.
Example:
Queries
EF Core automatically generates SELECT
queries when accessing objects. We can
also customize the query that EF Core executes on the DbContext
level.
Execution Methods
The following methods allow us to query on DbSet
s:
ToList()
First()
FirstOrDefault()
Single()
- expects just one result to be found in the DB, throws otherwiseSingleOrDefault()
Last()
- should be preceed by theOrderBy
callLastOrDefault()
- should be preceed by theOrderBy
callCount()
LongCount()
Min()
Max()
Average()
Sum()
Find(PK_value)
- not LINQ, it’sDbSet
’s method that looks for a row with a specified key
Parametrizing
When invoking queries on the DB, the parameters that we provide may be parametrized or not. If we use a variable to provide some parameter, SQL will be parametrized. If we provide a hardcoded string, it will not be parametrized.
Related Data
By default, when pulling some entities, their related entities are not fetched. E.g., if I have a 1-to-Many relationship between samurais and quotes, if I query for the samurai, the list of quotes will be empty.
Eager Loading
With Eager Loading and its Include
method, we can specify Navigation
Properties that should be populated with the query:
Filtered Query
When including some child, we can filter it as well:
Other Variations
Here are some other sceanarios for Eager Loading:
-
Include children and grandchildren:
-
Include just grandchildren:
-
Include different children:
Explicit Loading
Explicit Loading allows to load related data when the “base” entity is already loaded:
Lazy Loading
Lazy Loading fetches the data from the DB as soon as we try to access some related data via a Navigation Property.
Limiting Properties
We can limit the properties to be returned with Select
:
In this case a list of anoymous objects will be returned, but we could use some known type as well.
An interesting usecase of that is to bring in just the count of some related data:
LIKE
We can use SQL’s LIKE
with the EF.Functions.Like
function:
Removing Data
Here’s how to remove some entity
Many-to-Many
Here’s how to remove a Many-to-Many relationship:
The Include
above is crucial. If we fetched battles without samurais,
DbContext
would not “know” that any samurais were in the battle, and the
Remove
call would not have any result.
In case when we’re using a more explicit Many-to-Many relationship, with a Join table class defined, we can remove an association as follows:
Adding Rows
We can add new rows:
Updates
We can update the entities as follows:
Since the same DbContext
instance was used to retrieve the object and to
update it, EF Core knows which properties have been updated. The SQL sent to the
DB will contain only the modified values.
In disconnected scenarios, the case is that we have to update some entity without retrieving it first (e.g., in some Web API). Then, we can do that as follows:
In such a case, since the context is fresh and does not “know” which properties have been changed, the SQL sent to the DB will contain all the properties of the entity to be updated.
Attach
When using Update
in the disconnected scenarios with related data, EF Core is
going to send unneeded requests to the DB. Here’s an example:
In this case, not only a new quote will be created, but also samurai’s
properties will be updated (all of them). The newContext
is fresh, it did not
track anything, and it doesn’t “know” what exactly has changed. To make sure
that DB is in proper state it will send to it everything.
We can use Attach
to circumvent that.
When using Attach
the DbContext
(newContext
in this case) will not treat
the provided data as “Modified”. Instead, it will treat it as “Unchanged”.
However, EF Core sees that one of the quotes in the samurai does not have a
value for the Id
(PK), and for the SamuraiId
(FK). It will understand that
this data needs to be sent to the DB.
Another approach would be to add the Quote
to the DB directly, instead of
doing that via a samurai. For this to work, we need to have a FK property of
Quote
:
Updating Related Entity
A similar issue that we had with a single entity when updating it in a Disconnected scenario occurs if we want to update some entity with relations:
The Update()
call here might be very “heavy”. It will not only update the
quote, but also the samurai, and all the other quotes that it has! A fresh
DbContext
instance does not know what the previous state was, so it will try
to update everything.
Replacing Update
with Attach
method will not help in this case, because it
will just mark all entities as “Unmodified” and EF Core will not issue any
updates at all. The modified quote already has Id
and SamuraiId
, so the
previous solution doesn’t work. We should use the following approach:
We’re manually informing EF Core that this quote was modified. Only this one
entity will be updated, related data is left unchanged - just one UPDATE
command will be sent to the DB.
Keyless Entities
EF Core 3.0 introduced the possibility to have entities without PK. Here’s how we can use it:
Create an entity:
Add a DbSet
to the DbContext
:
Configure the entity to be keyless in OnModelCreating
of the DbContext
:
Views
In case the SamuraiBattleStat
is supposed to be a View in the DB, we need
to configure it as follows:
Without that, EF Core would create a new table.
Raw SQL
In special cases, we can use raw SQL in EF Core. It might be more performant than relying on EF Core’s queries.
Migrations
For example, we might want to have a View or a Stored Procedure in our DB. To create that, we’d create an empty migration and add SQL to that migration. Here’s an example:
Querying
Here’s an example of how we can invoke raw SQL with a DbContext
:
Since we used a DbSet
, the result will be mapped to the entity of that
DbSet
. The entities will be tracked as well.
Stored Procedures
Other Requests
There is also another way to invoke SQL commands:
As a response we’ll get just a number of rows affected by the command. We could use it to execute some stored procedure that does not return data.
Logging
We can configure logging in multiple ways.
Tagging
Every interaction with the DbContext
may be tagged with a comment that will be
visible on the SQL Server side.
LogTo
A DbContext
class may be configured with a LogTo
call:
We’ll see logs generated by EF Core in the console.
By default, all values are hidden, since they might be sensitive. We can disable that hiding with:
Generic Host
For apps using the Logging infrastructure, we don’t have to do anything, logs will already be there.
Batching
If at least 4 operations have been added to the DbContext
, EF Core will
execute a batch request instead of sending these operations/requests separately.