Пишем приложение на C#-стеке

от автора

Всем привет! Меня зовут Дмитрий Бахтенков, и я .NET-разработчик. Сегодня мы проведем эксперимент — напишем полноценное веб-приложение с использованием решений, которые написаны на C# и платформе .NET. Больше моих статей можно прочитать в медиа вАЙТИ.

Что я имею в виду?

Как мы знаем, в общем случае веб-приложение состоит из бэкенда, фронтенда, базы данных и иногда из кеша. С бэкендом и фронтендом всё понятно: у нас есть замечательный фреймворк ASP.NET Core для сервера и blazor или razor pages для клиента. Однако инфраструктурные части приложения — БД, кеши — чаще всего пишутся на других, более низкоуровневых языках, таких как C и C++.

К счастью, недавно Microsoft выпустила решение для кеширования — аналог Redis, который называется Garnet. В качестве основной базы данных можно использовать документную БД RavenDB, которая как раз написана на C#.

Пишем приложение на C#-стеке

Пишем приложение на C#-стеке

Что кодим?

Итак, со стеком разобрались. У нас будет:

  • ASP.NET Core для бэкенда;

  • Blazor для фронтенда;

  • RavenDB в качестве основной СУБД;

  • Garnet для кеша.

Hello World’ом в мире веб-приложений принято считать различные to do листы. Подобное приложение мы и напишем.

Я создал пустое решение (empty solution) в IDE, а затем добавил туда проект Web API на ASP.NET Core:

Бэкенд: репозиторий и эндпоинты

RavenDB — это документно-ориентированная NoSQL база данных, разработанная для упрощения работы с данными и обеспечения высокой производительности. Она поддерживает ACID-транзакции, time-series, полнотекстовый поиск и многое другое. Подробнее можно ознакомиться на сайте проекта.

Для общения с RavenDB используется библиотека RavenDB.Client. Ее можно добавить в интерфейсе или с помощью команды: 

dotnet add package RavenDB.Client

Далее добавим папку DataAccess и класс ToDoItem — это будет нашей моделью:

public class ToDoItem {     public string Id { get; set; }     public string Title { get; set; }     public string Description { get; set; }     public DateTime Deadline { get; set; } }

Туда же добавим класс ToDoRepository, который будет общаться с БД с помощью специальной абстракции IDocumentStore. Репозиторий отвечает за CRUD-операции: создание, чтение, обновление и удаление. Абстракция IDocumentStore позволяет создавать объекты-сессии, с помощью которых можно взаимодействовать с данными.

В файле Program.cs зарегистрируем наш репозиторий, а также инициализируем IDocumentStore коннектом к нашей БД. Вообще, взаимодействие с RavenDB очень похоже на работу с DBContext в Entity Framework.

Метод Create:

    public async Task Create(ToDoItem item)     {         using var session = store.OpenAsyncSession();         await session.StoreAsync(item);         await session.SaveChangesAsync();     }

Метод GetById:

    public async Task<ToDoItem> GetById(string id)     {         using var session = store.OpenAsyncSession();         return await session.LoadAsync<ToDoItem>(id);     }

Обновить сущность можно двумя способами:

  • Получить сущность по идентификатору, обновить набор полей у сущности и вызвать SaveChanges.

  • Вызвать метод Patch, в котором нужно указать сущность и ссылки на поля в ней.

    public async Task Update(ToDoItem item)     {         using var session = store.OpenAsyncSession();         session.Advanced.Patch(item, x => x.Deadline, item.Deadline);         session.Advanced.Patch(item, x => x.Title, item.Title);         session.Advanced.Patch(item, x => x.Description, item.Description);         await session.SaveChangesAsync();     }

Также необходимо зарегистрировать наш репозиторий в DI-контейнере, в файле Program.cs:

var store = new DocumentStore {     Urls = new[] { "http://localhost:8080" },     Database = "Todos" }; store.Initialize(); builder.Services.AddSingleton<IDocumentStore>(store);

Бэкенд: кеш

В качестве кеша мы используем Garnet. Это remote cache-store от Microsoft Research. В основе своей это решение написано на C#. Этот кеш поддерживает протокол RESP, поэтому в качестве клиента мы сможем использовать библиотеку StackExchange.Redis.

Установим библиотеку:

dotnet add package StackExchange.Redis

Добавим класс CacheService и реализуем первый метод GetOrAdd:

public async Task<T> GetOrAdd<T>(string key, Func<Task<T>> itemFactory, int expirationInSecond)     {         // если такой элемент уже есть, возвращаем его         var existingItem = await _database.StringGetAsync(key);         if (existingItem.HasValue)         {             return JsonSerializer.Deserialize<T>(existingItem);         }           // забираем новый элемент         var newItem = await itemFactory();           // добавляем элемент в кеш          await _database.StringSetAsync(key, JsonSerializer.Serialize(newItem), TimeSpan.FromSeconds(expirationInSecond));         return newItem;     

Метод Invalidate для очистки кеша:

    public async Task Invalidate(string key)     {         await _database.KeyDeleteAsync(key);     }

Бэкенд: соединяем всё вместе

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

public class ToDoService(ToDoRepository repository, CacheService cacheService) {     public async Task<IEnumerable<ToDoItem>> GetAllAsync()     {         return await cacheService.GetOrAdd($"ToDoItem:all",              async () => await repository.GetAll(), 30);     }       public async Task<ToDoItem> GetByIdAsync(string id)     {         return await cacheService.GetOrAdd($"ToDoItem:{id}",              async () => await repository.GetById(id), 30);     }       public async Task CreateAsync(ToDoItem item)     {         await repository.Create(item);         await cacheService.Invalidate($"ToDoItem:all");     }       public async Task UpdateAsync(ToDoItem item)     {         await repository.Update(item);         await cacheService.Invalidate($"ToDoItem:{item.Id}");         await cacheService.Invalidate($"ToDoItem:all");     }       public async Task DeleteAsync(string id)     {         await repository.Delete(id);         await cacheService.Invalidate($"ToDoItem:{id}");         await cacheService.Invalidate($"ToDoItem:all");     } }

 Зарегистрируем всё необходимое в Program.cs:

var store = new DocumentStore {     Urls = new[] { "http://localhost:8080" },     Database = "Todos" }; store.Initialize(); builder.Services.AddSingleton<IDocumentStore>(store); builder.Services.AddScoped<ToDoRepository>(); builder.Services.AddScoped<ToDoService>(); builder.Services.AddScoped<CacheService>(); builder.Services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect("localhost"));

И добавим эндпоинты с помощью подхода Minimal API в том же Program.cs:

app.MapGet("api/todo", async ([FromServices] ToDoService toDoService)      => await toDoService.GetAllAsync());      app.MapPost("api/todo", async ([FromBody] ToDoItem item, [FromServices] ToDoService toDoService)      => await toDoService.CreateAsync(item));   app.MapPut("api/todo", async ([FromBody] ToDoItem item, [FromServices] ToDoService toDoService)      => await toDoService.UpdateAsync(item));   app.MapGet("api/todo/{id}", async (string id, [FromServices] ToDoService toDoService)      => await toDoService.GetByIdAsync(id));   app.MapDelete("api/todo/{id}", async (string id, [FromServices] ToDoService toDoService)     => await toDoService.DeleteAsync(id));

Infrastructure

Чтобы проверить наше приложение, необходимо поднять RavenDB и Garnet. Это можно сделать с помощью Docker Compose.

Добавим в проект папку Launcher и файл docker-compose.yml:

version: '3.8'   services:   ravendb:     image: ravendb/ravendb:latest     environment:       RAVEN_DB_URL: "http://0.0.0.0:8080"       RAVEN_DB_PUBLIC_URL: "http://ravendb:8080"       RAVEN_DB_TCP_URL: "tcp://0.0.0.0:38888"     ports:       - "8080:8080"     garnet:     image: 'ghcr.io/microsoft/garnet'     ulimits:       memlock: -1     ports:       - "6379:6379"     volumes:       - garnetdata:/data   volumes:   ravendb_data:   garnetdata:

Выполним команду docker compose up -d. Теперь по адресу localhost:8080 доступна RavenDB, а по адресу localhost:6379 — Garnet.

На адрес RavenDB необходимо зайти и выполнить первичную настройку, а затем перейти в раздел Databases и создать БД Todos:

Теперь мы можем запустить наш API и проверить его работоспособность. Запустим приложение, откроем Swagger и выполним POST-запрос на создание задачи:

Запрос завершился успешно. Мы можем зайти в БД и увидеть задачу:

Кеш проще всего проверить в дебаггере: при первом выполнении GET-запроса мы должны сходить в БД, а при втором — уже в кеш:

Фронтенд

Теперь напишем UI для нашего таск-трекера. Будем использовать фреймворк Blazor, чтобы приложение было написано полностью на .NET 🙂

Добавим проект Blazor в наше решение:

По аналогии с бэкендом добавим классы ToDoItem для описания объекта задачи и ToDoService для взаимодействия с Backend.
ToDoItem:

public class ToDoItem {     public string Id { get; set; }     [Required]     public string Title { get; set; }     [Required]     public string Description { get; set; }     public DateTime Deadline { get; set; } }

ToDoService:

public class ToDoService(HttpClient httpClient) {     public async Task<List<ToDoItem>> GetToDoItemsAsync()          => await httpClient.GetFromJsonAsync<List<ToDoItem>>("todo");          public async Task<ToDoItem> GetToDoItemByIdAsync(string id)         => await httpClient.GetFromJsonAsync<ToDoItem>($"todo/{id}");       public async Task CreateToDoItemAsync(ToDoItem item)         => await httpClient.PostAsJsonAsync("todo", item);       public async Task UpdateToDoItemAsync(ToDoItem item)         => await httpClient.PutAsJsonAsync($"todo/{item.Id}", item);       public async Task DeleteToDoItemAsync(string id)         =>  await httpClient.DeleteAsync($"todo/{id}"); }

В файле Program.cs зарегистрируем сервис и HttpCilent:

builder.Services.AddScoped(_ =>  new HttpClient { BaseAddress = new Uri("http://localhost:5042/api/") });  builder.Services.AddScoped<ToDoService>();

Теперь визуал. Вся логика будет содержаться в файлах CreateItem.razor, EditItem.razor и ToDoList.razor. Полный код можно посмотреть на GitHub, здесь же сосредоточимся на ключевых моментах.

Для проброса различных сервисов на странице можно использовать хелпер @inject:

@inject ToDoService ToDoService @inject NavigationManager NavigationManager

 Для форм используется тег EditForm:

<EditForm EditContext="@editContext" Model="newItem" FormName="Create New Task" OnValidSubmit="HandleValidSubmit">     <DataAnnotationsValidator />     <ValidationSummary />     <div>         <label for="title">Title: </label>         <InputText id="title" class="form-control" @bind-Value="newItem.Title" />     </div>     <div>         <label for="description">Description: </label>         <InputText id="description" class="form-control" @bind-Value="newItem.Description" />     </div>     <div>         <label for="deadline">Deadline: </label>         <InputDate  id="deadline" class="form-control" @bind-Value="newItem.Deadline" />     </div>       <button type="submit" class="btn btn-primary">Save</button> </EditForm>

Если вы используете .NET8, в файлах страниц необходимо явно указать параметр rendermode для корректной работы методов:

@rendermode InteractiveServer

Запускаем приложения: сначала docker-compose для инфраструктуры, затем наш API и фронтенд:

Заключение

В этой статье мы попробовали использовать для создания веб-приложения всё, что написано на платформе .NET, — от фреймворка фронтенда до базы данных. Конечно, это не единственные приложения, написанные на C#. Ещё есть YARP, который очень удобно использовать в качестве прокси для микросервисов, или LiteDB — in-memory база данных, удобная для тестирования.

Полный код на GitHub


Другие статьи по теме

Бизнес-моделирование: как и зачем его использовать в разработке
Разбираем, чем помогают бизнесу диаграммы процессов и когда процессы реально нужно оптимизировать

Метрики IT-компании: какие цифры нужно знать, чтобы прийти к успеху в бизнесе
Двенадцать метрик, с помощью которых можно оценить эффективность бизнеса

Как я построил управление корпоративными знаниями по ИТ-продукту
Делимся опытом организации и построения корпоративной базы знаний для продуктовой ИТ-разработки


ссылка на оригинал статьи https://habr.com/ru/articles/847332/