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 :
# Vérifier .NET version
dotnet --version
# Vérifier Node.js
node --version
npm --version
# Vérifier Angular CLI
ng version
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)
- 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