Пример реализации бэкенда на C#
Пример работы бэкенда в C#
Эта статья полезна как "карта местности" для первого серверного проекта на ASP.NET Core. Материал можно читать линейно, а можно использовать как справочник по отдельным этапам: от БД и API до кэширования, тестов и развёртывания.
Создание веб-приложения требует разделения зон ответственности между клиентской и серверной частями. В случае интернет-магазина, как в данном примере, бэкенд (сервер) отвечает за хранение, обработку и передачу данных по запросу клиента. Клиент, в свою очередь — это браузер, мобильное приложение или другой сервис, инициирующий HTTP-запросы.
ASP.NET Core — это кроссплатформенная платформа от Microsoft, предназначенная для построения современных, высокопроизводительных и масштабируемых веб-приложений и API. Она реализует архитектуру MVC (Model–View–Controller), а также предоставляет средства для разработки RESTful-сервисов с минимальной конфигурацией.
В данной главе рассматривается полный цикл реализации бэкенд-логики для сценария "показать цену товара". Мы поэтапно пройдём следующие компоненты:
- Подготовка базы данных и модели данных.
- Настройка Entity Framework Core для взаимодействия с БД.
- Реализация контроллера, обрабатывающего HTTP-запросы.
- Организация клиентского взаимодействия (через JavaScript или Razor Pages).
- Внедрение Redis для кэширования ответов и снижения нагрузки на базу данных.
- Обсуждение жизненного цикла запроса и архитектурных особенностей 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
- Получение запроса — Kestrel (встроенный веб-сервер) принимает HTTP-запрос.
- Промежуточное ПО (Middleware) — запрос проходит через цепочку middleware (логирование, маршрутизация, аутентификация и т.д.).
- Маршрутизация — система маршрутизации ASP.NET Core сопоставляет URL с контроллером и методом.
- Привязка модели — параметры URL, тела запроса или заголовков автоматически преобразуются в аргументы метода контроллера.
- Выполнение логики — контроллер взаимодействует с сервисами (БД, кэш и т.д.).
- Формирование ответа — результат сериализуется (обычно в JSON) и отправляется клиенту.
- Освобождение ресурсов — DI-контейнер утилизирует scoped-объекты (например,
AppDbContext).
ASP.NET Core обеспечивает строгую изоляцию компонентов, поддержку async/await и эффективное управление памятью.
6.1. Короткий сценарий "что происходит на практике"
Представим запрос GET /api/products/5:
- Клиент отправляет запрос из браузера или мобильного приложения.
- Сервер
Kestrelпринимает запрос и передаёт его в middleware-пайплайн. - Middleware применяют правила CORS, аутентификацию, логирование и маршрутизацию.
- Контроллер вызывает репозиторий или
DbContextи получает товар из БД или кэша. - ASP.NET Core сериализует объект
Productв JSON. - Клиент получает ответ, отображает цену и, при необходимости, кэширует результат на своей стороне.
Такой "мысленный трейс" помогает быстрее отлаживать ошибки: на каждом шаге есть свой класс проблем, свой лог и свой уровень диагностики.
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 /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 как основа веб-интеграций.