Как расширить функциональность контроллера в ASP.NET Core?

Ответ

Есть несколько паттернов для расширения и переиспользования функциональности контроллеров в 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). Это канон, и спорить тут бесполезно.

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