Перейти к основному содержимому

Пример реализации бэкенда на C#

Разработчику Архитектору

Пример работы бэкенда в C#

Эта статья полезна как "карта местности" для первого серверного проекта на ASP.NET Core. Материал можно читать линейно, а можно использовать как справочник по отдельным этапам: от БД и API до кэширования, тестов и развёртывания.

Создание веб-приложения требует разделения зон ответственности между клиентской и серверной частями. В случае интернет-магазина, как в данном примере, бэкенд (сервер) отвечает за хранение, обработку и передачу данных по запросу клиента. Клиент, в свою очередь — это браузер, мобильное приложение или другой сервис, инициирующий HTTP-запросы.

ASP.NET Core — это кроссплатформенная платформа от Microsoft, предназначенная для построения современных, высокопроизводительных и масштабируемых веб-приложений и API. Она реализует архитектуру MVC (Model–View–Controller), а также предоставляет средства для разработки RESTful-сервисов с минимальной конфигурацией.

В данной главе рассматривается полный цикл реализации бэкенд-логики для сценария "показать цену товара". Мы поэтапно пройдём следующие компоненты:

  1. Подготовка базы данных и модели данных.
  2. Настройка Entity Framework Core для взаимодействия с БД.
  3. Реализация контроллера, обрабатывающего HTTP-запросы.
  4. Организация клиентского взаимодействия (через JavaScript или Razor Pages).
  5. Внедрение Redis для кэширования ответов и снижения нагрузки на базу данных.
  6. Обсуждение жизненного цикла запроса и архитектурных особенностей ASP.NET Core.
Маршрут чтения для новичка

Если вы только входите в ASP.NET Core, сначала пройдите Первая программа ASP.NET Core

ASP.NET — фреймворк для веб-приложений, после этого вернитесь к этой статье и повторите шаги на своём мини-проекте.


1. Подготовка базы данных и модели данных

1.1. Структура данных

Для простоты предположим, что в интернет-магазине используется реляционная СУБД, например PostgreSQL или Microsoft SQL Server. Таблица товаров может выглядеть следующим образом:

CREATE TABLE Products (
Id SERIAL PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Price DECIMAL(18, 2) NOT NULL
);

Разбор:

  • CREATE TABLE Products создает таблицу с именем Products в реляционной БД.
  • Id SERIAL PRIMARY KEY задает автоинкрементный первичный ключ для уникальной идентификации записей.
  • Name VARCHAR(255) NOT NULL хранит название товара и запрещает пустое значение NULL.
  • Price DECIMAL(18, 2) хранит денежное значение с фиксированной точностью, что безопаснее float.
  • Такая схема минимальна, но уже покрывает базовый сценарий чтения товара по идентификатору.

1.2. Создание модели на C#

В ASP.NET Core модели обычно размещаются в папке Models. Класс модели должен отражать структуру таблицы:

// Models/Product.cs
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}

Разбор:

  • public class Product описывает доменную модель, которая маппится на таблицу БД.
  • Id, Name, Price соответствуют колонкам SQL и используются EF Core для ORM-сопоставления.
  • string.Empty в Name убирает риск null в runtime при создании экземпляра.
  • decimal выбран для цены, чтобы избежать погрешностей двоичной арифметики.
  • Автосвойства { get; set; } позволяют EF Core читать и записывать значения через reflection.

Примечание. В современных версиях C# (начиная с C# 8 и особенно C# 9+) рекомендуется использовать инициализаторы по умолчанию, чтобы избежать null-значений при десериализации или конфигурации с nullable-аннотациями.


2. Настройка Entity Framework Core

Entity Framework Core (EF Core) — это ORM (Object-Relational Mapper), который абстрагирует работу с базой данных через работу с объектами C#. Это позволяет писать запросы на LINQ, а не на SQL.


2.1. Создание контекста базы данных

Контекст — это основной класс, через который приложение взаимодействует с БД:

// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using YourProjectName.Models;

public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }

public DbSet<Product> Products { get; set; }
}

Разбор:

  • AppDbContext : DbContext — центральная точка доступа EF Core к базе.
  • Конструктор с DbContextOptions<AppDbContext> получает конфигурацию через DI.
  • DbSet<Product> Products представляет набор строк таблицы Products как типизированную коллекцию.
  • Через контекст выполняются LINQ-запросы, трекинг изменений и сохранение данных.
  • Scoped-жизненный цикл контекста синхронизируется с жизненным циклом HTTP-запроса.

Здесь DbSet<Product> представляет таблицу Products в БД.


2.2. Регистрация контекста в DI-контейнере

В файле Program.cs (в ASP.NET Core 6+) необходимо зарегистрировать контекст с указанием строки подключения:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Добавление контекста с подключением к БД (например, PostgreSQL)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

Разбор:

  • CreateBuilder(args) поднимает конфигурацию, логирование и контейнер зависимостей.
  • AddDbContext<AppDbContext> регистрирует контекст как сервис и привязывает провайдер PostgreSQL.
  • GetConnectionString("DefaultConnection") берет строку подключения из конфигурации окружения.
  • builder.Build() собирает финальный WebApplication с middleware-пайплайном.
  • Эта конфигурация делает доступ к БД единообразным во всех контроллерах и сервисах.

В appsettings.json указывается строка подключения:

{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=ShopDb;Username=postgres;Password=yourpassword"
}
}

Для SQL Server используется UseSqlServer, для SQLite — UseSqlite и т.д.


2.3. Миграции

EF Core использует миграции для синхронизации модели C# с физической схемой БД. Создание и применение миграций выполняется через CLI:

dotnet ef migrations add InitialCreate
dotnet ef database update

Разбор:

  • migrations add InitialCreate фиксирует текущее состояние моделей как миграцию.
  • EF генерирует C#-класс миграции и SQL-операции изменения схемы.
  • database update применяет все непримененные миграции к целевой базе данных.
  • Команды обеспечивают управляемую эволюцию схемы без ручных SQL-правок.
  • В командной практике миграции версионируются вместе с кодом приложения.

Это сгенерирует SQL-скрипты и применит их к базе данных.


3. Реализация контроллера

Контроллер в ASP.NET Core — это класс, унаследованный от ControllerBase (для Web API) или Controller (для MVC/Razor Pages). Он содержит методы, соответствующие HTTP-методам (GET, POST и т.д.).


3.1. Простой API-контроллер

Код ITЗагрузка примера кода…

Разбор:

  • [ApiController] включает автоматическую валидацию и более строгие правила API-контроллера.
  • [Route("api/[controller]")] строит маршрут из имени контроллера (Products -> /api/products).
  • Конструктор принимает AppDbContext через DI, исключая ручное создание контекста.
  • GetProduct(int id) выполняет асинхронный запрос FindAsync по первичному ключу.
  • NotFound() возвращает HTTP 404, а return product; сериализует объект в JSON c кодом 200.

Запрос к /api/products/5 вернёт JSON-представление товара с ID = 5.

Атрибуты [ApiController] и [Route] включают автоматическую обработку ошибок, привязку моделей и маршрутизацию. Метод FindAsync использует первичный ключ для эффективного поиска.


4. Клиентское взаимодействие

4.1. Через JavaScript (SPA или статический HTML)

На клиенте можно отправить запрос с помощью fetch:

Код ITЗагрузка примера кода…

Разбор:

  • Кнопка вызывает fetchPrice(1) и передает идентификатор товара в клиентскую функцию.
  • fetch('/api/products/${productId}') инициирует HTTP GET к backend API.
  • Проверка response.ok разделяет успешные ответы от ошибок (например, 404).
  • response.json() десериализует JSON в JS-объект product.
  • innerText обновляет DOM и показывает пользователю цену либо сообщение "Товар не найден".

Этот подход характерен для одностраничных приложений (SPA) и не зависит от серверного рендеринга.


4.2. Через Razor Pages (серверный рендеринг)

Если используется Razor Pages, логика может быть реализована непосредственно в модели страницы:

Код ITЗагрузка примера кода…

Разбор:

  • DetailsModel : PageModel формирует серверную модель страницы Razor.
  • Свойство Product? хранит состояние, которое позже используется в .cshtml.
  • OnGetAsync(int id) выполняется при HTTP GET и загружает товар из БД.
  • NotFound() возвращает 404, если запись отсутствует, предотвращая пустой рендер.
  • return Page(); передает заполненную модель в шаблон и инициирует SSR-ответ.

И представление (Details.cshtml):

<h2>@Model.Product.Name</h2>
<p>Цена: @Model.Product.Price ₽</p>

В этом случае HTML-страница генерируется на сервере, и запрос к API не требуется.


5. Внедрение Redis для кэширования

Кэширование критически важно для снижения нагрузки на базу данных при высокой частоте повторяющихся запросов. Redis — это высокопроизводительное in-memory хранилище, которое часто используется как кэш.


5.1. Подключение Redis в ASP.NET Core

Установите пакет:

dotnet add package StackExchange.Redis

Добавьте в Program.cs:

builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")));

В appsettings.json:

{
"ConnectionStrings": {
"Redis": "localhost:6379"
}
}

5.2. Реализация кэширования в контроллере

Код ITЗагрузка примера кода…

Разбор:

  • Метод реализует read-through cache: сначала проверка Redis, затем fallback к БД.
  • StringGetAsync($"product:{id}") читает кэш по ключу с пространством имен product:.
  • При попадании в кэш JSON десериализуется обратно в Product и возвращается мгновенно.
  • При промахе читается _context.Products.FindAsync(id), затем результат сериализуется и кэшируется.
  • StringSetAsync(..., TimeSpan.FromMinutes(10)) ограничивает TTL и предотвращает бессрочное устаревание.

Здесь используется упрощённый подход. В production-коде рекомендуется вынести логику кэширования в отдельный сервис и использовать MemoryCache или IDistributedCache (ASP.NET Core предоставляет абстракцию IDistributedCache, которую можно реализовать через Redis).


5.3. Использование IDistributedCache

Более архитектурно чистый способ:

builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

Затем внедрить IDistributedCache:

private readonly IDistributedCache _cache;

public ProductsController(AppDbContext context, IDistributedCache cache)
{
_context = context;
_cache = cache;
}

И использовать:

var cachedBytes = await _cache.GetAsync(cacheKey);
if (cachedBytes != null)
{
var json = Encoding.UTF8.GetString(cachedBytes);
return JsonSerializer.Deserialize<Product>(json);
}

// ... загрузка из БД

await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(json), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});

Это упрощает тестирование и обеспечивает совместимость с другими провайдерами кэша (например, SQL Server Cache).


6. Жизненный цикл HTTP-запроса в ASP.NET Core

  1. Получение запроса — Kestrel (встроенный веб-сервер) принимает HTTP-запрос.
  2. Промежуточное ПО (Middleware) — запрос проходит через цепочку middleware (логирование, маршрутизация, аутентификация и т.д.).
  3. Маршрутизация — система маршрутизации ASP.NET Core сопоставляет URL с контроллером и методом.
  4. Привязка модели — параметры URL, тела запроса или заголовков автоматически преобразуются в аргументы метода контроллера.
  5. Выполнение логики — контроллер взаимодействует с сервисами (БД, кэш и т.д.).
  6. Формирование ответа — результат сериализуется (обычно в JSON) и отправляется клиенту.
  7. Освобождение ресурсов — DI-контейнер утилизирует scoped-объекты (например, AppDbContext).

ASP.NET Core обеспечивает строгую изоляцию компонентов, поддержку async/await и эффективное управление памятью.


6.1. Короткий сценарий "что происходит на практике"

Представим запрос GET /api/products/5:

  1. Клиент отправляет запрос из браузера или мобильного приложения.
  2. Сервер Kestrel принимает запрос и передаёт его в middleware-пайплайн.
  3. Middleware применяют правила CORS, аутентификацию, логирование и маршрутизацию.
  4. Контроллер вызывает репозиторий или DbContext и получает товар из БД или кэша.
  5. ASP.NET Core сериализует объект Product в JSON.
  6. Клиент получает ответ, отображает цену и, при необходимости, кэширует результат на своей стороне.

Такой "мысленный трейс" помогает быстрее отлаживать ошибки: на каждом шаге есть свой класс проблем, свой лог и свой уровень диагностики.


7. Архитектурные улучшения — паттерн Repository и разделение ответственности

Хотя прямое использование DbSet<T> в контроллере допустимо для простых приложений, в промышленной разработке рекомендуется изолировать логику доступа к данным с помощью паттерна Repository. Это упрощает тестирование, поддержку и замену хранилища.


7.1. Интерфейс и реализация репозитория

// Interfaces/IProductRepository.cs
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
Task UpdateAsync(Product product);
Task DeleteAsync(int id);
}

Разбор:

  • Интерфейс задает контракт слоя доступа к данным без привязки к конкретной ORM.
  • Асинхронные сигнатуры (Task) позволяют не блокировать поток при I/O операциях.
  • Набор методов покрывает базовые CRUD-операции для Product.
  • Абстракция упрощает unit-тесты: контроллер может работать с mock-реализацией.
  • Контракт стабилизирует API слоя данных при изменениях внутренней реализации.

Код ITЗагрузка примера кода…


7.2. Регистрация в DI-контейнере

builder.Services.AddScoped<IProductRepository, ProductRepository>();

Теперь контроллер зависит от абстракции:

Код ITЗагрузка примера кода…

Такой подход обеспечивает тестируемость: при unit-тестировании можно подставить mock-реализацию IProductRepository.


8. Обработка ошибок и централизованное логирование

8.1. Глобальный обработчик исключений

Вместо обработки ошибок в каждом методе контроллера, ASP.NET Core поддерживает middleware для централизованного перехвата исключений:

Код ITЗагрузка примера кода…

Разбор:

  • UseExceptionHandler создает глобальный перехватчик необработанных исключений.
  • Middleware централизованно возвращает код 500 и единый JSON-формат ошибок.
  • IExceptionHandlerPathFeature дает доступ к исходной ошибке и URL, где она возникла.
  • Логирование через ILogger<Program> сохраняет стек ошибки для диагностики.
  • Централизация обработки уменьшает дублирование try/catch в контроллерах.

Для среды разработки можно подключить детализированные страницы ошибок:

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

8.2. Валидация входных данных

Для предотвращения некорректных запросов используются атрибуты валидации:

public class CreateProductDto
{
[Required, MaxLength(200)]
public string Name { get; set; } = string.Empty;

[Range(0.01, 1_000_000)]
public decimal Price { get; set; }
}

Разбор:

  • DTO отделяет входной контракт API от внутренней доменной модели.
  • [Required, MaxLength(200)] ограничивает обязательность и размер поля Name.
  • [Range(0.01, 1_000_000)] валидирует допустимый диапазон для цены.
  • Атрибуты DataAnnotations выполняются до бизнес-логики контроллера.
  • Такой слой валидации блокирует некорректные данные на границе системы.

В контроллере:

[HttpPost]
public async Task<ActionResult> Create([FromBody] CreateProductDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);

// ... сохранение
}

ASP.NET Core автоматически возвращает 400 Bad Request при нарушении правил валидации, если включён атрибут [ApiController].


9. Документирование API — Swagger/OpenAPI

Для внутреннего использования и интеграции с фронтендом или внешними системами важно документировать API. Пакет Swashbuckle.AspNetCore генерирует интерактивную документацию в формате OpenAPI.

Установка:

dotnet add package Swashbuckle.AspNetCore

Настройка в Program.cs:

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Shop API", Version = "v1" });
});

// ...

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Shop API v1"));
}

Теперь по адресу /swagger доступен UI для тестирования методов, просмотра схем запросов и ответов.


10. Безопасность — CORS, аутентификация и авторизация

10.1. CORS (Cross-Origin Resource Sharing)

Если фронтенд размещён на другом домене (например, http://localhost:3000), необходимо настроить CORS:

builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});

// ...

app.UseCors("AllowFrontend");

10.2. Аутентификация через JWT

Для защищённых методов (например, "изменить товар") подключается JWT-аутентификация:

Код ITЗагрузка примера кода…

В appsettings.json:

{
"Jwt": {
"Key": "your-256-bit-secret",
"Issuer": "ShopApi",
"Audience": "ShopFrontend"
}
}

Методы, требующие авторизации, помечаются атрибутом:

[Authorize]
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, UpdateProductDto dto) { ... }

11. Docker-развёртывание и внешние зависимости

Для воспроизводимости окружения и упрощения развёртывания приложение упаковывается в Docker-контейнеры. Пример Dockerfile:

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "ShopApi.dll"]

Разбор:

  • Multi-stage build делит runtime (aspnet) и этап сборки (sdk) для уменьшения финального образа.
  • dotnet publish -c Release готовит self-contained набор бинарников для запуска.
  • COPY --from=build /app/publish . переносит только артефакты публикации в финальный слой.
  • EXPOSE 80 документирует порт приложения внутри контейнера.
  • ENTRYPOINT определяет процесс запуска API при старте контейнера.

Для развёртывания вместе с PostgreSQL и Redis используется docker-compose.yml:

Код ITЗагрузка примера кода…

Это обеспечивает локальное воспроизведение production-подобной среды.


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

12.1. Unit-тесты для репозитория

С использованием Moq и xUnit:

Код ITЗагрузка примера кода…

Разбор:

  • [Fact] помечает метод как тестовый сценарий xUnit без параметров.
  • Mock<AppDbContext> и Mock<DbSet<Product>> изолируют тест от реальной базы.
  • Setup(...FindAsync(1)).ReturnsAsync(...) задает предсказуемый ответ заглушки.
  • Act вызывает целевой метод репозитория, Assert проверяет контракт результата.
  • Тест подтверждает, что GetByIdAsync корректно возвращает найденный объект.

12.2. Интеграционные тесты с TestServer

ASP.NET Core предоставляет WebApplicationFactory<T> для запуска приложения в памяти:

Код ITЗагрузка примера кода…


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.