You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aspnet-basics/6-databases-with-entity-fra...

438 lines
16 KiB
Markdown

---
marp: true
paginate: true
math: mathjax
theme: buutti
title: 5. Databases with Entity Framework
---
# Databases with Entity Framework
<!-- headingDivider: 5 -->
<!-- class: invert -->
## Contents
- [Entity Framework (EF)](#entity-framework-ef)
- [Code First approach](#code-first-approach)
- [Migrations](#migrations)
- [Database First approach](#database-first-approach)
## Entity Framework (EF)
* [Entity Framework](https://learn.microsoft.com/en-us/ef/) is an Object-Relational Mapper (ORM) made by Microsoft for the .NET framework
* Object-Relational Mapping: converting from database representation to objects in a programming language
* Allows creation of CRUD operations without writing SQL
### Entity Framework Core (EF Core)
* [EF Core](https://learn.microsoft.com/en-us/ef/core/) is a cross-platform version of EF
* Can be used outside of the .NET framework unlike normal Entity Framework
* Open-source, lightweight, extensible
* Supports many database engines, such as MySQL, PostgreSQL, and so on
* This is what we'll be using
### Code First vs Database First vs Model First
* There are three approaches through which Entity Framework can be implemented
* Code First
* Database First
* Model First
* Database First and Code First are the most used ones and will be introduced in this lecture
### Note about loading data
* In EF Core, you can use [navigation properties](https://learn.microsoft.com/en-us/ef/ef6/fundamentals/relationships) in your model to load related entities
* There are three common ORM patterns to load related data
* [Eager loading](https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager): the related data is loaded from the database as part of the initial query.
* [Explicit loading](https://learn.microsoft.com/en-us/ef/core/querying/related-data/explicit): the related data is explicitly loaded from the database at a later time.
* [Lazy loading](https://learn.microsoft.com/en-us/ef/core/querying/related-data/lazy): the related data is transparently loaded from the database when the navigation property is accessed.
## Code First approach
### Code First
* In the Code First approach, Entity Framework will create databases and tables based on defined ***entity classes***
* Good for small applications
* Other advantages include:
* You can create the database and tables from your [business objects](https://en.wikipedia.org/wiki/Business_object)
* You can specify which related collections are
* eager loaded
* not serialized at all
* Database version control
* Not preferred for data intensive applications
### Required Packages
* Install and add the following packages to your project:
* `Microsoft.EntityFrameworkCore`
* `Microsoft.EntityFrameworkCore.Tools`
* `Npgsql.EntityFrameworkCore.PostgreSQL`
<div class='centered'>
![w:1000](imgs/7-databases-with-entity-framework_11.png)
</div>
### Code First: `DbContext`
* Let's begin with the Code First Approach
* The `DbContext` class of EFCore is the bridge between the code representation of your data (entities) and the database
* `DbContext` holds
a) methods to form the database schema with Code First approach and
b) classes to keep the database up-to-date with CRUD operations
* DATABASE $\Rightarrow$ CODE: `DbSet` class property in `DbContext` can be queried directly with LINQ and this results in an object in your code
* CODE $\Rightarrow$ DATABASE: `DbSet` also has methods like `Add`, `Update` and `Remove` to make changes to the database from your code
### Creating a context
* Create a context that inherits from `DbContext`
* Commonly located in the `Models` folder, but ideally should be in a separate abstraction/repository folder (for example `Repositories`)
* The class needs to have a constructor that calls the base constructor with
```csharp
: base(options)
```
* Create a `DbSet` property for each resource
```csharp
public class ContactsContext : DbContext
{
public DbSet<Contact> Contacts { get; set; }
public ContactsContext(DbContextOptions<ContactsContext> options) : base(options) { }
}
```
---
* To further configure how the database will be structured, override the `OnModelCreating` method
* In this example, one table named `Contact` with columns `Id`, `Name` and `Email` will be created:
```csharp
public class ContactsContext : DbContext
{
public DbSet<Contact> Contacts { get; set; }
public ContactsContext(DbContextOptions<ContactsContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Contact>().ToTable("Contact");
}
}
```
---
* In this example, the Contact table will be created with some starting values for `Id`, `Name` and `Email` columns:
```csharp
public class ContactsContext : DbContext
{
public DbSet<Contact> Contacts { get; set; }
public ContactsContext(DbContextOptions<ContactsContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Contact>().HasData(
new Contact { Id = 1, Name = "Johannes Kantola", Email = "johkant@example.com" },
new Contact { Id = 2, Name = "Rene Orosz", Email = "rene_king@example.com" }
);
}
}
```
### DbContext as a Service
* In `Program.cs`, add the context to services with `AddDbContext` method
* This is where you set the DB management system you want to use (MySQL, PostgreSQL, SQLite...)
* The EFCore support for PostgreSQL is called `Npgsql` as in the package name
* Add the server, host, port, username, password and the database name of the existing database inside `options.UseNpgsql` as a *__connection string__*:
```csharp
services.AddDbContext<ContactsContext>(options => options.UseNpgsql(
@"Server=PostgreSQL 12;Host=localhost;Port=5432;Username=postgres;Password=1234;Database=contacts"));
services.AddScoped<IContactRepository, ContactRepository>();
services.AddControllers().AddNewtonsoftJson();
```
## Migrations
* As the development progresses, models and database schemas change over time
* This means that both the database and the code needs to be updated to match each other
* Migrations allow for the database to keep in sync with the code schematically
* The data stored in the database is also preserved
* EFCore migrations have built-in version control; a snapshot of each version of the schema is stored
### Applying migrations
* Open the Package Manager Console in Visual Studio
* If the tab is not in the bottom of the window, open it from<br> _View > Other windows > Package Manager Console_
* Add your initial migration by entering the command `Add-Migration <name>` to the console, for example
`Add-Migration InitialMigration`
* This now creates the first "blueprint" of how the database should be structured
* Update the database by entering the command `Update-Database` to the console
* This will update the existing database according to the `ModelBuilder` options
---
* At this point, the values you have entered (`Contacts` table in this example) should show up in the database. You can check it up e.g. in pgAdmin.
<div class='columns12' markdown='1'>
<div markdown='1'>
![](imgs/7-databases-with-entity-framework_13.png)
</div>
<div markdown='1'>
![](imgs/7-databases-with-entity-framework_12.png)
</div>
</div>
* Notice that the table and column names are initialized with a capital letter
* The value naming in psql is case sensitive
$\Rightarrow$ All names have to be in quotation marks!
### Exercise 1: Adding Context
<!--_class: "exercise invert" -->
Continue working on the CourseAPI.
1) Create a new empty database `course_db` in pgAdmin or psql
2) Create a `DbContext` for the courses. Name it `CoursesContext`, and add a `DbSet` of type `Course` to it, named `Courses`
3) Add the `OnModelCreating` method to the context and add a couple of courses with some starting values to the `modelBuilder`
4) Add the `CoursesContext` to the services in `Program.cs` with a connection string pointing to `course_db`
5) Add the first migration and update the database from the Package Manager Console
6) Check that the `Course` table with the starting values has appeared to the database
### Using DbContext in the API
* Because `DbContext` is added to services, it can be accessed from any other service, such as the repository
* Using the `DbSet` for each model in your project, CRUD operations can be applied to the database from the repository with LINQ and `DbSet` methods
* `Add()`
* `Update()`
* `Remove()`
* After modifying the `DbSet`, update the changes to the database with the `DbContext.SaveChanges()` method
### Injecting DbContext
* Inject the `DbContext` to your repositories as you would any other service:
```csharp
public class ContactRepository : IContactRepository
{
private readonly ContactsContext _context;
public ContactRepository(ContactsContext context)
{
_context = context;
}
//...
}
```
### DbContext: Read Operations
```csharp
public class ContactRepository : IContactRepository
{
private readonly ContactsContext _context;
public ContactRepository(ContactsContext context) { ... }
public Contact GetContact(int id) =>
_context.Contacts.FirstOrDefault(c => c.Id == id);
public List<Contact> GetContacts() =>
_context.Contacts.ToList();
}
```
### DbContext: Create Operations
```csharp
public class ContactRepository : IContactRepository
{
private readonly ContactsContext _context;
public ContactRepository(ContactsContext context) { ... }
// Read operations
// ...
public void AddContact(Contact contact)
{
_context.Contacts.Add(contact);
_context.SaveChanges();
}
}
```
### DbContext: Update Operations
```csharp
public class ContactRepository : IContactRepository
{
private readonly ContactsContext _context;
public ContactRepository(ContactsContext context) { ... }
// Read & create operations
// ...
public void UpdateContact(int id, Contact newContact)
{
var contact = GetContact(id);
contact.Email = newContact.Email;
contact.Name = newContact.Name;
_context.Contacts.Update(contact);
_context.SaveChanges();
}
}
```
### DbContext: Delete Operations
```csharp
public class ContactRepository : IContactRepository
{
private readonly ContactsContext _context;
public ContactRepository(ContactsContext context) { ... }
// Read, create & update operations
// ...
public void DeleteContact(int id)
{
_context.Contacts.Remove(GetContact(id));
_context.SaveChanges();
}
}
```
### Exercise 2: CRUD on the DB
<!--_class: "exercise invert" -->
Continue working on CourseAPI.
1) Modify the `CourseRepository` to create, read, update and delete from the database instead of the locally stored list of courses
2) Test with Postman. Keep refreshing the DB in pgAdmin or creating queries with psql to make sure the requests work as intended
### Summing Things Up
* Now the API has been hooked up to a PostgreSQL database
* Changes to the schema are kept up-to-date with migrations
* Repository is processing CRUD operations to the database
* Controllers accepting HTTP requests have access to the repository
### EFCore Code First Checklist
1) Install required packages
2) Create `DbContext` for the database
3) Add `DbContext` to services
4) `Add-Migration` & `Update-Database`
5) Add CRUD operations to the database repository
### Modifying the Relations
* Let's change the structure of our Contacts API by adding a new class `Account`
* Instead of `Contact` directly having an `Email`, it will have an `Account` instead
* `Account` holds the information about the `Email`, as well as a `Description` about the nature of the account (personal, work, school etc.)
* Emails will be removed from the `Contacts` table
---
```csharp
// Models/Contact.cs
public class Contact
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Account> Accounts { get; set; }
}
```
```csharp
// Models/Account.cs
public class Account
{
public int Id { get; set; }
public string Email { get; set; }
public string Description { get; set; }
public int ContactId { get; set; }
public Contact Contact { get; set; }
}
```
---
* Adding a migration at this point will result in a warning:
![w:1000px](imgs/7-databases-with-entity-framework_14.png)
---
<div class='columns32' markdown='1'>
<div markdown='1'>
* In the generated migration file, you can find `Up` and `Down` methods
* The `Up` method describes the changes that will be made with the migration
* In this case, removing the `Email` column from `Contacts` table, and creating the new `Accounts` table
* The `Down` method describes the changes that will be made if the migration is reverted
* Updating the database will still work, and the database will have a new table `Accounts`
</div>
<div markdown='1'>
![](imgs/7-databases-with-entity-framework_15.png)
</div>
</div>
### Exercise 3: Adding Migrations
<!--_class: "exercise invert" -->
Continue working on CourseAPI.
1) Add a new model `Lecture` with properties `int Id`, `DateTime StartTime`, `int Length`, `Course Course`, and `int CourseId`
2) Add a new property `ICollection<Lecture> Lectures` to the `Course` model
3) Add a new migration named `AddLectures`
4) Update the database. Check that the changes show up in the database with pgAdmin
## Database First approach
### What is the Database First approach?
* This is the other approach for creating a connection between the database and the application
* Databases and tables are created first, then you create an entity data model using the created database
* This approach is preferred for data intense, large applications
* Other advantages include:
* Data model is simple to create
* GUI
* You do not need to write any code to create your database
### Scaffolding
* Use the Package Manager Console to "reverse engineer" the code for an existing database
* This is called *__scaffolding__*
* Scaffold the database with the following command:
```powershell
Scaffold-DbContext "Server=PostgreSQL 12;Host=localhost;Port=5432;Username=postgres;Password=1234;Database=sqlpractice" Npgsql.EntityFrameworkCore.PostgreSQL -OutputDir Models
```
* Using the connection string corresponding to your database, this will create all the classes for the entities in the DB as well as the context class
### Exercise 4: Database First
<!--_class: "exercise invert" -->
Create a new ASP.NET Core web app using the API template.
1) Install the required NuGet packages for using EFCore, EFCore Tools and PostgreSQL
a) by using the package manager, or
b) by copying the `<PackageReference>` lines from the `.csproj` file of the previous assignment to this project's `.csproj` file
2) Scaffold the `sqlpractice` database created in [SQL Databases Exercise 1](sql-databases#exercise-1-preparing-the-database) to the project by using the Database First approach. If you have not yet created the database in PostgreSQL, it can be found [here](code-examples/example-query.sql)
### Reading: Authentication with roles
* [Here's](https://www.c-sharpcorner.com/article/jwt-token-creation-authentication-and-authorization-in-asp-net-core-6-0-with-po/) an example how to do a role-based authentication by using JWT tokens
### Exercise 5 (Extra): Connection
<!--_class: "exercise invert" -->
Continuing the previous exercise,
1) Create and connect Postgres database to API and create a second entity with a relation to the first entity.
2) Test your solution.