Ответ
Есть несколько паттернов для расширения и переиспользования функциональности контроллеров в ASP.NET Core, каждый решает разные задачи:
1. Наследование от базового контроллера: Идеально для общей логики, которая должна быть доступна во множестве контроллеров (например, логирование, общие методы-хелперы).
// Базовый контроллер с общей функциональностью
[ApiController]
[Route("api/[controller]")]
public abstract class ApiBaseController : ControllerBase
{
// Общий метод для успешного ответа с данными
protected IActionResult ApiOk<T>(T data) => Ok(new ApiResponse<T> { Success = true, Data = data });
// Общий метод для обработки ошибок валидации ModelState
protected IActionResult ApiValidationError()
=> BadRequest(new ApiResponse { Success = false, Errors = ModelState.Values.SelectMany(v => v.Errors) });
// Общее свойство (например, идентификатор текущего пользователя)
protected Guid CurrentUserId => User.FindFirstValue(ClaimTypes.NameIdentifier);
}
// Контроллер, использующий базовый
public class ProductsController : ApiBaseController
{
[HttpGet]
public IActionResult Get()
{
// Используем метод из базового класса
return ApiOk(_repository.GetAllProducts());
}
}
2. Фильтры (Filters): Лучший способ для сквозной функциональности (cross-cutting concerns), которая должна выполняться до или после действия метода: авторизация, валидация, кэширование, логирование.
// Кастомный фильтр для логирования длительных запросов
public class LogExecutionTimeAttribute : ActionFilterAttribute
{
private Stopwatch _stopwatch;
public override void OnActionExecuting(ActionExecutingContext context)
{
_stopwatch = Stopwatch.StartNew();
}
public override void OnActionExecuted(ActionExecutedContext context)
{
_stopwatch.Stop();
var elapsedMs = _stopwatch.ElapsedMilliseconds;
if (elapsedMs > 500)
{
var logger = context.HttpContext.RequestServices.GetService<ILogger<LogExecutionTimeAttribute>>();
logger.LogWarning("Длительный запрос {ActionName}: {ElapsedMs}ms", context.ActionDescriptor.DisplayName, elapsedMs);
}
}
}
// Применение фильтра на уровне контроллера или метода
[LogExecutionTime]
[ApiController]
public class SlowController : ControllerBase { /* ... */ }
3. Методы расширения (Extension Methods) для ControllerBase:
Удобны для добавления небольших, часто используемых утилитарных методов.
public static class ControllerExtensions
{
public static string GetClientIpAddress(this ControllerBase controller)
{
return controller.HttpContext.Connection.RemoteIpAddress?.ToString();
}
public static IActionResult RedirectToLocal(this ControllerBase controller, string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
return controller.Redirect(returnUrl);
else
return controller.RedirectToAction("Index", "Home");
}
}
// Использование внутри действия: var ip = this.GetClientIpAddress();
4. Внедрение зависимостей (Dependency Injection): Самый чистый способ для предоставления контроллеру общих сервисов. Логика инкапсулируется в сервисе, который затем внедряется в конструктор любого контроллера.
// Сервис с общей бизнес-логикой
public interface IUserContextService
{
Guid GetCurrentUserId();
}
// Контроллер, использующий этот сервис
public class ProfileController : ControllerBase
{
private readonly IUserContextService _userContext;
public ProfileController(IUserContextService userContext) // DI
{
_userContext = userContext;
}
[HttpGet("me")]
public IActionResult GetMyProfile()
{
var userId = _userContext.GetCurrentUserId(); // Используем общий сервис
// ...
}
}
Выбор подхода:
- Общая логика ответов/хелперы -> Наследование.
- Сквозная функциональность (логи, проверки) -> Фильтры.
- Небольшие утилиты -> Методы расширения.
- Сложная бизнес-логика или доступ к данным -> Внедрение сервисов (DI).
Ответ 18+ 🔞
Так, слушай сюда, про контроллеры в ASP.NET Core. Тут, блядь, как в гараже: есть несколько ключей на выбор, но каждый от своей двери. Суть в том, чтобы не изобретать велосипед на каждый чих, а переиспользовать логику. Смотри, какие есть варианты.
1. Наследование, как от батиного жигуля. Идеально, когда тебе надо, чтобы во всех контроллерах была какая-то общая фигня. Типа стандартного формата ответов или хелпер-методов. Создаёшь одного предка и от него плодишь всех.
// Это типа наш общий предок, на котором все ездят
[ApiController]
[Route("api/[controller]")]
public abstract class ApiBaseController : ControllerBase
{
// Универсальный метод для нормального ответа
protected IActionResult ApiOk<T>(T data) => Ok(new ApiResponse<T> { Success = true, Data = data });
// Метод, когда валидация моделей обосралась
protected IActionResult ApiValidationError()
=> BadRequest(new ApiResponse { Success = false, Errors = ModelState.Values.SelectMany(v => v.Errors) });
// Ну и, допустим, ID текущего юзера, чтобы каждый раз не выковыривать
protected Guid CurrentUserId => User.FindFirstValue(ClaimTypes.NameIdentifier);
}
// А это уже конкретный контроллер-наследник
public class ProductsController : ApiBaseController
{
[HttpGet]
public IActionResult Get()
{
// Используем метод бати, не паримся
return ApiOk(_repository.GetAllProducts());
}
}
2. Фильтры (Filters). Это, блядь, мощь. Вот это твой инструмент для сквозняка, прости господи, для сквозной функциональности. Всё, что должно выполняться ДО или ПОСЛЕ твоего метода: проверка прав, логирование, замер времени. Не лезь в сам метод с этой хуйнёй.
// Фильтр, который будет материться в лог, если запрос выполняется долго
public class LogExecutionTimeAttribute : ActionFilterAttribute
{
private Stopwatch _stopwatch;
public override void OnActionExecuting(ActionExecutingContext context)
{
_stopwatch = Stopwatch.StartNew();
}
public override void OnActionExecuted(ActionExecutedContext context)
{
_stopwatch.Stop();
var elapsedMs = _stopwatch.ElapsedMilliseconds;
if (elapsedMs > 500) // Если дольше полсекунды — пиздец какой долгий
{
var logger = context.HttpContext.RequestServices.GetService<ILogger<LogExecutionTimeAttribute>>();
logger.LogWarning("Запрос {ActionName} ебёт мозги аж {ElapsedMs} мс!", context.ActionDescriptor.DisplayName, elapsedMs);
}
}
}
// Вешаем этот фильтр на контроллер или метод — и всё, он работает
[LogExecutionTime]
[ApiController]
public class SlowController : ControllerBase { /* ... */ }
3. Методы-расширения для ControllerBase.
Ну, это когда тебе нужна мелкая, но часто требуемая утилита. Типа "узнай IP клиента" или "редиректни, но проверь URL". Не засоряешь базовый класс, а просто добавляешь метод как будто он всегда там был.
public static class ControllerExtensions
{
// Где этот ковбой сидит?
public static string GetClientIpAddress(this ControllerBase controller)
{
return controller.HttpContext.Connection.RemoteIpAddress?.ToString();
}
// Редирект, но чтобы не уехать на левый сайт
public static IActionResult RedirectToLocal(this ControllerBase controller, string returnUrl)
{
if (controller.Url.IsLocalUrl(returnUrl))
return controller.Redirect(returnUrl);
else
return controller.RedirectToAction("Index", "Home");
}
}
// Использование: var ip = this.GetClientIpAddress(); — просто и понятно.
4. Внедрение зависимостей (Dependency Injection). Самый, блядь, чистый и правильный путь, когда речь про общую бизнес-логику или доступ к данным. Всю общую работу засовываешь в отдельный сервис, а потом просто просишь его в конструкторе. Никакого наследования, только чистая композиция.
// Сервис, который знает про текущего юзера
public interface IUserContextService
{
Guid GetCurrentUserId();
}
// Контроллер, который этот сервис использует
public class ProfileController : ControllerBase
{
private readonly IUserContextService _userContext;
// Просто говорим: "дай мне сервис" — фреймворк сам его подсунет
public ProfileController(IUserContextService userContext)
{
_userContext = userContext;
}
[HttpGet("me")]
public IActionResult GetMyProfile()
{
var userId = _userContext.GetCurrentUserId(); // Всё, получили ID
// ...
}
}
Итог, ёпта:
- Общие ответы и мелкие хелперы — бери наследование. Как отцовский молоток в гараже.
- Логирование, авторизация, замеры (сквозняк) — это фильтры. Ставишь и забышь.
- Мелкие утилитки на один раз — методы расширения. Прилепил и пользуешься.
- Серьёзная общая логика или работа с данными — внедряй сервисы (DI). Это канон, и спорить тут бесполезно.
Главное — не пытайся всё запихнуть в один подход, а то получится монстр, которого потом сам же и будешь проклинать.