Как создать простое Rest API на .NET Core

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Введение

Всем привет, в данной статье будет рассказано, как с использованием технологии C# ASP.NET Core написать простое Rest Api. Сделать Unit-тесты на слои приложений. Отправлять Json ответы. Также покажу, как выложить данное приложение в Docker.

В данной статье не будет описано, как делать клиентскую (далее Front) часть приложения. Здесь я покажу только серверную (далее Back).

Что используем?

Писать код я буду в Visual Studio 2019.

Для реализации приложения, я буду использовать такие библиотеки NuGet:

  1. Microsoft.EntityFrameworkCore

  2. Microsoft.EntityFrameworkCore.SqlServer

  3. Microsoft.EntityFrameworkCore.Tools

Для тестов вот эти библиотеки:

  1. Microsoft.NET.Test.Sdk

  2. Microsoft.NETCore.App

  3. Moq

  4. xunit

  5. xunit.runner.visualstudio

Для установки пакетов нужно зайти в обозреватель пакетов NuGet, сделать это можно, нажав ПКМ по проекту, и выбрав там пункт «управление пакетам NuGet»

Что программировать?

Для примера я возьму сильно упрощенную модель сервиса по ремонту автомобилей. В моей модели будут работники, которые будут заниматься ремонтом, автомобили, поступающие на ремонт, и документация по ремонту, которая будет отсылаться в ответе.

Настройка Базы Данных

Для настройки базы данных нужен класс ApplicationContext (реализация будет далее) и строка подключения, которая храниться в файле «appsettings.json». В этом классе будут прописаны все зависимости для генерации миграций. Строка подключения нужна для того, чтобы приложение знало в какую БД ей обращаться и с какими параметрами.

Чтобы добавить строку подключения, достаточно зайти в файл «appsettings.json» и прописать следующие строки:

"ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=testdb;Trusted_Connection=True;"
  },

Описание слоев приложения

Модели

В слое моделей будут находиться сущности, которые с помощью Entity Framework будут преобразованы в таблицы в базе данных.

Для описания модели в приложении достаточно просто описать класс, с нужными вам полями. Эти поля автоматически будут преобразованы в столбцы таблицы, а название таблицы будет соответствовать названию класса. Так задано по умолчанию, но есть специальные атрибуты, которые позволяют более гибко настраивать хранение данных в БД (но о них не в этой статье).

Первая модель, которая понадобиться для описания сервиса по ремонту - модель сотрудника. Что она будет из себя представлять?

  • Уникальный идентификатор сотрудника

  • Имя сотрудника

  • Должность сотрудника

  • Номер телефона для связи с сотрудником

Следующая модель для описания сервиса - автомобили, которые будут поступать на ремонт.

  • Уникальный идентификатор автомобиля

  • Название автомобиля

  • Номер автомобиля

И последняя модель, которую мы уже будем отсылать - документ (выписка) по ремонту.

  • Уникальный идентификатор документа

  • Сотрудник, который обслуживал автомобиль

  • Автомобиль, который был на ремонте

Чтобы модели попали в базу данных, необходимо создать миграцию. Миграция - описание того, как и что будет записано в базу данных. С помощью Entity Framework миграции можно генерировать автоматически. Для этого в пакетном менеджере надо прописать команду "Add-Migration". После этого Entity Framework сгенерирует миграцию по вашим моделям, которые указаны в классе DbContext. Чтобы применить миграцию, используем команду "Update-Database", после этого ваши данные попадут в базу данных (как это применять будет описано далее).

Контроллеры

Контроллер - посредник между бизнес-логикой, либо базой данных и Front частью приложения. Получая запрос с Front, контроллер обрабатывает запрос, вызывает необходимые сервисы для реализации некой бизнес-логики и отправляет полученные данные обратно на Front.

Для возвращаемого значения в контроллерах будут использоваться тип Json. Для этого достаточно в return прописать

new JsonResult(Ваш объект)

В данном примере, я покажу как сделать методы для GET, POST, PUT и DELETE запросов. В GET-запросе я буду выбирать все существующие документы и передавать их на Front, а в POST-запросе я буду вызывать сервис по ремонту автомобиля и возвращать выписку по ремонту, PUT будет отвечать за обновление существующего документа и DELETE за удаление документа.

DAO (Репозитории)

Репозитории нужны как посредники для обеспечения работы с БД, чтобы исключить прямое взаимодействие человека с данными. Это нужно для того, чтобы сокрыть логику работы автоматизировать многие моменты работы с БД, а также для безопасной работы с данными.

В своем приложении я сделал репозиторий, который может принимать любую модель, и выполнять такие действия как get, get all, update, create, delete.

Сервисы

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

В качестве примера сервиса, я сделал класс, всего с одним методом Work. Этот метод имитирует работу моего сервиса по починке машин. В этом методе «нанимается» рабочий, заводится автомобиль и заполняется документ о его починке.

Реализация

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

Создание проекта

При создании нового проекта, я выбрал веб-приложение ASP.NET Core, далее прописал его название (RestApi) и выбрал папку, где оно будет храниться. На экране выбора шаблона выбрал API.

Выбор шаблона приложения
Выбор шаблона приложения

Далее приступим к самому приложению.

Структура

Я разделил все приложение по папкам (также Unit-тесты в отдельном проекте) и получил вот такую структуру мое приложения:

Структура приложения
Структура приложения

Модели

Для реализации моделей я сделал абстрактный класс BaseModel. Он понадобиться в будущем для корректного наследования, а также в нем прописан Id каждой, модели (это помогает не дублировать код):

    public abstract class BaseModel
    {
        public Guid Id { get; set; }
    }

Далее вышеописанные модели:

    public class Car : BaseModel
    {
        public string Name { get; set; }
        public string Number { get; set; }
    }
    public class Document : BaseModel
    {
        public Guid CarId { get; set; }
        public Guid WorkerId { get; set; }
        public virtual Car Car { get; set; }
        public virtual Worker Worker { get; set; }
    }
  public class Worker : BaseModel
    {
        public string Name { get; set; }
        public string Position { get; set; }
        public string Telephone { get; set; }
    }

Репозиторий

Как уже было сказано репозиторий будет один, но сможет работать с абсолютно любой моделью. Также я сделал интерфейс для репозитория, чтобы инкапсулировать его работу.

Интерфейс:

public interface IBaseRepository<TDbModel> where TDbModel : BaseModel
    {
        public List<TDbModel> GetAll();
        public TDbModel Get(Guid id);
        public TDbModel Create(TDbModel model);
        public TDbModel Update(TDbModel model);
        public void Delete(Guid id);
    }

Реализация:

    public class BaseRepository<TDbModel> : IBaseRepository<TDbModel> where TDbModel : BaseModel
    {
        private ApplicationContext Context { get; set; }
        public BaseRepository(ApplicationContext context)
        {
            Context = context;
        }

        public TDbModel Create(TDbModel model)
        {
            Context.Set<TDbModel>().Add(model);
            Context.SaveChanges();
            return model;
        }

        public void Delete(Guid id)
        {
            var toDelete = Context.Set<TDbModel>().FirstOrDefault(m => m.Id == id);
            Context.Set<TDbModel>().Remove(toDelete);
            Context.SaveChanges();
        }

        public List<TDbModel> GetAll()
        {
            return Context.Set<TDbModel>().ToList();
        }

        public TDbModel Update(TDbModel model)
        {
            var toUpdate = Context.Set<TDbModel>().FirstOrDefault(m => m.Id == model.Id);
            if (toUpdate != null)
            {
                toUpdate = model;
            }
            Context.Update(toUpdate);
            Context.SaveChanges();
            return toUpdate;
        }

        public TDbModel Get(Guid id)
        {
            return Context.Set<TDbModel>().FirstOrDefault(m => m.Id == id);
        }
    }

Сервис

Сервис также как и репозиторий имеет интерфейс и его реализацию.

Интерфейс:

public interface IRepairService
    {
        public void Work();
    }

Реализация:

public class RepairService : IRepairService
    {
        private IBaseRepository<Document> Documents { get; set; }
        private IBaseRepository<Car> Cars { get; set; }
        private IBaseRepository<Worker> Workers { get; set; }

        public void Work()
        {
            var rand = new Random();
            var carId = Guid.NewGuid();
            var workerId = Guid.NewGuid();

            Cars.Create(new Car
            {
                Id = carId,
                Name = String.Format($"Car{rand.Next()}"),
                Number = String.Format($"{rand.Next()}")
            });

            Workers.Create(new Worker
            {
                Id = workerId,
                Name = String.Format($"Worker{rand.Next()}"),
                Position = String.Format($"Position{rand.Next()}"),
                Telephone = String.Format($"8916{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}")
            });

            var car = Cars.Get(carId);
            var worker = Workers.Get(workerId);

            Documents.Create(new Document {
                CarId = car.Id,
                WorkerId = worker.Id,
                Car = car,
                Worker = worker
            });
        }
    }

Контроллер

У меня в приложении всего один контроллер, но по его шаблону можно сделать сколько угодно контроллеров. Когда приложение запущено, для того чтобы обратиться к методу контроллера с Front части приложения, достаточно передать запрос, который выглядит примерно вот так:

ДоменноеИмя/НазваниеКонтроллера/НазваниеМетода?Параметры(если есть)

Пути гибко настраиваются с помощью специальных атрибутов (о них не в этой статье).

Мой MainController:

[ApiController]
    [Route("[controller]")]
    public class MainController : ControllerBase
    {
        private IRepairService RepairService { get; set; }
        private IBaseRepository<Document> Documents { get; set; }

        public MainController(IRepairService repairService, IBaseRepository<Document> document )
        {
            RepairService = repairService;
            Documents = document;
        }

        [HttpGet]
        public JsonResult Get()
        {
            return new JsonResult(Documents.GetAll());
        }

        [HttpPost]
        public JsonResult Post()
        {
            RepairService.Work();
            return new JsonResult("Work was successfully done");
        }

        [HttpPut]
        public JsonResult Put(Document doc)
        {
            bool success = true;
            var document = Documents.Get(doc.Id);
            try
            {
                if (document != null)
                {
                    document = Documents.Update(doc);
                }
                else
                {
                    success = false;
                }
            }
            catch (Exception)
            {
                success = false;
            }

            return success ? new JsonResult($"Update successful {document.Id}") : new JsonResult("Update was not successful");
        }

        [HttpDelete]
        public JsonResult Delete(Guid id)
        {
            bool success = true;
            var document = Documents.Get(id);

            try
            {
                if (document != null)
                {
                    Documents.Delete(document.Id);
                }
                else
                {
                    success = false;
                }
            }
            catch (Exception)
            {
                success = false;
            }

            return success ? new JsonResult("Delete successful") : new JsonResult("Delete was not successful");
        }
    }

Application Context

ApplicationContext – класс, который унаследован от класса DbContext. В нем прописываются все DbSet. С их помощью приложение знает, какие модели должны быть в базе данных, а какие нет.

public class ApplicationContext: DbContext
    {
        public DbSet<Car> Cars { get; set; }
        public DbSet<Document> Documents { get; set; }
        public DbSet<Worker> Workers { get; set; }

        public ApplicationContext(DbContextOptions<ApplicationContext> options): base(options)
        {
            Database.EnsureCreated();
        }
    }

Настройка зависимостей и инжектирования

А теперь немного про инжектирование. Правильная настройка зависимостей проекта Asp.net core позволяет упростить его работу и избежать лишнего написания кода. Все зависимости прописываются в файле «Startup.cs».

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

Также в этом же файле прописываются настройки для базы данных. Помните про строку подключения из начала статьи? Так вот сейчас мы ее и используем для настройки БД.

Вот как выглядит мой файл «Startup.cs»:

public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            string connection = Configuration.GetConnectionString("DefaultConnection");
            services.AddMvc();
            services.AddDbContext<ApplicationContext>(options =>
                options.UseSqlServer(connection));

            services.AddTransient<IRepairService, RepairService>();
            services.AddTransient<IBaseRepository<Document>, BaseRepository<Document>>();
            services.AddTransient<IBaseRepository<Car>, BaseRepository<Car>>();
            services.AddTransient<IBaseRepository<Worker>, BaseRepository<Worker>>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

Не забудьте создать БД перед запуском приложения. Для этого в Консоле диспетчера пакетов нужно прописать следующие команды:

Add-Migration init (или любое другое имя)

Update-Database

Поздравляю, если все шаги выполнены корректно, то вы создали свою базу данных. Также эти команды используются и для ее обновления, если ваши модели поменяются.

Тестирование

Здесь я покажу как создать UNIT-тесты для контроллера и сервиса. Для тестов я сделал отдельный проект (библиотека классов .Net Core).

Тест для контроллера

public class MainControllerTests
    {
        [Fact]
        public void GetDataMessage()
        {
            var mockDocs = new Mock<IBaseRepository<Document>>();
            var mockService = new Mock<IRepairService>();
            var document = GetDoc();
            mockDocs.Setup(x => x.GetAll()).Returns(new List<Document> { document });

            // Arrange
            MainController controller = new MainController(mockService.Object, mockDocs.Object);

            // Act
            JsonResult result = controller.Get() as JsonResult;

            // Assert
            Assert.Equal(new List<Document> { document }, result?.Value);
        }

        [Fact]
        public void GetNotNull()
        {
            var mockDocs = new Mock<IBaseRepository<Document>>();
            var mockService = new Mock<IRepairService>();
            mockDocs.Setup(x => x.Create(GetDoc())).Returns(GetDoc());

            // Arrange
            MainController controller = new MainController(mockService.Object, mockDocs.Object);
            // Act
            JsonResult result = controller.Get() as JsonResult;
            // Assert
            Assert.NotNull(result);
        }

        [Fact]
        public void PostDataMessage()
        {
            var mockDocs = new Mock<IBaseRepository<Document>>();
            var mockService = new Mock<IRepairService>();
            mockDocs.Setup(x => x.Create(GetDoc())).Returns(GetDoc());

            // Arrange
            MainController controller = new MainController(mockService.Object, mockDocs.Object);

            // Act
            JsonResult result = controller.Post() as JsonResult;

            // Assert
            Assert.Equal("Work was successfully done", result?.Value);
        }

        [Fact]
        public void UpdateDataMessage()
        {
            var mockDocs = new Mock<IBaseRepository<Document>>();
            var mockService = new Mock<IRepairService>();
            var document = GetDoc();

            mockDocs.Setup(x => x.Get(document.Id)).Returns(document);
            mockDocs.Setup(x => x.Update(document)).Returns(document);

            // Arrange
            MainController controller = new MainController(mockService.Object, mockDocs.Object);

            // Act
            JsonResult result = controller.Put(document) as JsonResult;

            // Assert
            Assert.Equal($"Update successful {document.Id}", result?.Value);
        }

        [Fact]
        public void DeleteDataMessage()
        {
            var mockDocs = new Mock<IBaseRepository<Document>>();
            var mockService = new Mock<IRepairService>();
            var doc = GetDoc();

            mockDocs.Setup(x => x.Get(doc.Id)).Returns(doc);
            mockDocs.Setup(x => x.Delete(doc.Id));

            // Arrange
            MainController controller = new MainController(mockService.Object, mockDocs.Object);

            // Act
            JsonResult result = controller.Delete(doc.Id) as JsonResult;

            // Assert
            Assert.Equal("Delete successful", result?.Value);
        }

        public Document GetDoc()
        {
            var mockCars = new Mock<IBaseRepository<Car>>();
            var mockWorkers = new Mock<IBaseRepository<Worker>>();

            var carId = Guid.NewGuid();
            var workerId = Guid.NewGuid();
            mockCars.Setup(x => x.Create(new Car()
            {
                Id = carId,
                Name = "car",
                Number = "123"
            }));

            mockWorkers.Setup(x => x.Create(new Worker()
            {
                Id = workerId,
                Name = "worker",
                Position = "manager",
                Telephone = "89165555555"
            }));

            return new Document
            {
                Id = Guid.NewGuid(),
                CarId = carId,
                WorkerId = workerId
            };
        }
    }

В данных тестах проверяется работа каждого метода контроллера на их корректное выполнение.

Тест для сервиса

public class RepairServiceTests
    {
        [Fact]
        public void WorkSuccessTest()
        {
            var serviceMock = new Mock<IRepairService>();
            var mockCars = new Mock<IBaseRepository<Car>>();
            var mockWorkers = new Mock<IBaseRepository<Worker>>();
            var mockDocs = new Mock<IBaseRepository<Document>>();
            var car = CreateCar(Guid.NewGuid());
            var worker = CreateWorker(Guid.NewGuid());
            var doc = CreateDoc(Guid.NewGuid(), worker.Id, car.Id);

            mockCars.Setup(x => x.Create(car)).Returns(car);
            mockDocs.Setup(x => x.Create(doc)).Returns(doc);
            mockWorkers.Setup(x => x.Create(worker)).Returns(worker);

            serviceMock.Object.Work();

            serviceMock.Verify(x => x.Work());
        }

        private Car CreateCar(Guid carId)
        {
            return new Car()
            {
                Id = carId,
                Name = "car",
                Number = "123"
            };
        }

        private Worker CreateWorker(Guid workerId)
        {
            return new Worker()
            {
                Id = workerId,
                Name = "worker",
                Position = "manager",
                Telephone = "89165555555"
            };
        }
        private Document CreateDoc(Guid docId, Guid workerId, Guid carId)
        {
            return new Document
            {
                Id = docId,
                CarId = carId,
                WorkerId = workerId
            };
        }
    }

В тесте для сервиса есть всего один тест для метода Work. Тут проверяется отработал этот метод или нет.

Запуск тестов

Чтобы запустить тесты достаточно зайти во вкладку «Тест» и нажать выполнить все тесты.

Выкладываем в Docker

В финале я покажу, как выложить данное приложение в Docker Hub. В Visual Studio 2019 это сделать крайне просто. Учтите, что у вас уже должен быть профиль в Docker и создан репозиторий в Docker Hub.

Нажимаете ПКМ на ваш проект и выбираете пункт опубликовать.

Там выбираем Docker Container Registry

На следующем окне, надо выбрать Docker Hub

Далее введите свои учетные данный Docker.

Если все прошло успешно, то осталось сделать последнюю вещь, нажать кнопку «Опубликовать».

Готово, вы опубликовали свое приложение в Docker Hub!

Заключение

В данной статье я показал, как использовать возможности C# ASP.NET Core для создания простого Rest API. Показал, как создавать модели, записывать их в БД, как создать свой репозиторий, как использовать сервисы и как создавать контроллеры, которые будут отправлять JSON ответы на ваш Front. Также показал, как сделать Unit-тесты для слоев контроллеров и сервисов. И в финале показал, как выложить приложение в Docker.

Надеюсь, что данная статья будет вам полезна!

Источник: https://habr.com/ru/post/531106/


Интересные статьи

Интересные статьи

В этой статье я опишу процесс создания сервера с gRPC и RESTful JSON API одновременно и Swagger документацию к нему. Эта статья — продолжение разбора различных способов реализаций API-сервера н...
В конце прошлого года мы выпустили .NET Core 3.0 и 3.1. В этих версиях добавлены модели настольных приложений Windows Forms (WinForms) и WPF, ASP.NET Blazor для создания одностраничных приложений...
В качестве небольшого экскурса для непосвященных в логическое программирование в этом тексте будет проведен сеанс магии с разоблачением приведен подход к созданию REST-сервера и замерены его п...
Приветствую вас (лично вас, а не всех кто это читает)! Сегодня мы: Создадим приложение (навык) Алисы с использованием нового (октябрь 2019) сервиса Yandex Cloud Functions. Настроим н...
Сегодняшняя статья рассмотрит основные вопросы про REST в Spring. Она будет особенно полезна для начинающих программистов. Официальный гид от Pivotal, в котором написано про темы для подготовки....