Partie 2, Pages Razor avec EF Core dans ASP.NET Core - CRUD

Par Tom Dykstra, Jeremy Likness et Jon P Smith

L’application web Contoso University montre comment créer des applications web Pages Razor avec EF Core et Visual Studio. Pour obtenir des informations sur la série de didacticiels, consultez le premier didacticiel.

Si vous rencontrez des problèmes que vous ne pouvez pas résoudre, téléchargez l’application finale et comparez ce code à celui que vous avez créé en suivant le tutoriel.

Dans ce didacticiel, nous allons examiner et personnaliser le code CRUD (créer, lire, mettre à jour, supprimer) généré automatiquement.

Aucun référentiel

Certains développeurs utilisent une couche de service ou un modèle de référentiel pour créer une couche d’abstraction entre l’interface utilisateur (Pages Razor) et la couche d’accès aux données. Ce n’est pas le cas de ce tutoriel. Pour que ce tutoriel soit moins complexe et traite exclusivement de EF Core, le code EF Core est directement ajouté aux classes de modèle de page.

Mettre à jour la page Details

Le code généré automatiquement pour les pages Students n’inclut pas les données d’inscription (« enrollment »). Dans cette section, les inscriptions sont ajoutées à la page Details.

Lire les inscriptions

Pour afficher les données d’inscription d’un étudiant sur la page, elles doivent être lues. Le code généré automatiquement dans Pages/Students/Details.cshtml.cs lit uniquement les données Student, sans les données Enrollment :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Remplacez la méthode OnGetAsync par le code suivant pour lire les données d’inscription de l’étudiant sélectionné. Les modifications sont mises en surbrillance.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Les méthodes Include et ThenInclude forcent le contexte à charger la propriété de navigation Student.Enrollments et, dans chaque inscription, la propriété de navigation Enrollment.Course. Ces méthodes sont examinées en détail dans le tutoriel Lire les données associées.

La méthode AsNoTracking améliore les performances dans les scénarios lorsque les entités retournées ne sont pas mises à jour dans le contexte actuel. Le sujet AsNoTracking est abordé plus loin dans ce didacticiel.

Afficher les inscriptions

Remplacez le code dans Pages/Students/Details.cshtml par le code suivant pour afficher une liste d’inscriptions. Les modifications sont mises en surbrillance.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

Le code précédent effectue une itération sur les entités dans la propriété de navigation Enrollments. Pour chaque inscription, il affiche le titre du cours et le niveau. Le titre du cours est récupéré à partir de l’entité Course qui est stockée dans la propriété de navigation Course de l’entité Inscriptions.

Exécutez l’application, sélectionnez l’onglet Students, puis cliquez sur le lien Details pour un étudiant. La liste des cours et les notes de l’étudiant sélectionné s’affiche.

Méthodes pour lire une entité

Le code généré utilise FirstOrDefaultAsync pour lire une entité. Cette méthode retourne la valeur Null si rien n’est trouvé ; sinon, elle retourne la première ligne trouvée qui répond aux critères de filtre de requête. FirstOrDefaultAsync est généralement un meilleur choix que les autres solutions suivantes :

  • SingleOrDefaultAsync – Lève une exception si plusieurs entités répondent au filtre de requête. Pour déterminer si plusieurs lignes peuvent être retournées par la requête, SingleOrDefaultAsync tente de récupérer plusieurs lignes. Ce travail supplémentaire est inutile si la requête ne peut retourner qu’une seule entité, comme quand elle effectue une recherche sur une clé unique.
  • FindAsync – Recherche une entité avec la clé primaire. Si une entité avec la clé primaire est suivie par le contexte, elle est retournée sans qu’aucune requête soit envoyée à la base de données. Cette méthode est optimisée pour la recherche d’une seule entité, mais vous ne pouvez pas appeler Include avec FindAsync. Par conséquent, si des données associées sont nécessaires, FirstOrDefaultAsync est le meilleur choix.

Données de route/chaîne de requête

L’URL de la page Details est https://localhost:<port>/Students/Details?id=1. La valeur de clé primaire de l’entité se trouve dans la chaîne de requête. Certains développeurs préfèrent passer la valeur de clé dans des données de route : https://localhost:<port>/Students/Details/1. Pour plus d'informations, consultez Mettre à jour le code généré.

Mettre à jour la page Create

Le code OnPostAsync généré automatiquement pour la page Create est vulnérable aux sur-publications. Remplacez la méthode OnPostAsync dans Pages/Students/Create.cshtml.cs par le code suivant.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

Le code précédent crée un objet Student, puis utilise des champs de formulaire publiés pour mettre à jour les propriétés de l’objet Student. La méthode TryUpdateModelAsync :

  • Utilise les valeurs de formulaire publiées de la propriété PageContext dans le PageModel.
  • Met à jour uniquement les propriétés listées (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Recherche les champs de formulaire dotés d’un préfixe « Student ». Par exemple : Student.FirstMidName. Il ne respecte pas la casse.
  • Utilise le système de liaison de modèles pour convertir les valeurs de formulaire de chaînes en types dans le modèle Student. Par exemple, EnrollmentDate est converti en DateTime.

Exécutez l’application, puis créez une entité Student pour tester la page Create.

Sur-publication

L’utilisation de TryUpdateModel pour mettre à jour des champs avec des valeurs publiées est une bonne pratique de sécurité, car cela empêche la sur-publication. Par exemple, supposez que l’entité Student comprend une propriété Secret que cette page web ne doit pas mettre à jour ou ajouter :

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Même si l’application n’a pas de champ Secret dans la page Razor de création ou de mise à jour, un pirate pourrait définir la valeur Secret par sur-publication. Un pirate pourrait utiliser un outil tel que Fiddler, ou écrire du JavaScript, pour publier une valeur de formulaire Secret. Le code d’origine ne limite pas les champs que le classeur de modèles utilise quand il crée une instance de Student.

La valeur spécifiée par le pirate pour le champ de formulaire Secret, quelle qu’elle soit, est mise à jour dans la base de données. L’illustration suivante montre l’outil Fiddler en train d’ajouter le champ Secret, avec la valeur « OverPost », aux valeurs du formulaire envoyé.

Fiddler adding Secret field

La valeur « OverPost » est ajoutée avec succès à la propriété Secret de la ligne insérée. Cela se produit même si le concepteur de l’application n’avait jamais prévu que la propriété Secret serait définie avec la page Create.

Afficher le modèle

Les modèles d’affichage fournissent une alternative pour empêcher la sur-publication.

Le modèle d’application est souvent appelé modèle de domaine. En règle générale, le modèle de domaine contient toutes les propriétés requises par l’entité correspondante dans la base de données. Le modèle d’affichage contient uniquement les propriétés nécessaires pour la page d’interface utilisateur, par exemple, la page Créer.

En plus du modèle d’affichage, certaines applications utilisent un modèle de liaison ou d’entrée pour transmettre des données entre la classe de modèles de pages de Pages Razor et le navigateur.

Considérez le modèle d’affichage StudentVM suivant :

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

Le code suivant utilise le modèle d’affichage StudentVM pour créer un étudiant :

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

La méthode SetValues définit les valeurs de cet objet en lisant les valeurs d’un autre objet PropertyValues. SetValues utilise la correspondance de nom de propriété. Type de modèle d’affichage :

  • N’a pas besoin d’être lié au type de modèle.
  • Doit avoir des propriétés qui correspondent.

L’utilisation de StudentVM nécessite que la page Créer utilise StudentVM plutôt que Student :

@page
@model CreateVMModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Mettre à jour la page Edit

Dans Pages/Students/Edit.cshtml.cs, remplacez les méthodes OnGetAsync et OnPostAsync par le code suivant.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

    if (studentToUpdate == null)
    {
        return NotFound();
    }

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Les modifications de code sont semblables à celles de la page Create, à quelques exceptions près :

  • FirstOrDefaultAsync a été remplacé par FindAsync. Quand vous n’êtes pas tenu d’inclure des données associées, FindAsync est plus efficace.
  • OnPostAsync contient un paramètre id.
  • Plutôt que de créer un étudiant vide, l’étudiant actuel est récupéré à partir de la base de données.

Exécutez l’application et testez-la en créant et modifiant un étudiant.

États des entités

Le contexte de base de données effectue un suivi pour déterminer si les entités en mémoire sont synchronisées avec les lignes correspondantes de la base de données. Ces informations de suivi déterminent ce qui se passe quand SaveChangesAsync est appelé. Par exemple, quand une nouvelle entité est passée à la méthode AddAsync, l’état de cette entité prend la valeur Added. Quand SaveChangesAsync est appelé, le contexte de base de données émet une commande SQL INSERT.

Une entité peut être dans l’un des états suivants :

  • Added : L’entité n’existe pas encore dans la base de données. La méthode SaveChanges émet une instruction INSERT.

  • Unchanged : Aucune modification ne doit être enregistrée avec cette entité. Une entité est dans cet état quand elle est lue à partir de la base de données.

  • Modified : Tout ou une partie des valeurs de propriété de l’entité ont été modifiées. La méthode SaveChanges émet une instruction UPDATE.

  • Deleted : L’entité a été marquée pour suppression. La méthode SaveChanges émet une instruction DELETE.

  • Detached : L’entité n’est pas suivie par le contexte de base de données.

Dans une application de bureau, les changements d’état sont généralement définis automatiquement. Une entité est lue, des modifications sont apportées et l’état d’entité passe automatiquement à Modified. L’appel de SaveChanges génère une instruction SQL UPDATE qui met à jour uniquement les propriétés modifiées.

Dans une application web, le DbContext qui lit une entité et affiche les données est supprimé après le rendu d’une page. Quand la méthode OnPostAsync d’une page est appelée, une nouvelle requête web est faite avec une nouvelle instance du DbContext. La relecture de l’entité dans ce nouveau contexte simule le traitement de bureau.

Mettre à jour la page Delete

Dans cette section, un message d’erreur personnalisé est implémenté quand l’appel à SaveChanges échoue.

Remplacez le code de Pages/Students/Delete.cshtml.cs par le code suivant :

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

Le code précédent :

  • Ajoute Journalisation.
  • Ajoute le paramètre facultatif saveChangesError à la signature de méthode OnGetAsync. saveChangesError indique si la méthode a été appelée après un échec de suppression de l’objet Student.

L’opération de suppression peut échouer en raison de problèmes réseau temporaires. Vous avez plus de chances de rencontrer des erreurs réseau temporaires quand la base de données est dans le cloud. Le paramètre saveChangesError a la valeur false quand la page Delete OnGetAsync est appelée à partir de l’interface utilisateur. Quand OnGetAsync est appelée par OnPostAsync (car l’opération de suppression a échoué), le paramètre saveChangesError a la valeur true.

La méthode OnPostAsync récupère l’entité sélectionnée, puis appelle la méthode Remove pour définir l’état de l’entité sur Deleted. Lorsque SaveChanges est appelée, une commande SQL DELETE est générée. Si Remove échoue :

  • L’exception de la base de données est interceptée.
  • La méthode OnGetAsync des pages est appelée avec saveChangesError=true.

Ajoute un message d'erreur à Pages/Students/Delete.cshtml :

@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Exécutez l’application et supprimez un étudiant pour tester la page Delete.

Étapes suivantes

Dans ce didacticiel, nous allons examiner et personnaliser le code CRUD (créer, lire, mettre à jour, supprimer) généré automatiquement.

Aucun référentiel

Certains développeurs utilisent une couche de service ou un modèle de référentiel pour créer une couche d’abstraction entre l’interface utilisateur (Pages Razor) et la couche d’accès aux données. Ce n’est pas le cas de ce tutoriel. Pour que ce tutoriel soit moins complexe et traite exclusivement de EF Core, le code EF Core est directement ajouté aux classes de modèle de page.

Mettre à jour la page Details

Le code généré automatiquement pour les pages Students n’inclut pas les données d’inscription (« enrollment »). Dans cette section, les inscriptions sont ajoutées à la page Details.

Lire les inscriptions

Pour afficher les données d’inscription d’un étudiant sur la page, elles doivent être lues. Le code généré automatiquement dans Pages/Students/Details.cshtml.cs lit uniquement les données Student, sans les données Enrollment :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Remplacez la méthode OnGetAsync par le code suivant pour lire les données d’inscription de l’étudiant sélectionné. Les modifications sont mises en surbrillance.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Les méthodes Include et ThenInclude forcent le contexte à charger la propriété de navigation Student.Enrollments et, dans chaque inscription, la propriété de navigation Enrollment.Course. Ces méthodes sont examinées en détail dans le tutoriel Lire les données associées.

La méthode AsNoTracking améliore les performances dans les scénarios lorsque les entités retournées ne sont pas mises à jour dans le contexte actuel. Le sujet AsNoTracking est abordé plus loin dans ce didacticiel.

Afficher les inscriptions

Remplacez le code dans Pages/Students/Details.cshtml par le code suivant pour afficher une liste d’inscriptions. Les modifications sont mises en surbrillance.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

Le code précédent effectue une itération sur les entités dans la propriété de navigation Enrollments. Pour chaque inscription, il affiche le titre du cours et le niveau. Le titre du cours est récupéré à partir de l’entité Course qui est stockée dans la propriété de navigation Course de l’entité Inscriptions.

Exécutez l’application, sélectionnez l’onglet Students, puis cliquez sur le lien Details pour un étudiant. La liste des cours et les notes de l’étudiant sélectionné s’affiche.

Méthodes pour lire une entité

Le code généré utilise FirstOrDefaultAsync pour lire une entité. Cette méthode retourne la valeur Null si rien n’est trouvé ; sinon, elle retourne la première ligne trouvée qui répond aux critères de filtre de requête. FirstOrDefaultAsync est généralement un meilleur choix que les autres solutions suivantes :

  • SingleOrDefaultAsync – Lève une exception si plusieurs entités répondent au filtre de requête. Pour déterminer si plusieurs lignes peuvent être retournées par la requête, SingleOrDefaultAsync tente de récupérer plusieurs lignes. Ce travail supplémentaire est inutile si la requête ne peut retourner qu’une seule entité, comme quand elle effectue une recherche sur une clé unique.
  • FindAsync – Recherche une entité avec la clé primaire. Si une entité avec la clé primaire est suivie par le contexte, elle est retournée sans qu’aucune requête soit envoyée à la base de données. Cette méthode est optimisée pour la recherche d’une seule entité, mais vous ne pouvez pas appeler Include avec FindAsync. Par conséquent, si des données associées sont nécessaires, FirstOrDefaultAsync est le meilleur choix.

Données de route/chaîne de requête

L’URL de la page Details est https://localhost:<port>/Students/Details?id=1. La valeur de clé primaire de l’entité se trouve dans la chaîne de requête. Certains développeurs préfèrent passer la valeur de clé dans des données de route : https://localhost:<port>/Students/Details/1. Pour plus d'informations, consultez Mettre à jour le code généré.

Mettre à jour la page Create

Le code OnPostAsync généré automatiquement pour la page Create est vulnérable aux sur-publications. Remplacez la méthode OnPostAsync dans Pages/Students/Create.cshtml.cs par le code suivant.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

Le code précédent crée un objet Student, puis utilise des champs de formulaire publiés pour mettre à jour les propriétés de l’objet Student. La méthode TryUpdateModelAsync :

  • Utilise les valeurs de formulaire publiées de la propriété PageContext dans le PageModel.
  • Met à jour uniquement les propriétés listées (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Recherche les champs de formulaire dotés d’un préfixe « Student ». Par exemple : Student.FirstMidName. Il ne respecte pas la casse.
  • Utilise le système de liaison de modèles pour convertir les valeurs de formulaire de chaînes en types dans le modèle Student. Par exemple, EnrollmentDate est converti en DateTime.

Exécutez l’application, puis créez une entité Student pour tester la page Create.

Sur-publication

L’utilisation de TryUpdateModel pour mettre à jour des champs avec des valeurs publiées est une bonne pratique de sécurité, car cela empêche la sur-publication. Par exemple, supposez que l’entité Student comprend une propriété Secret que cette page web ne doit pas mettre à jour ou ajouter :

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Même si l’application n’a pas de champ Secret dans la page Razor de création ou de mise à jour, un pirate pourrait définir la valeur Secret par sur-publication. Un pirate pourrait utiliser un outil tel que Fiddler, ou écrire du JavaScript, pour publier une valeur de formulaire Secret. Le code d’origine ne limite pas les champs que le classeur de modèles utilise quand il crée une instance de Student.

La valeur spécifiée par le pirate pour le champ de formulaire Secret, quelle qu’elle soit, est mise à jour dans la base de données. L’illustration suivante montre l’outil Fiddler en train d’ajouter le champ Secret, avec la valeur « OverPost », aux valeurs du formulaire envoyé.

Fiddler adding Secret field

La valeur « OverPost » est ajoutée avec succès à la propriété Secret de la ligne insérée. Cela se produit même si le concepteur de l’application n’avait jamais prévu que la propriété Secret serait définie avec la page Create.

Afficher le modèle

Les modèles d’affichage fournissent une alternative pour empêcher la sur-publication.

Le modèle d’application est souvent appelé modèle de domaine. En règle générale, le modèle de domaine contient toutes les propriétés requises par l’entité correspondante dans la base de données. Le modèle d’affichage contient uniquement les propriétés nécessaires pour la page d’interface utilisateur, par exemple, la page Créer.

En plus du modèle d’affichage, certaines applications utilisent un modèle de liaison ou d’entrée pour transmettre des données entre la classe de modèles de pages de Pages Razor et le navigateur.

Considérez le modèle d’affichage StudentVM suivant :

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

Le code suivant utilise le modèle d’affichage StudentVM pour créer un étudiant :

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

La méthode SetValues définit les valeurs de cet objet en lisant les valeurs d’un autre objet PropertyValues. SetValues utilise la correspondance de nom de propriété. Type de modèle d’affichage :

  • N’a pas besoin d’être lié au type de modèle.
  • Doit avoir des propriétés qui correspondent.

L’utilisation de StudentVM nécessite que la page Créer utilise StudentVM plutôt que Student :

@page
@model CreateVMModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Mettre à jour la page Edit

Dans Pages/Students/Edit.cshtml.cs, remplacez les méthodes OnGetAsync et OnPostAsync par le code suivant.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

    if (studentToUpdate == null)
    {
        return NotFound();
    }

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Les modifications de code sont semblables à celles de la page Create, à quelques exceptions près :

  • FirstOrDefaultAsync a été remplacé par FindAsync. Quand vous n’êtes pas tenu d’inclure des données associées, FindAsync est plus efficace.
  • OnPostAsync contient un paramètre id.
  • Plutôt que de créer un étudiant vide, l’étudiant actuel est récupéré à partir de la base de données.

Exécutez l’application et testez-la en créant et modifiant un étudiant.

États des entités

Le contexte de base de données effectue un suivi pour déterminer si les entités en mémoire sont synchronisées avec les lignes correspondantes de la base de données. Ces informations de suivi déterminent ce qui se passe quand SaveChangesAsync est appelé. Par exemple, quand une nouvelle entité est passée à la méthode AddAsync, l’état de cette entité prend la valeur Added. Quand SaveChangesAsync est appelé, le contexte de base de données émet une commande SQL INSERT.

Une entité peut être dans l’un des états suivants :

  • Added : L’entité n’existe pas encore dans la base de données. La méthode SaveChanges émet une instruction INSERT.

  • Unchanged : Aucune modification ne doit être enregistrée avec cette entité. Une entité est dans cet état quand elle est lue à partir de la base de données.

  • Modified : Tout ou une partie des valeurs de propriété de l’entité ont été modifiées. La méthode SaveChanges émet une instruction UPDATE.

  • Deleted : L’entité a été marquée pour suppression. La méthode SaveChanges émet une instruction DELETE.

  • Detached : L’entité n’est pas suivie par le contexte de base de données.

Dans une application de bureau, les changements d’état sont généralement définis automatiquement. Une entité est lue, des modifications sont apportées et l’état d’entité passe automatiquement à Modified. L’appel de SaveChanges génère une instruction SQL UPDATE qui met à jour uniquement les propriétés modifiées.

Dans une application web, le DbContext qui lit une entité et affiche les données est supprimé après le rendu d’une page. Quand la méthode OnPostAsync d’une page est appelée, une nouvelle requête web est faite avec une nouvelle instance du DbContext. La relecture de l’entité dans ce nouveau contexte simule le traitement de bureau.

Mettre à jour la page Delete

Dans cette section, un message d’erreur personnalisé est implémenté quand l’appel à SaveChanges échoue.

Remplacez le code de Pages/Students/Delete.cshtml.cs par le code suivant :

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

Le code précédent :

  • Ajoute Journalisation.
  • Ajoute le paramètre facultatif saveChangesError à la signature de méthode OnGetAsync. saveChangesError indique si la méthode a été appelée après un échec de suppression de l’objet Student.

L’opération de suppression peut échouer en raison de problèmes réseau temporaires. Vous avez plus de chances de rencontrer des erreurs réseau temporaires quand la base de données est dans le cloud. Le paramètre saveChangesError a la valeur false quand la page Delete OnGetAsync est appelée à partir de l’interface utilisateur. Quand OnGetAsync est appelée par OnPostAsync (car l’opération de suppression a échoué), le paramètre saveChangesError a la valeur true.

La méthode OnPostAsync récupère l’entité sélectionnée, puis appelle la méthode Remove pour définir l’état de l’entité sur Deleted. Lorsque SaveChanges est appelée, une commande SQL DELETE est générée. Si Remove échoue :

  • L’exception de la base de données est interceptée.
  • La méthode OnGetAsync des pages est appelée avec saveChangesError=true.

Ajoute un message d'erreur à Pages/Students/Delete.cshtml :

@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Exécutez l’application et supprimez un étudiant pour tester la page Delete.

Étapes suivantes

Dans ce didacticiel, nous allons examiner et personnaliser le code CRUD (créer, lire, mettre à jour, supprimer) généré automatiquement.

Aucun référentiel

Certains développeurs utilisent une couche de service ou un modèle de référentiel pour créer une couche d’abstraction entre l’interface utilisateur (Pages Razor) et la couche d’accès aux données. Ce n’est pas le cas de ce tutoriel. Pour que ce tutoriel soit moins complexe et traite exclusivement de EF Core, le code EF Core est directement ajouté aux classes de modèle de page.

Mettre à jour la page Details

Le code généré automatiquement pour les pages Students n’inclut pas les données d’inscription (« enrollment »). Dans cette section, les inscriptions sont ajoutées à la page Détails.

Lire les inscriptions

Pour afficher les données d’inscription d’un étudiant sur la page, elles doivent être lues. Le code généré automatiquement dans Pages/Students/Details.cshtml.cs lit uniquement les données d’étudiant, sans les données d’inscription :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Remplacez la méthode OnGetAsync par le code suivant pour lire les données d’inscription de l’étudiant sélectionné. Les modifications sont mises en surbrillance.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Les méthodes Include et ThenInclude forcent le contexte à charger la propriété de navigation Student.Enrollments et, dans chaque inscription, la propriété de navigation Enrollment.Course. Ces méthodes sont examinées en détail dans le tutoriel Lecture de données associées.

La méthode AsNoTracking améliore les performances dans les scénarios lorsque les entités retournées ne sont pas mises à jour dans le contexte actuel. Le sujet AsNoTracking est abordé plus loin dans ce didacticiel.

Afficher les inscriptions

Remplacez le code dans Pages/Students/Details.cshtml par le code suivant pour afficher une liste d’inscriptions. Les modifications sont mises en surbrillance.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

Le code précédent effectue une itération sur les entités dans la propriété de navigation Enrollments. Pour chaque inscription, il affiche le titre du cours et le niveau. Le titre du cours est récupéré à partir de l’entité de cours qui est stockée dans la propriété de navigation Course de l’entité Enrollments.

Exécutez l’application, sélectionnez l’onglet Students, puis cliquez sur le lien Details pour un étudiant. La liste des cours et les notes de l’étudiant sélectionné s’affiche.

Méthodes pour lire une entité

Le code généré utilise FirstOrDefaultAsync pour lire une entité. Cette méthode retourne la valeur Null si rien n’est trouvé ; sinon, elle retourne la première ligne trouvée qui répond aux critères de filtre de requête. FirstOrDefaultAsync est généralement un meilleur choix que les autres solutions suivantes :

  • SingleOrDefaultAsync – Lève une exception si plusieurs entités répondent au filtre de requête. Pour déterminer si plusieurs lignes peuvent être retournées par la requête, SingleOrDefaultAsync tente de récupérer plusieurs lignes. Ce travail supplémentaire est inutile si la requête ne peut retourner qu’une seule entité, comme quand elle effectue une recherche sur une clé unique.
  • FindAsync – Recherche une entité avec la clé primaire. Si une entité avec la clé primaire est suivie par le contexte, elle est retournée sans qu’aucune requête soit envoyée à la base de données. Cette méthode est optimisée pour la recherche d’une seule entité, mais vous ne pouvez pas appeler Include avec FindAsync. Par conséquent, si des données associées sont nécessaires, FirstOrDefaultAsync est le meilleur choix.

Données de route/chaîne de requête

L’URL de la page Details est https://localhost:<port>/Students/Details?id=1. La valeur de clé primaire de l’entité se trouve dans la chaîne de requête. Certains développeurs préfèrent passer la valeur de clé dans des données de route : https://localhost:<port>/Students/Details/1. Pour plus d'informations, consultez Mettre à jour le code généré.

Mettre à jour la page Create

Le code OnPostAsync généré automatiquement pour la page Create est vulnérable aux sur-publications. Remplacez la méthode OnPostAsync dans Pages/Students/Create.cshtml.cs par le code suivant.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

Le code précédent crée un objet Student, puis utilise des champs de formulaire publiés pour mettre à jour les propriétés de l’objet Student. La méthode TryUpdateModelAsync :

  • Utilise les valeurs de formulaire publiées de la propriété PageContext dans le PageModel.
  • Met à jour uniquement les propriétés listées (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Recherche les champs de formulaire dotés d’un préfixe « Student ». Par exemple : Student.FirstMidName. Il ne respecte pas la casse.
  • Utilise le système de liaison de modèles pour convertir les valeurs de formulaire de chaînes en types dans le modèle Student. Par exemple, EnrollmentDate doit être converti en DateTime.

Exécutez l’application, puis créez une entité Student pour tester la page Create.

Sur-publication

L’utilisation de TryUpdateModel pour mettre à jour des champs avec des valeurs publiées est une bonne pratique de sécurité, car cela empêche la sur-publication. Par exemple, supposez que l’entité Student comprend une propriété Secret que cette page web ne doit pas mettre à jour ou ajouter :

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Même si l’application n’a pas de champ Secret dans la page Razor de création ou de mise à jour, un pirate pourrait définir la valeur Secret par sur-publication. Un pirate pourrait utiliser un outil tel que Fiddler, ou écrire du JavaScript, pour publier une valeur de formulaire Secret. Le code d’origine ne limite pas les champs que le classeur de modèles utilise quand il crée une instance de Student.

La valeur spécifiée par le pirate pour le champ de formulaire Secret, quelle qu’elle soit, est mise à jour dans la base de données. L’illustration suivante montre l’outil Fiddler en train d’ajouter le champ Secret (avec la valeur « OverPost ») aux valeurs du formulaire envoyé.

Fiddler adding Secret field

La valeur « OverPost » est ajoutée avec succès à la propriété Secret de la ligne insérée. Cela se produit même si le concepteur de l’application n’avait jamais prévu que la propriété Secret serait définie avec la page Create.

Afficher le modèle

Les modèles d’affichage fournissent une alternative pour empêcher la sur-publication.

Le modèle d’application est souvent appelé modèle de domaine. En règle générale, le modèle de domaine contient toutes les propriétés requises par l’entité correspondante dans la base de données. Le modèle de vue contient uniquement les propriétés nécessaires à l’interface utilisateur pour laquelle il est utilisé (par exemple, la page Create).

En plus du modèle d’affichage, certaines applications utilisent un modèle de liaison ou d’entrée pour transmettre des données entre la classe de modèles de pages de Pages Razor et le navigateur.

Considérez le modèle d’affichage Student suivant :

using System;

namespace ContosoUniversity.Models
{
    public class StudentVM
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

Le code suivant utilise le modèle d’affichage StudentVM pour créer un étudiant :

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

La méthode SetValues définit les valeurs de cet objet en lisant les valeurs d’un autre objet PropertyValues. SetValues utilise la correspondance de nom de propriété. Le type de modèle d’affichage ne doit pas nécessairement être lié au type de modèle. Il doit simplement avoir des propriétés qui correspondent.

L’utilisation de StudentVM exige que Create.cshtml soit mis à jour pour utiliser StudentVM plutôt que Student.

Mettre à jour la page Edit

Dans Pages/Students/Edit.cshtml.cs, remplacez les méthodes OnGetAsync et OnPostAsync par le code suivant.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

    if (studentToUpdate == null)
    {
        return NotFound();
    }

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Les modifications de code sont semblables à celles de la page Create, à quelques exceptions près :

  • FirstOrDefaultAsync a été remplacé par FindAsync. Lorsque les données associées incluses ne sont pas nécessaires, FindAsync est plus efficace.
  • OnPostAsync contient un paramètre id.
  • Plutôt que de créer un étudiant vide, l’étudiant actuel est récupéré à partir de la base de données.

Exécutez l’application et testez-la en créant et modifiant un étudiant.

États des entités

Le contexte de base de données effectue un suivi pour déterminer si les entités en mémoire sont synchronisées avec les lignes correspondantes de la base de données. Ces informations de suivi déterminent ce qui se passe quand SaveChangesAsync est appelé. Par exemple, quand une nouvelle entité est passée à la méthode AddAsync, l’état de cette entité prend la valeur Added. Quand SaveChangesAsync est appelé, le contexte de base de données émet une commande SQL INSERT.

Une entité peut être dans l’un des états suivants :

  • Added : L’entité n’existe pas encore dans la base de données. La méthode SaveChanges émet une instruction INSERT.

  • Unchanged : Aucune modification ne doit être enregistrée avec cette entité. Une entité est dans cet état quand elle est lue à partir de la base de données.

  • Modified : Tout ou une partie des valeurs de propriété de l’entité ont été modifiées. La méthode SaveChanges émet une instruction UPDATE.

  • Deleted : L’entité a été marquée pour suppression. La méthode SaveChanges émet une instruction DELETE.

  • Detached : L’entité n’est pas suivie par le contexte de base de données.

Dans une application de bureau, les changements d’état sont généralement définis automatiquement. Une entité est lue, des modifications sont apportées et l’état d’entité passe automatiquement à Modified. L’appel de SaveChanges génère une instruction SQL UPDATE qui met à jour uniquement les propriétés modifiées.

Dans une application web, le DbContext qui lit une entité et affiche les données est supprimé après le rendu d’une page. Quand la méthode OnPostAsync d’une page est appelée, une nouvelle requête web est faite avec une nouvelle instance du DbContext. La relecture de l’entité dans ce nouveau contexte simule le traitement de bureau.

Mettre à jour la page Delete

Dans cette section, vous allez implémenter un message d’erreur personnalisé quand l’appel à SaveChanges échoue.

Remplacez le code dans Pages/Students/Delete.cshtml.cs par le code suivant. Les modifications (autres que le nettoyage des instructions using) sont mises en surbrillance.

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = "Delete failed. Try again";
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException /* ex */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

Le code précédent ajoute le paramètre facultatif saveChangesError à la signature de méthode OnGetAsync. saveChangesError indique si la méthode a été appelée après un échec de suppression de l’objet Student. L’opération de suppression peut échouer en raison de problèmes réseau temporaires. Vous avez plus de chances de rencontrer des erreurs réseau temporaires quand la base de données est dans le cloud. Le paramètre saveChangesError a la valeur false quand la méthode OnGetAsync de la page Delete est appelée à partir de l’interface utilisateur. Quand OnGetAsync est appelée par OnPostAsync (car l’opération de suppression a échoué), le paramètre saveChangesError a la valeur true.

La méthode OnPostAsync récupère l’entité sélectionnée, puis appelle la méthode Remove pour définir l’état de l’entité sur Deleted. Lorsque SaveChanges est appelée, une commande SQL DELETE est générée. Si Remove échoue :

  • L’exception de la base de données est interceptée.
  • La méthode OnGetAsync de la page Supprimer est appelée avec saveChangesError=true.

Ajoutez un message d’erreur à la page Supprimer Razor (Pages/Students/Delete.cshtml) :

@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Exécutez l’application et supprimez un étudiant pour tester la page Delete.

Étapes suivantes