Как создать простое Rest API на .NET Core
Всем привет, в данной статье будет рассказано, как с использованием технологии 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.

Далее приступим к самому приложению.
Структура
Я разделил все приложение по папкам (также 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 where TDbModel : BaseModel < public ListGetAll(); public TDbModel Get(Guid id); public TDbModel Create(TDbModel model); public TDbModel Update(TDbModel model); public void Delete(Guid id); >
public class BaseRepository : IBaseRepository where TDbModel : BaseModel < private ApplicationContext Context < get; set; >public BaseRepository(ApplicationContext context) < Context = context; >public TDbModel Create(TDbModel model) < Context.Set().Add(model); Context.SaveChanges(); return model; > public void Delete(Guid id) < var toDelete = Context.Set().FirstOrDefault(m => m.Id == id); Context.Set().Remove(toDelete); Context.SaveChanges(); > public List GetAll() < return Context.Set().ToList(); > public TDbModel Update(TDbModel model) < var toUpdate = Context.Set().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().FirstOrDefault(m => m.Id == id); > >
Сервис
Сервис также как и репозиторий имеет интерфейс и его реализацию.
public interface IRepairService
public class RepairService : IRepairService < private IBaseRepositoryDocuments < get; set; >private IBaseRepository Cars < get; set; >private IBaseRepository Workers < get; set; >public void Work() < var rand = new Random(); var carId = Guid.NewGuid(); var workerId = Guid.NewGuid(); Cars.Create(new Car < Name = String.Format($"Car"), Number = String.Format($"") >); Workers.Create(new Worker < Name = String.Format($"Worker"), Position = String.Format($"Position"), Telephone = String.Format($"8916") >); 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 части приложения, достаточно передать запрос, который выглядит примерно вот так:
Пути гибко настраиваются с помощью специальных атрибутов (о них не в этой статье).
[ApiController] [Route("[controller]")] public class MainController : ControllerBase < private IRepairService RepairService < get; set; >private IBaseRepository Documents < get; set; >public MainController(IRepairService repairService, IBaseRepository 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 ") : 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 DbSetCars < get; set; >public DbSet Documents < get; set; >public DbSet Workers < get; set; >public ApplicationContext(DbContextOptions 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(options => options.UseSqlServer(connection)); services.AddTransient(); services.AddTransient, BaseRepository>(); services.AddTransient, BaseRepository>(); services.AddTransient, BaseRepository>(); > // 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 (или любое другое имя)
Поздравляю, если все шаги выполнены корректно, то вы создали свою базу данных. Также эти команды используются и для ее обновления, если ваши модели поменяются.
Тестирование
Здесь я покажу как создать UNIT-тесты для контроллера и сервиса. Для тестов я сделал отдельный проект (библиотека классов .Net Core).
Тест для контроллера
public class MainControllerTests < [Fact] public void GetDataMessage() < var mockDocs = new Mock>(); var mockService = new Mock(); var document = GetDoc(); mockDocs.Setup(x => x.GetAll()).Returns(new List < document >); // Arrange MainController controller = new MainController(mockService.Object, mockDocs.Object); // Act JsonResult result = controller.Get() as JsonResult; // Assert Assert.Equal(new List < document >, result?.Value); > [Fact] public void GetNotNull() < var mockDocs = new Mock>(); var mockService = new Mock(); 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>(); var mockService = new Mock(); 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>(); var mockService = new Mock(); 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 ", result?.Value); > [Fact] public void DeleteDataMessage() < var mockDocs = new Mock>(); var mockService = new Mock(); 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>(); var mockWorkers = new Mock>(); var carId = Guid.NewGuid(); var workerId = Guid.NewGuid(); mockCars.Setup(x => x.Create(new Car() < Name = "car", Number = "123" >)); mockWorkers.Setup(x => x.Create(new Worker() < Name = "worker", Position = "manager", Telephone = "89165555555" >)); return new Document < CarId = carId, WorkerId = workerId >; > >
В данных тестах проверяется работа каждого метода контроллера на их корректное выполнение.
Тест для сервиса
public class RepairServiceTests < [Fact] public void WorkSuccessTest() < var serviceMock = new Mock(); var mockCars = new Mock>(); var mockWorkers = new Mock>(); var mockDocs = new Mock>(); 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() < Name = "car", Number = "123" >; > private Worker CreateWorker(Guid workerId) < return new Worker() < Name = "worker", Position = "manager", Telephone = "89165555555" >; > private Document CreateDoc(Guid docId, Guid workerId, Guid carId) < return new Document < 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.
Надеюсь, что данная статья будет вам полезна!
Реализация Web API OData в ASP.NET Core 3 и ASP.NET 5 (часть 1). Связи «многие-ко-многим»
Наша команда занимается развитием корпоративной системы электронного документооборота. В команде часть приложений разрабатывается на текущей LTS версии .NET Core 3.1, в частности, бэкэнд для SPA, а также ряд Worker Service’ов, которые с определенным интервалом взаимодействуют с СЭД.
Со временем, возникла необходимость использования этими приложениями общих мастер-данных. Для их хранения решили использовать БД PostgreSQL, так как имели свежий опыт и почти готовое окружение для его развертывания. Непосредственно для получения (а в будущем – и для записи) данных приложениями, решили реализовать Web API на .NET Core 3.1, чтобы инкапсулировать взаимодействие с БД в одном приложении и заложить возможность взаимодействия с любой системой. В качестве ORM, исходя из сложившихся практик и опыта, использовали EF Core. При этом, нужна была возможность фильтрации и получения связанных данных. Чтобы не придумывать велосипед в этой части, пришли к необходимости реализации API на основе стандартов OData.
В сети есть ряд хороших статей по реализации API OData на .NET Core, однако информация в них весьма разрозненна и зачастую авторы упускают важные нюансы, имеющиеся в реализации. В первой статье нами описана общая реализация API OData с использованием EF Core. Особое внимание при этом уделено неочевидным моментам при реализации типа связи «многие-ко-многим».
Реализация на ASP.NET Core 3.1
Вначале в Visual Studio 2019 создадим проект по шаблону ASP.NET Core Web API. Для взаимодействия с БД Postgres в проект добавим пакеты Microsoft.EntityFrameworkCore, Npgsql.EntityFrameworkCore.PostgreSQL, а также, для применения рекомендованного для Postgres нэйминга объектов БД, используем пакет EFCore.NamingConventions. Для реализации требований OData добавляем пакет Microsoft.AspNetCore.OData:
all runtime; build; native; contentfiles; analyzers; buildtransitive
Основная проблема с типом связи «многие-ко-многим» заключалась в том, что EF Core 3.1 не умеет самостоятельно создавать таблицу, связывающую два справочника. Эта опция доступна только в версиях под .NET Framework, либо начиная с .NET 5.
Для примера реализуем модель для справочников Систем и Шаблонов загружаемых файлов: каждая система может использовать несколько шаблонов загрузки, а каждый шаблон может быть использован в нескольких системах. Каждый из классов модели имеет навигационное свойство, указывающее на другую модель. Навигационные свойства обязательно инициализируются в конструкторе класса пустым списком.
// Базовый класс public class BaseDictionaryEntry < [Column(Order = 1)] [Key] public long Id < get; set; >public string Name < get; set; >public string Description < get; set; >> // Модель записи справочника использующих систем public class UsingSystem : BaseDictionaryEntry < public ListUploadTemplates < get; set; >public UsingSystem() < UploadTemplates = new List(); > > // Модель записи справочника шаблонов загрузки public class UploadTemplate : BaseDictionaryEntry < public string ProcessName < get; set; >public List UsingSystems < get; set; >public UploadTemplate() < UsingSystems = new List(); > >
Контекст БД определим следующим образом:
// Контекст БД public class MyDbContext : DbContext < public virtual DbSetUploadTemplates < get; set; >public virtual DbSet UsingSystems < get; set; >public MyDbContext(DbContextOptions options) : base(options) < >public MyDbContext() < >>
Startup.cs будет выглядеть следующим образом:
// Startup public class Startup < public IConfiguration Configuration < get; >public Startup(IConfiguration configuration) < Configuration = configuration; >public void ConfigureServices(IServiceCollection services) < // Конфигурируем контекст БД services.AddDbContext(options => options .UseNpgsql(Configuration.GetValue("ConString"), assembly => assembly.MigrationsAssembly(typeof(MyDbContext).Assembly.FullName)) .UseSnakeCaseNamingConvention()); services.AddControllers(); // Конфигурируем OData services.AddOData(); > public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (env.IsDevelopment()) < app.UseDeveloperExceptionPage(); >app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => < endpoints.MapControllers(); endpoints.Select().Filter().OrderBy().Count().MaxTop(10).Expand(); // Добавляем пути OData endpoints.MapODataRoute("odata", "odata", GetEdmModel()); >); > // Настройка модели OData private IEdmModel GetEdmModel() < var odataBuilder = new ODataConventionModelBuilder(); odataBuilder.EntitySet("UsingSystems"); odataBuilder.EntitySet("UploadTemplates"); return odataBuilder.GetEdmModel(); > >
Наконец, добавим простой контроллер для одного из справочников. Реализацию методов контроллера подробно рассмотрим в следующей статье.
// Контроллер public class UsingSystemsController : BaseDictionaryController < public UsingSystemsController(DbContext dbContext) : base(dbContext) < >[EnableQuery] public IActionResult Get() < return Ok(_dbContext.UsingSystems .Include(x =>x.UploadTemplates)); > [EnableQuery] public IActionResult Get(long key) < return Ok(_dbContext.UsingSystems .Where(x =>x.Id == key) .Include(x => x.UploadTemplates)); > >
После запуска проекта получим следующее исключение:
System.InvalidOperationException: Unable to determine the relationship represented by navigation property ‘UsingSystem.UploadTemplates’ of type ‘List’. Either manually configure the relationship, or ignore this property using the ‘[NotMapped]’ attribute or by using ‘EntityTypeBuilder.Ignore’ in ‘OnModelCreating’.
Это означает, что Entity Framework Core не может понять, как ему связать две наших модели.
Решение проблемы со связью «многие-ко-многим» для .NET Core 3.1
Создавать связующую таблицу придется самостоятельно, определив отдельный класс. В классах UsingSystem и UploadTemplates, необходимо переписать навигационное свойство и его инициализацию пустым списком. Модель и контекст БД теперь выглядят так:
// Базовый класс public class BaseDictionaryEntry < [Column(Order = 1)] [Key] public long Id < get; set; >public string Name < get; set; >public string Description < get; set; >> // Модель записи справочника использующих систем public class UsingSystem : BaseDictionaryEntry < public ListUploadTemplateUsingSystems < get; set; >public UsingSystem() < UploadTemplateUsingSystems = new List(); > > // Модель записи справочника шаблонов загрузки public class UploadTemplate : BaseDictionaryEntry < public string ProcessName < get; set; >public List UploadTemplateUsingSystems < get; set; >public UploadTemplate() < UploadTemplateUsingSystems = new List(); > > // Модель для связывания сиситем и шаблонов public class UploadTemplateUsingSystem < public long UploadTemplateId < get; set; >public UploadTemplate UploadTemplate < get; set; >public long UsingSystemId < get; set; >public UsingSystem UsingSystem < get; set; >> // Контекст БД public class MyDbContext : DbContext < public virtual DbSetUploadTemplates < get; set; >public virtual DbSet UsingSystems < get; set; >public virtual DbSet UploadTemplateUsingSystems < get; set; >public MyDbContext(DbContextOptions options) : base(options) < >public MyDbContext() < >protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder.Entity() .HasKey(x => new ); modelBuilder.Entity() .HasOne(x => x.UploadTemplate) .WithMany(x => x.UploadTemplateUsingSystems) .HasForeignKey(x => x.UploadTemplateId); modelBuilder.Entity() .HasOne(x => x.UsingSystem) .WithMany(x => x.UploadTemplateUsingSystems) .HasForeignKey(x => x.UsingSystemId); > >
Обратите внимание, что в связывающем классе следует определить свойства как для хранения внешнего ключа, так и для хранения самого объекта. Причем о том, что UploadTemplateUsingSystem имеет составной ключ, нужно сообщить и Edm модели. Это необходимо, чтобы использовать параметр $expand при обращении к нашему API. Метод GetEdmModel в Startup изменим следующим образом:
private IEdmModel GetEdmModel() < var odataBuilder = new ODataConventionModelBuilder(); odataBuilder.EntitySet("UsingSystems"); odataBuilder.EntitySet("UploadTemplates"); odataBuilder.EntityType() .HasKey(x => new ); return odataBuilder.GetEdmModel(); >
Изменятся и методы контроллера:
[EnableQuery] public IActionResult Get() < return Ok(_dbContext.UsingSystems .Include(x =>x.UploadTemplateUsingSystems) .ThenInclude(x => x.UploadTemplate)); > [EnableQuery] public IActionResult Get(long key) < return Ok(_dbContext.UsingSystems .Where(x =>x.Id == key) .Include(x => x.UploadTemplateUsingSystems) .ThenInclude(x => x.UploadTemplate)); >
После применения миграций в базе появится третья таблица с двумя полями – соответствующими внешними ключами. Кажется, проблема решена. Но давайте попробуем вытащить первую систему и из нее получить список разрешенных шаблонов. Вызовем наше API с параметром $expand следующим запросом:
OData выдаст ошибку, потому что не сможет найти навигационное свойство UploadTemplates в типе UsingSystem. Запрос нужно поправить следующим образом:
GET http://localhost:61268/odata/UsingSystems(1)?$expand= UploadTemplateUsingSystems($expand=UploadTemplates)
В ответ мы получим JSON следующего вида:
Видим, что идентификаторы передаются дважды, да и навигация по такому объекту усложняется. Это значит, что на вызывающей стороне также придется усложнять вызов API.
Обновление проекта до .NET 5
Готовый проект пришлось перетаскивать на .NET 5. Для этого мы изменили файл проекта следующим образом:
net5.0 all runtime; build; native; contentfiles; analyzers; buildtransitive
Модель, контекст БД и контроллеры вернули в тот вид, в котором они были приведены в начале статьи. А вот в Startup с переходом к 8 версии (превью) Microsoft.AspNetCore.OData разрешенные методы манипуляции переехали в метод ConfigureServices:
public class Startup < public IConfiguration Configuration < get; >public Startup(IConfiguration configuration) < Configuration = configuration; >public void ConfigureServices(IServiceCollection services) < services.AddDbContext(options => options .UseNpgsql(Configuration.GetValue("ConStrings:Mdm"), assembly => assembly.MigrationsAssembly(typeof(MyDbContext).Assembly.FullName)) .UseSnakeCaseNamingConvention()); services.AddControllers(); // OData services.AddOData(opt => opt .AddModel("odata", GetEdmModel()) .Select() .Filter() .OrderBy() .Count() .Expand() ); > public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (env.IsDevelopment()) < app.UseDeveloperExceptionPage(); >app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => < endpoints.MapControllers(); >); > private IEdmModel GetEdmModel() < var odataBuilder = new ODataConventionModelBuilder(); odataBuilder.EntitySet("UsingSystems"); odataBuilder.EntitySet("UploadTemplates"); return odataBuilder.GetEdmModel(); > >
При применении миграций в базе создалась третья таблица для связывания. Результирующий JSON при выполнении запроса
стал выглядеть так:
Таким образом, реализация Entity FrameworkCore для .NET 5 позволила нам не только избавиться от ручного создания таблиц связей, но и упростить EDM модель и облегчить взаимодействие с OData на стороне клиента. Поэтому для создания подобных решений считаем .NET 5+ более предпочтительным выбором, чем .NET Core 3.1.
Как передать с помощью DI объект IWebHostEnvironment в другой проект?
Здравствуйте
Я создал сервисный класс, который каким-то образом должен передать в модель картинку. Проблема заключается в том, что мне нужно получить путь к статической папке. Раньше я это делал через IWebHostEnvironment, но теперь, на .NET 6, я не могу его передать в сервисный класс.
Так же я не могу понять, как его правильно зарегистрировать в Program.cs, что бы внедрить его в конвейер DI
- Вопрос задан более года назад
- 88 просмотров
Ef core как получить iwebhostenvironment в модели
Для взаимодействия с окружением, в котором запущено приложение, фреймфорк ASP.NET Core предоставляет интерфейс IWebHostEnvironment . Этот интерфейс предлагает ряд свойств, с помощью которых мы можем получить информацию об окружении:
- ApplicationName : хранит имя приложения
- EnvironmentName : хранит название среды, в которой хостируется приложение
- ContentRootPath : хранит путь к корневой папке приложения
- WebRootPath : хранит путь к папке, в которой хранится статический контент приложения. По умолчанию это папка wwwroot
- ContentRootFileProvider : возвращает реализацию интерфейса Microsoft.AspNetCore.FileProviders.IFileProvider , которая может использоваться для чтения файлов из папки ContentRootPath
- WebRootFileProvider : возвращает реализацию интерфейса Microsoft.AspNetCore.FileProviders.IFileProvider , которая может использоваться для чтения файлов из папки WebRootPath
При разработке мы можем использовать эти свойства. Но наиболее часто при разработке придется сталкиваться со свойством EnvironmentName . По умолчанию имеются три варианта значений для этого свойства: Development, Staging и Production. В проекте это свойство задается через установку переменной среды ASPNETCORE_ENVIRONMENT . Текущее значение данного параметра задается в файле launchSettings.json , который располагается в проекте в папке Properties .

Откроем данный файл:
< "iisSettings": < "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": < "applicationUrl": "http://localhost:56234", "sslPort": 44384 >>, "profiles": < "HelloApp": < "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7256;http://localhost:5256", "environmentVariables": < "ASPNETCORE_ENVIRONMENT": "Development" >>, "IIS Express": < "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": < "ASPNETCORE_ENVIRONMENT": "Development" >> > >
Здесь можно увидеть, что переменная «ASPNETCORE_ENVIRONMENT» встречается два раза — для запуска через IISExpress и для запуска через Kestrel. В обоих случаях она имеет значение Development. Но мы можем поменять значение этой переменной.
Для определения значения этой переменной для интерфейса IWebHostEnvironment определены специальные методы расширения:
- IsEnvironment(string envName) : возвращает true , если имя среды равно значению параметра envName
- IsDevelopment() : возвращает true , если имя среды — Development
- IsStaging() : возвращает true , если имя среды — Staging
- IsProduction() : возвращает true , если имя среды — Production
Данная функциональность позволяет нам выполнять определенный код в зависимости от того, на какой стадии находится приложение. Если приложение в процессе разработки, то мы можем выполнять один код, а при разветывании для полноценного использования другой код:
var builder = WebApplication.CreateBuilder(); WebApplication app = builder.Build(); if (app.Environment.IsDevelopment()) < app.Run(async (context) =>await context.Response.WriteAsync("In Development Stage")); > else < app.Run(async (context) =>await context.Response.WriteAsync("In Production Stage")); > Console.WriteLine($""); app.Run();
Так, если мы посмотрим на код класса WebApplicationBuilder, который применяется для создания приложения, то мы там можем увидеть там следующие строки:
if (context.HostingEnvironment.IsDevelopment())
Здесь если имя среды имеет значение «Development» (то есть приложение находится в состоянии разработки), то при ошибке разработчик увидит детальное описание ошибки. Если же приложение развернуто на хостинге и соответственно имеет другое имя хостирующей среды, то простой пользователь при ошибке ничего не увидит. Таким образом, в зависимости от стадии, на которой находится проект, мы можем скрывать или задействовать часть функционала приложения.
Если мы хотим поменять значение среды, необязательно изменять файл launchSettings.json . Это можно сделать также программно:
var builder = WebApplication.CreateBuilder(); WebApplication app = builder.Build(); app.Environment.EnvironmentName = "Production";
Определение своих состояний среды
Хотя по умолчанию среда может принимать три состояния: Development, Staging, Production, но мы можем при желании вводить новые значения. Например, нам надо отслеживать какие-то дополнительные состояния. Это можно сделать через изменение файла launchSettings.json либо программно.
Например, изменим название среды на «Test» (значение может быть произвольное):
var builder = WebApplication.CreateBuilder(); WebApplication app = builder.Build(); app.Environment.EnvironmentName = "Test"; // изменяем название среды на Test if (app.Environment.IsEnvironment("Test")) // Если проект в состоянии "Test" < app.Run(async (context) =>await context.Response.WriteAsync("In Test Stage")); > else < app.Run(async (context) =>await context.Response.WriteAsync("In Development or Production Stage")); > app.Run();

Также можно изменить значение «ASPNETCORE_ENVIRONMENT» на «Test» или любое другой в файле launchSettings.json для используемого профиля:
< "iisSettings": < "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": < "applicationUrl": "http://localhost:56234", "sslPort": 44384 >>, "profiles": < "HelloApp": < "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7256;http://localhost:5256", "environmentVariables": < "ASPNETCORE_ENVIRONMENT": "Test" >>, "IIS Express": < "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": < "ASPNETCORE_ENVIRONMENT": "Development" >> > >