TGViewer
Библиотека шарписта | C#, F#, .NET, ASP.NET Библиотека шарписта | C#, F#, .NET, ASP.NET @csharpproglib · 21.7K subscribers
Post #7009 3.22K
🌐 REST DTO не должны попадать в сервис

Частая ошибка в .NET-проектах — использовать одни и те же модели на всех слоях. Контроллер принял CreateUserRequest, передал его прямо в сервис, сервис вернул UserResponse обратно в контроллер. Выглядит просто. Но это ловушка.

REST DTO это контракт с клиентом. Сервисный слой это бизнес-логика. Это разные ответственности.
Пример плохого кода:
// REST DTO
public class CreateUserRequest
{
public string Email { get; set; }
public string Password { get; set; }
public string Role { get; set; } // "admin", "user"
}

// Контроллер передаёт REST DTO прямо в сервис
[HttpPost]
public async Task<IActionResult> Create(CreateUserRequest request)
{
var user = await _userService.CreateAsync(request); // не надо
return Ok(user);
}

// Сервис знает о REST DTO
public async Task<UserResponse> CreateAsync(CreateUserRequest request)
{
// бизнес-логика завязана на HTTP-контракт
}


Теперь представьте, что появился второй клиент. Он шлёт другой формат. Или вы хотите вызвать CreateAsync из фоновой задачи, где нет никакого HTTP-запроса. Приходится либо тащить ненужный DTO, либо переписывать сервис.

Ещё хуже поле Role в CreateUserRequest. Клиент сам указывает, кем хочет стать. Даже если оно не используется или перезаписывается далее, выглядит это не очень.

Как правильно

Каждый слой работает со своей моделью. Контроллер маппит входящий DTO в команду или модель сервисного слоя. Сервис возвращает доменный результат. Контроллер маппит его в ответный DTO:
// REST DTO — только для HTTP-слоя
public class CreateUserRequest
{
public string Email { get; set; }
public string Password { get; set; }
// Role здесь нет — клиент не решает
}

public class UserResponse
{
public Guid Id { get; set; }
public string Email { get; set; }
public string Role { get; set; }
}

// Сервисная модель — внутренний контракт
public class CreateUserCommand
{
public string Email { get; set; }
public string Password { get; set; }
public UserRole Role { get; set; } // enum, не строка
}

public class UserResult
{
public Guid Id { get; set; }
public string Email { get; set; }
public UserRole Role { get; set; }
}


// Контроллер — маппит и не лезет в логику
[HttpPost]
public async Task<IActionResult> Create(CreateUserRequest request)
{
var command = new CreateUserCommand
{
Email = request.Email,
Password = request.Password,
Role = UserRole.User // роль задаётся здесь, не клиентом
};

var result = await _userService.CreateAsync(command);

var response = new UserResponse
{
Id = result.Id,
Email = result.Email,
Role = result.Role.ToString()
};

return Ok(response);
}

// Сервис — ничего не знает про HTTP
public async Task<UserResult> CreateAsync(CreateUserCommand command)
{
// чистая бизнес-логика
}


Почему это важно

Сервис перестаёт зависеть от формата HTTP-запроса. Его можно вызвать из воркера, gRPC-эндпоинта, теста без изменений.

Контракт с клиентом можно менять независимо от внутренней логики. Добавили поле в CreateUserRequest, сервис об этом не знает.

Валидация и маппинг живут в одном месте — в контроллере. Сервис получает уже проверенные данные в нужном формате.

Для маппинга удобно использовать AutoMapper или Mapster, но даже ручной маппинг лучше, чем слитые слои.

Граница между HTTP-слоем и бизнес-логикой — это не формальность. Это то, что позволяет менять одно, не трогая другое.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор
  • ❤ 12
  • 👍 12
  • 😁 2
  • 🥱 1
More from @csharpproglib
  1. Sep 23, 2026⚙️ yield return не бесплатный Итераторы выглядят просто, но работают иначе: IEnumerable<in…
  2. Sep 22, 2026💡 Replace, Regex или StringBuilder? Для замены текста в C# есть несколько инструментов. И…
  3. Sep 21, 2026⚙️ Настоящие атомарные операции Если Volatile решает проблему видимости, то Interlocked ре…
  4. Sep 20, 2026💪 Разминка перед трудовыми буднями Что произойдёт? ❤️ — список станет [1, 3] 🔥 — Invalid…
  5. Sep 19, 2026🧩 Middleware в ASP.NET Core: 3 ловушки Middleware — звено HTTP pipeline: app.Use(async (c…
  6. Sep 18, 2026📍 Навигация: Вакансии • Задачи • Собесы 🐸Библиотека шарписта #garbage_collector
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →