1. 12
How to use EF
Core
In this chapter, you’ll learn how to use Entity Framework (EF)
Core to work
with data in a database. This includes how to create a new
database from code
(Code First development) and how to create code from an
existing database
(Database First development).
By : Eng. Mahmoud Hassouna
Email : M.hassuna2@gmail.com
2. How to create a
database from code
Entity Framework (EF) Core is an object-relational
mapping (ORM)
framework that allows you to map your entity classes
to the tables of a database.
The most common way to work with EF Core is to
code your entity classes and
a database context class first. Then, you can use EF
to create a new database
from these classes. This is called Code First
development, and it’s the approach
that you’ll learn about now.
3. How to code entity and
DB context classes
Entity classes represent the data structure for an app.
For example, a
bookstore has entities like books, authors, and
genres. As a result, a Bookstore
app needs entity classes to store data for those
entities.
To illustrate, figure 12-1 begins with two examples
that show the Book and
Author entity classes. You can create objects from
these classes to store the data
that’s needed by your app.
4. Description
• Entity classes define the data structure for the app
and map to tables in a relational
database. A database (DB) context class inherits the
DB Context class and includes
entity classes as DB Set properties.
• In the DB context class, the On Configuring()
method allows you to configure the
context, such as providing the connection string.
Usually, though, you do this in the
Startup's file.
• In the DB context class, the OnModelCreating()
method allows you to configure the
entity classes, such as providing initial seed data.
• To work with EF, you need to configure the
database context. To do that, you can
modify your appsettings.json and Startup.cs files as
described in figure 4-6 of chapter 4.
5. How to configure the
database
You can configure your entity classes so they
create the database tables you
want in three ways. First, you can configure them
by convention. Second, you
can configure them with data attributes. Third,
you can configure them by using
code.
When you configure by convention, you code
your classes following
established conventions, and EF takes it from
there. Figure 12-2 starts by
presenting some of these conventions. For
example, if you code a property
named Id (or ID) or join the name of the entity
and Id (or ID), EF treats that
property as the primary key of the database
table. Additionally, if this property .
6. Some of the conventions for configuration in EF Core
• A property named Id or ClassNameId is the primary key. If the
property is of the int type,
the key is an identity column and its value is generated automatically
by the database.
• A string property has a database type of nvarchar(max) and is
nullable.
• An int or double property is not nullable.
Some of the data attributes for configuration
7. Some of the Fluent API methods for configuration
8. How to manage
configuration files
The first code example in figure 12-3 shows the
OnModelCreating() method
of a context class with several statements that
configure a Book entity. The
first statement sets the ISBN property as the primary
key. This is necessary
because that property doesn’t follow the naming
convention for a primary key.
The second statement makes the Title property not
allow nulls and to have a
maximum length of 200 characters. And the third
statement uses the HasData()
method to provide seed data when the table is
created.
The statements in this example are for a single
entity that has two properties.
However, imagine how this code will grow as you
add more properties to this
9. Description
• If you have a lot of Fluent API configuration code in
the OnModelCreating() method
of your context class, it can become difficult to
understand and maintain. In that
case, it’s considered a best practice to divide this
code into one configuration file per
entity.
• To create a configuration file, you code a class
that implements the
IEntityTypeConfiguration<T> interface and its
Configure() method.
• The Configure() method accepts an
EntityTypeBuilder<T> object that represents the
entity being configured.
• To apply the configuration code, you pass a new
instance of the configuration class
to the ApplyConfiguration() method of the
ModelBuilder object that’s passed to the
OnModelCreating() method of the context class.
10. EF commands for working with a
database
In chapter 4, you learned how to use the NuGet
Package Manager Console
(PMC) to execute PowerShell commands such as
Add-Migration and Update-
Database to create a database from DB context
and entity classes. Figure 12-4
starts by reviewing how you open the PMC window.
Then, it summarizes the
Add-Migration and Update-Database commands,
as well as some additional
commands for working with a database. For any of
these commands to work,
your app needs to be configured to access a
database as shown in figure 4-6.
11. The first command, Add-Migration, generates a
migration file based on
your context and entity classes. By default, this
command creates the file with
the name you specify in the Migrations folder. If that
folder doesn’t exist, this
command creates it. If you want, you can use the -
OutputDir parameter to
specify another folder, as you’ll see shortly.
The second command, Remove-Migration,
removes the last migration file
from the Migrations folder. This only works, though, if
the migration file hasn’t
yet been applied to the database. If it has, you need
to use the Update-Database
command to revert the migration before you can
remove the migration file.
You’ll see an example of this in the next figure.
12. The third command, Update-Database, applies
any migration files that
haven’t yet been applied to the database. You can
also use the Update-Database
command to revert migrations as shown in the next
figure.
The fourth command, Drop-Database, deletes
the database. If this command
doesn’t work correctly, you can use SQL Server
Object Explorer or another
comparable tool to delete the database.
The fifth command, Script-Migration, generates a
SQL script based on one
or more migration files.
The sixth command, Scaffold-DbContext,
generates DB context and entity
classes from an existing
13.
14. Description
• The PMC executes PowerShell commands to
create, apply, and revert migration files.
• A migration file contains C# code for creating or
updating database objects. Specifically,
each migration file has an Up() method with code
that runs when a migration is applied,
and a Down() method with code that runs when a
migration is reverted.
• The first time you run the Add-Migration command,
EF creates a file named
DbContextNameModelSnapshot.cs. This file contains
a “snapshot” of the current
database schema. When you add or remove
subsequent migrations, that file is updated.
15. How to use EF migration commands
Figure 12-5 presents some examples that use EF
commands in the Package
Manager Console (PMC) to work with the Bookstore
database. The first scenario
in this figure shows how to create and then update
the database. Step 1 creates a
migration file named Initial.cs in the Migrations folder.
Then, step 2 updates the
database. This applies the Initial migration and
creates the database.
16. How to create and then update a
database
1. Create a migration file named Initial based
on the context and entity classes
presented earlier in this chapter by entering this
command:
PM> Add-Migration Initial
2. Create a database based on the migration
file by entering this command:
PM> Update-Database
3. Add a property named Discount of the
double type to the Book class.
4. Create a migration file named AddDiscount
by entering this command:
PM> Add-Migration AddDiscount
5. Review the migration file and note that the
Discount property doesn’t accept nulls.
6. Change the Discount property in the Book
class to the data type of double?.
17. 7. Generate another migration file by entering this
command:
PM> Add-Migration MakeDiscountNullable
8. Apply the migration files to the database by
entering this command:
PM> Update-Database
18. How to revert one or more migrations
1. To revert changes to the database by running the
Down() method in every migration
file that comes after the AddDiscount file, enter this
command:
PM> Update-Database AddDiscount
2. To remove the unapplied MakeDiscountNullable
migration file that was reverted in
step 1 from the Migrations folder, enter this
command:
PM> Remove-Migration
.
19. How to revert all the migrations
1. To revert all changes that have been applied to
the database by running the Down()
method in all the migration files, enter this command:
PM> Update-Database 0
2. To remove all migration files from the Migrations
folder, enter the Remove-
Migration command repeatedly. Or, manually
delete all the migration files from the
Migrations folder, including the snapshot file
Note
• When you run one of these commands, you might
see a warning to update your
tools. To do that, you can run an Install-Package
command like this:
PM> Install-Package
Microsoft.EntityFrameworkCore.Tools -Version 3.0.1
However, you should use the version number that’s
included in the warning message.
20. How to work with
relationships
So far, you have learned how to code
entity classes and use those classes to
create database tables. However,
entities are typically related to other
entities.
Similarly, the corresponding database
tables are typically related to other
tables.
For example, a genre might have
many books. An author might have a
detailed
bio. A book might have several
authors. And an author might have
several books.
That’s why the next few figures show
how to configure relationships
between
entities and their corresponding tables.
How entities are
related
Entities can be related to other entities
by values in specific properties. The
two entities at the top of figure 12
-
6
illustrate this concept. Here, the Genre
entity is related to the Book entity
because they share a GenreId property.
Typically, relationships exist between the
primary key in one entity and the
foreign key in another entity. A primary
key (PK) uniquely identifies an instance
of an entity. A foreign key (FK) refers to a
primary key in another entity. In
this figure, the diagram shows that the
GenreId property in the Genre entity is
a primary key, and the GenreId property
in the Book entity is a foreign key. In
other words, the foreign key in the Book
entity refers to the primary key of the
Genre entity.
21. Description
•The PMC executes PowerShell
commands to create, apply, and revert
migration files.
•A migration file contains C# code for
creating or updating database objects.
Specifically,
each migration file has an Up() method
with code that runs when a migration is
applied,
and a Down() method with code that
runs when a migration is reverted.
•The first time you run the Add-
Migration command, EF creates a file
named
DbContextNameModelSnapshot.cs. This
file contains a “snapshot ”of the current
database schema. When you add or
remove subsequent migrations, that file
is updated.
22. How to configure a one-to-
many relationship
The first code example in figure 12
-
7
shows the simplest way to configure
a one-to-many relationship by
convention. Here, the relationship is
created by
nesting a Genre entity as a property of
the Book entity.
This is a common way of creating a
one-to-many relationship. However,
this approach can cause some
problems. For example, you need to
specifically
include the nested entity and all of its
data even if all you need is the ID
value.
.
As a result, it’s typically a better
practice to fully define the
relationship by
explicitly coding the foreign key
property. The second example in this
figure
shows how this works. Here, the Book
entity explicitly includes the foreign
key
In addition, you can run into
problems if the nested entity
has data validation
requirements
23. How to configure a one-
to-many relationship by
convention
public class Book{
public int BookId { get; set; }
public Genre Genre { get; set; }
}
public class Genre{
public int GenreId { get; set; }
public string Name { get; set; }
}
How to fully define the one-to-many
relationship by convention
)recommended)
public class Book{
public int BookId { get; set; }
public int GenreId { get; set; } // foreign key property
public Genre Genre { get; set; } // navigation
property
}
public class Genre{
public int GenreId { get; set; }
public string Name { get; set; }
public ICollection<Book> Books { get; set; } //
navigation property
}
24. How to configure a one-to-
many relationship with
attributes
public class Book{
public int BookId { get; set; }
public int CategoryId { get; set; }
ytreporp KF // ]("dIyrogetaC")yeKngieroF[
ssalc kooB ni
ytreporp van // ]("skooB")ytreporPesrevnI[
ssalc erneG ni
public Genre Category { get; set; }
}
public class Genre{
public int GenreId { get; set; }
public string Name { get; set; }
van // ]("yrogetaC")ytreporPesrevnI[
ssalc kooB ni ytreporp
public ICollection<Book> Books { get; set;
{
}
How to configure a one-to-many
relationship with the Fluent API
protected override void
OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book >
)(
.HasOne(b => b.Genre) // nav property in Book class
.WithMany(g => g.Books); // nav property in Genre
class
}
25. How to configure a one-
to-one relationship
If an entity has a one-to-one
relationship with another entity, the
data in the
two entities could be stored in one
entity, and the data in the two
underlying
database tables could be stored in
one table. This is useful when you
need to
store large objects such as images,
sound, and videos. Then, you can
store those
objects in a separate entity and
underlying table and only join them
when you
need to retrieve the large object.
A one-to-one
relationship can also
be useful when an
entity has both
essential data and
data that is frequently
null. For example,
suppose that in most
cases, the only data
you need to store for
an author is the name.
The first example in
figure 12
-
8 shows how
to configure a fully
defined
one-to-one
relationship by
convention. This
example works
similarly to the
second example in
the previous figure. To
start, each entity
configures its
primary key by
convention and
defines a navigation
property.
26. How to configure a one-to-
one relationship by
convention
public class Author{
public int AuthorId { get; set; } //
primary key property
public string Name { get; set; }
public AuthorBio Bio { get; set; } //
navigation property
}
public class AuthorBio{
public int AuthorBioId { get; set; } //
primary key property
public int AuthorId { get; set; } // foreign
key property
public DateTime? DOB { get; set; }
public Author Author { get; set; } //
navigation property
}
How to configure a one-to-
one relationship with attributes
public class Author{
//same as above
}
public class AuthorBio{
[Key]
public int AuthorId { get; set; } // PK and
FK property
public DateTime? DOB { get; set; }
ytreporp KF // ]("dIrohtuA")yeKngieroF[
public Author Author { get; set; } //
navigation property
}
27. How to configure a one-to-
one relationship with the
Fluent API
protected override void
OnModelCreating(ModelBuilder
modelBuilder){
modelBuilder.Entity<Author >
)(
.HasOne(a => a.Bio) // nav property in
Author class
.WithOne(ab => ab.Author) // nav
property in AuthorBio class
.HasForeignKey<AuthorBio>(ab =>
ab.AuthorId); // FK property
}
How to configure a one-to-
one relationship within a single
table
protected override void
OnModelCreating(ModelBuilder
modelBuilder){
modelBuilder.Entity<Author >
)(
.HasOne(a => a.Bio)
.WithOne(ab => ab.Author)
.HasForeignKey<AuthorBio>(ab =>
ab.AuthorId);
modelBuilder.Entity<Author>().ToTable("A
uthors");
modelBuilder.Entity<AuthorBio>().ToTable
("Authors");
}
28. How to configure a
many-to-many
relationship
A many-to-many relationship is
configured by using an
intermediate entity
called a join entity or a linking
entity. This creates an
intermediate table in the
database called a join table or a
linking table.
The linking entity has a one-to-
many relationship with the two
entities in
the many-to-many relationship. In
other words, a many-to-many
relationship is
broken down into two one-to-
many relationships.
Unlike the other two relationships, a many-to-many
relationship can only be
configured with the Fluent API. Figure 12
-
9 shows how
to configure a many-to-
many relationship between the Book entity and the
Author entity.
The first example in this figure presents the Book and
Author entities that
have a many-to-many relationship. Each of these
entities contains a navigation
property of a collection of BookAuthor entities. These
navigation properties
represent the many side of the two one-to-many
relationships.
29. The entities to be linked
public class Book{
public int BookId { get; set; }
public string Title { get; set; }
//navigation property to linking
entity
public ICollection<BookAuthor>
BookAuthors { get; set; }
}
public class Author{
public int AuthorId { get; set; }
public string Name { get; set; }
//navigation property to linking
entity
public ICollection<BookAuthor>
BookAuthors { get; set; }
}
The linking entity
public class BookAuthor{
//composite primary key
public int BookId { get; set; } // foreign
key for Book
public int AuthorId { get; set; } // foreign
key for Author
//navigation properties
public Book Book { get; set; }
public Author Author { get; set; }
}
30. How to configure a many-to-many
relationship with
the Fluent API
protected override void
OnModelCreating(ModelBuilder modelBuilder){
//composite primary key for BookAuthor
modelBuilder.Entity<BookAuthor >
)(
.HasKey(ba => new { ba.BookId, ba.AuthorId });
Description
•A many-to-many relationship requires a third entity
known as a join entity or
linking entity. The linking entity has two one-to-many
relationships with the
entities to be joined.
•The linking entity has a composite primary key that
consists of the primary key of
each linked entity.
31. How to control delete behavior
When an app deletes a row from a database,
related rows that are dependent
on that row can become corrupted. For example, if
an app deletes the genre
“Novel ”from the Genre table, all the rows in the
Books table whose genre is
“Novel ”have invalid data.
You can use the OnDelete() method of the Fluent
API to configure how
dependent rows are handled when a parent row is
deleted. This method accepts a
value of the DeleteBehavior enum as an argument.
The table at the top of figure
12
-
10 shows the values of this enum.
32. Description
•When a row is deleted from a database, related rows that are
dependent on that row
can become corrupted. To prevent this, most databases throw an
exception when
you try to delete a row that has dependent rows.
•Most databases also allow you to configure cascading deletes,
which cause
dependent rows to be automatically deleted.
•You can use the OnDelete() method of the Fluent API to configure
how dependent
rows are handled when a parent row is deleted.
33. The Bookstore
database classes
Now that you know how to
code entity classes that are
related to each
other, you’re ready to see the
entity, context, and
configuration classes for the
Bookstore website that’s
presented in the next
chapter.
The entity classes
Figure 12
-
11 shows the
classes for the Author,
Book, BookAuthor, and
Genre entities that are
used by the Bookstore
website. For the most
part, these
entities are configured
by convention. For
example, the Author,
The context and configuration classes
Figure 12
-
12 shows the context class for the Bookstore
website. This class
begins by defining the DbSet properties for the
Author, Book, BookAuthor, and
Genre entities presented in figure 12
-
11
.
34. The Author class
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace Bookstore.Models
The Book class
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
The BookAuthor class
namespace Bookstore.Models
{
public class BookAuthor
{
//composite primary key and foreign keys
public int BookId { get; set; }
public int AuthorId { get; set; }
//navigation properties
public Author Author { get; set; }
public Book Book { get; set; }
}
}
35. The Genre class
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
The BookstoreContext class
using Microsoft.EntityFrameworkCore;
namespace Bookstore.Models
{
public class BookstoreContext : DbContext
{
public
BookstoreContext(DbContextOptions<BookstoreCont
ext> options)
:base(options)
The SeedAuthors class
using Microsoft.EntityFrameworkCore;
using
Microsoft.EntityFrameworkCore.Metadata.Builders;
36. The SeedGenres class
using Microsoft.EntityFrameworkCore;
using
Microsoft.EntityFrameworkCore.Metadata.Builders;
How to create code from a database
So far in this book, you have learned how to use
Code First development
to create a database from your context and entity
classes. Sometimes, though,
you already have a database. In that case, you
need to create context and entity
classes from your database. This approach is called
Database First development.
37. How to generate DB context and entity
classes
To get started with Database First development, you
can use the Scaffold-
DbContext command to generate the code for the
context and entity classes. The
table at the top of figure
38. Description
•You can use the Scaffold-Database command to
generate context and entity classes
based on an existing database.
•You can omit the flag for required parameters.
•When specifying the connection string, it’s
considered a best practice to use a
connection string that’s stored in the appsettings.json
file.
How to configure a generated DB
context class
When you use the Scaffold-DbContext command to
generate context and
entity classes, the OnConfiguring() method of the
context class contains the
connection string and provider information. Usually,
though, you need to make
some adjustments before your project is ready to run.
39. The OnConfiguring() method in the
generated context class
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("name=BookstoreContext
"(;
}
} The Startup.cs file that injects the DB
context into the app
using Microsoft.EntityFrameworkCore;
using Bookstore.Models.DataLayer;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
40. Description
•When you use the Scaffold-
Database command to
create DB context and entity
classes, it adds configuration
code to the OnConfiguring()
method of the context
class.
•To get the DB context to
work, you need to edit the
Startup.cs file so it injects the
context into the app.
•For this code to work, the
appsettings.json file must store
the specified connection
string. If it doesn’t, you can
edit this file so it contains the
specified connection
string.
How to modify a generated
entity class
When you work with Database First
development, it’s common to enhance
the entity classes that it generates. For
instance, you may want to add data
validation attributes. Or, you may want to
add more properties such as read-only
properties that indicate the state of the
entity.
However, if you make changes to the database, it’s
common to need to regenerate the entity classes.
When this happens, though, you lose any additions
you’ve made to those classes. Fortunately, you can
use partial classes to avoid this problem.
41. A partial class generated by EF Database First
namespace Bookstore.Models.DataLayer
{
public partial class Books
{
[Key]
public int BookId { get; set; }
[Required]
[StringLength(200)]
public string Title { get; set; }
public double Price { get; set; }
...
}
}
A partial class that adds a metadata class and
a read-only property
using Microsoft.AspNetCore.Mvc;
namespace Bookstore.Models.DataLayer
{
[ModelMetadataType(typeof(BooksMetadata))]
public partial class Books
{
public bool HasTitle => !string.IsNullOrEmpty(Title);
}
}
42. How to work with data in a database
Chapter 4 introduced some basic skills for working with the data in a
database. Now, you’ll review those skills and learn some new ones.
How to query data
You can use LINQ to Entities to query the DbSet properties of a context
class. The first code example in figure 12-17 shows a context property
named context that’s used by subsequent examples. This context has
DbSet properties named Books and Authors.
The second set of examples shows how to create and execute a query. When you do
that using two statements, you create a query variable of the IQueryable type that
holds a query, not data. Then, when you execute the query, the data is retrieved from
the database and stored in an object of the IEnumerable type. When you do that
using one statement, you can use the var keyword to declare the variable where the
results are stored so the compiler will automatically determine the type. In this case, it
uses the List type, which is usually what you want.
43. The context class used in the following examples
private BookstoreContext context { get; set; }
Code that creates and executes a query
In two statements
IQueryable query = context.Books; // create the query
IEnumerable books = query.ToList(); // execute the query
In one statement
var books = context.Books.ToList(); // implicit typing is common
Code that sorts the results
Sort by Title in ascending order (A to Z)
var books = context.Books.OrderBy(b => b.Title).ToList();
Sort by Title in descending order (Z to A)
var books = context.Books.OrderByDescending(b => b.Title).ToList();
44. Code that filters the results
Get a single book by ID
var book = context.Books.Where(b => b.BookId == 1).FirstOrDefault();
var book = context.Books.Find(1); // shortcut for above
Get a list of books by genre
var books = context.Books.Where(b => b.Genre.Name == "Mystery").ToList();
Get a list of books in a price range
var books = context.Books.Where(b => b.Price > 10 && b.Price < 20).ToList();
Conditionally filter by multiple criteria
// build the query (can’t use implicit typing here)
IQueryable query = context.Books;
if (selectedMaxPrice != null)
query = query.Where(b => b.Price < selectedMaxPrice);
if (selectedGenre != null)
query = query.Where(b => b.Genre.Name == selectedGenre);
// execute the query
var books = query.ToList();
45. How to work with projections and related entities
When you query data as described in the last figure, the query retrieves all of the
properties in an entity, whether you need them or not. Usually, this is fine. Sometimes,
though, you may have entities with so many properties that it’s inefficient to retrieve all
of them. Or, you may have properties with sensitive data, like passwords, that you don’t
want to retrieve.
One way to limit the number of properties returned is to use table splitting as mentioned
earlier in this chapter. Another way to do that is to use projections. A projection uses the
LINQ Select() method to retrieve only some of an entity’s properties.
One way to create a projection is to use an anonymous type like the one shown in
the first example in figure 12-18. This type consists of the AuthorId property of the Authors
entity and a property that’s a combination of the FirstName and LastName properties.
Because the second property doesn’t match an entity property, it must be explicitly
named as shown here.
Anonymous types are convenient and quick, but they can be hard to work with in
an MVC app. That’s because they’re hard to pass to views, as the error message below
the first example shows.
46. How to create a projection with an anonymous type
var authors = context.Authors
.Select(a => new {
a.AuthorId, // can infer property name
Name = a.FirstName + ' ' + a.LastName // must specify property name
})
.ToList();
Error when you pass the projection to a view that expects a list of Authors
InvalidOperationException: The model item passed into the ViewDataDictionary
is of type 'System.Collections.Generic.List`1[<>f_AnonymousType0`2 [System.
Int32,System.String]]', but this ViewDataDictionary instance requires a model
item of type System.Collections.Generic.IEnumberable`1 [BookList.
Models.Author]'.
47. How to create a projection with a concrete type
The concrete type
public class DropdownDTO
{
public string Value { get; set; }
public string Text { get; set; }
}
The projection
var authors = context.Authors
.Select(a => new DropdownDTO {
Value = a.AuthorId.ToString(),
Text = a.FirstName + ' ' + a.LastName
})
.ToList();
Description
• A projection allows you to retrieve a subset of the properties of an entity.
• To create a projection, you can use an anonymous type or a concrete type.
However, anonymous types can be hard to use in views.
• You can use the Include() and ThenInclude() methods to include related entities
in your query. These methods accept lambda expressions to identify the entities to
include.
48. How to insert, update, and delete data
In addition to querying data, you need to know how to insert, update, and
delete data. Fortunately, the methods of the DbSet and DbContext classes make that
easy to do. You learned about these methods in chapter 4, but now figure 12-19
reviews them and explains them in more detail.
The Add(), Modify(), and Delete() methods of the DbSet class only affect the
DbSet. The Add() method adds a new entity to the DbSet and marks it as Added, and
the Update() and Remove() methods mark an existing entity as Modified or Deleted. To
mark an entity, EF uses the EntityState enum. In most cases, you can let EF mark these
changes for you automatically. However, you can also set the state of an entity
manually, if necessary.
The SaveChanges() method of the DbContext class, by contrast, affects the
underlying database. When you call this method, all the Added, Modified, and
Deleted actions pending in the DbSet properties are executed against the database.
The code examples show how to use these methods. The first example adds a
new entity, the second and third examples update an existing entity, and the fourth
example deletes an existing entity. While this code is easy to follow, you should notice a
few things.
49. First, although these examples all perform a single action and then call SaveChanges(),
that’s not a requirement. You can make several calls to Add(), Update(), or Delete() before you call
SaveChanges(). In fact, in some scenarios, it can improve efficiency to only call SaveChanges() once
after all your modifications are done.
Second, when it comes to updates, the second example shows how to work with a disconnected
scenario because it’s commonly used with web apps. In this scenario, the Edit() action method gets
the Book entity from an HTTP POST request. As a result, the code in this figure must use Update() to
update the state of the entity before calling SaveChanges().
With desktop apps, it’s more common to use a connected scenario. However, a connected
scenario can also occur in web apps as shown by the third example. Here, the first statement of the
Edit() action method uses the Find() method to retrieve the Book entity from the database. This causes
the context to track the state of this entity. As a result, you don’t need to call the Update() method.
Instead, this code just sets the new price for the book and calls the SaveChanges() method. This
generates SQL that’s slightly more efficient than calling Update(). However, if you wanted to make the
code easier to read and understand, you could add a statement that calls Update() before calling
SaveChanges().
Finally, when you delete a parent entity in EF Core, it’s common for all of the related child entities
to be automatically deleted too. That’s because EF Core enables cascading deletes by default for
foreign keys that are not nullable. However, if you disable cascading deletes as described earlier in
this chapter, you’ll need to delete all the child entities before you can delete the parent entity.
50. Code that adds a new entity
[HttpPost]
public IActionResult Add(Book book)
{
context.Books.Add(book);
context.SaveChanges();
return RedirectToAction("List", "Book");
}
51. Code that updates an existing entity in a disconnected scenario
[HttpPost]
public IActionResult Edit(Book book) // Book object is disconnected
{
context.Books.Update(book); // call to Update() required
context.SaveChanges();
return RedirectToAction("List", "Book");
}
Code that updates an existing entity in a connected scenario
[HttpPost]
public IActionResult Edit(int id, double price)
{
Book book = context.Books.Find(id); // Book object is connected
book.Price = price; // call to Update() not required
context.SaveChanges();
return RedirectToAction("List", "Book");
}
52. How to handle concurrency conflicts
Concurrency allows two or more users to work with a
database at the same time. However, if two users
retrieve and then attempt to update the same entity
(row in a table), their updates may conflict with
each other, and you need to handle this
concurrency conflict.
In EF Core, you have two options for concurrency control.
The default option is called “last in wins”. This option
doesn’t perform any checking. Instead, the last update
overwrites any previous changes.
In some cases, this option is adequate, but it can lead to
corrupted data.
The other option is called optimistic concurrency.
It checks whether a row has been changed since it was
retrieved. If so, EF refuses the update or deletion and throws an
exception. Then, the app can handle this exception.
You can use data attributes or the Fluent API to configure your
apps to use optimistic concurrency.
53. How to simulate a concurrency conflict
var book = context.Books.Find(1); // get a book from the database
book.Price = 14.99; // change price in memory
context.Database.ExecuteSqlRaw( // change price in the database
"UPDATE dbo.Books SET Price = Price + 1 WHERE BookId = 1");
context.SaveChanges();
Description
• A concurrency conflict is when data is modified after it’s retrieved for editing or
deletion.
• A rowversion property lets you check all the properties in an entity for conflicts
and must be an array of bytes.
• The DbContext class has a Database property whose ExecuteSqlRaw() method
can be used to simulate concurrency conflicts.
• A concurrency token lets you check an individual property for conflicts.
54. How handle a concurrency exception
If you’ve enabled optimistic concurrency and an update or delete operation fails
because of a concurrency problem, EF throws a concurrency exception. To handle this
exception, you call the SaveChanges() method within the try block of a try-catch
statement, and you include a catch block that catches the exception.
Figure 12-21 shows a controller action method named Edit() that handles a
concurrency exception. Here, within the catch block, this code uses the Entries property
of the concurrency exception to get the current values of the entity being updated. The
Entries property is a collection of DbEntityEntry objects, but it only has one entry here. As a
result, the code uses the LINQ Single() method to retrieve the entry. If you want to be
more cautious, you can use the SingleOrDefault() method and check for null.
The DbEntityEntry class has properties named CurrentValues and OriginalValues, but
those properties typically aren’t useful in a disconnected scenario. Instead, this example
uses the GetDatabaseValues() method to retrieve the current values of the entity from
the database.
After calling the GetDatabaseValues() method, the code checks whether the
return value is null. If it is, the entity has been deleted and is no longer in the database. As
a result, the code adds a class-level error to the ModelState object notifying the user that
the row no longer exists.
55. Some of the code in the Edit view
@* both primary key and row version value needed for edit *@
<input type=“hidden” asp-for=“Book ID”/>
<button type=“submit” class=“btn”>submit</button>
Description
• The DbUpdateConcurrencyException is thrown when there’s a
concurrency conflict.
• The DbUpdateConcurrencyException has an Entries property
that provides a way to get the new database values for the
row that’s being saved.
<input type=“hidden” asp-for=“RowVersion”/>
56. So far, the EF Core code you’ve seen has been in the action
methods of controllers. Usually, though, this code should be in its own
data layer. This makes your code easier to test. In addition, it allows
you to store your data access code in a separate project. Finally, it
makes it possible to change from EF to another ORM framework
without affecting the rest of your code.
How to encapsulate your EF code
A class that adds an extension method to the Iqueryable <T> interface
public static class QueryExtensions
{
public static Iqueryable <T> <T>(this IQueryable <T> query,
int pageNumber, int pageSize)
{
return query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
}
}
PageBy
57. How to use a generic query options class
In the previous figure, you learned how to code a data
access class that provides two methods for querying: one for
retrieving a page of data and one for sorting and filtering data.
However, these methods might cause logistical problems. For
instance, what if you don’t need to sort the data? What if you
don’t need to filter the data? Where does paging fit?
One way to avoid these problems is to code a generic class
for query options like the one presented in the first example of
figure 12-23. Here, the QueryOptions class begins by defining public
lambda expression properties for sorting and filtering and public int
properties for paging. Then, it defines a private array for storing
strings that indicate any related entities the query should include.
The write-only Includes property accepts a comma-separated
string, removes any spaces, splits the string at the commas, and
stores the resulting string array in the private field. The method
named GetIncludes() is used to return the value of the private field.
Or, if the private field is null, it returns an empty string array.
58. A generic query options class
using System.Linq.Expressions;
public class QueryOptions <T>
{
// public properties for sorting, filtering, and paging
public Expression<Func> OrderBy { get; set; }
public Expression<Func> Where { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
// public write-only property for includes private string array
private string[] includes;
public string Includes {
set => includes = value.Replace(" ", "").Split(',');
}
// public method returns includes array
public string[] GetIncludes() => includes ?? new string[0];
// read-only properties
public bool HasWhere => Where != null;
public bool HasOrderBy => OrderBy != null;
public bool HasPaging => PageNumber > 0 && PageSize > 0;
}
59. How to use the repository pattern
One popular way to implement a data layer is to use
the repository pattern. This pattern encapsulates data
code within a data access layer and also uses
interfaces to provide a layer of abstraction. One benefit
of this pattern is that it makes it easier to automate
testing as shown in chapter 14.
The first example in figure 12-24 presents a simple
generic interface named IRepository that you can
create to work with a repository. This interface has six
methods. The first two query data, the next three modify
data, and the last one saves changes to the data.
When using the repository pattern, you should use
one repository per entity. For example, a Bookstore app
shouldn’t use one big Bookstore repository. Instead, it
should use a Book repository, an Author repository, a
Genre repository, and so on.
You can use this generic
repository class to create
various entity collections.
For example, this code creates
an Author repository:
var data = new Repository(ctx);
60. The generic IRepository interface
public interface IRepository <T>where T : class {
Ienumerable <T> List (QueryOptions <T> options);
T Get(int id);
void Insert(T entity);
void Update(T entity);
void Delete(T entity);
void Save();
} A controller that uses the generic Repository class
public class AuthorController : Controller {
private Repository data { get; set; }
public AuthorController(BookstoreContext ctx) =>
data = new Repository(ctx);
public ViewResult Index() {
var authors = data.List(new QueryOptions <Auther> {
OrderBy = a => a.FirstName
});
return View(authors);
}
...
}
61. How to use the unit of work pattern
Although some programmers think that including a
Save() method in each repository is OK, others think that a
repository should never have a Save() method. Either way, if
you need to coordinate between multiple repositories,
including a Save() method in each repository doesn’t work. In
those cases, you can combine the repository pattern with the
unit of work pattern.
The unit of work pattern adds a central class that has
repository objects as properties. Then, the central class passes
each repository its context object, so they all share the same
DB context. Finally, the central class includes a Save()
method that calls the SaveChanges() method of the DB
context. That way, when you call the Save() method of the
central class, EF executes all changes in all repositories
against the database. Then, if one change fails, they all fail.
The first code example in
figure 12-25 shows a unit of
work class for a Bookstore
app.
This class implements an
interface that specifies all
of its methods.
This isn’t required, but it’s
generally considered a
good practice since it
makes it easier to
automate testing.
62. A controller that uses the unit of work class to update a book
public class BookController : Controller
{
private BookstoreUnitOfWork data { get; set; }
public BookController(BookstoreContext ctx) =>
data = new BookstoreUnitOfWork(ctx);
[HttpPost]
public IActionResult Edit(BookViewModel vm) {
if (ModelState.IsValid){
data.DeleteCurrentBookAuthors(vm.Book);
data.AddNewBookAuthors(vm.Book, vm.SelectedAuthors);
data.Books.Update(vm.Book);
data.Save();
...
}
...
}
63. Perspective
This chapter showed how to use EF Core to work with data in a database,
including how to encapsulate your EF code in a data layer. This presents the most
important classes of the data layer that are used by the Bookstore website
presented in the next chapter. As a result, studying that app is a great way to get
started with EF Core.
Terms
entity class
database (DB) context class
Entity Framework (EF) Core
object relational mapping (ORM)
Code First development
Database First development
Fluent API
primary key (PK)
foreign key (FK)
one-to-many relationship
many-to-many relationship
one-to-one relationship
navigation property
table splitting
join entity
linking entity
join table
linking table
composite primary key
cascading delete
LINQ to Entities
projection
data transfer object (DTO)
disconnected scenario
connected scenario
concurrency
concurrency conflict optimistic
concurrency rowversion
property concurrency token
extension method
repository pattern
CRUD (Create, Read, Update,
Delete)
unit of work pattern
64. Exercise 12-1 Review the data layer
of an app and improve it
Review the domain model and configuration files
1. Open the Ch12Ex1ClassSchedule web app in the ex_starts directory.
2. Open the Models/DomainModels folder and review the code in the
three class files it contains.
3. Open the Models/Configuration folder and review the code in the
three configuration files it contains.
Review the migration file
4. Open the Migrations folder and review the code in the Initial migration file.
5. Note how the migration file determines the primary and foreign keys based on
how the properties in the domain model are coded (configuration by convention).
65. Create the database
6. Open the Package Manager Console and enter the Update-Database
command to run the migration and create the database.
7. Take a moment to review the SQL code in the console after the
command runs.
8. Run the app and review the data it displays for classes and teachers.
Test the cascading delete behavior of the Teacher object
9. Run the app and navigate to the Show All Teachers page. Click the Add
Teacher link and add your name as a teacher.
10. Navigate to the Show All Classes page. Click the Add Class link and add
a new class with yourself as the teacher.
11. Review the updated class list to make sure it displays the class you just
added.
12. Navigate back to the Show All Teachers page and delete your name.
13. Navigate back to the Show All Classes page, and note that the class you
previously added has also been deleted.
66. Restrict the cascading delete behavior of the Teacher object
14. In the Models/DomainModels folder, open the configuration file for the domain
model named Class.
15. Add code to change the delete behavior for the Teacher foreign key to Restrict.
16. Open the Package Manager Console and use the Add-Migration command to
create a new migration file. When you do that, use a descriptive name for the
migration file.
17. Review the code in the migration file that’s generated.
18. Enter the Update-Database command to apply the migration to the database.
19. Repeat steps 9 through 13 to test the cascading delete behavior. When you
attempt to delete your name from the teacher list, you should get an error message.
20. Navigate to the Show All Classes page, find the class you added, click on Edit,
and change the teacher to another teacher.
21. Navigate to the Show All Teachers page and delete your name from the teacher
list. This time, the app should let you.
67. Update the app to use the unit of work pattern
22. Open the Controllers folder and review the three controller class files. Note that
two of them initialize more than one Repository class in the constructor.
23. Open the Models/DataLayer folder, add an interface named
IClassScheduleUnitOfWork and a class named ClassScheduleUnitOfWork.
24. Adjust the namespaces of the interface and class to match the namespaces of
the other classes in the DataLayer folder.
25. Code the interface to have read-only properties for a Class, Teacher, and Day
repository and a Save() method that returns void.
26. Code the class to implement the interface, have a private ClassScheduleContext
object that it gets in its constructor, initialize and return Repository objects in its
properties, and call the context object’s SaveChanges() method in the Save()
method.
27. Update the Home and Class controllers to use the unit of work class.
28. Run the app and make sure it still works the same.
68. Change how the list of classes is ordered
29. Open the Home controller and review the code in its Index() action method.
Note that the classes are ordered by day on first load and ordered by time when
filtering by day.
30. Run the app. When it displays all classes, note that it doesn’t display the classes
for Monday in ascending order by time. Then, click on the filter link for Monday and
note that it does display the classes in ascending order by time.
31. In the Models/DataLayer folder, open the QueryOptions class. Note that this
version is shorter than the QueryOptions class presented in the chapter. That’s
because it doesn’t provide for paging.
32. Add a new property named ThenOrderBy that works like the OrderBy property.
33. Add a new read-only property named HasThenOrderBy that works like the
HasOrderBy property.
69. 34. Open the Repository class and find the List() method. Update the code that
handles ordering so it uses the ThenOrderBy property like this:
if (options.HasOrderBy) {
if (options.HasThenOrderBy) {
query = query.OrderBy(options.OrderBy).ThenBy(options.ThenOrderBy);
}
else {
query = query.OrderBy(options.OrderBy);
}
}
35. In the Home controller, update the Index() action method to use the new
property to sort by time as well as day on first load.
36. Repeat step 30. The class times should be in ascending order on both pages.
70. Add an overload for the Get() method in the Repository class
37. Open the Class controller and review the private helper method named GetClass().
Note that it uses the List() method of the Repository class to get an IEnumerable with
one Class item. Then, it uses the LINQ FirstOrDefault() method to retrieve that item.
38. In the Models folder, open the IRepository interface and add a second Get()
method that accepts a QueryOptions object.
39. Open the Repository class and implement the new Get() method. To do that, you
can copy the code from the repository’s List() method that builds a query, leaving
out the code for ordering the list. Then, you can return the single object by calling
the FirstOrDefault() method instead of the List() method.
40. In the Class controller, update the GetClass() method to use the new Get() method.
41. Run the app and test it. It should work the same as it did before.