Un ghid minunat despre cum să construiți API-uri RESTful cu ASP.NET Core

Un ghid pas cu pas despre cum să implementați API-uri RESTful curate și de întreținut

Fotografie de Jefferson Santos, publicată la Unsplash

Prezentare generală

RESTful nu este un termen nou. Se referă la un stil arhitectural în care serviciile web primesc și trimit date din și către aplicațiile clientului. Scopul acestor aplicații este de a centraliza datele pe care le vor folosi diferite aplicații client.

Alegerea instrumentelor potrivite pentru a scrie serviciile RESTful este crucială, deoarece trebuie să ne preocupăm de scalabilitate, întreținere, documentație și toate celelalte aspecte relevante. ASP.NET Core ne oferă o API puternică, ușor de utilizat, care este excelentă pentru atingerea acestor obiective.

În acest articol, vă voi arăta cum să scrieți o API RESTful bine structurată pentru un scenariu „aproape” din lumea reală, folosind cadrul principal ASP.NET. Am să detaliez tiparele și strategiile comune pentru simplificarea procesului de dezvoltare.

Vă voi arăta, de asemenea, cum să integrați cadre și biblioteci comune, cum ar fi Entity Framework Core și AutoMapper, pentru a oferi funcționalitățile necesare.

Cerințe preliminare

Mă aștept să aveți cunoștințe despre concepte de programare orientate pe obiecte.

Chiar dacă voi acoperi multe detalii despre limbajul de programare C #, vă recomand să aveți cunoștințe de bază despre acest subiect.

De asemenea, presupun că știți ce este REST, cum funcționează protocolul HTTP, care sunt punctele finale ale API și ce este JSON. Iată un excelent tutorial introductiv pe acest subiect. Cerința finală este să înțelegeți cum funcționează bazele de date relaționale.

Pentru a coda împreună cu mine, va trebui să instalați .NET Core 2.2, precum și Postman, instrumentul pe care îl voi folosi pentru a testa API. Vă recomand să utilizați un editor de cod, cum ar fi Visual Studio Code pentru a dezvolta API. Alege editorul de coduri pe care îl preferi. Dacă alegeți acest editor de coduri, vă recomand să instalați extensia C # pentru a avea o evidențiere mai bună a codului.

Puteți găsi un link către depozitul Github al API-ului la sfârșitul acestui articol, pentru a verifica rezultatul final.

Domeniul de aplicare

Să scriem o API web fictivă pentru un supermarket. Să ne imaginăm că trebuie să punem în aplicare următorul scop:

  • Creați un serviciu RESTful care permite aplicațiilor client să gestioneze catalogul de produse al supermarketului. Trebuie să expună obiective pentru a crea, citi, edita și șterge categorii de produse, cum ar fi produse lactate și produse cosmetice, precum și pentru a gestiona produsele din aceste categorii.
  • Pentru categorii, trebuie să le stocăm numele. Pentru produse, trebuie să le stocăm numele, unitatea de măsură (de exemplu, KG pentru produsele măsurate în greutate), cantitatea din pachet (de exemplu, 10 dacă produsul este un pachet de biscuiți) și categoriile respective.

Pentru a simplifica exemplul, nu voi gestiona produsele în stoc, transportul produselor, securitatea și orice alte funcționalități. Domeniul de aplicare dat este suficient pentru a vă arăta cum funcționează ASP.NET Core.

Pentru a dezvolta acest serviciu, avem nevoie, practic, de două puncte finale ale API: unul pentru gestionarea categoriilor și unul pentru gestionarea produselor. În ceea ce privește comunicarea JSON, putem gândi răspunsurile după cum urmează:

Endpoint API: / api / categories

Răspuns JSON (pentru solicitările GET):

{
  [
    {"id": 1, "nume": "Fructe și legume"},
    {"id": 2, "nume": "Pâine"},
    ... // Alte categorii
  ]
}

Obiectivul API: / api / produse

Răspuns JSON (pentru solicitările GET):

{
  [
    {
      "id": 1,
      "nume": "Zahăr",
      "QuantInPackage": 1,
      "unitOfMeasurement": "KG"
      "categorie": {
        „id”: 3,
        "nume": "Zahăr"
      }
    },
    … // Alte produse
  ]
}

Să începem să scriu cererea.

Pasul 1 - Crearea API-ului

În primul rând, trebuie să creăm structura de foldere pentru serviciul web, apoi trebuie să folosim instrumentele .NET CLI pentru a schela o API Web de bază. Deschideți terminalul sau promptul de comandă (depinde de sistemul de operare pe care îl utilizați) și introduceți următoarele comenzi, în secvență:

mkdir src / Supermarket.API
cd src / Supermarket.API
dotnet webapi nou

Primele două comenzi creează pur și simplu un nou director pentru API și schimbă locația curentă în noul folder. Ultimul generează un nou proiect după modelul API-ului Web, adică tipul de aplicație pe care îl dezvoltăm. Puteți citi mai multe despre aceste comenzi și alte șabloane de proiect pe care le puteți genera verificând acest link.

Noul director va avea acum următoarea structură:

Structura proiectului

Prezentare generală a structurii

O aplicație ASP.NET Core constă dintr-un grup de centrale (bucăți mici din aplicația atașată conductei de aplicație, care gestionează cererile și răspunsurile) configurate în clasa Startup. Dacă ați lucrat deja cu cadre precum Express.js înainte, acest concept nu vă este nou.

Când începe aplicația, este apelată metoda principală, din clasa Program. Creează o gazdă web implicită folosind configurația de pornire, expunând aplicația prin HTTP printr-un port specific (implicit, portul 5000 pentru HTTP și 5001 pentru HTTPS).

Aruncați o privire la clasa ValuesController din folderul Controllers. Acesta expune metode care vor fi apelate atunci când API primește solicitări prin ruta / api / valori.

Nu vă faceți griji dacă nu înțelegeți o parte din acest cod. O să le detaliez pe fiecare atunci când dezvoltă punctele finale necesare API. Deocamdată ștergeți această clasă, deoarece nu o vom folosi.

Pasul 2 - Crearea modelelor de domeniu

Voi aplica câteva concepte de design care vor păstra aplicația simplă și ușor de întreținut.

Nu este dificil să scrieți un cod care să poată fi înțeles și întreținut de dvs., dar trebuie să rețineți că veți lucra ca parte a unei echipe. Dacă nu aveți grijă cum vă scrieți codul, rezultatul va fi un monstru care vă va da și colegilor de echipă dureri de cap constante. Sună extrem, nu? Dar crede-mă, acesta este adevărul.

wtf - măsurarea calității codului de către smitty42 este licențiată conform CC-BY-ND 2.0

Să începem prin scrierea stratului de domeniu. Acest strat va avea clasele noastre de modele, clasele care vor reprezenta produsele și categoriile noastre, precum și depozite și interfețe de servicii. Voi explica aceste două ultime concepte în timp.

În directorul Supermarket.API, creați un nou folder numit Domeniu. În cadrul noului folder de domeniu, creați un alt numit Modele. Primul model pe care trebuie să îl adăugăm la acest folder este Categoria. Inițial, va fi o clasă simplă Obiecte CLR vechi (POCO). Înseamnă că clasa va avea doar proprietăți pentru a descrie informațiile sale de bază.

Clasa are o proprietate Id, pentru identificarea categoriei și un Nameproperty. De asemenea, avem o proprietate Produse. Aceasta ultima va fi utilizată de Entity Framework Core, ORM cele mai multe aplicații Core ASP.NET folosesc pentru a persista datele într-o bază de date, pentru a cartografia relația dintre categorii și produse. De asemenea, are sens gândirea în termeni de programare orientată pe obiecte, deoarece o categorie are multe produse conexe.

De asemenea, trebuie să creăm modelul produsului. În același dosar, adăugați o nouă clasă de produse.

Produsul are, de asemenea, proprietăți pentru ID și nume. Aceasta este, de asemenea, o proprietate QuantityInPackage, care indică câte unități de produs avem într-un singur pachet (amintiți-vă exemplu de biscuiți din domeniul de aplicare al aplicației) și o proprietate UnitOfMeasurement. Acesta este reprezentat de un tip enum, care reprezintă o enumerare a unităților de măsură posibile. Ultimele două proprietăți, CategorieId și Categorie vor fi utilizate de ORM pentru a cartografia relația dintre produse și categorii. Acesta indică faptul că un produs are o singură categorie și doar una.

Să definim ultima parte a modelelor noastre de domeniu, enumerarea EUnitOfMeasurement.

Prin convenție, enumerele nu trebuie să înceapă cu un „E” în ​​fața numelor lor, dar în unele biblioteci și cadre veți găsi acest prefix ca o modalitate de a distinge enumele de interfețe și clase.

Codul este foarte simplu. Aici am definit doar o serie de posibilități pentru unitățile de măsură, cu toate acestea, într-un sistem de supermarket real, este posibil să aveți multe alte unități de măsură și poate un model separat pentru asta.

Observați atributul Descriere aplicat peste fiecare posibilitate de enumerare. Un atribut este o modalitate de a defini metadatele peste clase, interfețe, proprietăți și alte componente ale limbajului C #. În acest caz, îl vom folosi pentru a simplifica răspunsurile punctului final al API-ului produselor, dar nu trebuie să vă preocupe deocamdată. Vom reveni aici mai târziu.

Modelele noastre de bază sunt gata de utilizare. Acum putem începe să scriem endpoint-ul API care va gestiona toate categoriile.

Pasul 3 - API-ul Categoriilor

În folderul Controlere, adăugați o nouă clasă numită CategoriesController.

Prin convenție, toate clasele din acest folder care se termină cu sufixul „Controller” vor deveni controlori ai aplicației noastre. Înseamnă că vor gestiona cererile și răspunsurile. Trebuie să moșteniți această clasă din clasa Controller, definită în spațiul de nume Microsoft.AspNetCore.Mvc.

Un spațiu de nume este format dintr-un grup de clase, interfețe, enume și structuri conexe. Vă puteți gândi la el ca la ceva similar cu module din limba Javascript sau pachete din Java.

Noul controlor ar trebui să răspundă prin rute / api / categorii. Obținem acest lucru prin adăugarea atributului Route deasupra numelui clasei, specificând un marcator de locație care indică faptul că ruta ar trebui să folosească numele clasei fără sufixul controller, prin convenție.

Să începem să gestionăm cererile GET. În primul rând, când cineva solicită date din / api / categorii prin intermediul verbului GET, API trebuie să returneze toate categoriile. Putem crea un serviciu de categorii în acest scop.

Conceptual, un serviciu este practic o clasă sau o interfață care definește metodele de gestionare a unor logici de afaceri. Este o practică obișnuită în multe limbaje de programare diferite de a crea servicii care să se ocupe de logica de afaceri, cum ar fi autentificarea și autorizarea, plățile, fluxurile de date complexe, caching și sarcini care necesită o oarecare interacțiune între alte servicii sau modele.

Folosind servicii, putem izola gestionarea cererii și răspunsurilor de logica reală necesară pentru finalizarea sarcinilor.

Serviciul pe care îl vom crea inițial va defini un singur comportament sau o metodă: o metodă de listare. Ne așteptăm ca această metodă să returneze toate categoriile existente în baza de date.

Pentru simplitate, nu vom face față paginării sau filtrării datelor în acest caz. Voi scrie un articol în viitor, arătând cum să gestionați cu ușurință aceste funcții.

Pentru a defini un comportament așteptat pentru ceva în C # (și în alte limbaje orientate pe obiect, cum ar fi Java, de exemplu), definim o interfață. O interfață spune cum ar trebui să funcționeze ceva, dar nu implementează logica reală a comportamentului. Logica este implementată în clase care implementează interfața. Dacă acest concept nu este clar pentru dvs., nu vă faceți griji. Vei înțelege într-un timp.

În folderul Domeniu, creați un nou director numit Servicii. Acolo, adăugați o interfață numită ICategoryService. Prin convenție, toate interfețele ar trebui să înceapă cu majusculă „I” din C #. Definiți codul interfeței după cum urmează:

Implementările metodei ListAsync trebuie să returneze în mod asincron o enumerare de categorii.

Clasa Task, care încapsulează întoarcerea, indică asincronie. Trebuie să ne gândim într-o metodă asincronă datorită faptului că trebuie să așteptăm ca baza de date să finalizeze o anumită operație pentru a returna datele, iar acest proces poate dura ceva. Observați și sufixul „async”. Este o convenție care indică faptul că metoda noastră ar trebui să fie executată în mod asincron.

Avem o mulțime de convenții, nu? Mie personal îmi place, deoarece menține aplicațiile ușor de citit, chiar dacă sunteți nou pentru o companie care folosește tehnologia .NET.

„- Bine, am definit această interfață, dar nu face nimic. Cum poate fi util? "

Dacă provii dintr-o limbă precum Javascript sau o altă limbă care nu este tipată puternic, acest concept poate părea ciudat.

Interfețele ne permit să abstractizăm comportamentul dorit din implementarea reală. Folosind un mecanism cunoscut sub numele de injecție de dependență, putem implementa aceste interfețe și le putem izola de alte componente.

Practic, când utilizați o injecție de dependență, definiți anumite comportamente folosind o interfață. Apoi, creați o clasă care implementează interfața. În cele din urmă, legați referințele din interfață la clasa creată.

„- Pare foarte confuz. Nu putem crea o clasă care să facă aceste lucruri pentru noi? ”

Continuăm să implementăm API-ul nostru și veți înțelege de ce să folosiți această abordare.

Schimbați codul CategoriesController după cum urmează:

Am definit o funcție de constructor pentru controlerul nostru (un constructor este numit atunci când este creată o nouă instanță a unei clase) și primește o instanță a ICategoryService. Înseamnă că instanța poate fi orice care implementează interfața de serviciu. Stochez această instanță într-un câmp privat, numai de citire _categoryService. Vom folosi acest câmp pentru a accesa metodele de implementare a serviciilor noastre de categorii.

Apropo, prefixul de subliniere este o altă convenție comună pentru a denumi un câmp. Această convenție, în special, nu este recomandată de ghidul oficial al convenției de denumire de .NET, dar este o practică foarte comună ca o modalitate de a evita să folosești cuvântul cheie „acest” pentru a distinge câmpurile clasei de variabilele locale. Personal consider că este mult mai curat de citit și o mulțime de cadre și biblioteci folosesc această convenție.

Sub constructor, am definit metoda care va gestiona cererile pentru / api / categorii. Atributul HttpGet spune ASP.NET Core pipeline să-l folosească pentru a gestiona cererile GET (acest atribut poate fi omis, dar este mai bine să-l scrii pentru o lizibilitate mai ușoară).

Metoda folosește instanța noastră de serviciu de categorii pentru a lista toate categoriile și apoi returnează categoriile către client. Conducta cadru gestionează serializarea datelor la un obiect JSON. Tipul IEnumerable spune cadrului că dorim să returneze o enumerare de categorii, iar tipul Task, precedat de cuvântul cheie async, spune conductei că această metodă ar trebui să fie executată asincron. În cele din urmă, când definim o metodă async, trebuie să utilizăm cuvântul cheie așteptat pentru sarcini care pot dura un timp.

Ok, am definit structura inițială a API-ului nostru. Acum, este necesar să implementăm cu adevărat serviciul de categorii.

Pasul 4 - Implementarea serviciului de categorii

În folderul rădăcină al API-ului (folderul Supermarket.API), creați unul nou numit Servicii. Aici vom pune toate implementările de servicii. În noul dosar, adăugați o nouă clasă numită CategoryService. Modificați codul după cum urmează:

Este pur și simplu codul de bază pentru implementarea interfeței, dar încă nu gestionăm nicio logică. Să ne gândim la modul în care ar trebui să funcționeze metoda de înregistrare.

Trebuie să accesăm baza de date și să returnăm toate categoriile, apoi trebuie să returnăm aceste date clientului.

O clasă de servicii nu este o clasă care ar trebui să gestioneze accesul la date. Există un model numit Pattern Repository care este utilizat pentru gestionarea datelor din baze de date.

Când utilizăm Pattern Repository, definim clase de repositorii, care, practic, încapsulează toată logica pentru a gestiona accesul la date. Aceste depozite expun metode pentru listarea, crearea, editarea și ștergerea obiectelor unui model dat, la fel cum puteți manipula colecțiile. Intern, aceste metode vorbesc cu baza de date pentru a efectua operațiuni CRUD, izolând accesul la baza de date de restul aplicației.

Serviciul nostru trebuie să discute cu un depozit de categorii, pentru a obține lista obiectelor.

Conceptual, un serviciu poate „discuta” cu unul sau mai multe depozite sau alte servicii pentru a efectua operațiuni.

Poate părea redundant să creezi o nouă definiție pentru gestionarea logicii de acces la date, dar vei vedea într-un timp că izolarea acestei logici de clasa de servicii este într-adevăr avantajoasă.

Să creăm un depozit care să fie responsabil de intermedierea comunicării bazei de date ca o modalitate de a persista categoriile.

Pasul 5 - Depozitul de categorii și stratul de persistență

În cadrul folderului Domeniu, creați un nou director numit Depozite. Apoi, adăugați o nouă interfață numită ICategoryRespository. Definiți interfața după cum urmează:

Codul inițial este practic identic cu cel al interfeței de serviciu.

După definirea interfeței, ne putem întoarce la clasa de servicii și terminăm de implementat metoda de listare, folosind o instanță a ICategoryRepository pentru a returna datele.

Acum trebuie să implementăm logica reală a depozitului de categorii. Înainte de a face acest lucru, trebuie să ne gândim cum vom accesa baza de date.

Apropo, încă nu avem o bază de date!

Vom folosi Entity Framework Core (îl voi numi EF Core pentru simplitate) ca ORM al bazei noastre de date. Acest cadru vine cu ASP.NET Core ca ORM implicit și expune o API prietenoasă care ne permite să mapăm clasele aplicațiilor noastre în tabelele bazei de date.

EF Core ne permite, de asemenea, să proiectăm aplicația noastră mai întâi, și apoi să generăm o bază de date în conformitate cu ceea ce am definit în codul nostru. Această tehnică se numește mai întâi cod. Vom folosi prima abordare a codului pentru a genera o bază de date (în acest exemplu, de fapt, voi folosi o bază de date în memorie, dar veți putea să o modificați cu ușurință într-o instanță de server SQL sau MySQL, de exemplu).

În folderul rădăcină al API-ului, creați un nou director numit Persistență. Acest director va avea tot ce avem nevoie pentru a accesa baza de date, cum ar fi implementările depozitelor.

În noul dosar, creați un nou director numit Context, apoi adăugați o nouă clasă numită AppDbContext. Această clasă trebuie să moștenească DbContext, o clasă EF Core pe care o folosești pentru a-ți mapa modelele în tabelele bazei de date. Schimbați codul în felul următor:

Constructorul adăugat la această clasă este responsabil pentru trecerea configurației bazei de date la clasa de bază prin injecția de dependență. Vei vedea într-o clipă cum funcționează acest lucru.

Acum, trebuie să creăm două proprietăți DbSet. Aceste proprietăți sunt seturi (colecții de obiecte unice) care mapează modelele către tabelele bazei de date.

De asemenea, trebuie să mapăm proprietățile modelelor în coloanele tabelelor respective, specificând ce proprietăți sunt chei primare, care sunt chei străine, tipuri de coloane, etc. Putem face acest lucru înlocuind metoda OnModelCreating, folosind o caracteristică numită API fluentă pentru specificați maparea bazei de date. Modificați clasa AppDbContext după cum urmează:

Codul este intuitiv.

Precizăm în ce tabele ar trebui mapate modelele noastre. De asemenea, stabilim cheile primare, folosind metoda HasKey, coloanele tabelului, folosind metoda Property și unele constrângeri precum IsRequired, HasMaxLength și ValueGeneratedOnAdd, totul cu expresii lambda într-un mod „fluent” (metode de înlănțuire).

Uitați-vă la următoarea bucată de cod:

builder.Entity  ()
       .HasMany (p => p.Produse)
       .WithOne (p => p.Categorie)
       .HasForeignKey (p => p.CategoryId);

Aici specificăm o relație între tabele. Spunem că o categorie are multe produse și setăm proprietățile care vor cartografia această relație (Produse, din clasa Categorie și Categorie, din Clasa de produse). De asemenea, am stabilit cheia străină (CategoryId).

Aruncați o privire la acest tutorial dacă doriți să aflați cum să configurați relațiile unu la unu și multe-la-multe utilizând EF Core, precum și cum să-l utilizați în ansamblu.

Există, de asemenea, o configurație pentru semănarea datelor, prin metoda HasData:

builder.Entity  (). HasData
(
  new Category {Id = 100, Name = "Fructe și legume"},
  new Category {Id = 101, Name = "Dairy"}
);

Aici adăugăm pur și simplu două categorii de exemple în mod implicit. Este necesar să testăm punctul nostru final API după ce îl terminăm.

Observație: setăm manual proprietățile ID aici, deoarece furnizorul în memorie necesită să funcționeze. Setează identificatorii pe un număr mare pentru a evita coliziunea între identificatorii generați automat și datele despre semințe.
Această limitare nu există la furnizorii de baze de date relaționale adevărate, astfel încât dacă doriți să utilizați o bază de date precum SQL Server, de exemplu, nu trebuie să specificați acești identificatori. Verificați această problemă Github dacă doriți să înțelegeți acest comportament.

După implementarea clasei de context a bazei de date, putem implementa depozitul de categorii. Adăugați un nou folder numit Repozitorii în folderul Persistență, apoi adăugați o nouă clasă numită BaseRepository.

Această clasă este doar o clasă abstractă pe care o vor moșteni toate depozitele noastre. O clasă abstractă este o clasă care nu are instanțe directe. Trebuie să creați clase directe pentru a crea instanțele.

BaseRepository primește o instanță a AppDbContext noastră prin injecție de dependență și expune o proprietate protejată (o proprietate care poate fi accesată doar de clasele pentru copii) numită _context, care oferă acces la toate metodele de care avem nevoie pentru a gestiona operațiunile bazei de date.

Adăugați o clasă nouă în același folder numit CategoryRepository. Acum vom implementa cu adevărat logica depozitului:

Depozitul moștenește BaseRepository și implementează ICategoryRepository.

Observați cât de simplu este să implementați metoda de listare. Utilizăm setul de baze de date Categorii pentru a accesa tabelul de categorii și apoi apelăm la metoda de extensie ToListAsync, care este responsabilă pentru transformarea rezultatului unei interogări într-o colecție de categorii.

EF Core traduce apelul nostru de metodă la o interogare SQL, cel mai eficient mod posibil. Interogarea este executată numai atunci când apelați la o metodă care vă va transforma datele într-o colecție sau când utilizați o metodă pentru a lua date specifice.

Avem acum o implementare curată a controlorului de categorii, a serviciului și a depozitului.

Am separat preocupările, creând clase care fac doar ceea ce se presupune că fac.

Ultimul pas înainte de testarea aplicației este legarea interfețelor noastre la clasele respective folosind mecanismul de injecție de dependență ASP.NET Core.

Pasul 6 - Configurarea injecției de dependență

Este timpul să înțelegeți în sfârșit cum funcționează acest concept.

În folderul rădăcină al aplicației, deschideți clasa Startup. Această clasă este responsabilă de configurarea tuturor tipurilor de configurații la pornirea aplicației.

Metodele ConfigureServices și Configure sunt apelate la runtime de conducta cadru pentru a configura modul în care aplicația ar trebui să funcționeze și ce componente trebuie să utilizeze.

Aruncați o privire la metoda ConfigureServices. Aici avem o singură linie, care configurează aplicația pentru a utiliza conducta MVC, ceea ce înseamnă că aplicația va gestiona cererile și răspunsurile folosind clase de controler (există mai multe lucruri aici în culise, dar asta trebuie să știți deocamdata).

Putem folosi metoda ConfigureServices, accesând parametrul servicii, pentru a configura legăturile noastre de dependență. Curățați codul clasei eliminând toate comentariile și schimbați codul după cum urmează:

Uită-te la această bucată de cod:

services.AddDbContext  (options => {
  options.UseInMemoryDatabase ( "supermarket-api-in-memory");
});

Aici configuram contextul bazei de date. Spunem ASP.NET Core să folosească AppDbContext cu o implementare a bazei de date în memorie, care este identificată prin șirul transmis ca argument la metoda noastră. De obicei, furnizorul în memorie este folosit atunci când scriem teste de integrare, dar îl folosesc aici pentru simplitate. În acest fel, nu trebuie să ne conectăm la o bază de date reală pentru a testa aplicația.

Configurația acestor linii configurează intern contextul bazei noastre de date pentru injecția de dependență folosind o durată de viață determinată.

Durata de viață a scopului spune conductei core ASP.NET că de fiecare dată când trebuie să rezolve o clasă care primește o instanță de AppDbContext ca argument constructor, ar trebui să utilizeze aceeași instanță a clasei. Dacă nu există nicio instanță în memorie, conducta va crea o nouă instanță și o va reutiliza în toate clasele care au nevoie de ea, în timpul unei solicitări date. În acest fel, nu este necesar să creați manual instanța de clasă atunci când trebuie să o utilizați.

Există și alte domenii de viață pe care le puteți verifica citind documentația oficială.

Tehnica de injecție de dependență ne oferă multe avantaje, cum ar fi:

  • Reutilizarea codului;
  • Productivitate mai bună, întrucât atunci când trebuie să schimbăm implementarea, nu trebuie să ne deranjăm să schimbăm o sută de locuri în care utilizați această caracteristică;
  • Puteți testa cu ușurință aplicația, deoarece putem izola ceea ce avem de testat folosind simulări (implementarea falsă a claselor) unde trebuie să trecem interfețe ca argumente de constructor;
  • Când o clasă trebuie să primească mai multe dependențe prin intermediul unui constructor, nu trebuie să schimbați manual toate locurile în care instanțele sunt create (asta este minunat!).

După configurarea contextului bazei de date, se leagă, de asemenea, serviciul și depozitul nostru la clasele respective.

services.AddScoped  ();
services.AddScoped  ();

Aici folosim, de asemenea, o durată de viață cu scopuri, deoarece aceste clase trebuie să folosească intern contextul bazei de date. Este logic să specificăm același domeniu de aplicare în acest caz.

Acum că ne configurăm legăturile de dependență, trebuie să facem o mică modificare la clasa Program, pentru ca baza de date să semene corect datele noastre inițiale. Acest pas este necesar numai atunci când utilizați furnizorul de baze de date în memorie (consultați această problemă Github pentru a înțelege de ce).

A fost necesară schimbarea metodei principale pentru a garanta că baza noastră de date va fi „creată” atunci când începe aplicația, deoarece folosim un furnizor de memorie. Fără această modificare, categoriile pe care vrem să le semănăm nu vor fi create.

Cu toate caracteristicile de bază implementate, este timpul să testăm punctul nostru final API.

Pasul 7 - Testarea API-ului Categorii

Deschideți terminalul sau promptul de comandă în folderul rădăcină API și tastați următoarea comandă:

rulare dotnet

Comanda de mai sus pornește aplicația. Consola va arăta o ieșire similară cu aceasta:

info: Microsoft.EntityFrameworkCore.Infrastructure [10403]
Entity Framework Core 2.2.0-rtm-35687 a inițializat „AppDbContext” folosind furnizorul „Microsoft.EntityFrameworkCore.InMemory” cu opțiuni: StoreName = supermarket-api-in-memory
info: Microsoft.EntityFrameworkCore.Update [30100]
Au salvat 2 entități în magazinul de memorie.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager [0]
Profilul de utilizator este disponibil. Folosind „C: \ Users \ evgomes \ AppData \ Local \ ASP.NET \ DataProtection-Keys” ca depozit de chei și Windows DPAPI pentru criptarea cheilor în repaus.
Mediul de găzduire: Dezvoltare
Calea rădăcină a conținutului: C: \ Users \ evgomes \ Desktop \ Tutoriale \ src \ Supermarket.API
Acum ascultați pe: https: // localhost: 5001
Acum ascultați pe: http: // localhost: 5000
Aplicația a început. Apăsați Ctrl + C pentru a opri.

Puteți vedea că EF Core a fost apelat la inițializarea bazei de date. Ultimele rânduri arată în ce porturi rulează aplicația.

Deschideți un browser și navigați la http: // localhost: 5000 / api / categories (sau la adresa URL afișată la ieșirea consolei). Dacă vedeți o eroare de securitate din cauza HTTPS, adăugați doar o excepție pentru aplicație.

Browserul va afișa următoarele date JSON ca ieșire:

[
  {
     „id”: 100,
     "nume": "Fructe și legume",
     "produse": []
  },
  {
     „id”: 101,
     "nume": "lactate",
     "produse": []
  }
]

Aici vedem datele pe care le-am adăugat la baza de date atunci când am configurat contextul bazei de date. Această ieșire confirmă faptul că codul nostru funcționează.

Ați creat un endpoint API GET cu foarte puține linii de cod și aveți o structură de cod care este cu adevărat ușor de modificat datorită arhitecturii API.

Acum, este timpul să vă arătăm cât de ușor este să schimbați acest cod atunci când trebuie să îl ajustați datorită nevoilor de afaceri.

Pasul 8 - Crearea unei resurse de categorie

Dacă vă amintiți specificația punctului final al API-ului, ați observat că răspunsul nostru JSON are o proprietate suplimentară: o serie de produse. Aruncați o privire la exemplul răspunsului dorit:

{
  [
    {"id": 1, "nume": "Fructe și legume"},
    {"id": 2, "nume": "Pâine"},
    ... // Alte categorii
  ]
}

Gama de produse este prezentă la răspunsul nostru actual JSON, deoarece modelul nostru de categorii are o proprietate Produse, necesară de către EF Core pentru a corecta harta produselor unei anumite categorii.

Nu dorim această proprietate în răspunsul nostru, dar nu putem schimba clasa noastră de model pentru a exclude această proprietate. Ar putea cauza EF Core să arunce erori atunci când încercăm să gestionăm datele categoriilor și ar rupe și designul modelului nostru de domeniu, deoarece nu are sens să avem o categorie de produse care nu are produse.

Pentru a returna datele JSON care conțin doar identificatorii și numele categoriilor de supermarketuri, trebuie să creăm o clasă de resurse.

O clasă de resurse este o clasă care conține doar informații de bază care vor fi schimbate între aplicațiile client și punctele finale ale API, în general sub formă de date JSON, pentru a reprezenta anumite informații particulare.

Toate răspunsurile din punctele finale ale API trebuie să returneze o resursă.

Este o practică proastă să returnați reprezentarea modelului real ca răspuns, deoarece poate conține informații de care aplicația client nu are nevoie sau că nu are permisiunea de a avea (de exemplu, un model de utilizator ar putea returna informațiile parolei utilizatorului , ceea ce ar fi o mare problemă de securitate).

Avem nevoie de o resursă care să reprezinte doar categoriile noastre, fără produse.

Acum că știți ce este o resursă, să o implementăm. În primul rând, opriți aplicația care rulează apăsând Ctrl + C la linia de comandă. În folderul rădăcină al aplicației, creați un nou folder numit Resurse. Acolo, adăugați o nouă clasă numită CategoryResource.

Trebuie să mapăm colecția noastră de modele de categorii, care este furnizată de serviciul nostru de categorii, într-o colecție de resurse de categorie.

Vom folosi o bibliotecă numită AutoMapper pentru a gestiona maparea între obiecte. AutoMapper este o bibliotecă foarte populară în lumea .NET și este folosită în multe proiecte comerciale și open source.

Introduceți următoarele linii în linia de comandă pentru a adăuga AutoMapper în aplicația noastră:

dotnet add pachet AutoMapper
dotnet add pachet AutoMapper.Extensions.Microsoft.DependencyInjection

Pentru a utiliza AutoMapper, trebuie să facem două lucruri:

  • Înregistrați-l pentru injecția de dependență;
  • Creați o clasă care să vă spună AutoMapper cum să gestionați maparea claselor.

În primul rând, deschideți clasa Startup. În metoda ConfigureServices, după ultima linie, adăugați următorul cod:

services.AddAutoMapper ();

Această linie gestionează toate configurațiile necesare ale AutoMapper, cum ar fi înregistrarea acesteia pentru injecția de dependență și scanarea aplicației în timpul pornirii pentru a configura profilele de mapare.

Acum, în directorul rădăcină, adăugați un nou folder numit Mapping, apoi adăugați o clasă numită ModelToResourceProfile. Modificați codul astfel:

Clasa moștenește Profil, un tip de clasă pe care AutoMapper îl folosește pentru a verifica modul în care funcționează mapările noastre. Pe constructor, creăm o hartă între clasa modelului Categorie și clasa CategoryResource. Deoarece proprietățile claselor au aceleași nume și tipuri, nu trebuie să utilizăm nicio configurație specială pentru ele.

Ultimul pas constă în schimbarea controlerului de categorii pentru a utiliza AutoMapper pentru a gestiona maparea obiectelor noastre.

Am schimbat constructorul pentru a primi o instanță de implementare IMapper. Puteți utiliza aceste metode de interfață pentru a utiliza metode de mapare AutoMapper.

De asemenea, am schimbat metoda GetAllAsync pentru a face o enumerare a categoriilor noastre într-o enumerare a resurselor folosind metoda Map. Această metodă primește o instanță a clasei sau colecției pe care dorim să o mapăm și, prin definiții de tip generice, definește ce tip de clasă sau colecție trebuie mapată.

Observați că am modificat ușor implementarea fără a fi necesară adaptarea clasei de service sau a depozitului, pur și simplu prin injectarea unei noi dependențe (IMapper) către constructor.

Injecția de dependență face ca aplicația dvs. să poată fi menținută și ușor de modificat, deoarece nu trebuie să spargeți toată implementarea codului pentru a adăuga sau a elimina funcții.

Probabil v-ați dat seama că nu numai clasa de controler, ci toate clasele care primesc dependențe (inclusiv dependențele în sine) au fost rezolvate automat pentru a primi clasele corecte în conformitate cu configurațiile de legare.

Injecția de dependență este uimitoare, nu-i așa?

Acum, porniți din nou API-ul folosind comanda de rulare dotnet și treceți la http: // localhost: 5000 / api / categories pentru a vedea noul răspuns JSON.

Acestea sunt datele de răspuns pe care ar trebui să le vezi

Avem deja obiectivul nostru GET. Acum, să creăm un nou punct final pentru categoriile POST (create).

Pasul 9 - Crearea de noi categorii

Când ne ocupăm de crearea de resurse, trebuie să ne preocupăm de multe lucruri, cum ar fi:

  • Validarea datelor și integritatea datelor;
  • Autorizarea creării de resurse;
  • Eroare de manipulare;
  • Logging.

Nu voi arăta cum să fac față autentificării și autorizării în acest tutorial, dar puteți vedea cum puteți implementa cu ușurință aceste funcții citind tutorialul meu pe autentificarea cu jeton web JSON.

De asemenea, există un cadru foarte popular numit ASP.NET Identity care oferă soluții încorporate în ceea ce privește securitatea și înregistrarea utilizatorilor pe care le puteți folosi în aplicațiile dvs. Include furnizori care să lucreze cu EF Core, cum ar fi un IdentityDbContext integrat pe care îl puteți utiliza. Puteți afla mai multe despre asta aici.

Să scriem un punct final HTTP POST care va acoperi celelalte scenarii (cu excepția înregistrării, care se poate schimba în funcție de diferite scopuri și instrumente).

Înainte de a crea noul punct final, avem nevoie de o nouă resursă. Această resursă va face o hartă a datelor pe care aplicațiile client le transmit acestui punct (în acest caz, numele categoriei) unei clase din aplicația noastră.

Deoarece creăm o nouă categorie, nu avem încă un ID și înseamnă că avem nevoie de o resursă care să reprezinte o categorie care să conțină doar numele acesteia.

În folderul Resurse, adăugați o nouă clasă numită SaveCategoryResource:

Observați atributele obligatorii și MaxLength aplicate peste proprietatea Name. Aceste atribute se numesc adnotări de date. Conducta principală ASP.NET utilizează aceste metadate pentru a valida cererile și răspunsurile. După cum sugerează numele, numele categoriei este obligatoriu și are o lungime maximă de 30 de caractere.

Să definim acum forma noului punct final al API. Adăugați următorul cod la controlorul de categorii:

Spunem cadrului că acesta este un punct final HTTP POST folosind atributul HttpPost.

Observați tipul de răspuns al acestei metode, Task . Metodele prezente în clasele controlerului se numesc acțiuni și au această semnătură deoarece putem întoarce mai mult de un rezultat posibil după ce aplicația execută acțiunea.

În acest caz, dacă numele categoriei nu este valabil sau dacă ceva nu merge bine, trebuie să returnăm un răspuns de 400 de coduri (solicitare necorespunzătoare), care conține în general un mesaj de eroare pe care aplicațiile client le pot folosi pentru a trata problema sau putem avea un 200 răspuns (succes) cu date dacă totul merge bine.

Există multe tipuri de tipuri de acțiune pe care le puteți utiliza ca răspuns, dar, în general, putem utiliza această interfață, iar ASP.NET Core va folosi o clasă implicită pentru asta.

Atributul FromBody spune ASP.NET Core să analizeze datele corpului de solicitare în noua noastră clasă de resurse. Înseamnă că atunci când un JSON care conține numele categoriei este trimis către aplicația noastră, cadrul va analiza automat noua noastră clasă.

Acum, să implementăm logica rutelor noastre. Trebuie să urmăm câțiva pași pentru a crea cu succes o nouă categorie:

  • În primul rând, trebuie să validăm cererea primită. Dacă solicitarea nu este valabilă, trebuie să returnăm un răspuns necorespunzător la solicitarea care conține mesajele de eroare;
  • Apoi, dacă solicitarea este valabilă, trebuie să mapăm noua noastră resursă la clasa noastră de model de categorie folosind AutoMapper;
  • Acum trebuie să apelăm la serviciul nostru, spunându-i să salveze noua noastră categorie. Dacă logica de salvare este executată fără probleme, ar trebui să returneze un răspuns care conține datele noastre din noua categorie. Dacă nu, ar trebui să ne ofere un indiciu că procesul a eșuat și un mesaj de eroare potențial;
  • În cele din urmă, dacă există o eroare, returnăm o solicitare necorespunzătoare. Dacă nu, mapăm noul nostru model de categorie într-o resursă de categorie și returnăm un răspuns de succes clientului, conținând noile date de categorie.

Pare a fi complicat, dar este foarte ușor să implementăm această logică folosind arhitectura de servicii pe care am structurat-o pentru API-ul nostru.

Să începem prin validarea cererii primite.

Pasul 10 - Validarea organismului de solicitare folosind starea modelului

Controloarele ASP.NET Core au o proprietate numită ModelState. Această proprietate este completată în timpul executării cererii înainte de a ajunge la executarea acțiunii noastre. Este o instanță a ModelStateDictionary, o clasă care conține informații cum ar fi dacă solicitarea este validă și potențialele mesaje de eroare de validare.

Modificați codul final astfel:

Codul verifică dacă starea modelului (în acest caz, datele trimise în organismul de solicitare) nu sunt valide, verificând adnotările noastre de date. Dacă nu este, API-ul returnează o solicitare necorespunzătoare (cu 400 de coduri de stare) și mesajele de eroare implicite au fost furnizate de adnotările noastre.

Metoda ModelState.GetErrorMessages () nu este încă implementată. Este o metodă de extensie (o metodă care extinde funcționalitatea unei clase sau interfețe deja existente) pe care o voi implementa pentru a converti erorile de validare în șiruri simple pentru a reveni la client.

Adăugați un nou folder Extensii în rădăcina API-ului nostru, apoi adăugați o nouă clasă ModelStateExtensions.

Toate metodele de extensie trebuie să fie statice, precum și clasele în care sunt declarate. Înseamnă că nu gestionează date specifice despre instanță și că sunt încărcate o singură dată la începerea aplicației.

Acest cuvânt cheie din fața declarației de parametri spune compilatorului C # să-l trateze ca o metodă de extensie. Rezultatul este că îl putem numi ca o metodă normală din această clasă, întrucât includem respectiva directivă unde dorim să utilizăm extensia.

Extensia folosește interogări LINQ, o caracteristică foarte utilă .NET care ne permite să interogăm și să transformăm date folosind expresii în lanț. Expresiile de aici transformă metodele de eroare de validare într-o listă de șiruri care conțin mesajele de eroare.

Importați spațiul de nume Supermarket.API.Extensii în controlerul de categorii înainte de a trece la pasul următor.

folosirea Supermarket.API.Extensions;

Continuăm să punem în aplicare logica noastră finală, mapând noua noastră resursă într-o clasă de model de categorie.

Pasul 11 ​​- Maparea noii resurse

Am definit deja un profil de mapare pentru a transforma modelele în resurse. Acum avem nevoie de un profil nou care să facă invers.

Adăugați o nouă clasă ResourceToModelProfile în folderul Mapping:

Nimic nou aici. Datorită magiei injecției de dependență, AutoMapper va înregistra automat acest profil la începerea aplicației și nu trebuie să schimbăm niciun alt loc în care să îl folosim.

Acum putem să mapăm noua noastră resursă la clasa de model respectivă:

Pasul 12 - Aplicarea modelului Cerere-Răspuns pentru gestionarea logicii de salvare

Acum trebuie să implementăm cea mai interesantă logică: salvarea unei noi categorii. Ne așteptăm ca serviciul nostru să o facă.

Logica de salvare poate eșua din cauza problemelor la conectarea la baza de date sau poate din cauza faptului că orice regulă de afaceri internă ne invalidează datele.

Dacă ceva nu merge bine, nu putem arunca pur și simplu o eroare, deoarece ar putea opri API-ul, iar aplicația client nu ar ști să rezolve problema. De asemenea, putem avea un mecanism de înregistrare care ar înregistra eroarea.

Contractul metodei de economisire înseamnă că semnătura metodei și tipul de răspuns trebuie să ne indice dacă procesul a fost executat corect. Dacă procesul decurge, vom primi datele categoriei. Dacă nu, trebuie să primim, cel puțin, un mesaj de eroare care să spună de ce procesul a eșuat.

Putem implementa această caracteristică aplicând modelul cerere-răspuns. Acest model de proiectare a întreprinderii încapsulează parametrii cererii și răspunsului nostru în clase ca o modalitate de a încapsula informațiile pe care serviciile noastre le vor folosi pentru a procesa o anumită sarcină și pentru a returna informațiile la clasa care utilizează serviciul.

Acest model ne oferă câteva avantaje, cum ar fi:

  • Dacă trebuie să ne schimbăm serviciul pentru a primi mai mulți parametri, nu trebuie să rupem semnătura acestuia;
  • Putem defini un contract standard pentru solicitarea și / sau răspunsurile noastre;
  • Putem gestiona logica de afaceri și eșecurile potențiale fără a opri procesul de solicitare și nu va trebui să folosim tone de blocuri de încercare.

Să creăm un tip de răspuns standard pentru metodele noastre de servicii care gestionează schimbările de date. Pentru fiecare solicitare de acest tip, dorim să știm dacă solicitarea este executată fără probleme. Dacă nu reușește, dorim să returnăm un mesaj de eroare clientului.

În folderul Domeniu, din interiorul serviciilor, adăugați un nou director numit Comunicare. Adăugați o nouă clasă acolo numită BaseResponse.

Aceasta este o clasă abstractă pe care tipurile noastre de răspuns o vor moșteni.

Abstracția definește o proprietate de succes, care va indica dacă cererile au fost finalizate cu succes, și o proprietate Mesaj, care va avea mesajul de eroare dacă ceva nu reușește.

Observați că aceste proprietăți sunt necesare și doar clasele moștenite pot seta aceste date, deoarece clasele pentru copii trebuie să treacă aceste informații prin funcția de constructor.

Sfat: nu este o practică bună să definiți clase de bază pentru orice, deoarece clasele de bază cuplă codul dvs. și vă împiedică să îl modificați cu ușurință. Preferă să folosești compoziția peste mostenire.
În ceea ce privește domeniul de aplicare al acestei API, nu este chiar o problemă să folosești clase de bază, deoarece serviciile noastre nu vor crește mult. Dacă vă dați seama că un serviciu sau o aplicație va crește și se va schimba frecvent, evitați utilizarea unei clase de bază.

Acum, în același dosar, adăugați o nouă clasă numită SaveCategoryResponse.

Tipul de răspuns stabilește, de asemenea, o proprietate Categorie, care va conține datele categoriei noastre dacă cererea se va finaliza cu succes.

Rețineți că am definit trei constructori diferiți pentru această clasă:

  • Unul privat, care va transmite parametrii de succes și de mesaj la clasa de bază și stabilește, de asemenea, proprietatea Categorie;
  • Un constructor care primește doar categoria ca parametru. Acesta va crea un răspuns de succes, apelând constructorul privat să stabilească proprietățile respective;
  • Un al treilea constructor care specifică doar mesajul. Acesta va fi folosit pentru a crea un răspuns la eșec.

Deoarece C # acceptă mai mulți constructori, am simplificat crearea răspunsului fără a defini o metodă diferită pentru a gestiona acest lucru, doar folosind constructori diferiți.

Acum putem schimba interfața de servicii pentru a adăuga noul contract de salvare a metodei.

Schimbați interfața ICategoryService după cum urmează:

Vom trece pur și simplu o categorie la această metodă și se va ocupa de toate logicile necesare pentru salvarea datelor modelului, orchestrării depozitelor și a altor servicii necesare pentru a face acest lucru.

Observați Nu creez aici o clasă de solicitare specifică, deoarece nu avem nevoie de alți parametri pentru a efectua această sarcină. Există un concept în programarea computerului numit KISS - scurt pentru Keep it Simple, Stupid. Practic, se spune că ar trebui să vă păstrați aplicația cât mai simplă.

Amintiți-vă acest lucru atunci când proiectați aplicațiile: aplicați doar ceea ce aveți nevoie pentru a rezolva o problemă. Nu vă supraîncărcați aplicația.

Acum putem să ne terminăm logica finală:

După validarea datelor solicitării și maparea resursei la modelul nostru, o transmitem serviciului nostru pentru a persista datele.

Dacă ceva nu reușește, API-ul returnează o solicitare greșită. Dacă nu, API mapează noua categorie (care include acum date precum noul ID) la CategoryResource-ul nostru creat anterior și îl trimite clientului.

Acum implementăm adevărata logică a serviciului.

Pasul 13 - Logica bazei de date și unitatea modelului de lucru

Având în vedere că vom persista datele în baza de date, avem nevoie de o nouă metodă în depozitul nostru.

Adăugați o nouă metodă AddAsync la interfața ICategoryRepository:

Acum, să implementăm această metodă la clasa noastră reală de depozitare:

Aici adăugăm pur și simplu o nouă categorie la setul nostru.

Când adăugăm o clasă la un DBSet <>, EF Core începe să urmărească toate modificările care se întâmplă modelului nostru și folosește aceste date la starea actuală pentru a genera interogări care vor insera, actualiza sau șterge modele.

Implementarea actuală adaugă pur și simplu modelul la setul nostru, dar datele noastre nu vor fi încă salvate.

Există o metodă numită SaveChanges prezentă la clasa de context pe care trebuie să o apelăm pentru a executa cu adevărat interogările în baza de date. Nu l-am sunat aici, deoarece un depozit nu ar trebui să persiste date, ci doar o colecție de obiecte în memorie.

Acest subiect este foarte controversat chiar și între dezvoltatorii .NET experimentați, dar permiteți-mi să vă explic de ce nu ar trebui să apelați SaveChanges în clase de depozit.

Ne putem gândi la un depozit conceptual ca orice altă colecție prezentă la cadrul .NET. Când aveți de-a face cu o colecție în .NET (și în multe alte limbaje de programare, cum ar fi Javascript și Java), în general puteți:

  • Adăugați elemente noi (cum ar fi atunci când împingeți date pe liste, tablouri și dicționare);
  • Găsiți sau filtrați articole;
  • Eliminați un articol din colecție;
  • Înlocuiți un articol dat sau actualizați-l.

Gândiți-vă la o listă din lumea reală. Imaginați-vă că scrieți o listă de cumpărături pentru a cumpăra lucruri la un supermarket (ce coincidență, nu?).

În listă, scrieți toate fructele pe care trebuie să le cumpărați. Puteți adăuga fructe în această listă, puteți elimina un fruct dacă renunțați să îl cumpărați sau puteți înlocui numele unui fruct. Dar nu puteți salva fructe în listă. Nu are sens să spunem așa ceva în engleză simplă.

Sfat: atunci când proiectați clase și interfețe în limbaje de programare orientate pe obiecte, încercați să utilizați limbajul natural pentru a verifica dacă ceea ce faceți pare să fie corect.
De exemplu, are sens să spunem că un bărbat implementează o interfață de persoană, dar nu are sens să spunem că un om își pune în aplicare un cont.

Dacă doriți să „salvați” listele de fructe (în acest caz, pentru a cumpăra toate fructele), îl plătiți și supermarketul procesează datele din stoc pentru a verifica dacă trebuie să cumpere mai multe fructe de la un furnizor sau nu.

Aceeași logică poate fi aplicată și în cazul programării. Depozitele nu ar trebui să salveze, să actualizeze sau să șteargă datele. În schimb, ar trebui să o delege într-o altă clasă pentru a se ocupa de această logică.

Există o altă problemă la salvarea datelor direct într-un depozit: nu puteți utiliza tranzacții.

Imaginează-ți că aplicația noastră are un mecanism de înregistrare care stochează un nume de utilizator și acțiunea efectuată de fiecare dată când se face o modificare a datelor API.

Acum imaginați-vă că, din anumite motive, aveți un apel la un serviciu care actualizează numele de utilizator (nu este un scenariu comun, dar să îl luăm în considerare).

Sunteți de acord că pentru a schimba numele de utilizator dintr-un tabel de utilizatori fictivi, trebuie mai întâi să actualizați toate jurnalele pentru a spune corect cine a efectuat acea operație, nu?

Acum imaginați-vă că am implementat metoda de actualizare a utilizatorilor și a jurnalelor în diferite depozite și ambii apelează la SaveChanges. Ce se întâmplă dacă una dintre aceste metode eșuează la mijlocul procesului de actualizare? Vei ajunge la inconsecvența datelor.

Ar trebui să ne salvăm modificările în baza de date numai după ce totul se termină. Pentru a face acest lucru, trebuie să utilizăm o tranzacție, care este practic o caracteristică pe care majoritatea bazelor de date o implementează pentru a salva datele numai după ce o operațiune complexă se termină.

„- Ok, deci dacă nu putem salva lucrurile aici, unde ar trebui să o facem?”

Un model comun pentru a trata această problemă este Unitatea de model de lucru. Acest model constă dintr-o clasă care primește instanța noastră AppDbContext ca dependență și expune metode pentru a începe, finaliza sau anula tranzacțiile.

Vom folosi o simplă implementare a unei unități de lucru pentru a aborda problema noastră aici.

Adăugați o nouă interfață în folderul Repositories din stratul de domeniu numit IUnitOfWork:

După cum vedeți, acesta expune doar o metodă care va finaliza asincron operațiunile de gestionare a datelor.

Să adăugăm acum implementarea reală.

Adăugați o nouă clasă numită UnitOfWork la folderul RepositoriesRepositories din stratul Persistence:

Aceasta este o implementare simplă, curată, care va salva toate modificările în baza de date numai după ce vei finaliza modificarea folosind depozitele.

Dacă studiați implementările modelului unității de lucru, veți găsi altele mai complexe care implementează operațiuni de returnare.

Deoarece EF Core implementează deja modelul de depozitare și unitatea de lucru din culise, nu trebuie să ne intereseze de o metodă de returnare.

" - Ce? De ce trebuie să creăm toate aceste interfețe și clase? ”

Separarea logicii de persistență de regulile de afaceri oferă multe avantaje în ceea ce privește reutilizarea și întreținerea codului. Dacă folosim direct EF Core, vom avea clase mai complexe care nu vor fi atât de ușor de modificat.

Imaginați-vă că în viitor vă decideți să schimbați cadrul ORM într-unul diferit, cum ar fi Dapper, de exemplu, sau dacă trebuie să implementați interogări SQL simple din cauza performanței. Dacă asociați logica interogărilor cu serviciile dvs., va fi dificil să schimbați logica, deoarece va trebui să o faceți în multe clase.

Utilizând modelul de depozit, puteți pur și simplu să implementați o nouă clasă de depozit și să o legați folosind injecția de dependență.

Deci, practic, dacă utilizați EF Core direct în serviciile dvs. și trebuie să schimbați ceva, asta veți obține:

Așa cum spuneam, EF Core implementează modelele Unității de Lucru și Depozitare din culise. Putem considera proprietățile noastre DbSet <> ca depozite. De asemenea, SaveChanges persistă doar datele în caz de succes pentru toate operațiunile bazei de date.

Acum că știți ce este o unitate de lucru și de ce să o utilizați cu depozite, să implementăm logica serviciului real.

Datorită arhitecturii noastre decupleate, putem trece pur și simplu o instanță a UnitOfWork ca o dependență pentru această clasă.

Logica noastră de afaceri este destul de simplă.

În primul rând, încercăm să adăugăm noua categorie în baza de date și apoi API încercăm să o salvăm, înglobând totul într-un bloc try-catch.

Dacă ceva nu reușește, API apelează la un serviciu de logare fictiv și returnează un răspuns care indică eșecul.

Dacă procesul se termină fără probleme, aplicația returnează un răspuns de succes, trimițând datele categoriei noastre. Simplu, nu?

Sfat: în aplicațiile din lumea reală, nu ar trebui să înveliți totul într-un bloc generic try-catch, ci în schimb ar trebui să gestionați toate erorile posibile separat.
Simpla adăugare a unui bloc de încercare nu va acoperi majoritatea scenariilor care nu pot fi reușite. Asigurați-vă că corectați implementarea procesării erorilor.

Ultimul pas înainte de testarea API-ului nostru este legarea unității de interfață de lucru la clasa respectivă.

Adăugați această nouă linie la metoda ConfigureServices din clasa Startup:

servicii.AddScoped  ();

Acum, să o testăm!

Pasul 14 - Testarea POST-ului nostru final cu ajutorul Postman

Porniți aplicația noastră din nou folosind rularea dotnet.

Nu putem folosi browserul pentru a testa un punct final POST. Să folosim Postman pentru a ne testa obiectivele. Este un instrument foarte util pentru testarea API-urilor RESTful.

Deschide Postman și închide mesajele de introducere. Veți vedea un ecran ca acesta:

Ecran care arată opțiunile pentru a testa punctele finale

Modificați GET-ul selectat în mod implicit în caseta de selectare în POST.

Tastați adresa API în câmpul Introducere URL adresă solicitare.

Trebuie să furnizăm datele organismului de solicitare pentru a le trimite la API-ul nostru. Faceți clic pe elementul de meniu Body, apoi modificați opțiunea afișată sub acesta în brută.

Poștașul va arăta o opțiune Text în dreapta. Schimbați-l în JSON (aplicație / json) și lipiți următoarele date JSON de mai jos:

{
  "Nume": ""
}
Ecranează înainte de a trimite o solicitare

După cum vedeți, vom trimite un șir de nume gol la noul nostru punct de vedere.

Faceți clic pe butonul Trimite. Veți primi o ieșire astfel:

După cum vedeți, logica noastră de validare funcționează!

Vă amintiți logica de validare pe care am creat-o pentru final? Această ieșire este dovada că funcționează!

Observați și codul de stare 400 afișat în partea dreaptă. Rezultatul BadRequest adaugă automat acest cod de stare la răspuns.

Acum să schimbăm datele JSON într-una valabilă pentru a vedea noul răspuns:

În cele din urmă, rezultatul pe care ne-am așteptat să îl avem

API-ul a creat corect noua noastră resursă.

Până acum, API-ul nostru poate lista și crea categorii. Ați învățat o mulțime de lucruri despre limbajul C #, cadrul ASP.NET Core și, de asemenea, abordări comune de proiectare pentru a vă structura API-urile.

Continuăm API-ul categoriilor noastre creând punctul final pentru actualizarea categoriilor.

De acum înainte, de când v-am explicat cele mai multe concepte, voi grăbi explicațiile și mă voi concentra pe subiecte noi pentru a nu pierde timpul. Sa mergem!

Pasul 15 - Actualizarea categoriilor

Pentru a actualiza categoriile, avem nevoie de un punct final HTTP PUT.

Logica pe care trebuie să o codăm este foarte asemănătoare cu cea POST:

  • În primul rând, trebuie să validăm cererea primită folosind ModelState;
  • Dacă solicitarea este valabilă, API-ul ar trebui să asocieze resursa primită la o clasă de model folosind AutoMapper;
  • Apoi, trebuie să apelăm la serviciul nostru, spunându-i să actualizeze categoria, furnizând ID-ul categoriei respective și datele actualizate;
  • Dacă nu există nicio categorie cu ID-ul dat în baza de date, returnăm o solicitare necorespunzătoare. Am putea folosi în schimb un rezultat NotFound, dar nu contează cu adevărat pentru acest domeniu, deoarece oferim un mesaj de eroare aplicațiilor client;
  • Dacă logica de salvare este executată corect, serviciul trebuie să returneze un răspuns care conține datele categoriei actualizate. Dacă nu, ar trebui să ne ofere un indiciu că procesul a eșuat și un mesaj care să indice de ce;
  • În cele din urmă, dacă există o eroare, API-ul returnează o solicitare necorespunzătoare. Dacă nu, acesta mapează modelul de categorie actualizat într-o resursă de categorie și returnează un răspuns de succes aplicației client.

Să adăugăm noua metodă PutAsync în clasa de controler:

Dacă îl comparați cu logica POST, veți observa că aici avem o singură diferență: atributul HttPut specifică un parametru pe care ar trebui să-l primească ruta dată.

Vom numi acest punct final specificând ID-ul categoriei drept ultimul fragment URL, cum ar fi / api / categories / 1. Conducta de bază ASP.NET analizează acest fragment pe parametrul cu același nume.

Acum trebuie să definim semnătura metodei UpdateAsync în interfața ICategoryService:

Acum să trecem la logica reală.

Pasul 16 - Logica de actualizare

Pentru a actualiza categoria noastră, mai întâi, trebuie să returnăm datele curente din baza de date, dacă există. De asemenea, trebuie să-l actualizăm în DBSet-ul nostru <>.

Să adăugăm două noi contracte de metodă la interfața noastră ICategoryService:

Am definit metoda FindByIdAsync, care va returna asincron o categorie din baza de date și metoda Actualizare. Atenție că metoda de actualizare nu este asincronă, deoarece API-ul EF Core nu necesită o metodă asincronă pentru actualizarea modelelor.

Acum implementăm adevărata logică în clasa CategoryRepository:

În sfârșit, putem codifica logica serviciului:

API încearcă să obțină categoria din baza de date. Dacă rezultatul este nul, returnăm un răspuns care spune că categoria nu există. Dacă categoria există, trebuie să-i stabilim noul nume.

Apoi, API încearcă să salveze modificările, ca atunci când creăm o nouă categorie. Dacă procesul se finalizează, serviciul returnează un răspuns de succes. Dacă nu, logica de înregistrare se execută, iar punctul final primește un răspuns care conține un mesaj de eroare.

Acum să o testăm. În primul rând, să adăugăm o nouă categorie pentru a avea un ID valid de utilizat. Am putea folosi identificatorii categoriilor pe care le semănăm în baza noastră de date, dar vreau să o fac în acest fel pentru a vă arăta că API-ul nostru va actualiza resursa corectă.

Rulați aplicația din nou și, utilizând Postman, POST o nouă categorie în baza de date:

Adăugarea unei noi categorii pentru actualizarea ulterioară

Având un ID valid pe mâini, schimbați opțiunea POST în PUT în caseta selectată și adăugați valoarea ID la sfârșitul adresei URL. Schimbați proprietatea nume cu un alt nume și trimiteți solicitarea pentru a verifica rezultatul:

Datele categoriei au fost actualizate cu succes

Puteți trimite o solicitare GET către punctul final al API pentru a vă asigura că ați modificat corect numele categoriei:

Acesta este rezultatul unei solicitări GET acum

Ultima operație pe care trebuie să o implementăm pentru categorii este excluderea categoriilor. Să o facem creând un punct final de ștergere HTTP.

Pasul 17 - Ștergerea categoriilor

Logica pentru ștergerea categoriilor este cu adevărat ușor de implementat, deoarece majoritatea metodelor de care avem nevoie au fost construite anterior.

Acestea sunt pașii necesari pentru ca ruta noastră să funcționeze:

  • API trebuie să apeleze serviciul nostru, spunându-i să șterge categoria noastră, furnizând ID-ul respectiv;
  • Dacă în baza de date nu există nicio categorie cu ID-ul dat, serviciul ar trebui să returneze un mesaj care să îl indice;
  • Dacă logica de ștergere este executată fără probleme, serviciul ar trebui să returneze un răspuns care conține datele categoriei noastre șterse. Dacă nu, ar trebui să ne ofere un indiciu că procesul a eșuat și un mesaj de eroare potențial;
  • În cele din urmă, dacă există o eroare, API-ul returnează o solicitare necorespunzătoare. Dacă nu, API mapează categoria actualizată într-o resursă și returnează un răspuns de succes clientului.

Să începem adăugând noua logică a punctului final:

Atributul HttpDelete definește, de asemenea, un șablon de id.

Înainte de a adăuga semnătura DeleteAsync la interfața noastră ICategoryService, trebuie să facem o mică refactorizare.

Noua metodă de serviciu trebuie să returneze un răspuns care să conțină datele categoriei, la fel cum am procedat și pentru metodele PostAsync și UpdateAsync. Am putea reutiliza răspunsul SaveCategoryResponse în acest scop, dar nu salvăm date în acest caz.

Pentru a evita crearea unei clase noi cu aceeași formă pentru a livra această cerință, putem redenumi pur și simplu SaveCategoryResponse la CategoryResponse.

Dacă utilizați Visual Studio Code, puteți deschide clasa SaveCategoryResponse, puneți cursorul mouse-ului deasupra numelui clasei și folosiți opțiunea Change All Ocurrencies pentru a redenumi clasa:

Mod simplu de a schimba numele în toate fișierele

Asigurați-vă că redenumiți și numele fișierului.

Să adăugăm semnătura metodei DeleteAsync în interfața ICategoryService:

Înainte de a implementa logica de ștergere, avem nevoie de o nouă metodă în depozitul nostru.

Adăugați semnătura metodei Eliminare la interfața ICategoryRepository:

void Remove (categorie categorie);

Și acum adăugați implementarea reală în clasa depozitelor:

EF Core necesită ca instanța modelului nostru să fie transmisă metodei Eliminare pentru a înțelege corect ce model ștergem, în loc să trecem pur și simplu cu un ID.

În cele din urmă, să implementăm logica în clasa CategoryService:

Nu este nimic nou aici. Serviciul încearcă să găsească categoria după ID și apoi apelează la depozitul nostru pentru a șterge categoria. În cele din urmă, unitatea de lucru finalizează tranzacția executând operația reală în baza de date.

„- Hei, dar ce zici de produsele din fiecare categorie? Nu este necesar să creați un depozit și să ștergeți produsele pentru a evita erorile? ”

Raspunsul este nu. Datorită mecanismului de urmărire EF Core, atunci când încărcăm un model din baza de date, cadrul știe ce relații are modelul. Dacă îl ștergem, EF Core știe că ar trebui să șteargă mai întâi toate modelele conexe, recursiv.

Putem dezactiva această caracteristică atunci când mapăm clasele noastre în tabelele bazei de date, dar este în afara acestui domeniu. Uitați-vă aici dacă doriți să aflați despre această caracteristică.

Acum este timpul să testăm noul nostru punct de vedere. Rulați aplicația din nou și trimiteți o solicitare Ștergeți utilizând Postman după cum urmează:

După cum vedeți, API a șters categoria existentă fără probleme

Putem verifica dacă API-ul nostru funcționează corect trimitând o solicitare GET:

Ca rezultat, primim o singură categorie

Am terminat API-ul categoriilor. Acum este timpul să treceți la API-ul produselor.

Pasul 18 - API-ul Produselor

Până acum ați învățat cum să implementați toate verbele HTTP de bază pentru a gestiona operațiunile CRUD cu ASP.NET Core. Să trecem la nivelul următor, punând în aplicare API-ul produselor noastre.

Nu voi detalia din nou toate verbele HTTP, deoarece ar fi exhaustiv. Pentru partea finală a acestui tutorial, voi acoperi doar cererea GET, pentru a vă arăta cum să includeți entități conexe atunci când interogați date din baza de date și cum să utilizați atributele Descriere pe care le-am definit pentru valorile de enumerare EUnitOfMeasurement.

Adăugați un nou controler în folderul Controlere numit ProductsController.

Înainte de a codifica ceva aici, trebuie să creăm resursa produsului.

Permiteți-mi să vă reîmprospătați memoria arătând din nou cum ar trebui să arate resursa noastră:

{
 [
  {
   "id": 1,
   "nume": "Zahăr",
   "QuantInPackage": 1,
   "unitOfMeasurement": "KG"
   "categorie": {
   „id”: 3,
   "nume": "Zahăr"
   }
  },
  … // Alte produse
 ]
}

Vrem un tablou JSON care să conțină toate produsele din baza de date.

Datele JSON diferă de modelul produsului prin două aspecte:

  • Unitatea de măsură este afișată într-un mod mai scurt, arătând doar prescurtarea acesteia;
  • Producem datele categoriei fără a include proprietatea CategoryId.

Pentru a reprezenta unitatea de măsură, putem folosi o proprietate de șir simplu în loc de tip enum (apropo, nu avem un tip de enumeră implicit pentru datele JSON, deci trebuie să o transformăm într-un alt tip).

Acum că avem acum cum să modelăm noua resursă, să o creăm. Adăugați o nouă clasă ProductResource în folderul Resurse:

Acum trebuie să configurăm maparea între clasa model și noua noastră clasă de resurse.

Configurația de mapare va fi aproape aceeași cu cea utilizată pentru alte mapări, dar aici trebuie să ne ocupăm de transformarea enumerului nostru EUnitOfMeasurement într-un șir.

Vă amintiți atributul StringValue aplicat peste tipurile de enumerare? Acum vă voi arăta cum să extrageți aceste informații folosind o caracteristică puternică a cadrului .NET: API-ul Reflection.

API-ul Reflection este un set puternic de resurse care ne permite să extragem și să manipulăm metadatele. O mulțime de cadre și biblioteci (inclusiv ASP.NET Core în sine) folosesc aceste resurse pentru a gestiona multe lucruri din culise.

Acum să vedem cum funcționează în practică. Adăugați o clasă nouă în folderul Extensii numit EnumExtensions.

Poate părea că sperie prima dată când te uiți la cod, dar nu este atât de complex. Să descompunem definiția codului pentru a înțelege cum funcționează.

În primul rând, am definit o metodă generică (o metodă care poate primi mai mult de un tip de argument, în acest caz, reprezentată prin declarația TEnum) care primește un enumer dat ca argument.

Întrucât enum este un cuvânt cheie rezervat în C #, am adăugat un @ în fața numelui parametrului pentru a-l face un nume valid.

Prima etapă de execuție a acestei metode este obținerea informațiilor despre tip (clasa, interfața, enumerarea sau definiția struct) a parametrului folosind metoda GetType.

Apoi, metoda obține valoarea specifică de enumerare (de exemplu, Kilogram) folosind GetField (@ enum.ToString ()).

Următoarea linie găsește toate atributele Descriere aplicate peste valoarea de enumerare și stochează datele lor într-un tablou (putem specifica mai multe atribute pentru aceeași proprietate în unele cazuri).

Ultima linie folosește o sintaxă mai scurtă pentru a verifica dacă avem cel puțin un atribut de descriere pentru tipul de enumerare. Dacă avem, returnăm valoarea Descriere furnizată de acest atribut. Dacă nu, vom returna enumerarea ca o șir, folosind turnarea implicită.

? operatorul (un operator nul condiționat) verifică dacă valoarea este nulă înainte de a accesa proprietatea sa.

?? operator (un operator cu coalescență nulă) cere aplicației să returneze valoarea din stânga dacă nu este goală sau altfel valoarea din dreapta.

Acum că avem o metodă de extensie pentru extragerea descrierilor, să configurăm maparea noastră între model și resursă. Mulțumită AutoMapper, o putem face cu o singură linie suplimentară.

Deschideți clasa ModelToResourceProfile și schimbați codul astfel:

Această sintaxă spune AutoMapper să utilizeze noua metodă de extensie pentru a converti valoarea noastră EUnitOfMeasurement într-un șir care conține descrierea sa. Simplu, nu? Puteți citi documentația oficială pentru a înțelege sintaxa completă.

Observați că nu am definit nicio configurație de mapare pentru proprietatea categoriei. Deoarece anterior am configurat maparea pentru categorii și pentru că modelul de produs are o proprietate de categorie de același tip și același nume, AutoMapper știe implicit că ar trebui să o mapeze folosind configurația respectivă.

Acum adăugăm codul final. Schimbați codul ProductsController:

Practic, aceeași structură definită pentru controlorul de categorii.

Să mergem la partea de service. Adăugați o nouă interfață IProductService în folderul Servicii prezent la stratul de domeniu:

Ar fi trebuit să vă dați seama că avem nevoie de un depozit înainte de a implementa cu adevărat noul serviciu.

Adăugați o nouă interfață numită IProductRepository în folderul respectiv:

Acum implementăm depozitul. Trebuie să îl implementăm aproape în același mod în care am făcut-o pentru depozitul de categorii, cu excepția faptului că trebuie să returnăm datele de categorie respective ale fiecărui produs la interogarea datelor.

În mod implicit, EF Core nu include entități legate de modelele dvs. atunci când întrebați date, deoarece acestea ar putea fi foarte lente (imaginați-vă un model cu zece entități conexe, toate entitățile conexe având relații proprii).

Pentru a include datele categoriilor, avem nevoie de o singură linie suplimentară:

Observați apelul pentru a include (p => p.Categorie). Putem înlătura această sintaxă pentru a include cât mai multe entități necesare atunci când interogăm date. EF Core o va traduce într-o aderare atunci când efectuați selecția.

Acum putem implementa clasa ProductService așa cum am făcut-o pentru categorii:

Să legăm noile dependențe care schimbă clasa Startup:

În cele din urmă, înainte de a testa API, schimbăm clasa AppDbContext pentru a include unele produse la inițializarea aplicației, astfel încât să putem vedea rezultatele:

Am adăugat două produse fictive care le asociază categoriilor pe care le semințăm la inițializarea aplicației.

Timpul de a testa! Rulați API-ul din nou și trimiteți o solicitare GET către / api / produse utilizând Postman:

Voilà! Iată produsele noastre

Si asta e! Felicitări!

Acum aveți o bază despre cum să construiți o API RESTful folosind ASP.NET Core folosind o arhitectură decuplată. Ați învățat multe lucruri din cadrul .NET Core, cum să lucrați cu C #, elementele de bază ale EF Core și AutoMapper și multe modele utile pe care să le utilizați la proiectarea aplicațiilor.

Puteți verifica implementarea completă a API-ului, care conține celelalte verbe HTTP pentru produse, verificând depozitul Github:

Concluzie

ASP.NET Core este un cadru excelent de utilizat atunci când creați aplicații web. Este livrat cu multe API-uri utile pe care le puteți utiliza pentru a crea aplicații curate și de întreținut. Luați în considerare o opțiune atunci când creați aplicații profesionale.

Acest articol nu a acoperit toate aspectele unei API profesionale, dar ai aflat toate elementele de bază. De asemenea, ați învățat multe tipare utile pentru a rezolva tiparele cu care ne confruntăm zilnic.

Sper că v-a plăcut acest articol și sper că v-a fost util. Apreciez feedback-ul dvs. pentru a înțelege cum pot îmbunătăți acest lucru.

Referințe la Continuarea învățării

.NET Core Tutoriale - Documente Microsoft

Documentare de bază ASP.NET - Documente Microsoft