Страхотно ръководство за това как да изградите RESTful APIs с ASP.NET Core

Стъпка по стъпка ръководство за това как да внедрите чисти, поддържани RESTful API

Снимка на Джеферсън Сантос, публикувана в Unsplash

Преглед

RESTful не е нов термин. Тя се отнася до архитектурен стил, при който уеб услугите получават и изпращат данни от и към клиентски приложения. Целта на тези приложения е да централизират данни, които различните клиентски приложения ще използват.

Изборът на правилните инструменти за писане на RESTful услуги е от решаващо значение, тъй като трябва да се грижим за мащабируемост, поддръжка, документация и всички други важни аспекти. ASP.NET Core ни дава мощен, лесен за използване API, който е чудесен за постигане на тези цели.

В тази статия ще ви покажа как да напишете добре структуриран RESTful API за „почти“ сценарий в реалния свят, използвайки основата на ASP.NET Core. Ще опиша подробни модели и стратегии за опростяване на процеса на разработка.

Ще ви покажа и как да интегрирате общи рамки и библиотеки, като Entity Framework Core и AutoMapper, за да осигурите необходимите функционалности.

Предварителни

Очаквам да имате познания за обектно-ориентираните концепции за програмиране.

Въпреки че ще покрия много подробности за езика за програмиране на C #, препоръчвам ви да имате основни познания по тази тема.

Предполагам също, че знаете какво е REST, как работи HTTP протоколът, какви са крайните точки на API и какво е JSON. Ето един страхотен уводен урок по тази тема. Крайното изискване е да разберете как работят релационните бази данни.

За да кодирате заедно с мен, ще трябва да инсталирате .NET Core 2.2, както и Postman, инструмента, който ще използвам за тестване на API. Препоръчвам ви да използвате редактор на кодове, като например Visual Studio Code, за да разработите API. Изберете редактора на кода, който предпочитате. Ако изберете този редактор на кодове, препоръчвам ви да инсталирате разширението C #, за да имате по-добро подчертаване на кода.

Можете да намерите линк към хранилището на Github на API в края на тази статия, за да проверите крайния резултат.

Обхвата

Да напишем измислен уеб API за супермаркет. Нека си представим, че трябва да приложим следния обхват:

  • Създайте RESTful услуга, която позволява на клиентските приложения да управляват продуктовия каталог на супермаркета. Необходимо е да се разкрият крайни точки за създаване, четене, редактиране и изтриване на категории продукти, като млечни продукти и козметика, както и за управление на продукти от тези категории.
  • За категориите трябва да съхраняваме техните имена. За продуктите трябва да съхраняваме техните имена, единица за измерване (например KG за продукти, измерени по тегло), количество в опаковката (например 10, ако продуктът е пакет бисквити) и съответните им категории.

За да опростя примера, няма да обработвам продукти на склад, доставка на продукти, сигурност и друга функционалност. Даденият обхват е достатъчен, за да ви покаже как работи ASP.NET Core.

За да развием тази услуга, ние основно се нуждаем от две крайни точки на API: една за управление на категории и една за управление на продукти. По отношение на комуникацията с JSON можем да мислим за отговорите, както следва:

Крайна точка на API: / api / категории

Отговор JSON (за GET заявки):

{
  [
    {"id": 1, "name": "Плодове и зеленчуци"},
    {"id": 2, "name": "Хлябове"},
    … // Други категории
  ]
}

Крайна точка на API: / api / продукти

Отговор JSON (за GET заявки):

{
  [
    {
      "id": 1,
      "име": "Захар",
      „количество в опаковка“: 1,
      "unitOfMeasurement": "KG"
      "категория": {
        "id": 3,
        "име": "Захар"
      }
    }
    … // Други продукти
  ]
}

Нека започнем да пишем молбата.

Стъпка 1 - Създаване на API

На първо място, ние трябва да създадем структурата на папките за уеб услугата, а след това трябва да използваме инструментите .NET CLI, за да изработим основен уеб API. Отворете терминала или командния ред (зависи от операционната система, която използвате) и въведете следните команди последователно:

mkdir src / Супермаркет.API
cd src / Супермаркет.API
dotnet нови webapi

Първите две команди просто създават нова директория за API и променят текущото местоположение в новата папка. Последният генерира нов проект, следвайки шаблона на Web API, който е вида на приложението, което разработваме. Можете да прочетете повече за тези командни и други шаблони на проекти, които можете да генерирате, проверявайки тази връзка.

Новата директория сега ще има следната структура:

Структура на проекта

Преглед на структурата

Приложението ASP.NET Core се състои от група междинни продукти (малки парчета от приложението, прикачени към тръбопровода на приложението, които обработват заявки и отговори), конфигурирани в клас Startup. Ако вече сте работили с рамки като Express.js преди, тази концепция не е нова за вас.

Когато стартира приложението, се извиква основният метод от класа на програмата. Той създава уеб хост по подразбиране, използвайки стартиращата конфигурация, излагайки приложението чрез HTTP през специфичен порт (по подразбиране, порт 5000 за HTTP и 5001 за HTTPS).

Разгледайте класа ValuesController в папката Controllers. Той излага методи, които ще бъдат извиквани, когато API получава заявки през route / api / values.

Не се притеснявайте, ако не разбирате част от този код. Ще опиша подробно всяка от тях при разработването на необходимите крайни точки за API. Засега просто изтрийте този клас, тъй като няма да го използваме.

Стъпка 2 - Създаване на модели на домейни

Ще прилагам някои дизайнерски концепции, които ще направят приложението просто и лесно за поддръжка.

Написването на код, който можете да разберете и поддържате сами, не е толкова трудно, но трябва да имате предвид, че ще работите като част от екип. Ако не се грижите как пишете кода си, резултатът ще бъде чудовище, което ще доведе до вас и вашите съотборници постоянно главоболие. Звучи екстремно, нали? Но повярвайте ми, това е истината

wtf - измерването на качеството на кода от smitty42 е лицензирано под CC-BY-ND 2.0

Нека започнем с писането на домейн слоя. Този слой ще има класовете на нашите модели, класовете, които ще представят нашите продукти и категории, както и интерфейси за хранилища и услуги. Ще обясня тези две последни понятия след време.

Вътре в директорията Supermarket.API създайте нова папка, наречена Domain. В новата папка на домейна създайте друга, наречена Модели. Първият модел, който трябва да добавим към тази папка, е категорията. Първоначално това ще бъде обикновен клас на обикновен CLR обект (POCO). Това означава, че класът ще има само свойства, които да описват неговата основна информация.

Класът има свойство Id, за да идентифицира категорията и свойство Name. Имаме и собственост на Продукти. Този последен ще бъде използван от Entity Framework Core, който повечето ORP.NET Core приложения на ORM използват, за да съхраняват данни в база данни, за да картографират връзката между категории и продукти. Освен това има смисъл да се мисли по отношение на обектно-ориентираното програмиране, тъй като категорията има много свързани продукти.

Трябва да създадем и модела на продукта. В същата папка добавете нов клас продукт.

Продуктът също има свойства за Id и име. Това е също свойство QuantityInPackage, което показва колко единици от продукта имаме в една опаковка (не забравяйте примера с бисквити от обхвата на приложението) и свойството UnitOfMeasurement. Този е представен от enum тип, който представлява изброяване на възможни мерни единици. Последните две свойства, CategoryId и Category ще бъдат използвани от ORM за картографиране на връзката между продукти и категории. Указва, че продуктът има една и само една категория.

Да дефинираме последната част от нашите модели на домейни, сумата EUnitOfMeasurement.

По конвенция не е необходимо enums да започват с „E“ пред имената им, но в някои библиотеки и рамки ще намерите този префикс като начин за разграничаване на enums от интерфейси и класове.

Кодът е наистина прям. Тук дефинирахме само няколко възможности за мерни единици, но в истинска супермаркет система може да имате много други измервателни единици и може би отделен модел за това.

Забележете атрибута Description, приложен върху всяка възможност за изброяване. Атрибут е начин за дефиниране на метаданни за класове, интерфейси, свойства и други компоненти на езика C #. В този случай ще го използваме, за да опростим отговорите на крайната точка на API на продуктите, но засега не е нужно да се интересувате от това. Ще се върнем тук по-късно.

Основните ни модели са готови за използване. Сега можем да започнем да пишем крайната точка на API, която ще управлява всички категории.

Стъпка 3 - API на категориите

В папката Controllers добавете нов клас, наречен CategoriesController.

По конвенция всички класове в тази папка, които завършват с наставка "Контролер", ще станат контролери на нашето приложение. Това означава, че ще се справят с искания и отговори. Трябва да наследите този клас от класа Controller, дефиниран в пространството на имената Microsoft.AspNetCore.Mvc.

Пространството от имена се състои от група от свързани класове, интерфейси, enums и структури. Можете да го мислите за нещо подобно на модули на езика Javascript или пакети от Java.

Новият контролер трябва да реагира чрез маршрута / api / категории. Постигаме това чрез добавяне на атрибута Route над името на клас, като посочваме заместител, който показва, че маршрутът трябва да използва името на класа без суфикса на контролера, по конвенция.

Нека започнем да обработваме GET заявки. На първо място, когато някой поиска данни от / api / категории чрез глагол GET, API трябва да върне всички категории. За тази цел можем да създадем категория услуга.

В концептуален план услугата е основно клас или интерфейс, който определя методите за работа с някаква бизнес логика. Често срещана практика е в много различни езици на програмиране да се създават услуги за обработка на бизнес логика, като автентификация и оторизация, плащания, сложни потоци от данни, кеширане и задачи, които изискват известно взаимодействие между други услуги или модели.

Използвайки услуги, можем да изолираме обработката на заявки и отговори от реалната логика, необходима за изпълнение на задачите.

Услугата, която ще създадем първоначално, ще дефинира едно поведение или метод: метод за изброяване. Очакваме, че този метод връща всички съществуващи категории в базата данни.

За простота в този случай няма да се занимаваме с странициране на данни или филтриране. В бъдеще ще напиша статия, която показва как лесно да се справя с тези функции.

За да дефинираме очаквано поведение за нещо в C # (и в други обектно-ориентирани езици, като Java например), ние дефинираме интерфейс. Интерфейс казва как трябва да работи нещо, но не прилага реалната логика на поведението. Логиката се реализира в класове, които реализират интерфейса. Ако тази концепция не ви е ясна, не се притеснявайте. Ще го разберете след време.

В папката на домейна създайте нова директория, наречена Услуги. Там добавете интерфейс, наречен ICategoryService. По конвенция всички интерфейси трябва да започват с главна буква "I" в C #. Определете интерфейсния код, както следва:

Реализациите на метода ListAsync трябва да връщат асинхронно изброяване на категории.

Класът Task, капсулирайки връщането, показва асинхронност. Трябва да мислим по асинхронен метод поради факта, че трябва да изчакаме базата данни да завърши някаква операция, за да върне данните и този процес може да отнеме известно време. Забележете и наставката „async“. Това е конвенция, която показва, че нашият метод трябва да се изпълнява асинхронно.

Имаме много условности, нали? На мен лично ми харесва, защото поддържа приложенията лесни за четене, дори ако сте нов в компания, която използва .NET технология.

„- Добре, дефинирахме този интерфейс, но той не прави нищо. Как може да бъде полезно? “

Ако идвате от език като Javascript или друг не силно въведен език, тази концепция може да изглежда странна.

Интерфейсите ни позволяват да абстрахираме желаното поведение от реалното изпълнение. Използвайки механизъм, известен като инжектиране на зависимост, можем да реализираме тези интерфейси и да ги изолираме от други компоненти.

По принцип, когато използвате инжектиране на зависимост, дефинирате някои поведения, използвайки интерфейс. След това създавате клас, който реализира интерфейса. Накрая обвързвате препратките от интерфейса към класа, който сте създали.

„- Звучи наистина объркващо. Не можем ли просто да създадем клас, който прави тези неща за нас? “

Нека продължим да прилагаме нашия API и ще разберете защо да използвате този подход.

Променете кода на категорииController, както следва:

Определих функция конструктор за нашия контролер (конструктор се извиква, когато се създаде нов екземпляр от клас), и той получава екземпляр от ICategoryService. Това означава, че инстанцията може да бъде всичко, което реализира сервизния интерфейс. Съхранявам този случай в частно поле _categoryService, само за четене. Ще използваме това поле за достъп до методите за изпълнение на нашата категория услуги.

Между другото, префиксът за подчертаване е друга често срещана конвенция за означаване на поле. Тази конвенция, по-специално, не се препоръчва от официалното ръководство за конвенционално именуване на .NET, но е много често срещана практика като начин да се избегне използването на ключовата дума „това“ за разграничаване на класовите полета от локалните променливи. Аз лично смятам, че е много по-чисто да се чете и много рамки и библиотеки използват тази конвенция.

Под конструктора дефинирах метода, който ще обработва заявки за / api / категории. Атрибутът HttpGet казва на тръбопровода ASP.NET Core да го използва за обработка на GET заявки (този атрибут може да бъде пропуснат, но е по-добре да го напишете за по-лесна четливост).

Методът използва нашия потребителски екземпляр за категория, за да изброи всички категории и след това връща категориите на клиента. Рамковият тръбопровод обработва сериализацията на данни към обект JSON. Типът IEnumerable казва на рамката, че искаме да върнем изброяване на категории, а типът Task, предшестван от ключовата дума async, казва на тръбопровода, че този метод трябва да бъде изпълнен асинхронно. И накрая, когато дефинираме метод за асинхронизация, трябва да използваме ключовата дума в очакване за задачи, които могат да отнемат известно време.

Добре дефинирахме първоначалната структура на нашия API. Сега е необходимо наистина да се приложи услугата за категории.

Стъпка 4 - Реализиране на услугата за категории

В основната папка на API (папката Supermarket.API) създайте нова, наречена Услуги. Тук ще поставим всички реализации на услуги. Вътре в новата папка добавете нов клас, наречен CategoryService. Променете кода, както следва:

Това е просто основният код за внедряване на интерфейса, но все още не се справяме с никаква логика. Нека помислим как трябва да работи методът на изброяване

Трябва да имаме достъп до базата данни и да върнем всички категории, тогава трябва да върнем тези данни на клиента.

Класът на обслужване не е клас, който трябва да обработва достъпа до данни. Има модел, наречен шаблон на хранилище, който се използва за управление на данни от бази данни.

Когато използваме шаблона на хранилището, ние дефинираме класове на хранилище, които по същество капсулират цялата логика за обработка на достъпа до данни. Тези хранилища излагат методи за изброяване, създаване, редактиране и изтриване на обекти от даден модел, по същия начин, по който можете да манипулирате колекциите. Вътрешно тези методи говорят с базата данни за извършване на CRUD операции, изолирайки достъпа до базата данни от останалата част от приложението.

Нашата услуга трябва да говори с хранилище за категории, за да получи списъка с обекти.

В концептуален план услугата може да „разговаря“ с едно или повече хранилища или други услуги за извършване на операции.

Може да изглежда излишно да се създаде нова дефиниция за работа с логиката за достъп до данни, но след време ще видите, че изолирането на тази логика от клас на услуги е наистина изгодно.

Нека създадем хранилище, което ще отговаря за посредничеството на комуникацията в базата данни като начин за запазване на категориите.

Стъпка 5 - хранилището на категориите и слоя за устойчивост

Вътре в папката на домейна създайте нова директория, наречена Repositories. След това добавете нов интерфейс, наречен ICategoryRespository. Определете интерфейса, както следва:

Първоначалният код е основно идентичен с кода на сервизния интерфейс.

След като дефинираме интерфейса, можем да се върнем към класа на услугите и да завършим прилагането на метода на списъка, използвайки екземпляр от ICategoryRepository за връщане на данните.

Сега трябва да приложим реалната логика на категорията хранилище. Преди да го направим, трябва да помислим как ще имаме достъп до базата данни.

Между другото, ние все още нямаме база данни!

Ще използваме Entity Framework Core (ще го нарека EF Core за простота) като наша база данни ORM. Тази рамка идва с ASP.NET Core като ORM по подразбиране и излага приятелски API, който ни позволява да пренасочваме класовете на нашите приложения към таблиците с бази данни.

EF Core също така ни позволява първо да проектираме нашето приложение и след това да генерираме база данни според това, което сме дефинирали в нашия код. Тази техника се нарича първо код. Първият подход ще използваме кода, за да генерираме база данни (в този пример всъщност аз ще използвам база данни в паметта, но вие ще можете лесно да я промените в екземпляр на SQL Server или MySQL сървър, например).

В основната папка на API създайте нова директория, наречена Устойчивост. Тази директория ще има всичко необходимо за достъп до базата данни, като например реализации на хранилища.

Вътре в новата папка създайте нова директория, наречена Contexts, и след това добавете нов клас, наречен AppDbContext. Този клас трябва да наследи DbContext, клас EF Core използва за картографиране на вашите модели в таблиците на базата данни. Променете кода по следния начин:

Конструкторът, който добавихме към този клас, е отговорен за предаването на конфигурацията на базата данни към базовия клас чрез инжектиране на зависимост. След време ще видите как става това.

Сега трябва да създадем две свойства на DbSet. Тези свойства са набори (колекции от уникални обекти), които преобразуват моделите в таблиците на базата данни.

Също така, ние трябва да картографираме свойствата на моделите в съответните колони на таблицата, като посочваме кои свойства са първични ключове, кои са чужди ключове, типове колони и т.н. Можем да направим това, като отменим метода OnModelCreating, използвайки функция, наречена Fluent API, за да посочете картографирането на базата данни. Променете класа на AppDbContext, както следва:

Кодът е интуитивен.

Ние уточняваме към кои таблици трябва да се картографират нашите модели. Също така, ние задаваме първичните ключове, използвайки метода HasKey, колоните в таблицата, използвайки метода на свойството и някои ограничения като IsRequired, HasMaxLength и ValueGeneratedOnAdd, всичко с ламбда изрази по „плавен начин“ (верижни методи).

Обърнете внимание на следния код:

builder.Entity <Категория> ()
       .HasMany (p => p.Products)
       .WithOne (p => p.Category)
       .HasForeignKey (p => p.CategoryId);

Тук уточняваме връзка между таблиците. Казваме, че категорията има много продукти и ние задаваме свойствата, които ще картографират тази връзка (Продукти, от категория Категория и Категория, от Продуктов клас). Поставяме и външния ключ (CategoryId).

Разгледайте този урок, ако искате да научите как да конфигурирате взаимоотношенията един към един и много на много, като използвате EF Core, както и как да го използвате като цяло.

Има и конфигурация за засяване на данни чрез метода HasData:

builder.Entity <Категория> (). HasData
(
  нова категория {Id = 100, име = "Плодове и зеленчуци"},
  нова категория {Id = 101, име = "млечна"}
);

Тук просто добавяме две примерни категории по подразбиране. Това е необходимо, за да тестваме нашата крайна точка на API след като я завършим.

Забележете: ние ръчно настройваме свойствата на Id тук, тъй като доставчикът в паметта изисква той да работи. Настройвам идентификаторите на големи числа, за да избегна сблъсък между автоматично генерирани идентификатори и данни за семена.
Това ограничение не съществува при истинските доставчици на релационни бази данни, така че ако искате например да използвате база данни като SQL Server, не е нужно да посочвате тези идентификатори. Проверете този проблем с Github, ако искате да разберете това поведение.

След като внедрихме контекстния клас на базата данни, можем да реализираме хранилището на категории. Добавете нова папка, наречена Repositories, в папката Persistence и след това добавете нов клас, наречен BaseRepository.

Този клас е просто абстрактен клас, който всички наши хранилища ще наследят. Абстрактен клас е клас, който няма директни инстанции. Трябва да създадете директни класове, за да създадете екземпляри.

BaseRepository получава екземпляр от нашия AppDbContext чрез инжектиране на зависимост и излага защитено свойство (свойство, което може да бъде достъпно само от детските класове), наречено _context, което дава достъп до всички методи, от които се нуждаем за работа с операции с база данни.

Добавете нов клас в същата папка, наречена CategoryRepository. Сега наистина ще приложим логиката на хранилището:

Репозиторият наследява BaseRepository и реализира ICategoryRepository.

Забележете колко лесно е да приложите метода на изброяване. Използваме набор от бази данни Категории за достъп до таблицата с категории и след това извикваме метода за разширение ToListAsync, който е отговорен за трансформирането на резултата от заявка в колекция от категории.

EF Core превежда нашето обаждане на метод към SQL заявка, възможно най-ефективният начин. Заявката се изпълнява само когато се обадите на метод, който ще преобразува вашите данни в колекция или когато използвате метод за вземане на конкретни данни.

Вече имаме чиста реализация на контролера за категории, услугата и хранилището.

Разделихме притесненията си, създавайки класове, които правят само онова, което трябва да правят.

Последната стъпка преди тестване на приложението е да свържем нашите интерфейси към съответните класове, използвайки механизма за инжектиране на зависимост ASP.NET Core.

Стъпка 6 - Конфигуриране на инжектирането на зависимост

Време е най-накрая да разберете как работи тази концепция.

В основната папка на приложението отворете класа Startup. Този клас е отговорен за конфигурирането на всички видове конфигурации при стартиране на приложението.

Методите ConfigureServices и Configure се извикват по време на изпълнение от тръбопровода за рамка, за да конфигурират как приложението трябва да работи и кои компоненти трябва да използва.

Разгледайте метода ConfigureServices. Тук имаме само една линия, която конфигурира приложението да използва MVC тръбопровода, което всъщност означава, че приложението ще обработва заявки и отговори, използвайки класове на контролери (тук се случват още неща отвъд кулисите, но това е, което трябва да знаете за сега).

Можем да използваме метода ConfigureServices, достъп до параметъра на услугите, за да конфигурираме нашите връзки за зависимост. Почистете кода на класа, премахвайки всички коментари и променете кода, както следва:

Вижте този код:

services.AddDbContext  (опции => {
  options.UseInMemoryDatabase ( "супермаркет апи-в-памет");
});

Тук конфигурираме контекста на базата данни. Ние казваме на ASP.NET Core да използва нашия AppDbContext с внедряване на база данни в паметта, която се идентифицира от низ, предаден като аргумент на нашия метод. Обикновено доставчикът на памет се използва, когато пишем тестове за интеграция, но аз го използвам тук за простота. По този начин не е необходимо да се свързваме с истинска база данни, за да тестваме приложението.

Конфигурацията на тези линии вътрешно конфигурира контекста на нашата база данни за инжектиране на зависимост, като се използва обхватът на експлоатационния живот.

Обхванатият живот казва на тръбопровода ASP.NET Core, че всеки път, когато трябва да разреши клас, който получава екземпляр на AppDbContext като аргумент на конструктор, той трябва да използва същата инстанция на класа. Ако няма инстанция в паметта, тръбопроводът ще създаде нов екземпляр и ще го използва отново във всички класове, които се нуждаят от него, по време на дадена заявка. По този начин не е необходимо ръчно да създавате екземпляра на класа, когато трябва да го използвате.

Има и други обхвати за целия живот, които можете да проверите, като прочетете официалната документация.

Техниката на инжектиране на зависимост ни дава много предимства, като например:

  • Използване на код;
  • По-добра производителност, тъй като когато трябва да променим внедряването, няма нужда да си правим труда да променяме сто места, където използвате тази функция;
  • Можете лесно да тествате приложението, тъй като можем да изолираме това, което трябва да тестваме, като използваме макети (фалшива реализация на класове), където трябва да предаваме интерфейси като конструкторски аргументи;
  • Когато клас трябва да получи повече зависимости чрез конструктор, не е необходимо да променяте ръчно всички места, където се създават инстанциите (това е страхотно!).

След конфигуриране на контекста на базата данни, ние също обвързваме нашата услуга и хранилище към съответните класове.

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

Тук също използваме обхват на експлоатация, тъй като тези класове трябва да използват контекстния клас на базата данни. Има смисъл да посочваме същия обхват и в случая.

Сега, когато конфигурираме нашите връзки за зависимост, трябва да направим малка промяна в класа на програмата, за да може базата данни правилно да зареди първоначалните ни данни. Тази стъпка е необходима само когато използвате доставчика на база данни в паметта (вижте този проблем с Github, за да разберете защо).

Беше необходимо да променим метода Main, за да гарантираме, че нашата база данни ще бъде „създадена“, когато приложението стартира, тъй като използваме доставчик в паметта. Без тази промяна няма да бъдат създадени категориите, които искаме да посяваме.

С всички основни функции, внедрени, е време да тестваме нашата крайна точка на API.

Стъпка 7 - Тестване на API за категории

Отворете терминала или командния ред в основната папка на API и въведете следната команда:

dotnet run

Командата по-горе стартира приложението. Конзолата ще покаже изход, подобен на този:

информация: Microsoft.EntityFrameworkCore.Infrastructure [10403]
Entity Framework Core 2.2.0-rtm-35687 се инициализира „AppDbContext“, използвайки доставчика „Microsoft.EntityFrameworkCore.InMemory“ с опции: StoreName = supermarket-api-in-memory
информация: Microsoft.EntityFrameworkCore.Update [30100]
Запазени 2 обекта в магазина за памет.
информация: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager [0]
Потребителският профил е наличен. Използване на „C: \ Потребители \ evgomes \ AppData \ Local \ ASP.NET \ DataProtection-Keyys“ като хранилище на ключове и Windows DPAPI за криптиране на ключовете в покой.
Хостинг среда: Развитие
Корен път на съдържанието: C: \ Потребители \ evgomes \ Desktop \ Ръководства \ src \ Supermarket.API
Сега слушам на: https: // localhost: 5001
Сега слушам на: http: // localhost: 5000
Приложението стартира. Натиснете Ctrl + C, за да изключите.

Можете да видите, че EF Core беше повикан да инициализира базата данни. Последните редове показват в кои портове работи приложението.

Отворете браузър и отидете до http: // localhost: 5000 / api / категории (или до URL адреса, показан на изхода на конзолата). Ако видите грешка в защитата поради HTTPS, просто добавете изключение за приложението.

Браузърът ще показва следните данни от JSON като изход:

[
  {
     "id": 100,
     "name": "Плодове и зеленчуци",
     "продукти": []
  }
  {
     "id": 101,
     "име": "Млечна",
     "продукти": []
  }
]

Тук виждаме данните, които добавихме към базата данни, когато конфигурирахме контекста на базата данни. Този изход потвърждава, че кодът ни работи.

Създадохте крайна точка на GET API с наистина малко редове код и имате структура на кода, която е много лесна за промяна поради архитектурата на API.

Сега е време да ви покажем колко лесно е да промените този код, когато трябва да го коригирате поради бизнес нуждите.

Стъпка 8 - Създаване на ресурс за категория

Ако си спомняте спецификацията на крайната точка на API, забелязахте, че реалният ни отговор на JSON има едно допълнително свойство: масив от продукти. Разгледайте примера на желания отговор:

{
  [
    {"id": 1, "name": "Плодове и зеленчуци"},
    {"id": 2, "name": "Хлябове"},
    … // Други категории
  ]
}

Масивът на продуктите присъства в настоящия ни отговор на JSON, тъй като моделът ни Категория има свойство Продукти, необходимо на EF Core за коригиране на картографирането на продуктите от дадена категория.

Не искаме тази собственост в отговора си, но не можем да променим нашия модел модел, за да изключим тази собственост. Това би причинило EF Core да хвърля грешки, когато се опитваме да управляваме данни за категории, а също така ще наруши дизайна на нашия модел на домейни, тъй като няма смисъл да има категория продукти, в която няма продукти.

За да върнем JSON данни, съдържащи само идентификаторите и имената на категориите супермаркети, трябва да създадем клас на ресурси.

Класът на ресурсите е клас, който съдържа само основна информация, която ще се обменя между клиентски приложения и крайни точки на API, обикновено под формата на JSON данни, за да представлява някаква конкретна информация.

Всички отговори от крайните точки на API трябва да върнат ресурс.

Лоша практика е да се върне реалното представяне на модел като отговор, тъй като може да съдържа информация, която клиентското приложение не се нуждае или че няма разрешение да има (например, потребителски модел може да върне информация за паролата на потребителя , което би било голям проблем за сигурността).

Нуждаем се от ресурс, който да представлява само нашите категории, без продуктите.

Сега, когато знаете какво е ресурс, нека го приложим. Първо, спрете стартиращото приложение да натиска Ctrl + C в командния ред. В основната папка на приложението създайте нова папка, наречена Resources. Там добавете нов клас, наречен CategoryResource.

Трябва да картографираме нашата колекция от модели на категории, която се предоставя от нашата категорийна услуга, към колекция от ресурси на категорията.

Ще използваме библиотека, наречена AutoMapper, за да обработваме картографиране между обекти. AutoMapper е много популярна библиотека в .NET света и се използва в много търговски проекти и проекти с отворен код.

Въведете следните редове в командния ред, за да добавите AutoMapper към нашето приложение:

dotnet добавете пакет AutoMapper
dotnet добави пакет AutoMapper.Extensions.Microsoft.DependencyInjection

За да използваме AutoMapper, трябва да направим две неща:

  • Регистрирайте го за инжектиране на зависимост;
  • Създайте клас, който ще каже на AutoMapper как да борави с картографирането на класове.

На първо място, отворете класа Startup. В метода ConfigureServices след последния ред добавете следния код:

services.AddAutoMapper ();

Тази линия обработва всички необходими конфигурации на AutoMapper, като например регистрирането му за инжектиране на зависимост и сканиране на приложението по време на стартиране, за да конфигурирате картографски профили.

Сега в основната директория добавете нова папка, наречена Mapping, след това добавете клас, наречен ModelToResourceProfile. Променете кода по този начин:

Класът наследява Profile, тип клас, който AutoMapper използва, за да провери как ще работят нашите карти. На конструктора създаваме карта между модела клас Категория и класа CategoryResource. Тъй като свойствата на класовете имат еднакви имена и типове, не е необходимо да използваме специална конфигурация за тях.

Последната стъпка се състои в промяна на контролера на категории, за да се използва AutoMapper за обработка на нашите обекти.

Смених конструктора, за да получа екземпляр на реализация на IMapper. Можете да използвате тези методи за интерфейс, за да използвате методите за картографиране на AutoMapper.

Също така промених метода GetAllAsync, за да преброя изброяването ни от категории в изброяване на ресурси, използвайки метода Map. Този метод получава екземпляр от класа или колекцията, която искаме да картографираме, и чрез дефиниции на общия тип, той определя към какъв тип клас или колекция трябва да бъде картографиран.

Забележете, че лесно променихме внедряването, без да се налага да адаптираме сервизния клас или хранилище, просто чрез инжектиране на нова зависимост (IMapper) към конструктора.

Инжектирането на зависимост прави приложението ви поддържано и лесно за промяна, тъй като не е нужно да прекъсвате цялата си кодова реализация, за да добавяте или премахвате функции.

Вероятно сте разбрали, че не само класовете на контролера, но всички класове, които получават зависимости (включително самите зависимости), са автоматично разрешени за получаване на правилните класове в съответствие с обвързващите конфигурации.

Инжектирането на зависимост е невероятно, нали?

Сега стартирайте API отново, използвайки команда за изпълнение на dotnet и преминете към http: // localhost: 5000 / api / категории, за да видите новия отговор на JSON.

Това са данните за отговорите, които трябва да видите

Вече имаме нашата GET крайна точка. Сега, нека създадем нова крайна точка за категориите POST (създаване).

Стъпка 9 - Създаване на нови категории

Когато се занимаваме със създаване на ресурси, трябва да се грижим за много неща, като например:

  • Проверка на данните и целостта на данните;
  • Разрешение за създаване на ресурси;
  • Грешка при работа;
  • Регистрирането.

Няма да покажа как да се справя с удостоверяването и упълномощаването в този урок, но можете да видите как лесно да прилагате тези функции, като четете моя урок в удостоверяването на маркера на JSON web token.

Също така, има много популярна рамка, наречена ASP.NET Identity, която предоставя вградени решения относно сигурността и регистрацията на потребителите, които можете да използвате във вашите приложения. Той включва доставчици за работа с EF Core, като например вграден IdentityDbContext, който можете да използвате. Можете да научите повече за това тук.

Нека напишем HTTP POST крайна точка, която ще покрие останалите сценарии (с изключение на регистрация, която може да се променя в зависимост от различни обхвати и инструменти).

Преди да създадем новата крайна точка, се нуждаем от нов ресурс. Този ресурс ще картографира данни, които клиентските приложения изпращат до тази крайна точка (в този случай името на категорията) в клас на нашето приложение.

Тъй като създаваме нова категория, все още нямаме идентификационен номер и това означава, че се нуждаем от ресурс, който представлява категория, съдържаща само нейното име.

В папката Resources добавете нов клас, наречен SaveCategoryResource:

Забележете необходимите и MaxLength атрибути, приложени върху свойството Name. Тези атрибути се наричат ​​пояснения към данните. Основният тръбопровод ASP.NET Core използва тези метаданни за валидиране на заявки и отговори. Както подсказват имената, името на категорията е задължително и има максимална дължина от 30 знака.

Сега нека да определим формата на новата крайна точка на API. Добавете следния код към контролера за категории:

Ние казваме на рамката, че това е крайна точка на HTTP POST, използвайки атрибута HttpPost.

Забележете вида на отговора на този метод, Задача . Методите, присъстващи в класовете на контролерите, се наричат ​​действия и те имат този подпис, защото можем да върнем повече от един възможен резултат, след като приложението изпълни действието.

В този случай, ако името на категорията е невалидно или ако нещо се обърка, трябва да върнем отговор 400 код (лоша заявка), съдържащ обикновено съобщение за грешка, което клиентските приложения могат да използват за лечение на проблема, или можем да имаме 200 отговор (успех) с данни, ако всичко върви ок.

Има много видове типове действия, които можете да използвате като отговор, но като цяло можем да използваме този интерфейс и ASP.NET Core ще използва клас по подразбиране за това.

Атрибутът FromBody казва на ASP.NET Core да анализира данните на тялото на заявката в нашия нов клас ресурси. Това означава, че когато JSON, съдържащ името на категорията, бъде изпратено към нашето приложение, рамката автоматично ще го анализира на нашия нов клас.

Сега, нека да приложим нашата логика на маршрута. Трябва да следваме няколко стъпки, за да създадем успешно нова категория:

  • Първо, трябва да потвърдим входящата заявка. Ако заявката е невалидна, трябва да върнем лош отговор на заявката, съдържащ съобщенията за грешка;
  • След това, ако заявката е валидна, трябва да картографираме новия ни ресурс в нашия модел модел клас с помощта на AutoMapper;
  • Сега трябва да се обадим на нашата услуга и да я кажем, за да запазим новата си категория. Ако логиката за записване се изпълнява без проблеми, тя трябва да върне отговор, съдържащ данните от новата ни категория. Ако не, това трябва да ни даде индикация, че процесът не е успешен, и съобщение за потенциална грешка;
  • Накрая, ако има грешка, връщаме лоша заявка. Ако не, ние преобразуваме новия ни модел на категория в категория ресурс и връщаме отговор на успех на клиента, съдържащ данните за новата категория.

Изглежда, че е сложно, но наистина е лесно да се реализира тази логика, използвайки архитектурата на услугата, която структурирахме за нашия API.

Нека започнем с валидиране на входящата заявка.

Стъпка 10 - Валидиране на органа на заявка, използвайки състоянието на модела

Контролерите на ASP.NET Core имат свойство, наречено ModelState. Това свойство се попълва по време на изпълнение на заявката, преди да стигнем до нашето действие. Това е екземпляр на ModelStateDictionary, клас, който съдържа информация, например дали заявката е валидна и потенциални съобщения за грешка при валидиране.

Променете кода на крайната точка, както следва:

Кодът проверява дали състоянието на модела (в този случай данните, изпратени в тялото на заявката) е невалидно, като проверява нашите пояснения към данните. Ако не е, API връща лоша заявка (с 400 кода на състоянието) и съобщения за грешка по подразбиране, предоставени от нашите метаданни за пояснения.

Методът ModelState.GetErrorMessages () все още не е приложен. Това е метод за разширение (метод, който разширява функционалността на вече съществуващ клас или интерфейс), който ще прилагам, за да преобразувам грешките за валидиране в прости низове, за да се върна на клиента.

Добавете нова папка Разширения в корена на нашия API и след това добавете нов клас ModelStateExtensions.

Всички методи за разширение трябва да са статични, както и класовете, в които са декларирани. Това означава, че не обработват конкретни данни за екземпляри и че се зареждат само веднъж, когато стартира приложението.

Тази ключова дума пред декларацията на параметър казва на компилатора на C # да го третира като метод на разширение. Резултатът е, че можем да го наречем като нормален метод от този клас, тъй като включваме съответния usedirective, където искаме да използваме разширението.

Разширението използва LINQ заявки, много полезна функция на .NET, която ни позволява да заявяваме и трансформираме данни с помощта на изпълними изрази. Тук изразите трансформират методите за грешка при валидиране в списък от низове, съдържащи съобщения за грешка.

Импортирайте пространството на имена Supermarket.API.Extensions в контролера за категории преди да преминете към следващата стъпка.

използване на Supermarket.API.Extensions;

Нека продължим да прилагаме нашата логика на крайната точка, като картографираме новия ни ресурс в категория модел на категория.

Стъпка 11 - Картиране на новия ресурс

Вече сме дефинирали картографски профил за трансформиране на модели в ресурси. Сега имаме нужда от нов профил, който прави обратното.

Добавете нов клас ResourceToModelProfile в папката Mapping:

Нищо ново тук. Благодарение на магията на инжектирането на зависимост, AutoMapper автоматично ще регистрира този профил при стартиране на приложението и не е нужно да променяме друго място, за да го използваме.

Сега можем да картографираме новия ни ресурс в съответния модел модел:

Стъпка 12 - Прилагане на образец на заявка-отговор за обработка на запаметяващата логика

Сега трябва да приложим най-интересната логика: да запишем нова категория. Очакваме нашата услуга да го направи.

Логиката на запис може да се провали поради проблеми при свързването към базата данни или може би защото всяко вътрешно бизнес правило обезсилва нашите данни.

Ако нещо се обърка, не можем просто да хвърлим грешка, защото това може да спре API, а клиентското приложение няма да знае как да се справи с проблема. Също така потенциално бихме имали някакъв механизъм за регистриране, който да регистрира грешката.

Договорът на метода за записване, това означава, че подписът на метода и типа отговор, трябва да ни посочи дали процесът е изпълнен правилно. Ако процесът върви добре, ще получим данните за категорията. Ако не, трябва да получим поне съобщение за грешка, в което се казва защо процесът се е провалил.

Ние можем да реализираме тази функция, като приложим модела на отговор на заявка. Този модел на дизайн на предприятието капсулира нашите параметри на заявката и отговора в класове като начин за капсулиране на информация, която нашите услуги ще използват за обработка на някаква задача и връщане на информация в класа, който използва услугата.

Този модел ни дава някои предимства, като например:

  • Ако трябва да променим услугата си, за да получим повече параметри, не е нужно да нарушаваме нейния подпис;
  • Можем да определим стандартен договор за нашето искане и / или отговори;
  • Можем да се справим с бизнес логиката и потенциалните грешки, без да спираме процеса на кандидатстване и няма да е необходимо да използваме тонове опитни блокове.

Нека създадем стандартен тип отговор за нашите методи за услуги, които се справят с промените на данните. За всяка заявка от този тип искаме да знаем дали заявката се изпълнява без проблеми. Ако не успее, искаме да върнем съобщение за грешка на клиента.

В папката на домейна, вътре в Services, добавете нова директория, наречена комуникация. Добавете нов клас там, наречен BaseResponse.

Това е абстрактен клас, който нашите типове отговори ще наследят.

Абстракцията дефинира свойство Success, което ще покаже дали заявките са завършени успешно и свойство Message, което ще има съобщение за грешка, ако нещо не успее.

Забележете, че тези свойства са задължителни и само наследени класове могат да зададат тези данни, тъй като децата класове трябва да предадат тази информация чрез конструкторската функция.

Съвет: не е добра практика да се определят базовите класове за всичко, защото базовите класове свързват кода ви и ви пречат лесно да го променяте. Предпочитайте да използвате композиция над наследяването.
За обхвата на този API не е наистина проблем да се използват базови класове, тъй като нашите услуги няма да растат много. Ако осъзнаете, че дадена услуга или приложение ще се разраства и променя често, избягвайте да използвате базов клас.

Сега в същата папка добавете нов клас, наречен SaveCategoryResponse.

Типът отговор също задава свойство Категория, което ще съдържа данни от категорията ни, ако заявката приключи успешно.

Забележете, че съм дефинирал три различни конструктора за този клас:

  • Частна, която ще предаде параметрите за успех и съобщение на базовия клас и също така определя свойството Category;
  • Конструктор, който получава само категорията като параметър. Този ще създаде успешен отговор, призовавайки частния конструктор да зададе съответните свойства;
  • Трети конструктор, който само посочва съобщението. Този ще бъде използван за създаване на отговор за отказ.

Тъй като C # поддържа множество конструктори, ние опростихме създаването на отговор, без да дефинираме различен метод за справяне с това, само с помощта на различни конструктори.

Сега можем да променим нашия сервизен интерфейс, за да добавим новия договор за метод за запазване.

Променете интерфейса на ICategoryService, както следва:

Просто ще предадем категория на този метод и той ще се справи с цялата логика, необходима за запазване на данните за модела, оркестриране на хранилища и други необходими услуги за това.

Забележете, че тук не създавам конкретен клас заявки, тъй като не ни трябват други параметри за изпълнение на тази задача. В компютърното програмиране има концепция, наречена KISS - кратко за Keep it Simple, Stupid. По принцип той казва, че трябва да поддържате приложението си възможно най-просто.

Помнете това при проектирането на вашите приложения: прилагайте само онова, което ви е необходимо, за да разрешите проблем. Не преразглеждайте приложението си.

Сега можем да завършим нашата логика на крайната точка:

След валидиране на данните на заявката и картографиране на ресурса към нашия модел, ние го предаваме на нашата услуга, за да запазят данните.

Ако нещо не успее, API връща лоша заявка. Ако не, API картографира новата категория (сега включва данни като новия Id) в нашия създаден по-рано CategoryResource и го изпраща на клиента.

Сега нека приложим истинската логика за услугата.

Стъпка 13 - Логиката на базата данни и моделът на единица работа

Тъй като ще поддържаме данни в базата данни, се нуждаем от нов метод в нашето хранилище.

Добавете нов метод на AddAsync към интерфейса на ICategoryRepository:

Сега, нека да приложим този метод в нашия истински клас хранилище:

Тук просто добавяме нова категория към нашия комплект.

Когато добавим клас към DBSet <>, EF Core започва да проследява всички промени, които се случват с нашия модел и използва тези данни в текущото състояние, за да генерира заявки, които ще вмъкват, актуализират или изтриват модели.

Сегашната имплементация просто добавя модела към нашия набор, но нашите данни все още няма да бъдат запазвани.

Има метод, наречен SaveChanges, присъстващ в контекстния клас, който трябва да извикаме, за да изпълним наистина заявките в базата данни. Не съм го нарекъл тук, тъй като хранилище не трябва да съдържа данни, това е само в памет памет от обекти.

Тази тема е много противоречива дори между опитни разработчици на .NET, но позволете ми да ви обясня защо не трябва да извиквате SaveChanges в класове на хранилища.

Можем да мислим за хранилище концептуално като всяка друга колекция, присъстваща в .NET рамката. Когато се занимавате с колекция в .NET (и много други езици за програмиране, като Javascript и Java), обикновено можете:

  • Добавете към него нови елементи (например, когато натискате данни към списъци, масиви и речници);
  • Намерете или филтрирайте елементи;
  • Премахване на елемент от колекцията;
  • Заменете даден елемент или го актуализирайте.

Помислете за списък от реалния свят. Представете си, че пишете списък за пазаруване, за да купувате неща в супермаркет (какво съвпадение, не?).

В списъка пишете всички плодове, които трябва да купите. Можете да добавите плодове в този списък, да премахнете плод, ако се откажете да го купите, или можете да замените името на плод. Но не можете да запишете плодове в списъка. Няма смисъл да казваш такова нещо на обикновен английски.

Съвет: когато проектирате класове и интерфейси на обектно-ориентирани езици за програмиране, опитайте се да използвате естествен език, за да проверите дали това, което правите, изглежда правилно.
Има смисъл например да казваме, че човек реализира интерфейс на човек, но няма смисъл да казваме, че човек реализира акаунт.

Ако искате да „запишете“ списъците с плодове (в този случай, за да закупите всички плодове), го плащате и супермаркетът обработва данните за запасите, за да провери дали трябва да купуват повече плодове от доставчик или не.

Същата логика може да се приложи при програмиране. Хранилищата не трябва да съхраняват, актуализират или изтриват данни. Вместо това, те трябва да го делегират на друг клас, за да се справят с тази логика.

Има друг проблем при запазването на данни директно в хранилище: не можете да използвате транзакции.

Представете си, че нашето приложение има механизъм за регистриране, който съхранява някакво потребителско име и действието, изпълнявано всеки път, когато се извърши промяна в данните на API.

А сега си представете, че по някаква причина имате обаждане до услуга, която актуализира потребителското име (това не е често срещан сценарий, но нека го вземем предвид).

Съгласявате се, че за да промените потребителското име в таблицата с измислени потребители, първо трябва да актуализирате всички регистри, за да разберете правилно кой е извършил тази операция, нали?

Сега си представете, че сме внедрили метода на актуализация за потребители и регистрационни файлове в различни хранилища и двамата извикват SaveChanges. Какво се случва, ако някой от тези методи не успее в средата на процеса на актуализиране? Ще стигнете до несъответствие с данни.

Ние трябва да запазваме промените си в базата данни само след като всичко приключи. За да направите това, трябва да използваме транзакция, която по същество е функция, която повечето бази данни прилагат, за да запишете данни само след приключване на сложна операция.

„- Добре, така че ако не можем да запишем нещата тук, къде да го направим?“

Често срещан модел за справяне с този проблем е моделът на единица работа. Този модел се състои от клас, който приема нашия екземпляр AppDbContext като зависимост и излага методи за стартиране, завършване или прекратяване на транзакции.

Ще използваме проста реализация на единица работа, за да подходим към проблема ни тук.

Добавете нов интерфейс в папката Repositories на слоя Domain, наречен IUnitOfWork:

Както можете да видите, той разкрива само метод, който ще асинхронно завърши операциите за управление на данни.

Нека сега добавим истинската реализация

Добавете нов клас, наречен UnitOfWork в папката RepositoriesRepositories на слоя Персистенция:

Това е проста, чиста имплементация, която ще запише всички промени в базата данни само след като приключите с модифицирането й, използвайки вашите хранилища.

Ако проучвате внедряването на модела на единица работа, ще намерите по-сложни, изпълняващи операции за връщане.

Тъй като EF Core вече внедрява модела на хранилището и единицата работа зад кулисите, не е нужно да се интересуваме от метода на връщане.

" - Какво? Така че защо трябва да създаваме всички тези интерфейси и класове? "

Отделянето на логиката за постоянство от правилата на бизнеса дава много предимства по отношение на повторната употреба и поддръжка на кода. Ако използваме EF Core директно, ще имаме по-сложни класове, които няма да бъде толкова лесно да се променят.

Представете си, че в бъдеще решавате да промените рамката на ORM на друга, като например Dapper, или ако трябва да внедрите обикновени SQL заявки поради производителност. Ако свържете логиката на вашите заявки към услугите си, ще бъде трудно да промените логиката, защото ще трябва да го направите в много класове.

Използвайки шаблона на хранилището, можете просто да внедрите нов клас на хранилище и да го свържете с помощта на инжектиране на зависимост.

Така че, ако използвате EF Core директно в услугите си и трябва да промените нещо, това ще получите:

Както казах, EF Core прилага моделите на единица работа и хранилище зад кулисите. Можем да считаме нашите DbSet <> свойства като хранилища. Освен това SaveChanges запазва данни само в случай на успех за всички операции с база данни.

Сега, когато знаете какво е единица работа и защо да го използвате с хранилища, нека да приложим логиката на реалната услуга.

Благодарение на отделената ни архитектура можем просто да предадем инстанция на UnitOfWork като зависимост за този клас.

Нашата бизнес логика е доста проста.

Първо се опитваме да добавим новата категория в базата данни и след това API се опитваме да я запишем, обвивайки всичко в блок за пробвания.

Ако нещо не успее, API извиква някаква измислена услуга за регистрация и връща отговор, показващ неуспех.

Ако процесът приключи без проблеми, приложението връща отговор за успех, като изпраща данни от категорията ни. Просто, нали?

Съвет: В приложения от реалния свят не трябва да обвивате всичко в общ блок за пробвания, а вместо това трябва да обработвате всички възможни грешки отделно.
Просто добавянето на блок за пробвания не покрива повечето от възможните неуспешни сценарии. Не забравяйте да коригирате работата с грешки на машината.

Последната стъпка преди тестване на нашия API е да обвържете единицата на работния интерфейс със съответния клас.

Добавете този нов ред към метода ConfigureServices от клас Startup:

услуги.AddScoped  ();

Сега нека го тестваме!

Стъпка 14 - Тестване на нашата POST Endpoint с помощта на Postman

Стартирайте нашето приложение отново с помощта на dotnet run.

Не можем да използваме браузъра за тестване на POST крайна точка. Нека използваме Пощальон за тестване на крайните ни точки. Това е много полезен инструмент за тестване на RESTful API.

Отворете Пощальон и затворете въведените съобщения. Ще видите екран като този:

Екран, показващ опции за тестване на крайните точки

Променете избраното по подразбиране GET в полето за избор на POST.

Въведете адреса на API в полето за въвеждане на заявка.

Трябва да предоставим данните на тялото на заявката, които да изпратим към нашия API. Кликнете върху елемента от менюто Body, след което променете опцията, показана под него, на сурова.

Пощальонът ще покаже опция за текст вдясно. Променете го на JSON (application / json) и поставете следните JSON данни по-долу:

{
  "име": ""
}
Екран точно преди изпращане на заявка

Както виждате, ще изпратим празен низ за име до новата ни крайна точка.

Кликнете върху бутона Изпращане. Ще получите резултат като този:

Както виждате, нашата логика за валидиране работи!

Спомняте ли си логиката за валидиране, която създадохме за крайната точка? Този изход е доказателството, че работи!

Забележете и 400-те кода на състоянието, показани вдясно. Резултатът от BadRequest автоматично добавя този код на състоянието към отговора.

Сега нека променим JSON данните на валидни, за да видите новия отговор:

И накрая, резултатът, който очаквахме да имаме

API правилно създаде нашия нов ресурс.

Досега нашият API може да изброява и създава категории. Научихте много неща за езика на C #, основата на ASP.NET Core, както и общи дизайнерски подходи за структуриране на вашите API.

Нека да продължим API на нашите категории, създавайки крайната точка за актуализиране на категории.

Отсега нататък, тъй като ви обясних повечето концепции, ще ускоря обясненията и ще се съсредоточа върху нови теми, за да не губите времето си. Да тръгваме!

Стъпка 15 - Актуализиране на категории

За да актуализираме категории, се нуждаем от HTTP PUT крайна точка.

Логиката, която трябва да кодираме, е много подобна на POST:

  • Първо, трябва да потвърдим входящата заявка, използвайки ModelState;
  • Ако заявката е валидна, API трябва да преобразува входящия ресурс в моделен клас, използвайки AutoMapper;
  • След това трябва да се обадим на нашата услуга, като й кажем да актуализира категорията, предоставяйки съответния идентификатор на категорията и актуализираните данни;
  • Ако в базата данни няма категория с дадения Id, връщаме лоша заявка. Можем да използваме резултат NotFound вместо това, но всъщност няма значение за този обхват, тъй като предоставяме съобщение за грешка на клиентските приложения;
  • Ако логиката за запис е изпълнена правилно, услугата трябва да върне отговор, съдържащ актуализираните данни за категорията. Ако не, трябва да ни даде указание, че процесът не е успешен, и съобщение, указващо защо;
  • И накрая, ако има грешка, API връща лоша заявка. Ако не, тя преобразува актуализирания модел на категория в ресурс от категория и връща отговор за успех на клиентското приложение.

Нека добавим новия метод на PutAsync в класа на контролера:

Ако го сравните с логиката на POST, ще забележите, че тук имаме само една разлика: атрибутът HttPut указва параметър, който трябва да получи даденият маршрут.

Ще се обадим на тази крайна точка, като посочим категорията Id като последен фрагмент от URL адрес, като / api / категории / 1. ASP.NET Core pipeline анализира този фрагмент към едноименния параметър.

Сега трябва да определим подписа на метода UpdateAsync в интерфейса на ICategoryService:

Сега да преминем към истинската логика.

Стъпка 16 - Логиката на актуализацията

За да актуализираме нашата категория, първо трябва да върнем текущите данни от базата данни, ако тя съществува. Ние също трябва да го актуализираме в нашия DBSet <>.

Нека добавим два нови метода договори към нашия ICategoryService интерфейс:

Дефинирахме метода FindByIdAsync, който ще върне асинхронно категория от базата данни и метода на актуализиране. Обърнете внимание, че методът на актуализация не е асинхронен, тъй като API на Core Core не изисква асинхронен метод за актуализиране на модели.

Сега нека да приложим истинската логика в класа CategoryRepository:

Накрая можем да кодираме логиката на услугата:

API се опитва да получи категорията от базата данни. Ако резултатът е нулев, връщаме отговор, казващ, че категорията не съществува. Ако категорията съществува, трябва да зададем нейното ново име.

API след това се опитва да запази промените, като например когато създаваме нова категория. Ако процесът завърши, услугата връща отговор за успех. Ако не, логиката на регистриране се изпълнява и крайната точка получава отговор, съдържащ съобщение за грешка.

Сега нека го тестваме Първо, нека добавим нова категория, за да имате валиден идентификационен номер, който да използвате. Бихме могли да използваме идентификаторите на категориите, които посяваме в нашата база данни, но искам да го направя по този начин, за да ви покажа, че нашият API ще актуализира правилния ресурс.

Стартирайте приложението отново и използвайте Postman, POST нова категория в базата данни:

Добавяне на нова категория, за да я актуализирате по-късно

Като имате валиден идентификатор в ръце, променете опцията POST на PUT в полето за избор и добавете стойността на идентификатора в края на URL адреса. Променете свойството име на друго име и изпратете заявката, за да проверите резултата:

Данните за категорията бяха актуализирани успешно

Можете да изпратите GET заявка до крайната точка на API, за да се уверите, че правилно сте редактирали името на категорията:

Това е резултат от GET заявка сега

Последната операция, която трябва да приложим за категории, е изключването на категории. Нека направим това, създавайки крайна точка за изтриване на HTTP.

Стъпка 17 - Изтриване на категории

Логиката за изтриване на категории е наистина лесна за изпълнение, тъй като повечето методи, от които се нуждаем, са изградени преди.

Това са необходимите стъпки, за да работи нашия маршрут:

  • API трябва да се обади на нашата услуга, като му каже да изтрие нашата категория, предоставяйки съответния идентификационен номер;
  • Ако в базата данни няма категория с дадения идентификатор, услугата трябва да върне съобщение, посочващо я;
  • Ако логиката за изтриване се изпълнява без проблеми, услугата трябва да върне отговор, съдържащ нашите изтрити данни от категорията. Ако не, това трябва да ни даде индикация, че процесът не е успешен, и съобщение за потенциална грешка;
  • И накрая, ако има грешка, API връща лоша заявка. Ако не, API преобразува актуализираната категория в ресурс и връща отговор на успеха на клиента.

Нека започнем с добавяне на новата логика на крайната точка:

Атрибутът HttpDelete също определя шаблон за id.

Преди да добавим подписа на DeleteAsync към нашия ICategoryService интерфейс, трябва да направим малък рефакторинг.

Новият метод на услуга трябва да върне отговор, съдържащ данните за категорията, по същия начин, както по методите PostAsync и UpdateAsync. Можем да използваме отново SaveCategoryResponse за тази цел, но в този случай не спестяваме данни.

За да избегнем създаването на нов клас със същата форма, за да изпълним това изискване, можем просто да преименуваме нашия SaveCategoryResponse в CategoryResponse.

Ако използвате Visual Studio Code, можете да отворите класа SaveCategoryResponse, да поставите курсора на мишката над името на класа и да използвате опцията Промяна на всички събития, за да преименувате класа:

Лесен начин да промените името във всички файлове

Не забравяйте също да преименувате името на файла.

Нека добавим подписа на метода DeleteAsync към интерфейса на ICategoryService:

Преди да приложим логиката за изтриване, се нуждаем от нов метод в нашето хранилище.

Добавете подписа на метода за премахване към интерфейса на ICategoryRepository:

void Remove (категория категория);

И сега добавете реалната реализация на класа на хранилището:

EF Core изисква екземплярът на нашия модел да бъде предаден на метода за премахване, за да разбере правилно кой модел изтриваме, вместо просто да предава идентификатор.

И накрая, нека да приложим логиката в клас CategoryService:

Тук няма нищо ново. Услугата се опитва да намери категорията по идентификатор и след това извиква нашето хранилище, за да изтрие категорията. И накрая, единицата за работа завършва транзакцията, изпълнявайки реалната операция в базата данни.

„- Хей, но какво ще кажете за продуктите от всяка категория? Не е нужно да създавате хранилище и да изтривате продуктите първо, за да избегнете грешки? “

Отговорът е не. Благодарение на проследяващия механизъм на EF Core, когато зареждаме модел от базата данни, рамката знае кои взаимоотношения има моделът. Ако го изтрием, EF Core знае, че трябва да изтрие всички свързани модели първо, рекурсивно.

Можем да деактивираме тази функция, когато картографираме класовете си в таблици с бази данни, но това не е в обхвата на този урок. Разгледайте тук, ако искате да научите за тази функция.

Сега е време да тестваме новата си крайна точка. Стартирайте приложението отново и изпратете DELETE заявка, използвайки Postman, както следва:

Както виждате, API изтрива съществуващата категория без проблеми

Можем да проверим дали нашия API работи правилно, като изпратим GET заявка:

Сега в резултат получаваме само една категория

Завършихме API за категории. Сега е време да преминете към API на продуктите.

Стъпка 18 - API на продуктите

Досега научихте как да внедрите всички основни HTTP глаголи за обработка на CRUD операции с ASP.NET Core. Нека преминем на следващото ниво, като внедрим API на нашите продукти.

Няма да опиша отново всички HTTP глаголи отново, защото би било изчерпателно. В последната част на този урок ще покрия само GET заявката, за да ви покажа как да включвате свързани лица при заявка на данни от базата данни и как да използвате атрибутите Description, които дефинирахме за стойностите на изброяване на EUnitOfMeasurement.

Добавете нов контролер в папката Контролери, наречен ProductsController.

Преди да кодираме нещо тук, трябва да създадем ресурса на продукта.

Позволете ми да освежа паметта ви, показвайки отново как трябва да изглежда ресурсът ни:

{
 [
  {
   "id": 1,
   "име": "Захар",
   „количество в опаковка“: 1,
   "unitOfMeasurement": "KG"
   "категория": {
   "id": 3,
   "име": "Захар"
   }
  }
  … // Други продукти
 ]
}

Искаме масив JSON, съдържащ всички продукти от базата данни.

Данните JSON се различават от модела на продукта с две неща:

  • Мерната единица се показва по-кратък начин, като се показва само нейното съкращение;
  • Извеждаме данните за категорията, без да включваме свойството CategoryId.

За да представим единицата за измерване, можем да използваме просто свойство на низ вместо тип enum (между другото, ние нямаме тип enum по подразбиране за JSON данни, така че трябва да го трансформираме в друг тип).

Сега, когато сега как да оформяме новия ресурс, нека го създадем. Добавете нов клас ProductResource в папката Resources:

Сега трябва да конфигурираме картографирането между моделния клас и нашия нов клас ресурси.

Конфигурацията за картографиране ще бъде почти същата като тази, използвана за други карти, но тук трябва да се справим с трансформацията на нашия ennit EUnitOfMeasurement в низ.

Спомняте ли си атрибута StringValue, приложен върху видовете изброяване? Сега ще ви покажа как да извличате тази информация, използвайки мощна функция на .NET рамката: API за размисъл.

API за размисъл е мощен набор от ресурси, който ни позволява да извличаме и манипулираме метаданни. Много рамки и библиотеки (включително самата ASP.NET Core) използват тези ресурси, за да се справят с много неща зад кулисите.

Сега нека да видим как работи на практика Добавете нов клас в папката Разширения, наречена EnumExtensions.

Може да изглежда страшно при първия поглед на кода, но той не е толкова сложен. Нека разбием определението на кода, за да разберем как работи.

Първо, ние дефинирахме общ метод (метод, който може да получи повече от един тип аргумент, в случая представен от декларацията TEnum), който получава даден enum като аргумент.

Тъй като enum е запазена ключова дума в C #, добавихме @ пред името на параметъра, за да го направим валидно име.

Първата стъпка на изпълнение на този метод е да получите информация за типа (клас, интерфейс, enum или структура дефиниция) на параметъра, използвайки метода GetType.

След това методът получава специфичната стойност на изброяване (например Kilogram), използвайки GetField (@ enum.ToString ()).

Следващият ред намира всички атрибути Description, приложени върху стойността на изброяване, и съхранява техните данни в масив (можем да посочим множество атрибути за едно и също свойство в някои случаи).

Последният ред използва по-кратък синтаксис, за да провери дали имаме поне един атрибут за описание за типа на изброяване. Ако имаме, връщаме стойността на Description, предоставена от този атрибут. Ако не, връщаме изброяването като низ, използвайки кастинга по подразбиране.

The ?. Оператор (нулев условен оператор) проверява дали стойността е нулева, преди да получи достъп до нейното свойство.

В ?? Оператор (оператор с нулево коализиране) казва на приложението да върне стойността вляво, ако не е празна, или стойността вдясно в противен случай.

Сега, когато имаме метод за разширение за извличане на описания, нека да конфигурираме нашето картографиране между модел и ресурс. Благодарение на AutoMapper можем да го направим само с една допълнителна линия.

Отворете класа ModelToResourceProfile и променете кода по този начин:

Този синтаксис казва на AutoMapper да използва новия метод на разширение, за да преобразува стойността ни EUnitOfMeasurement в низ, съдържащ неговото описание. Просто, нали? Можете да прочетете официалната документация, за да разберете пълния синтаксис.

Забележете, че не сме дефинирали конфигурация за картографиране за свойството категория. Тъй като преди това конфигурирахме картографирането за категории и защото моделът на продукта има свойство категория от същия тип и име, AutoMapper явно знае, че трябва да го картографира, като използва съответната конфигурация.

Сега нека добавим кода на крайната точка Променете кода на ProductsController:

По принцип същата структура, определена за контролера на категории.

Нека да преминем към сервизната част. Добавете нов IProductService интерфейс в папката Services, присъстваща в слой Domain:

Трябваше да осъзнаете, че се нуждаем от хранилище, преди наистина да внедрим новата услуга.

Добавете нов интерфейс, наречен IProductRepository в съответната папка:

Сега нека да приложим хранилището. Трябва да го реализираме почти по същия начин, както направихме за хранилището на категории, с изключение на това, че трябва да връщаме съответните данни за категорията на всеки продукт при заявка на данни.

EF Core по подразбиране не включва свързани обекти към вашите модели, когато питате данни, тъй като може да е много бавен (представете си модел с десет свързани субекти, всички свързани лица имат свои взаимоотношения).

За да включим данните за категориите, се нуждаем само от един допълнителен ред:

Забележете обаждането към Включване (p => p.Category). Можем да веригираме този синтаксис, за да включим колкото се може повече субекти при заявка на данни. EF Core ще го преведе на присъединяване, когато извършва селектирането.

Сега можем да реализираме класа ProductService по същия начин, както направихме за категории:

Нека вържем новите зависимости, променящи класа за стартиране:

И накрая, преди да тестваме API, нека променим класа на AppDbContext, за да включим някои продукти при инициализиране на приложението, за да можем да видим резултатите:

Добавих два измислени продукта, свързвайки ги с категориите, които посяваме при инициализиране на приложението.

Време е за тест! Стартирайте отново API и изпратете GET заявка към / api / products, използвайки Postman:

Готово! Ето нашите продукти

И това е! Честито!

Сега имате база как да изградите RESTful API, използвайки ASP.NET Core, използвайки отделена архитектура. Научихте много неща от .NET Core Framework, как да работите със C #, основите на EF Core и AutoMapper и много полезни модели, които да използвате при проектирането на вашите приложения.

Можете да проверите пълната реализация на API, съдържащ другите HTTP глаголи за продукти, като проверите хранилището на Github:

заключение

ASP.NET Core е чудесна рамка за използване при създаване на уеб приложения. Той се предлага с много полезни API, които можете да използвате за изграждане на чисти, поддържащи се приложения. Разгледайте го като вариант при създаване на професионални приложения.

Тази статия не е обхванала всички аспекти на професионалния API, но сте научили всички основи. Освен това научихте много полезни модели за решаване на модели, с които се сблъскваме ежедневно.

Надявам се да ви е харесала тази статия и се надявам да ви е била полезна. Оценявам отзивите ви, за да разберете как мога да подобря това.

Позовавания за продължаване на ученето

.NET Основни уроци - Microsoft Docs

Основна документация на ASP.NET - Microsoft Docs