BLAZOR - Faire un CRUD en utilisant Entity Framework Core et une BD PostGreSQL

SORO Nacoumblé Olivier Martial

Prérequis :

- .Net Core 3.1 SDK ;

- Visual Studio 2019 ;

- PostGreSQL.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : BLAZOR - Faire un CRUD en utilisant Entity Framework Core et une BD PostGreSQL

- Auteur : SORO Nacoumblé Olivier Martial

- Parution : 6 juillet 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 SORO Nacoumblé Olivier Martial. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction à Blazor

- I-A. Qu’est-ce que Blazor ?

- I-B. Les différents modèles de projet Blazor

- II. Création d’un projet Blazor Serveur

- III. Connexion à la base de données PostgreSQL

- III-A. Entity Framework Core (EF Core)

- III-B. Le modèle de données

- III-C. Le contexte de données

- III-D. La migration en base de données

- IV. Le service de gestion des plats

- V. Les vues

- V-A. Les pop-ups avec BlazoredModal

- V-B. Les vues

- V-B-1. Vue d’affichage de la liste des plats

- V-B-2. Vue de création de nouveaux plats

- V-B-3. Vue de mise à jour de plat

- V-B-4. Vue de suppression de plat

- V-B-5. Résultat

- VI. Remerciements Developpez.com

I - Introduction à Blazor

I-A - Qu’est-ce que Blazor ?

Les développeurs web .Net ont très souvent recours à JavaScript afin de réaliser des interfaces utilisateur riches, rapides et interactives. En effet les technologies JavaScript sont beaucoup utilisées pour gérer les appels asynchrones, faire des animations, rafraichir une partie de la page sans la recharger complètement et plein d’autres choses qui peuvent s’avérer très utiles pour les utilisateurs.

Et bien Blazor est un Framework permettant de créer des applications web interactives en utilisant C# au lieu de JavaScript. Il devient alors possible pour un développeur C# d’utiliser un seul langage pour écrire la partie Client et la partie Serveur d’une application web.

Blazor vient avec deux modèles différents : Blazor Server et Blazor Web Assembly. Les deux versions sont aujourd'hui disponibles pour un environnement de production .

I-B - Les différents modèles de projet Blazor

Blazor Web Assembly

L’application Blazor, ses dépendances et le Runtime .Net sont téléchargés dans le navigateur. Ainsi l’application s’exécute directement à partir du thread d’interface utilisateur du navigateur web :

Blazor Serveur

L’application Blazor est exécutée sur le serveur à partir d’une application ASP.Net Core. Les échanges entre l’interface utilisateur et le serveur sont effectués à l’aide de Signal R. Pour ceux qui ne savent pas, signal R est une technologie facilitant les échanges temps réel entre le client et le serveur. Le client peut réceptionner des informations du serveur sans avoir préalablement émis une demande.

C’est ce modèle qui sera utilisé au cours de ce tutoriel.

II - Création d’un projet Blazor Serveur

L’exemple consistera à enregistrer des plats dans une BD PostgreSQL, à afficher la liste des plats à l’écran et à permettre la modification et la suppression de plats enregistrés.

Étape 1 : dans Visual Studio 2019, on clique sur Créer un nouveau projet. Parmi la liste des types de projets qui apparaissent, on choisit Application Blazor.

Étape 2 : on nommera notre application DemoBlazorServerApp et ensuite on choisira le modèle Application serveur Blazor.

Lorsqu’on exécute l’application, on obtient cette page d’accueil :

III - Connexion à la base de données PostgreSQL

III-A - Entity Framework Core (EF Core)

Pour gérer les échanges avec la base de données nous allons utiliser EF Core.

Étape 1 : installer Entity Framework CLI tool afin de pouvoir utiliser les commandes EF Core dans la ligne de commande.

Pour cela nous devons exécuter la commande suivante dans la console de gestionnaire de package :

dotnet tool install --global dotnet-ef

Une fois installé, il ne sera plus nécessaire d’exécuter cette commande à nouveau pour utiliser les commandes EF Core dans d’autres projets sur le même ordinateur.

Étape 2 : installer la bibliothèque EFCore pour PostGreSQL :

Install-Package Npgsql.EntityFrameworkCore.PostgreSQL -Version 3.1.4

III-B - Le modèle de données

Dans le répertoire Data, nous allons créer notre modèle qui permettra de manipuler les objets de type plat :

public class Meal
{
public int Id { get; set; }

[Required]
[StringLength(20, ErrorMessage = "Le nom du plat est trop long.")]
public string Name { get; set; }

public string Description { get; set; }

[Required]
[Range(1, 200, ErrorMessage = "Le montant doit être compris entre 1 et 200 $")]
public decimal Price { get; set; }

public DateTime CreatedDate { get; set; }
}

III-C - Le contexte de données

Cet objet se connectera à la base de données PostGreSQL afin d’effectuer les requêtes.

public class DemoBlazorServerAppContext : DbContext
{
public virtual DbSet<Meal> Meals { get; set; }

public DemoBlazorServerAppContext(DbContextOptions<DemoBlazorServerAppContext> options) : base(options) { }

protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}

Pour que le contexte puisse fonctionner, nous avons besoin de définir la chaine de connexion à la BD. On le fera dans le fichier appsettings.json. Le fichier devrait ressembler à ceci :

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=127.0.0.1;Port=5432;Database=postgres;User Id=postgres;Password=password_postgres;"
}
}

À la racine du projet, nous allons créer la classe ConfigurationHelper qui nous permettra de récupérer les données depuis le fichier appsettings.json. Il sera utilisé dans ce projet pour récupérer la chaine de connexion.

public class ConfigurationHelper
{
public static string GetCurrentSettings(string key)
{
var builder = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();

IConfigurationRoot configuration = builder.Build();

return configuration.GetValue<string>(key);
}
}

Pour conclure avec le contexte, nous allons définir une classe ContextFactory qui permet d’instancier un contexte qui se connecte à la BD PostGreSQL à l’aide de la chaine de connexion récupérée par l’objet ConfigurationHelper.

public class DemoBlazorServerAppContextFactory : IDesignTimeDbContextFactory<DemoBlazorServerAppContext>
{
public DemoBlazorServerAppContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<DemoBlazorServerAppContext>();
var connStr = ConfigurationHelper.GetCurrentSettings("ConnectionStrings:DefaultConnection");
optionsBuilder.UseNpgsql(connStr);
return new DemoBlazorServerAppContext(optionsBuilder.Options);
}
}

III-D - La migration en base de données

Afin de migrer notre modèle de données dans la base de données, nous devons installer le package EntityFrameworkCore.Design :

Install-Package Microsoft.EntityFrameworkCore.Design -Version 3.1.4

Nous pouvons maintenant appliquer la première migration en exécutant ces deux commandes :

dotnet ef migrations add InitialCreate --project DemoBlazorServerApp

dotnet ef database update --project DemoBlazorServerApp

Nous sommes désormais capables d’effectuer toutes les opérations de CRUD avec la BD PostGreSQL.

IV - Le service de gestion des plats

Nous allons user d’un service qui utilisera le contexte pour effectuer les requêtes de l’interface utilisateur. On nommera ce service MealService.

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DemoBlazorServerApp.Data
{
public class MealService : IMealService
{
DemoBlazorServerAppContext _context;

public MealService(DemoBlazorServerAppContext context)
{
_context = context;
}

public async Task<List<Meal>> GetMealsAsync()
{
return await _context.Meals.OrderBy(x => x.CreatedDate).ToListAsync();
}

public async Task<Meal> GetMealByIdAsync(int id)
{
return await _context.Meals.FindAsync(id);
}

public async Task<Meal> InsertMealAsync(Meal meal)
{
meal.CreatedDate = DateTime.Now;
_context.Meals.Add(meal);
await _context.SaveChangesAsync();

return meal;
}

public async Task<Meal> UpdateMealAsync(int id, Meal m)
{
var meal = await _context.Meals.FindAsync(id);

if (meal == null)
return null;

meal.Name = m.Name;
meal.Description = m.Description;
meal.Price = m.Price;

_context.Meals.Update(meal);
await _context.SaveChangesAsync();

return meal;
}

public async Task<Meal> DeleteMealAsync(int id)
{
var meal = await _context.Meals.FindAsync(id);

if (meal == null)
return null;

_context.Meals.Remove(meal);
await _context.SaveChangesAsync();

return meal;
}

}
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DemoBlazorServerApp.Data
{
public interface IMealService
{
Task<List<Meal>> GetMealsAsync();
Task<Meal> GetMealByIdAsync(int id);
Task<Meal> InsertMealAsync(Meal meal);
Task<Meal> UpdateMealAsync(int id, Meal m);
Task<Meal> DeleteMealAsync(int id);
}
}

Il faut inscrire le service MealService et le contexte de données dans le fichier startup.cs avant de pouvoir les utiliser dans l’application :

public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddScoped<IMealService, MealService>();
services.AddDbContext<DemoBlazorServerAppContext>(
option => option.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));
}

V - Les vues

On décide d’afficher la liste des plats sur la page d’accueil par défaut de l’application. Pour cela nous allons modifier le composant Index.razor. La création, modification et suppression de plats se fera par des pop-ups qui seront appelés à partir du composant Index.razor. Nous utiliserons la bibliothèque BlazoredModal pour les popups.

V-A - Les pop-ups avec BlazoredModal

Étape 1 : installer le package BlazoredModal :

Install-Package Blazored.Modal

Étape 2 : inscrire le service BlazoredModal dans le startup.cs.

La version finale de la méthode ConfigureServices de la classe startup.cs devrait être :

public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddScoped<IMealService, MealService>();
services.AddDbContext<DemoBlazorServerAppContext>(
option => option.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));
services.AddBlazoredModal();
}

Étape 3 : ajouter les références dans le fichier _Imports.razor :

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using DemoBlazorServerApp
@using DemoBlazorServerApp.Shared
@using Blazored
@using Blazored.Modal
@using Blazored.Modal.Services

Étape 4 : ajouter le composant BlazoredModal dans le MainLayout.Razor :

@inherits LayoutComponentBase

<BlazoredModal />
<div class="sidebar">
<NavMenu />
</div>

<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>

<div class="content px-4">
@Body
</div>

</div>

Étape 5 : mettre à jour le fichier _Host :

@page "/"
@namespace DemoBlazorServerApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Blazored.Modal
@{
Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DemoBlazorServerApp</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="_content/Blazored.Modal/blazored-modal.css" rel="stylesheet" />
</head>
<body>
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>

<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>

<script src="_framework/blazor.server.js"></script>
</body>
</html>

V-B - Les vues

V-B-1 - Vue d’affichage de la liste des plats

Comme on l’a dit plus haut, le composant Index.razor servira à afficher la liste des plats. Cette liste sera affichée sous forme de tableau :

@page "/"
@using DemoBlazorServerApp.Data
@inject IModalService Modal
@inject IMealService MealService

<div class="row">
<div class="col-12">
<h4><span class="oi oi-list" aria-hidden="true"></span> Liste de plats</h4>
</div>

</div>
<div class="row">
<div class="col-6">
<button @onclick="@(() => AddMeal())" class="btn btn-sm btn-primary"><span class="oi oi-plus" aria-hidden="true"></span> Ajouter plat </button>
</div>

</div>
<br />

@if (meals == null)
{
<p><em>Loading...</em></p>
}
else
{

<table class="table">
<thead>
<tr>
<th>Nom</th>
<th>Description</th>
<th>Prix</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var meal in meals)
{
<tr>
<td>@meal.Name</td>
<td>@meal.Description</td>
<td>@meal.Price $</td>
<th>
<button @onclick="@(() => DeleteMeal(meal.Id))" class="btn btn-sm btn-primary">Delete</button>
| <button @onclick="@(() => EditMeal(meal.Id))" class="btn btn-sm btn-secondary">Edit</button>
</th>
</tr>
}
</tbody>
</table>
}

@code {
private List<Meal> meals;

protected override async Task OnInitializedAsync()
{
meals = await MealService.GetMealsAsync();
}

async Task AddMeal()
{
var mealModal = Modal.Show<AddMeal>("Ajout de plat");
var result = await mealModal.Result;

if (!result.Cancelled)
{
meals = await MealService.GetMealsAsync();
}
}

async Task DeleteMeal(int id)
{
var parameters = new ModalParameters();
parameters.Add(nameof(Meal.Id), id);

var mealModal = Modal.Show<DeleteMeal>("Suppression de plat", parameters);
var result = await mealModal.Result;

if (!result.Cancelled)
{
meals = await MealService.GetMealsAsync();
}
}

async Task EditMeal(int id)
{
var parameters = new ModalParameters();
parameters.Add(nameof(Meal.Id), id);

var mealModal = Modal.Show<EditMeal>("Mise à jour de plat", parameters);
var result = await mealModal.Result;

if (!result.Cancelled)
{
meals = await MealService.GetMealsAsync();
}
}

}

Comme le code source de cette page nous le montre, avant d’utiliser un service dans une page Razor, il faut d’abord l’injecter dans cette page.

V-B-2 - Vue de création de nouveaux plats

Nous allons ajouter un composant AddMeal.razor qui sera utilisé comme popup pour l’ajout de plat.

Faisons un clic droit sur le répertoire Pages, cliquons sur Ajouter et ensuite sélectionnons Composant Razor :

Le code source du composant AddMeal :

@page "/addMeal"
@using DemoBlazorServerApp.Data
@inject IModalService ModalService
@inject IMealService MealService

<EditForm Model="@meal" OnValidSubmit=@FormSubmitted>

<DataAnnotationsValidator />

<div class="form-group">
<label for="nom">Nom</label><br />
<InputText id="nom" @bind-Value="meal.Name" />
<ValidationMessage For="@(() => meal.Name)" />
</div>
<div class="form-group">
<label for="description">Description</label><br/>
<InputTextArea id="description" @bind-Value="meal.Description" />
</div>
<div class="form-group">
<label for="price">Price</label><br />
<InputNumber id="price" @bind-Value="meal.Price" />
<ValidationMessage For="@(() => meal.Price)" />
</div>

<button type="submit" class="btn btn-primary">Ajouter</button>
<button @onclick="BlazoredModal.Cancel" class="btn btn-secondary">Annuler</button>
</EditForm>

@code {
private string StatusMessage;
private string StatusClass;

[CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; }

Meal meal = new Meal();

private async void FormSubmitted()
{
await MealService.InsertMealAsync(meal);

BlazoredModal.Close(ModalResult.Ok<Meal>(meal));
}

}

V-B-3 - Vue de mise à jour de plat

On crée ensuite le composant EditMeal.razor pour la mise à jour des plats :

@page "/editMeal"
@using DemoBlazorServerApp.Data
@inject IModalService ModalService
@inject IMealService MealService

<EditForm Model="@meal" OnValidSubmit=@FormSubmitted>

<DataAnnotationsValidator />

<div class="form-group">
<label for="nom">Nom</label><br />
<InputText id="nom" @bind-Value="meal.Name" />
<ValidationMessage For="@(() => meal.Name)" />
</div>
<div class="form-group">
<label for="description">Description</label><br />
<InputTextArea id="description" @bind-Value="meal.Description" />
</div>
<div class="form-group">
<label for="price">Price</label><br />
<InputNumber id="price" @bind-Value="meal.Price" />
<ValidationMessage For="@(() => meal.Price)" />
</div>

<button type="submit" class="btn btn-primary">Modifier</button>
<button @onclick="BlazoredModal.Cancel" class="btn btn-secondary">Annuler</button>
</EditForm>

@code {
[CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; }

Meal meal = new Meal();
[Parameter] public int id { get; set; }

protected async override void OnInitialized()
{
meal = await MealService.GetMealByIdAsync(id);
}

private async void FormSubmitted()
{
await MealService.UpdateMealAsync(id, meal);
BlazoredModal.Close(ModalResult.Ok<Meal>(meal));
}
}

V-B-4 - Vue de suppression de plat

Et enfin le composant DeleteMeal.razor pour la suppression de plats :

@inject IMealService MealService

<EditForm Model="@meal" OnSubmit=@FormSubmitted>

Voulez vous vraiment supprimer ?

<button type="submit" class="btn btn-primary">Oui</button>
<button @onclick="BlazoredModal.Cancel" class="btn btn-secondary">Annuler</button>
</EditForm>

@code {
[CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; }

Meal meal = new Meal();
[Parameter] public int id { get; set; }

private async void FormSubmitted()
{
await MealService.DeleteMealAsync(id);

BlazoredModal.Close(ModalResult.Ok<Meal>(meal));

}
}

V-B-5 - Résultat

Quand on lance l’application, on obtient  une interface utilisateur rapide ; la page principale qui se met à jour sans rafraichissement total. Et tout cela, c’est fait en n’utilisant que du C-sharp.

VI - Remerciements Developpez.com

Nous tenons à remercier Claude Leloup pour la mise au gabarit et la relecture orthographique.