Parte 2 — CRUD pe API
Pasul 4 — ArticlesApiController — GET
Controllers/Api/ArticlesApiController.cs — fișier nou
using Lab08.DTOs;
using Lab08.Mappings;
using Lab08.Models;
using Lab08.Services;
using Microsoft.AspNetCore.Mvc;
namespace Lab08.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class ArticlesApiController : ControllerBase
{
private readonly IArticleService _articleService;
public ArticlesApiController(IArticleService articleService)
{
_articleService = articleService;
}
// GET: /api/articlesapi
[HttpGet]
[ProducesResponseType(typeof(List<ArticleDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<ArticleDto>>> GetAll(CancellationToken cancellationToken)
{
var articles = await _articleService.GetAllAsync(cancellationToken);
return Ok(articles.ToDtoList());
}
// GET: /api/articlesapi/5
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ArticleDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ArticleDto>> GetById(int id, CancellationToken cancellationToken)
{
var article = await _articleService.GetByIdAsync(id, cancellationToken);
if (article == null)
return NotFound();
return Ok(article.ToDto());
}
}
Ce e de observat:
[ApiController]— activează validare automată a modelului (400 Bad RequestdacăModelStatee invalid), binding automat din body și erori de validare standardizate.[Route("api/[controller]")]— token-ul[controller]se înlocuiește cu numele clasei fără sufixulController. Aici →api/articlesapi.- Constrângerea
{id:int}— dacă cineva trimite/api/articlesapi/abc, ruta nu se potrivește și primește404direct, fără să ajungă în acțiune. ActionResult<ArticleDto>— permite atâtreturn Ok(dto)cât șireturn NotFound()în aceeași semnătură.IArticleServicee refolosit din Lab 6 — nu rescriem logica. Aceasta e valoarea Service Layer: aceeași logică servește și MVC, și API.[ProducesResponseType]— metadata pentru Swagger: spune ce status codes returnează metoda și cu ce schema de body.
Întrebare: Care e diferența între REST și RPC? Endpoint-urile noastre sunt RESTful?
RESTful înseamnă resurse identificate prin URI, verbe HTTP care exprimă operația (GET/POST/PUT/DELETE), stateless.
/api/articlesapi/5cu GET = „citește articolul cu id 5" — RESTful./api/articlesapi/deleteArticle?id=5cu POST ar fi RPC — URI-ul conține verbul.
Pasul 5 — CRUD complet pe API
Adăugați în același ArticlesApiController:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
// ... în corpul clasei ArticlesApiController ...
// POST: /api/articlesapi
[HttpPost]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(ArticleDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<ArticleDto>> Create(
CreateArticleDto dto,
CancellationToken cancellationToken)
{
var article = dto.ToEntity();
article.AuthorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
await _articleService.AddAsync(article, cancellationToken);
// Reîncarcă articolul cu Category și Author populate, pentru DTO-ul de răspuns
var created = await _articleService.GetByIdAsync(article.Id, cancellationToken);
return CreatedAtAction(nameof(GetById), new { id = article.Id }, created!.ToDto());
}
// PUT: /api/articlesapi/5
[HttpPut("{id:int}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(
int id,
UpdateArticleDto dto,
CancellationToken cancellationToken)
{
var article = await _articleService.GetByIdAsync(id, cancellationToken);
if (article == null)
return NotFound();
if (!IsOwnerOrAdmin(article))
return Forbid();
dto.ApplyTo(article);
await _articleService.UpdateAsync(article, cancellationToken);
return NoContent();
}
// DELETE: /api/articlesapi/5
[HttpDelete("{id:int}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
var article = await _articleService.GetByIdAsync(id, cancellationToken);
if (article == null)
return NotFound();
if (!IsOwnerOrAdmin(article))
return Forbid();
await _articleService.DeleteAsync(id, cancellationToken);
return NoContent();
}
private bool IsOwnerOrAdmin(Article article)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
return article.AuthorId == userId || User.IsInRole("Admin");
}
De ce
AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme? Identity (din Lab 7) a setat Cookies ca schema default de autentificare. Un[Authorize]simplu ar cere cookie — dar API-ul trebuie să accepte token JWT în header. Atributul spune explicit „folosește schema JWT Bearer pentru această acțiune". Schema în sine o configurăm înProgram.csla Pasul 7 — până atunci codul compilează, dar dacă apelați POST/PUT/DELETE înainte să terminați Pasul 7, cererea eșuează fiindcă schema încă nu e înregistrată.
Status codes — recapitulare
| Cod | Metoda helper | Când |
|---|---|---|
200 OK |
Ok(value) |
GET reușit, cu body |
201 Created |
CreatedAtAction(action, routeValues, value) |
POST reușit — Location header pointează la resursa nouă |
204 No Content |
NoContent() |
PUT/DELETE reușit, fără body |
400 Bad Request |
BadRequest(errors) |
Validare eșuată (automat cu [ApiController]) |
401 Unauthorized |
automat — JWT middleware | Utilizatorul nu e autentificat |
403 Forbidden |
Forbid() |
Autentificat, dar fără drept (nu e owner, nu e admin) |
404 Not Found |
NotFound() |
Resursa nu există |
401 vs 403: 401 înseamnă „cine ești?" — trimite token. 403 înseamnă „știu cine ești, dar nu ai voie". Nu le inversați — clienții au logică diferită pentru fiecare.
De ce CreatedAtAction și nu Ok?
CreatedAtActionreturnează automat status201și adaugă un headerLocation: /api/articlesapi/42către resursa nou creată. Clientul știe imediat URL-ul la care poate să o citească. Este pattern RESTful standard pentru POST.