Controller
Trebuie clarificat în primul rând cum funcționează aplicația de backend în .NET. Când aplicația de tip WebApplication este pornită, se deschide un port și se așteaptă cererile HTTP. Cererea este parsată și transformată într-un context HTTP. Contextul este trimis către un pipeline de execuție care va apela rutinele adecvate pentru acea cerere și va întoarce răspunsul înapoi în acel pipeline. Fiecare pas executat în pipeline se numește middleware, iar o parte din acestea se pot defini de către dezvoltatori. Ultimul middleware executat la tratarea cererii apelează clase definite de dezvoltator de tip controller. În controller se specifică ce endpoint-uri/rute din API-ul serverului corespund la ce metode din acea clasă. O clasă controller este o clasă specială ai cărei metode publice sunt apelate la accesul rutelor corespunzătoare metodei, acestea moștenesc clasa ControllerBase. Pentru ca framework-ul să identifice controllerele și rutele, se decorează clasa și metodele cu atribute, clase ce extind clasa Attibute. De exemplu, [ApiController] care decorează o clasă controller specifică framework-ului că această clasă trebuie să fie folosită ca controller; [Route("api/[controller]")] pus pe clasă și [HttpGet("my-route")] pe metoda din controller specifică că atunci când se accesează ruta "/api/<nume_clasa_controller>/my-route" cu un HTTP GET se apelează acea metodă în cauză.
Decorarea claselor și metodelor cu atribute, sau în Java cu adnotări, pentru ca acestea să dobândească mai multe funcționalități, la runtime sau compiletime, este un caz de @OP (attribute-oriented programming).
În cereri HTTP, datele transmise către server pot fi specificate în mai multe locații din cerere care pot fi extrase și pasate automat ca parametri pentru metoda din controller corespunzătoare rutei. Aceste locații se specifică folosind atribute în fața parametrilor în următoarele moduri:
- Pentru parametri specificați într-o rută, ca de exemplu "api/{type}/user/{id:guid}", se poate vedea descrisă mai jos. Se poate vedea și că se pot pune constrângeri ca parametrul "id" să fie formatat ca fiind un Guid și să se întoarcă automat cod de BadRequest către client dacă șirul este în formatul greșit.
[HttpGet("api/{type}/user/{id:guid}")]
public Task<IActionResult> MyMethod([FromRoute] string type, [FromRoute] Guid id);
- Parametri de url/query sunt specificați cu [FromQuery].
- Campurile din header-ul cererii sunt extrase prin [FromHeader].
- Parametri din ruta sunt extrase prin [FromRoute] dacă acestea au fost declarate în șablonul de rută dintr-un atribut care specifică ruta.
- Pentru form-uri, campurile din form pot fi extrase prin [FromForm], un caz special este când un camp este un fișier, iar acesta poate fi extras într-un obiect de tip IFromFile sau IFormfileCollection.
- Body-ul cererii poate fi extras doar o singură dată și deserializat într-un singur obiect fie lăsând parametrul fără atribut sau cu atributul [FromBody]; doar metodele de POST și PUT acceptă body.
Rutele apelate din backend vor răspunde cu un obiect care va fi automat serializat într-un răspuns HTTP ca JSON dacă trebuie întors și un cod de status pentru răspuns. Pentru a seta codul de status și a împacheta răspunsul întors, se folosesc metodele din ControllerBase cum ar fi Ok(), BadRequest() sau Forbid(), sau mai explicit prin metoda StatusCode().
Folosiți codurile de status corespunzătoare pentru diferitele cazuri de succes sau eroare. Este o practică bună pentru a vă documenta API-ul, atât pentru persoane cât și pentru sistemele conectate la acesta, reducând ambiguități și prevenind cazuri de eroare pentru clienții API-ului.
Puteți urmări în Gitlab codul pentru controllere cu explicații și exemple de cum se pot folosi informațiile din cerere pentru acțiunile din backend. Modul de lucru cu un controller este în mare măsură foarte intuitiv. Pe lângă decorarea metodelor publice cu atribute, trebuie să fie injectate serviciile care implementează logica aplicației pentru a fi apelate aici.
Nu implementați logica direct în controller, fiecare componentă trebuie să îndeplinească o funcționalitate specifică. Dacă o componentă de tip repository doar lucrează cu spațiul de stocare a datelor, iar serviciile implementează logica aplicației, atunci o componentă de tip controller trebuie doar să trateze cererea și să împacheteze răspunsul, ocazional verificând rudimentar accesul utilizatorului pe rute pentru a preveni accesul nedorit. O măsură a codului bine scris este segregarea responsabilităților pe componente cât mai bine.
Mai jos aveți un exemplu pentru un controller care pe ruta "api/UserFile/Download/{id}" la metoda GET va trimite un fișier către client sau un răspuns de eroare cu corp și cod de status.
[ApiController]
[Route("api/[controller]/[action]")]
public class UserFileController : BaseController
{
private readonly IUserFileService _userFileService;
public UserFileController(IUserFileService userFileService)
{
_userFileService = userFileService;
}
[HttpGet("{id:guid}")]
[Produces(MediaTypeNames.Application.Octet, MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
public async Task<ActionResult<RequestResponse>> Download([FromRoute] Guid id)
{
var file = await _userFileService.GetFileDownload(id);
return file.Result != null ?
File(file.Result.Stream, MediaTypeNames.Application.Octet, file.Result.Name) :
NotFound(file.Error);
}
}
Operatii CRUD
Pentru o mai mare claritate, vom implementa operațiile CRUD (Create Read Update Delete) în fiecare controller. În general, pentru fiecare entitate din baza de date, sau cel puțin pentru un subset care să fie expus utilizatorului, se pot efectua operații de bază. Acestea sunt cele patru mari operații CRUD:
- Create - sunt operații de adăugare, de obicei sunt executate prin cereri de tip POST.
- Read - sunt operații de citire a obiectelor, de obicei sunt cereri de tip GET. Se pot citi un singur obiect sau mai multe. Dacă datele care se pot extrage sunt foarte multe, listele de obiecte trebuie să fie extrase paginat pentru a nu încărca atât serverul cât și clientul cu date în mod inutil.
- Update - sunt operații de modificare a datelor pe server, de obicei sunt cereri de tip PUT.
- Delete - sunt operații de ștergere, fie de ștergere completă a datelor sau doar de invalidare (soft delete), de obicei sunt cereri de tip DELETE.
Pentru transferul efectiv al datelor, nu se folosesc entități de bază de date în mod direct, ci se utilizează DTO-uri (Data Transfer Objects), proiectând entitățile către acestea din motive de securitate, performanță și pentru a preveni erori. DTO-urile sunt doar obiecte clasice care nu sunt gestionate de ORM și prin intermediul cărora se pot transfera informațiile din entități în diversele locuri din aplicație și către exterior.
Respectați pe cât posibil convenția asocierea acestor tipuri de operații cu metodele HTTP, este o bună practică pentru ca persoanele care vor folosi API-ul să-l înțeleagă mai ușor.
Swagger/OpenAPI Specifications
Avantajul de a folosi .NET este că orice proiect nou de web API este creat cu pachetul Swashbuckle care expune rute OpenAPI, sau cum se numește istoric Swagger. OpenAPI este un standard prin care un web API REST poate fi descris în format JSON sau YML. Avantajele folosirii OpenAPI sunt următoarele:
- Ușurința de a înțelege API-ul, informațiile expuse despre API incluzând rutele, metodele pe rute și tipul de obiecte schimbate între client și server.
- Automatizarea interconectării între clienți externi și API.
- UI de testare a interfetei Swagger actualizat automat.
- Posibilitatea de a folosi generatoare de cod pentru clienții ai API-ului.
- Servește ca documentație.