---
title: 'Créer une application web avec ASP.NET Razor Pages et EF Core'
description: 'Je vous explique comment créer une application web avec ASP.NET Razor Pages et Entity Framework Core avec une mise en application concrète.'
pubDate: 2022-05-08
family: 'dotnet-csharp'
tags: []
heroImage: /images/creer-une-application-web-avec-asp-net-razor-pages-et-ef-core.webp
videoUrl: 'https://youtu.be/QjQ5_Lw5urc'
---

## À propos de ce tutoriel

Je vous explique comment créer une application web avec ASP.NET Razor Pages et Entity Framework Core avec une mise en application concrète.

## 1\. Créer les modèles de données

### Models/Fruit.cs

```
public class Fruit
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    [Display(Name = "Nom")]
    public string Name { get; set; }

    public string Description { get; set; } = string.Empty;

    public virtual Image Image { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    [Required]
    [Display(Name = "Prix")]
    public decimal Price { get; set; } = decimal.One;
}
```

### Models/Image.cs

```
public class Image
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Path { get; set; }

    [NotMapped]
    [DisplayName("Image")]
    public IFormFile File { get; set; }

    public int FruitId { get; set; }
    public Fruit Fruit { get; set; }
}
```

## 2\. Générer automatiquement un modèle de CRUD

- Ajouter le package Microsoft.EntityFrameworkCore.Design
- Créer la structure de dossier suivante : Areas/Fruits/Pages
- Faites un clique-droit sur Pages => Nouvel élément généré automatiquement
- Choisissez **Page Razor** => Page Razor avec Entity Framework (CRUD)
- Renseignez la classe de modèle (Fruit), la classe de contexte de donnés et utilisez une page de disposition

## 3\. Créer le schéma de la base de données

### a) Modifier le contexte de la base de données

```
public DbSet<Fruit> Fruits { get; set; }
public DbSet<Image> Images { get; set; }
```

### b) Exécutez les commandes suivantes

```
Add-Migration InitialCreate
Update-Database
```

La première commande "Add-Migration" permet de générer le script SQL nécessaire à la création de nos entités en base de données.

La seconde applique le script SQL et crée les tables correspondantes.

## 4\. La liste des fruits

```
@page
@model MyFruits.Areas.Fruits.Pages.IndexModel

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

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Fruit[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Fruit[0].Description)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Fruit[0].Image.File)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Fruit[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Fruit)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Description)
                </td>
                <td>
                    @if (null != item.Image)
                    {
                        <img src="@Url.Content(item.Image.Path)" width="128" height="128">
                    }
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

```
#nullable disable
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyFruits.Data;
using MyFruits.Models;

namespace MyFruits.Areas.Fruits.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ApplicationDbContext ctx;

        public IndexModel(ApplicationDbContext ctx)
        {
            this.ctx = ctx;
        }

        public IList<Fruit> Fruit { get;set; }

        public async Task OnGetAsync()
        {
            Fruit = await ctx.Fruits.Include(f => f.Image).ToListAsync();
        }
    }
}
```

## 5\. Ajout d'une option globale pour le dossier d'uploads

```
{
	"Path": {
	    "FruitsImages":  "fruits/images"
	  }
}
```

```
public class PathOptions
{
    public const string Path = "Path";

    public string FruitsImages{ get; set; } = string.Empty;
}
```

## 6\. Ajout de l'upload d'images

```
public class PathService
{
    private readonly IConfiguration configuration;
    private readonly IWebHostEnvironment env;

    public PathService(IWebHostEnvironment env, IConfiguration configuration)
    {
        this.configuration = configuration;
        this.env = env;
    }

    public string GetUploadsPath(string filename = null, bool withWebRootPath = true)
    {
        var pathOptions = new PathOptions();

        configuration.GetSection(PathOptions.Path).Bind(pathOptions);

        var uploadsPath = pathOptions.FruitsImages;

        if (null != filename)
            uploadsPath = Path.Combine(uploadsPath, filename);

        return withWebRootPath ? Path.Combine(env.WebRootPath, uploadsPath) : uploadsPath;
    }
}
```

```
public class ImageService
{
    private readonly PathService pathService;

    public ImageService(PathService pathService)
    {
        this.pathService = pathService;
    }

    public async Task<Image> UploadAsync(Image image)
    {
        var uploadsPath = pathService.GetUploadsPath();

        var imageFile = image.File;
        var imageFileName = GetRandomFileName(imageFile.FileName);
        var imageUploadPath = Path.Combine(uploadsPath, imageFileName);

        using (var fs = new FileStream(imageUploadPath, FileMode.Create))
        {
            await imageFile.CopyToAsync(fs);
        }

        image.Name = imageFile.FileName;
        image.Path = pathService.GetUploadsPath(imageFileName, withWebRootPath: false);

        return image;
    }

    public void DeleteFile(Image? image)
    {
        if (image == null)
            return;

        var imagePath = pathService.GetUploadsPath(Path.GetFileName(image.Path));

        if (File.Exists(imagePath))
            File.Delete(imagePath);
    }

    private static string GetRandomFileName(string path)
    {
        return Guid.NewGuid() + Path.GetExtension(path);
    }
}
```

## 7\. La page de création d'un fruit

```
@model TutoWebApp.Models.Fruit

@{
    ViewData.TemplateInfo.HtmlFieldPrefix = "Fruit";
}

<div class="form-group">
    <div class="mb-2">
        <label asp-for="Name" class="form-label"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
</div>

<div class="form-group">
    <div class="mb-2">
        <label asp-for="Description" class="form-label"></label>
        <textarea asp-for="Description" class="form-control"></textarea>
        <span asp-validation-for="Description" class="text-danger"></span>
    </div>
</div>

<div class="form-group">
    <div class="mb-2">
        <label asp-for="Image.File" class="form-label"></label>

        @if (null != @Model.Image)
        {
            <span>(<a href="@Url.Content($"~/{@Model.Image.Path}")" target="_blank">@Model.Image.Name</a>)</span>
        }

        <input class="form-control" type="file" asp-for="Image.File" />
        <span asp-validation-for="Image.File" class="text-danger"></span>
    </div>
</div>

<div class="form-group">
    <div class="mb-2">
        <label asp-for="Price" class="form-label"></label>
        <input asp-for="Price" class="form-control" />
        <span asp-validation-for="Price" class="text-danger"></span>
    </div>
</div>
```

```
@page
@model TutoWebApp.Pages.Fruits.CreateModel

@{
    ViewData["Title"] = "Ajouter un fruit";
}

<h2>Ajouter un fruit</h2>

<hr />

<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">

            <partial name="~/Pages/Shared/EditorTemplates/Fruit.cshtml" model="@Model.Fruit" />

            <div class="form-group mt-2">
                <input type="submit" value="Créer" class="btn btn-primary" />
                 <a class="btn btn-success" asp-page="./Index">Retour à la liste</a>
            </div>

        </form>
    </div>
</div>
```

```
public class CreateModel : PageModel
{
    private readonly TutoWebAppContext ctx;
    private readonly ImageService imageService;

    public CreateModel(TutoWebAppContext context, ImageService imageService)
    {
        ctx = context;
        this.imageService = imageService;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Fruit Fruit { get; set; } = new();

    public async Task<IActionResult> OnPostAsync()
    {
        var emptyFruit = new Fruit();

        if (null != Fruit.Image)
            emptyFruit.Image = await imageService.UploadAsync(Fruit.Image);

        if (await TryUpdateModelAsync(emptyFruit, "fruit", f => f.Name, f => f.Description, f => f.Price))
        {
            ctx.Fruits.Add(emptyFruit);
            await ctx.SaveChangesAsync();
            return RedirectToPage("./Index");
        }

        return Page();
    }
}
```

## 8\. La page d’édition d’un fruit

```
@page
@model MyFruits.Areas.Fruits.Pages.EditModel

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

<h1>Edit</h1>

<h4>Fruit</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <partial name="~/Pages/Shared/EditorTemplates/Fruit.cshtml" model="@Model.Fruit" />

            <div class="form-group mt-2">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>

        </form>
    </div>
</div>

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

```
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MyFruits.Data;
using MyFruits.Models;
using MyFruits.Services;

namespace MyFruits.Areas.Fruits.Pages
{
    public class EditModel : PageModel
    {
        private readonly ApplicationDbContext ctx;
        private readonly ImageService imageService;

        public EditModel(ApplicationDbContext ctx, ImageService imageService)
        {
            this.ctx = ctx;
            this.imageService = imageService;
        }

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

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

            Fruit = await ctx.Fruits
                .Include(f => f.Image)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.Id == id);

            if (Fruit == null)
                return NotFound();

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            var fruitToUpdate = await ctx.Fruits
                .Include(f => f.Image)
                .FirstOrDefaultAsync(f => f.Id == id);

            if (fruitToUpdate == null)
                return NotFound();

            var uploadedImage = Fruit.Image;

            if (null != uploadedImage)
            {
                uploadedImage = await imageService.UploadAsync(uploadedImage);

                if (fruitToUpdate.Image != null)
                {
                    imageService.DeleteUploadedFile(fruitToUpdate.Image);
                    fruitToUpdate.Image.Name = uploadedImage.Name;
                    fruitToUpdate.Image.Path = uploadedImage.Path;
                }
                else
                    fruitToUpdate.Image = uploadedImage;
            }

            if (await TryUpdateModelAsync(fruitToUpdate, "fruit", f => f.Name, f => f.Description, f => f.Price))
            {
                await ctx.SaveChangesAsync();

                return RedirectToPage("./Index");
            }

            return Page();
        }

        private bool FruitExists(int id)
        {
            return ctx.Fruits.Any(e => e.Id == id);
        }
    }
}
```

## 9\. La page de suppression d'un fruit

```
@page
@model MyFruits.Areas.Fruits.Pages.DeleteModel

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

<h1>Delete</h1>

<h3>Are you sure you want to delete this?</h3>

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

<div>
    <h4>Fruit</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Fruit.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Fruit.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Fruit.Description)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Fruit.Description)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Fruit.Price)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Fruit.Price)
        </dd>
    </dl>

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

```
#nullable disable
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyFruits.Data;
using MyFruits.Models;
using MyFruits.Services;

namespace MyFruits.Areas.Fruits.Pages
{
    public class DeleteModel : PageModel
    {
        private readonly ApplicationDbContext ctx;
        private ImageService imageService;

        public DeleteModel(ApplicationDbContext ctx, ImageService imageService)
        {
            this.ctx = ctx;
            this.imageService = imageService;
        }

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

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

            Fruit = await ctx.Fruits
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.Id == id);

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

            if (hasErrorMessage.GetValueOrDefault())
            {
                ErrorMessage = $"Une erreur est survenue lors de la tentative de suppression de {Fruit.Name} ({Fruit.Id})";
            }

            return Page();
        }

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

            var fruitToDelete = await ctx.Fruits
                .Include(f => f.Image)
                .FirstOrDefaultAsync(f => f.Id == id);

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

            try
            {
                imageService.DeleteUploadedFile(fruitToDelete.Image);
                ctx.Fruits.Remove(fruitToDelete);
                await ctx.SaveChangesAsync();

                return RedirectToPage("./Index");
            }
            catch (Exception)
            {
                return RedirectToAction("./Delete", new { id, hasErrorMessage = true });
            }
        }
    }
}
```
