Введение
Всем привет, в данной статье будет рассказано, как с использованием технологии C# ASP.NET Core написать простое Rest Api. Сделать Unit-тесты на слои приложений. Отправлять Json ответы. Также покажу, как выложить данное приложение в Docker.
В данной статье не будет описано, как делать клиентскую (далее Front) часть приложения. Здесь я покажу только серверную (далее Back).
Что используем?
Писать код я буду в Visual Studio 2019.
Для реализации приложения, я буду использовать такие библиотеки NuGet:
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Для тестов вот эти библиотеки:
Microsoft.NET.Test.Sdk
Microsoft.NETCore.App
Moq
xunit
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.
![Выбор шаблона приложения Выбор шаблона приложения](https://habrastorage.org/getpro/habr/upload_files/f6d/4ff/24e/f6d4ff24e2a705dc09cc75badc04d8ce.jpg)
Далее приступим к самому приложению.
Структура
Я разделил все приложение по папкам (также Unit-тесты в отдельном проекте) и получил вот такую структуру мое приложения:
![Структура приложения Структура приложения](https://habrastorage.org/getpro/habr/upload_files/23a/656/724/23a6567248d099f2288a708c620df583.jpg)
Модели
Для реализации моделей я сделал абстрактный класс 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
![](https://habrastorage.org/getpro/habr/upload_files/f09/924/87d/f0992487db960d0206ba005d308f2e0b.jpg)
На следующем окне, надо выбрать Docker Hub
![](https://habrastorage.org/getpro/habr/upload_files/092/f9e/eff/092f9eeffd65fe938a307378335bce86.jpg)
Далее введите свои учетные данный Docker.
Если все прошло успешно, то осталось сделать последнюю вещь, нажать кнопку «Опубликовать».
Готово, вы опубликовали свое приложение в Docker Hub!
Заключение
В данной статье я показал, как использовать возможности C# ASP.NET Core для создания простого Rest API. Показал, как создавать модели, записывать их в БД, как создать свой репозиторий, как использовать сервисы и как создавать контроллеры, которые будут отправлять JSON ответы на ваш Front. Также показал, как сделать Unit-тесты для слоев контроллеров и сервисов. И в финале показал, как выложить приложение в Docker.
Надеюсь, что данная статья будет вам полезна!