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.
439 lines
16 KiB
Markdown
439 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)
|
|
|
|
* This lecture will assume you have a basic understanding of SQL databases
|
|
* Read [Webdev basics: SQL Databases](https://gitea.buutti.com/education/webdev-basics/src/branch/main/sql-databases.md) first!
|
|
* [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'>
|
|
|
|

|
|
|
|
</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'>
|
|
|
|

|
|
|
|
</div>
|
|
<div markdown='1'>
|
|
|
|

|
|
|
|
</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:
|
|

|
|
|
|
---
|
|
|
|
<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'>
|
|
|
|

|
|
|
|
</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.
|