Créer une application Blazor Server CRUD avec EF Core et DB2 for i (IBM i)

Introduction

Ce tutoriel montre comment créer une application Blazor Server connectée à une base DB2 for i via l'extension Entity Framework Core de NTi, compatible avec .NET 8, .NET 9 et .NET 10.

L'objectif est de mettre en place un CRUD complet pour gérer des produits, catégories et commandes depuis une application web .NET moderne.

Cette approche permet d'utiliser EF Core sur IBM i pour simplifier l'accès aux données et accélérer le développement d'applications métiers.

Les commandes de ce tutoriel ciblent .NET 8 : pour .NET 9 ou .NET 10, remplacez simplement la version du framework et les versions de packages indiquées à l'étape 1. Côté serveur, le volet EF Core de NTi requiert IBM i 7.2 ou supérieur.


Étape 1 - Créer et configurer le projet

Créez le projet depuis la ligne de commande :

dotnet new blazor -n myApp -f net8.0 -int Server
cd myApp

💡 Le template blazorserver n'existe plus depuis .NET 8. Le template blazor avec l'option -int Server (interactivité Server) crée une Blazor Web App au comportement équivalent, dont les pages se trouvent dans Components/Pages. Pour .NET 9 ou .NET 10, remplacez net8.0 par net9.0 ou net10.0.

Ajoutez les packages nécessaires :

dotnet add package Aumerial.Data.Nti
dotnet add package Aumerial.EntityFrameworkCore --version 8.5.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.*

La version d'Aumerial.EntityFrameworkCore se choisit selon la version d'EF Core ciblée : la majeure suit EF Core, la mineure correspond à la génération NTi.

Cible Aumerial.EntityFrameworkCore Packages Microsoft EF Core
.NET 8 (EF Core 8) 8.5.0 8.0.*
.NET 9 (EF Core 9) 9.5.0 9.0.*
.NET 10 (EF Core 10) 10.5.0 10.0.*

💡 Les trois versions s'appuient sur le provider ADO.NET Aumerial.Data.Nti 5.0.0, installé automatiquement comme dépendance si vous omettez la première commande.

Ajoutez la chaîne de connexion dans appsettings.json en spécifiant le schéma par défaut (la bibliothèque) dans lequel toutes les entités créées seront placées :

{
    "ConnectionStrings": {
        "DefaultConnection": "server=Server;user=User;password=Pwd;database=Db;pooling=true"
    }
}

💡 Le pooling de connexions est inactif par défaut avec NTi 5 : pooling=true est recommandé pour toute application web.


Étape 2 - Définir les entités

Créez un dossier Models et ajoutez les classes suivantes :

  • Category.cs

Une catégorie peut contenir plusieurs produits :

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection Products { get; set; }
}
  • Product.cs

Un produit appartient à une catégorie et peut être lié à plusieurs commandes :

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public decimal Weight { get; set; }
    public bool IsAvailable { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
    public ICollection Orders { get; set; } = new List();
}
  • Order.cs

Une commande peut contenir plusieurs produits :

public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public DateTime? DeliveryDate { get; set; }
    public decimal TotalAmount { get; set; }
    public ICollection Products { get; set; }
}

Étape 3 - Configurer le DbContext

Ajoutez une classe AppDbContext héritant de DbContext pour gérer les entités et leurs relations :

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions options) : base(options) { }

    public DbSet Products { get; set; }
    public DbSet Categories { get; set; }
    public DbSet Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

    }
}

Étape 4 - Configurer Program.cs

Configurez le DbContext dans Program.cs et enregistrez-le comme service via l'injection de dépendances pour vos composants Blazor. Ajoutez using Aumerial.EntityFrameworkCore; en haut du fichier pour rendre la méthode UseNTi accessible :

using Aumerial.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext(options =>
    options.UseNTi(connectionString));

💡 AddDbContext est recommandé pour la plupart des applications Blazor Server. Le DbContext est instancié avec une durée de vie Scoped, recréé à chaque requête utilisateur. Utilisez AddDbContextFactory pour les tâches en arrière-plan ou les traitements multi-threads (voir la documentation Microsoft).


Étape 5 - Créer et gérer les migrations

Générez une migration initiale pour créer les tables dans la base de données. Cette commande va créer un fichier dans le dossier Migrations contenant les instructions SQL pour créer vos tables :

dotnet ef migrations add InitialCreate

Appliquez ensuite la migration :

dotnet ef database update

Visuel entités créées sur DB2 for i via ACS

Pour ajouter ou modifier une table, créez ou modifiez l'entité concernée, ajoutez-la dans AppDbContext si nécessaire, puis générez une nouvelle migration :

dotnet ef migrations add MyNewMigration
dotnet ef database update

Pour supprimer la dernière migration avant son application :

dotnet ef migrations remove

Pour revenir à une version antérieure de la base :

dotnet ef database update MigrationName

Remplacez MigrationName par le nom de la migration vers laquelle vous souhaitez revenir.


Étape 6 - Ajouter un jeu de données initial

Ajoutez un jeu de données dans Program.cs, après var app = builder.Build(); :

using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService();

    var categories = new List
    {
        new Category { Name = "Electronics" },
        new Category { Name = "Books" },
        new Category { Name = "Home Appliances" },
        new Category { Name = "Fashion" },
        new Category { Name = "Toys" }
    };
    context.Categories.AddRange(categories);

    var products = new List
    {
        new Product { Name = "Smartphone", Price = 500, StockQuantity = 10, Category = categories[0], IsAvailable = true },
        new Product { Name = "Laptop", Price = 1200, StockQuantity = 5, Category = categories[0], IsAvailable = true },
        new Product { Name = "Washing Machine", Price = 300, StockQuantity = 8, Category = categories[2], IsAvailable = true },
        new Product { Name = "T-Shirt", Price = 20, StockQuantity = 50, Category = categories[3], IsAvailable = true },
        new Product { Name = "Children's Book", Price = 15, StockQuantity = 100, Category = categories[1], IsAvailable = true },
        new Product { Name = "Toy Car", Price = 30, StockQuantity = 20, Category = categories[4], IsAvailable = true },
        new Product { Name = "Microwave Oven", Price = 250, StockQuantity = 6, Category = categories[2], IsAvailable = true },
        new Product { Name = "Jeans", Price = 40, StockQuantity = 30, Category = categories[3], IsAvailable = true }
    };
    context.Products.AddRange(products);

    var orders = new List
    {
        new Order
        {
            OrderDate = DateTime.Now.AddDays(-10),
            DeliveryDate = DateTime.Now.AddDays(-7),
            TotalAmount = 750,
            Products = new List { products[0], products[1], products[3] }
        },
        new Order
        {
            OrderDate = DateTime.Now.AddDays(-5),
            DeliveryDate = DateTime.Now.AddDays(-3),
            TotalAmount = 600,
            Products = new List { products[4], products[5], products[6] }
        },
        new Order
        {
            OrderDate = DateTime.Now.AddDays(-2),
            DeliveryDate = null,
            TotalAmount = 290,
            Products = new List { products[2], products[7] }
        }
    };
    context.Orders.AddRange(orders);

    await context.SaveChangesAsync();
}

💡 SaveChangesAsync est la voie normale avec NTi : l'async y est réel de bout en bout, sans sync-over-async. Le SaveChanges synchrone reste disponible.


Étape 7 - Générer les pages CRUD

Visual Studio peut générer automatiquement les composants Razor CRUD pour chacune de vos entités en quelques clics.

  1. Clic droit sur le dossier Components/Pages de votre projet
  2. Sélectionnez Ajouter > Nouvel élément généré automatiquement > Composants Razor avec Entity Framework (CRUD)
  3. Configurez les options :
    • Classe de Modèle : sélectionnez l'entité souhaitée (ex. Product)
    • Classe de DbContext : sélectionnez AppDbContext

Visual Studio génère automatiquement un ensemble de composants Razor CRUD dans un dossier dédié (ex. Components/Pages/ProductPages) :

  • Index.razor - liste des enregistrements
  • Create.razor - formulaire d'ajout
  • Edit.razor - formulaire de modification
  • Details.razor - affichage du détail d'un enregistrement
  • Delete.razor - confirmation et suppression

Répétez l'opération pour chaque entité : Category, Order.


Étape 8 - Ajouter un champ image (BLOB)

Ajoutez un champ Image de type byte[] à l'entité Product :

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public decimal Weight { get; set; }
    public bool IsAvailable { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
    public ICollection Orders { get; set; } = new List();

    [Column(TypeName = "BLOB(1M)"), DataType(DataType.Upload)]
    public byte[] Image { get; set; }
}

Générez et appliquez la migration :

dotnet ef migrations add AddProductImage
dotnet ef database update

Modifiez ensuite les composants Components/Pages/ProductPages/Create.razor et Edit.razor pour ajouter le champ d'upload :

@if (Product?.Image != null && Product.Image.Length > 0) {

Image actuelle :

} else {

Aucune image disponible

}

Et la méthode pour gérer l'upload :

private async Task UploadFile(InputFileChangeEventArgs e)
{
    var file = e.File;

    if (file != null)
    {
        using var memoryStream = new MemoryStream();
        await file.OpenReadStream().CopyToAsync(memoryStream);
        Product.Image = memoryStream.ToArray();
    }
}

Illustration CRUD Blazor page produit


Et maintenant ?

Reconnexion au serveur...

La connexion au serveur a été perdue. La page va se recharger.