Back-end angularforall.com

- CRUD C# ASP.NET Core : Angular & REST API

Csharp Aspnet-Core Angular Crud Rest-Api Sql-Server Entity-Framework Full-Stack Httpclient Jwt
CRUD C# ASP.NET Core : Angular & REST API

Construisez une application CRUD complète avec C#, ASP.NET Core, Entity Framework et Angular : API REST, SQL Server, HttpClient et déploiement.

Prérequis et installation

Avant de commencer, assurez-vous d'avoir installé les outils suivants sur votre système.

Environnement requis

  • .NET 8 SDK ou supérieur (télécharger depuis dotnet.microsoft.com)
  • Visual Studio 2022 Community (IDE recommandé) ou VS Code avec extensions C#
  • SQL Server Express 2019+ ou SQL Server LocalDB
  • Node.js 18+ et Angular CLI 17+
  • Git pour le contrôle de version

Vérifiez votre installation :

Structure du projet backend

Une architecture bien organisée facilite la maintenance et la scalabilité. Voici la structure recommandée pour le projet ASP.NET Core.

Créer le projet ASP.NET Core


# Créer un nouveau projet Web API
dotnet new webapi -n TodoApi -o TodoApi
cd TodoApi

# Ajouter les packages NuGet nécessaires
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.IdentityModel.Tokens
dotnet add package System.IdentityModel.Tokens.Jwt

Arborescence du projet


TodoApi/
├── Controllers/           # Contrôleurs API
│   └── TodoController.cs
├── Models/               # Modèles de données
│   ├── Todo.cs
│   ├── CreateTodoDto.cs
│   └── UpdateTodoDto.cs
├── Data/                 # Contexte de base de données
│   ├── AppDbContext.cs
│   └── Migrations/
├── Services/             # Logique métier
│   ├── ITodoService.cs
│   └── TodoService.cs
├── Middleware/           # Middleware personnalisé
│   └── ErrorHandlingMiddleware.cs
├── Configuration/        # Configuration
│   └── JwtSettings.cs
├── Program.cs           # Configuration de l'application
├── appsettings.json     # Configuration environnement
└── appsettings.Production.json

Configuration Entity Framework Core

Entity Framework Core (EF Core) est un ORM puissant qui simplifie l'accès aux données avec SQL Server.

Créer le modèle Todo

Dans Models/Todo.cs :


using System;
using System.ComponentModel.DataAnnotations;

namespace TodoApi.Models
{
    // Modèle Todo représentant une tâche
    public class Todo
    {
        // Clé primaire auto-incrémentée
        [Key]
        public int Id { get; set; }

        // Titre obligatoire (max 200 caractères)
        [Required(ErrorMessage = "Le titre est obligatoire")]
        [StringLength(200, MinimumLength = 3,
            ErrorMessage = "Le titre doit contenir entre 3 et 200 caractères")]
        public string Title { get; set; }

        // Description (optionnelle)
        [StringLength(1000)]
        public string Description { get; set; }

        // Statut de complétude
        public bool IsCompleted { get; set; } = false;

        // Priorité (1=Basse, 2=Moyenne, 3=Haute)
        [Range(1, 3, ErrorMessage = "La priorité doit être entre 1 et 3")]
        public int Priority { get; set; } = 2;

        // Dates de création et modification
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;

        // Identifiant de l'utilisateur (pour multi-utilisateur)
        public int UserId { get; set; }
    }
}

Créer le DbContext

Dans Data/AppDbContext.cs :


using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Data
{
    // Contexte de base de données pour l'application Todo
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options)
            : base(options)
        {
        }

        // Tables de la base de données
        public DbSet<Todo> Todos { get; set; }

        // Configuration du modèle (conventions, contraintes, etc.)
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Configuration de la table Todo
            modelBuilder.Entity<Todo>()
                .HasKey(t => t.Id);

            // Index sur UserId pour optimiser les requêtes
            modelBuilder.Entity<Todo>()
                .HasIndex(t => t.UserId);

            // Indexer sur CreatedAt pour les tris chronologiques
            modelBuilder.Entity<Todo>()
                .HasIndex(t => t.CreatedAt);

            // Valeurs par défaut
            modelBuilder.Entity<Todo>()
                .Property(t => t.IsCompleted)
                .HasDefaultValue(false);
        }
    }
}

Configurer la chaîne de connexion

Dans appsettings.json :


{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TodoDb;Trusted_Connection=true;TrustServerCertificate=true;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256",
    "Issuer": "TodoApi",
    "Audience": "TodoApiUsers",
    "ExpirationMinutes": 60
  }
}

Créer l'API REST avec ASP.NET Core

L'API REST expose les opérations CRUD via des endpoints HTTP standard. C# avec ASP.NET Core offre un cadre robuste et performant.

Configurer Program.cs


using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using TodoApi.Data;

var builder = WebApplication.CreateBuilder(args);

// Ajouter les services à l'injection de dépendances
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configurer la connexion à SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Configurer l'authentification JWT
var jwtSettings = builder.Configuration.GetSection("Jwt");
var secretKey = Encoding.ASCII.GetBytes(jwtSettings["SecretKey"]);

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(secretKey),
            ValidateIssuer = true,
            ValidIssuer = jwtSettings["Issuer"],
            ValidateAudience = true,
            ValidAudience = jwtSettings["Audience"],
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    });

// Configurer CORS pour permettre les requêtes Angular
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAngular", policy =>
    {
        policy.WithOrigins("http://localhost:4200", "https://yourdomain.com")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

var app = builder.Build();

// Configure le pipeline HTTP
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("AllowAngular");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

Contrôleur API CRUD complet

Dans Controllers/TodoController.cs - 150+ lignes avec tous les endpoints :


using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Data;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    // Contrôleur API pour la gestion des todos (CRUD)
    [ApiController]
    [Route("api/[controller]")]
    [Authorize] // Protéger tous les endpoints par JWT
    public class TodoController : ControllerBase
    {
        private readonly AppDbContext _context;
        private readonly ILogger<TodoController> _logger;

        public TodoController(AppDbContext context, ILogger<TodoController> logger)
        {
            _context = context;
            _logger = logger;
        }

        // GET: api/todo - Récupérer tous les todos
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Todo>>> GetTodos(
            [FromQuery] int? priority = null,
            [FromQuery] bool? isCompleted = null)
        {
            try
            {
                var userId = int.Parse(User.FindFirst("UserId")?.Value ?? "0");
                var query = _context.Todos.Where(t => t.UserId == userId).AsQueryable();

                if (priority.HasValue)
                    query = query.Where(t => t.Priority == priority.Value);

                if (isCompleted.HasValue)
                    query = query.Where(t => t.IsCompleted == isCompleted.Value);

                var todos = await query.OrderByDescending(t => t.CreatedAt).ToListAsync();
                return Ok(todos);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Erreur: {ex.Message}");
                return StatusCode(500, "Erreur serveur");
            }
        }

        // GET: api/todo/{id} - Récupérer un todo spécifique
        [HttpGet("{id}")]
        public async Task<ActionResult<Todo>> GetTodo(int id)
        {
            var userId = int.Parse(User.FindFirst("UserId")?.Value ?? "0");
            var todo = await _context.Todos
                .FirstOrDefaultAsync(t => t.Id == id && t.UserId == userId);

            if (todo == null)
                return NotFound(new { message = "Todo non trouvé" });

            return Ok(todo);
        }

        // POST: api/todo - Créer un nouveau todo
        [HttpPost]
        public async Task<ActionResult<Todo>> CreateTodo([FromBody] CreateTodoDto dto)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var userId = int.Parse(User.FindFirst("UserId")?.Value ?? "0");
            var todo = new Todo
            {
                Title = dto.Title.Trim(),
                Description = dto.Description?.Trim(),
                Priority = dto.Priority,
                UserId = userId,
                CreatedAt = DateTime.UtcNow,
                UpdatedAt = DateTime.UtcNow
            };

            _context.Todos.Add(todo);
            await _context.SaveChangesAsync();

            return CreatedAtAction(nameof(GetTodo), new { id = todo.Id }, todo);
        }

        // PUT: api/todo/{id} - Mettre à jour un todo
        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateTodo(int id, [FromBody] UpdateTodoDto dto)
        {
            var userId = int.Parse(User.FindFirst("UserId")?.Value ?? "0");
            var todo = await _context.Todos
                .FirstOrDefaultAsync(t => t.Id == id && t.UserId == userId);

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

            if (!string.IsNullOrEmpty(dto.Title))
                todo.Title = dto.Title.Trim();

            if (dto.Description != null)
                todo.Description = dto.Description.Trim();

            if (dto.Priority.HasValue)
                todo.Priority = dto.Priority.Value;

            if (dto.IsCompleted.HasValue)
                todo.IsCompleted = dto.IsCompleted.Value;

            todo.UpdatedAt = DateTime.UtcNow;
            _context.Todos.Update(todo);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        // DELETE: api/todo/{id} - Supprimer un todo
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteTodo(int id)
        {
            var userId = int.Parse(User.FindFirst("UserId")?.Value ?? "0");
            var todo = await _context.Todos
                .FirstOrDefaultAsync(t => t.Id == id && t.UserId == userId);

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

            _context.Todos.Remove(todo);
            await _context.SaveChangesAsync();

            return NoContent();
        }
    }
}

Services Angular et HttpClient

Angular offre HttpClient pour communiquer avec l'API C#. Les services encapsulent la logique d'accès aux données.

Service TodoService complet

Dans src/app/services/todo.service.ts :


import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';

export interface Todo {
  id: number;
  title: string;
  description?: string;
  isCompleted: boolean;
  priority: 1 | 2 | 3;
  createdAt: Date;
  updatedAt: Date;
  userId: number;
}

export interface CreateTodoDto {
  title: string;
  description?: string;
  priority?: 1 | 2 | 3;
}

@Injectable({
  providedIn: 'root'
})
export class TodoService {
  private apiUrl = '/api/todo';

  constructor(private http: HttpClient) { }

  getTodos(priority?: number, isCompleted?: boolean): Observable<Todo[]> {
    let params = new HttpParams();
    if (priority !== undefined) params = params.set('priority', priority.toString());
    if (isCompleted !== undefined) params = params.set('isCompleted', isCompleted.toString());

    return this.http.get<Todo[]>(this.apiUrl, { params })
      .pipe(
        tap(todos => console.log(`Récupéré ${todos.length} todos`)),
        catchError(error => {
          console.error('Erreur:', error);
          throw error;
        })
      );
  }

  getTodoById(id: number): Observable<Todo> {
    return this.http.get<Todo>(`${this.apiUrl}/${id}`);
  }

  createTodo(todo: CreateTodoDto): Observable<Todo> {
    return this.http.post<Todo>(this.apiUrl, todo);
  }

  updateTodo(id: number, todo: Partial<Todo>): Observable<void> {
    return this.http.put<void>(`${this.apiUrl}/${id}`, todo);
  }

  deleteTodo(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }

  completeTodo(id: number): Observable<void> {
    return this.updateTodo(id, { isCompleted: true });
  }
}

Composants Angular CRUD

Les composants affichent les données et gèrent les interactions utilisateur.

Composant Liste (TypeScript)


import { Component, OnInit, OnDestroy } from '@angular/core';
import { TodoService, Todo } from '../../services/todo.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html'
})
export class TodoListComponent implements OnInit, OnDestroy {
  todos: Todo[] = [];
  loading = false;
  error: string | null = null;
  private destroy$ = new Subject<void>();

  constructor(private todoService: TodoService) { }

  ngOnInit(): void {
    this.loadTodos();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  loadTodos(): void {
    this.loading = true;
    this.todoService.getTodos()
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: (data) => {
          this.todos = data;
          this.loading = false;
        },
        error: (err) => {
          this.error = err.message;
          this.loading = false;
        }
      });
  }

  deleteTodo(id: number): void {
    if (!confirm('Confirmer la suppression ?')) return;

    this.todoService.deleteTodo(id)
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: () => {
          this.todos = this.todos.filter(t => t.id !== id);
        },
        error: (err) => this.error = err.message
      });
  }

  toggleComplete(todo: Todo): void {
    this.todoService.updateTodo(todo.id, { isCompleted: !todo.isCompleted })
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: () => {
          todo.isCompleted = !todo.isCompleted;
        },
        error: (err) => this.error = err.message
      });
  }
}

Template HTML


<div class="container mt-4">
  <h2>Mes Todos</h2>

  <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
  <div *ngIf="loading" class="spinner-border"></div>

  <div class="list-group" *ngIf="!loading">
    <div *ngFor="let todo of todos" class="list-group-item">
      <input type="checkbox" [checked]="todo.isCompleted"
             (change)="toggleComplete(todo)">
      <span [class.text-decoration-line-through]="todo.isCompleted">
        {{ todo.title }}
      </span>
      <button class="btn btn-sm btn-danger float-end"
              (click)="deleteTodo(todo.id)">
        Supprimer
      </button>
    </div>
  </div>
</div>

Validation côté front et back

DTOs C# avec validation


public class CreateTodoDto
{
    [Required(ErrorMessage = "Le titre est requis")]
    [StringLength(200, MinimumLength = 3)]
    public string Title { get; set; }

    [StringLength(1000)]
    public string Description { get; set; }

    [Range(1, 3)]
    public int Priority { get; set; } = 2;
}

Validation Angular Reactive Forms


import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TodoService } from '../../services/todo.service';

@Component({
  selector: 'app-todo-form',
  templateUrl: './todo-form.component.html'
})
export class TodoFormComponent {
  form: FormGroup;
  submitted = false;

  constructor(private fb: FormBuilder, private todoService: TodoService) {
    this.form = this.fb.group({
      title: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(200)]],
      description: ['', Validators.maxLength(1000)],
      priority: [2, [Validators.required, Validators.min(1), Validators.max(3)]]
    });
  }

  onSubmit(): void {
    this.submitted = true;
    if (this.form.invalid) return;

    this.todoService.createTodo(this.form.value).subscribe({
      next: (todo) => {
        alert('Todo créé avec succès !');
        this.form.reset();
        this.submitted = false;
      },
      error: (err) => alert('Erreur: ' + err.message)
    });
  }

  get f() { return this.form.controls; }
}

Authentification JWT

JWT sécurise l'API en s'assurant que seuls les utilisateurs authentifiés accèdent aux données.

Service Auth C#


using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

public class AuthService
{
    private readonly IConfiguration _config;

    public AuthService(IConfiguration config) => _config = config;

    public string GenerateToken(int userId, string email)
    {
        var jwtSettings = _config.GetSection("Jwt");
        var secretKey = new SymmetricSecurityKey(
            Encoding.ASCII.GetBytes(jwtSettings["SecretKey"]));
        var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

        var claims = new[]
        {
            new Claim("UserId", userId.ToString()),
            new Claim(ClaimTypes.Email, email)
        };

        var token = new JwtSecurityToken(
            issuer: jwtSettings["Issuer"],
            audience: jwtSettings["Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(60),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Intercepteur HTTP Angular


import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('token');

    if (token) {
      req = req.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    return next.handle(req);
  }
}

Gestion des erreurs et logs

Middleware d'erreur C#


public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlingMiddleware> _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Erreur: {ex}");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new { error = "Erreur serveur" });
        }
    }
}

Déploiement en production

Publier l'API C#


# Build production
dotnet publish -c Release -o ./publish

# Sur le serveur
cd publish
./TodoApi

Build Angular


ng build --configuration production
# Résultat dans dist/TodoAngularApp/

Configuration Nginx


server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # Angular SPA
    location / {
        root /var/www/todo-angular;
        try_files $uri $uri/ /index.html;
    }

    # API C#
    location /api/ {
        proxy_pass http://localhost:5000;
        proxy_set_header Host $host;
    }

    # Assets statiques (1 an de cache)
    location ~* \.(js|css|png|jpg|gif|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Docker Compose


version: '3.8'

services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:latest
    environment:
      ACCEPT_EULA: 'Y'
      SA_PASSWORD: 'YourPassword123!'
    ports:
      - "1433:1433"

  api:
    build: .
    ports:
      - "5000:5000"
    environment:
      ConnectionStrings__DefaultConnection: "Server=sqlserver;Database=TodoDb;User=sa;Password=YourPassword123!;"
    depends_on:
      - sqlserver

  web:
    build: ./TodoAngularApp
    ports:
      - "80:80"
    depends_on:
      - api

Conclusion et checklist

✅ Checklist production

  • Variables d'environnement sécurisées
  • HTTPS avec certificats SSL
  • CORS configuré (whitelist domaines)
  • Authentification JWT testée
  • Validation front et back implémentée
  • Gestion d'erreurs complète
  • Tests unitaires passés
  • Backup BDD automatique
  • Monitoring et alertes
  • Documentation API (Swagger)
🎯 Stack récapitulé :
  • Backend: C# + ASP.NET Core 8 + EF Core + SQL Server
  • Frontend: Angular 17+ + TypeScript + Bootstrap 5
  • API: REST + JWT + Validation globale
  • Infrastructure: Docker + Nginx + HTTPS

Partager