TLDR: Переместите action методы из контроллеров в лениво загружаемые команды с помощью паттерна «Ленивая команда».
При написании контроллеров в ASP.NET Core, Вы можете столкнуться с очень длинным классом, если не будете осторожны. Представим, что Вы написали несколько action методов в контроллере, по несколько строк кода в каждом, и прокомментировали их для поддержки Swagger.
Например:
[Route("api/[controller]")] [ApiVersion("1.0")] public class CarsController : Controller { private readonly ICarRepository _carRepository; private readonly IMapper<Models.Car, Car> _carMapper; private readonly IMapper<SaveCar, Models.Car> _saveCarToCarMapper; public CarsController(ICarRepository carRepository, IMapper<Models.Car, Car> carMapper, IMapper<SaveCar, Models.Car> saveCarToCarMapper) { _carRepository = carRepository; _carMapper = carMapper; _saveCarToCarMapper = saveCarToCarMapper; } /// <summary> /// Получает автомобиль с указанным ID. /// </summary> /// <param name="carId">ID автомобиля.</param> /// <param name="cancellationToken">Маркер отмены, используемый для отмены HTTP-запроса.</param> /// <returns>Ответ 200 OK, содержащий автомобиль или 404 Не найден, если автомобиль с указанным ID не найден.</returns> /// <response code="200">Автомобиль с указанным ID.</response> /// <response code="304">Автомобиль не изменился с даты, указанной в HTTP-заголовке If-Modified-Since.</response> /// <response code="404">Не удалось найти автомобиль с указанным идентификатором.</response> [HttpGet("{carId}", Name = CarsControllerRoute.GetCar)] [HttpHead("{carId}")] [ProducesResponseType(typeof(Car), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] public async Task<IActionResult> Get(int carId, CancellationToken cancellationToken) { var car = await this._carRepository.Get(carId, cancellationToken); if (car == null) { return NotFound(); } if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfModifiedSince, out StringValues stringValues)) { if (DateTimeOffset.TryParse(stringValues, out DateTimeOffset modifiedSince) && (modifiedSince >= car.Modified)) { return new StatusCodeResult(StatusCodes.Status304NotModified); } } var carViewModel = this._carMapper.Map(car); HttpContext.Response.Headers.Add(HeaderNames.LastModified, car.Modified.ToString("R")); return Ok(carViewModel); } /// <summary> /// Создает новый автомобиль. /// </summary> /// <param name="car">Автомобиль для создания.</param> /// <param name="cancellationToken">Маркер отмены, используемый для отмены HTTP-запроса.</param> /// <returns>201 Созданный ответ, содержащий вновь созданный автомобиль или 400 Bad Request, если автомобиль недействителен.</returns> /// <response code="201">Автомобиль был создан.</response> /// <response code="400">Автомобиль недействителен.</response> [HttpPost("", Name = CarsControllerRoute.PostCar)] [ProducesResponseType(typeof(Car), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] public async Task<IActionResult> Post([FromBody] SaveCar saveCar, CancellationToken cancellationToken) { var car = _saveCarToCarMapper.Map(saveCar); car = await this._carRepository.Add(car, cancellationToken); var carViewModel = this._carMapper.Map(car); return CreatedAtRoute(CarsControllerRoute.GetCar, new {carId = carViewModel.CarId}, carViewModel); } }
Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов.
А на YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!
Паттерн проектирования Ленивая команда
Именно здесь и может быть полезен паттерн «Ленивая команда». Паттерн перемещает логику из каждого action метода и вводит зависимости в свой собственный класс.
[Route("api/[controller]")] [ApiVersion("1.0")] public class CarsController : ControllerBase { private readonly Lazy<IGetCarCommand> _getCarCommand; private readonly Lazy<IPostCarCommand> _postCarCommand; public CarsController( Lazy<IGetCarCommand> getCarCommand, Lazy<IPostCarCommand> postCarCommand) { this._getCarCommand = getCarCommand; this._postCarCommand = postCarCommand; } /// <summary> /// Получает автомобиль с указанным ID. /// </summary> /// <param name="carId">Идентификатор автомобиля.</param> /// <param name="cancellationToken">Маркер отмены, используемый для отмены HTTP-запроса.</param> /// <returns>Ответ 200 OK, содержащий автомобиль или 404 Не найден, если автомобиль с указанным ID не найден.</returns> /// <response code="200">Автомобиль с указанным ID.</response> /// <response code="304">Автомобиль не изменился с даты, указанной в HTTP-заголовке If-Modified-Since.</response> /// <response code="404">Не удалось найти автомобиль с указанным идентификатором.</response> [HttpGet("{carId}", Name = CarsControllerRoute.GetCar)] [HttpHead("{carId}")] [ProducesResponseType(typeof(Car), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] public Task<IActionResult> Get(int carId, CancellationToken cancellationToken) => this._getCarCommand.Value.ExecuteAsync(carId, cancellationToken); /// <summary> /// Создает новый автомобиль. /// </summary> /// <param name="car">Автомобиль для создания.</param> /// <param name="cancellationToken">Маркер отмены, используемый для отмены HTTP-запроса.</param> /// <returns>201 Созданный ответ, содержащий вновь созданный автомобиль или 400 Bad Request, если автомобиль недействителен.</returns> /// <response code="201">Автомобиль был создан.</response> /// <response code="400">Автомобиль недействителен.</response> [HttpPost("", Name = CarsControllerRoute.PostCar)] [ProducesResponseType(typeof(Car), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] public Task<IActionResult> Post([FromBody] SaveCar car, CancellationToken cancellationToken) => this._postCarCommand.Value.ExecuteAsync(car, cancellationToken); }
public interface IPostCarCommand : IAsyncCommand<SaveCar> public class PostCarCommand : IPostCarCommand { private readonly ICarRepository carRepository; private readonly IMapper<Models.Car, Car> carToCarMapper; private readonly IMapper<SaveCar, Models.Car> saveCarToCarMapper; public PostCarCommand( ICarRepository carRepository, IMapper<Models.Car, Car> carToCarMapper, IMapper<SaveCar, Models.Car> saveCarToCarMapper) { this.carRepository = carRepository; this.carToCarMapper = carToCarMapper; this.saveCarToCarMapper = saveCarToCarMapper; } public async Task<IActionResult> ExecuteAsync(SaveCar saveCar, CancellationToken cancellationToken) { var car = this.saveCarToCarMapper.Map(saveCar); car = await this.carRepository.Add(car, cancellationToken); var carViewModel = this.carToCarMapper.Map(car); return new CreatedAtRouteResult( CarsControllerRoute.GetCar, new { carId = carViewModel.CarId }, carViewModel); } }
Вся логика и зависимости в контроллерах перемещаются в команду, котоорая имеет теперь одну ответственность. Теперь у контроллера другой набор зависимостей и он лениво создаёт команду для каждого запроса.
Возможно, Вы заметили интерфейс IAsyncCommand. У меня их имеется 4 штуки, каждый со своим набором параметров (от 0 до 3). Все они описывают метод ExecuteAsync для выполнения команды и возвращают IActionResult. Я лично считаю, что если Вам нужно передавать более 3 параметров, для этого вы можете использовать класс для представления своих параметров.
public interface IAsyncCommand { Task<IActionResult> ExecuteAsync(CancellationToken cancellationToken); } public interface IAsyncCommand<T> { Task<IActionResult> ExecuteAsync(T parameter, CancellationToken cancellationToken); } public interface IAsyncCommand<T1, T2> { Task<IActionResult> ExecuteAsync(T1 parameter1, T2 parameter2, CancellationToken cancellationToken); } public interface IAsyncCommand<T1, T2, T3> { Task<IActionResult> ExecuteAsync(T1 parameter1, T2 parameter2, T3 parameter3, CancellationToken cancellationToken); }
Почему Lazy?
Почему мы используем Lazy<T>? Ответ заключается в том, что если у нас есть несколько action методов в нашем контроллере, мы не хотим создавать экземпляры зависимостей для каждого action метода, если мы планируем использовать только один. Для регистрации наших Lazy команд требуется немного дополнительной работы в Startup.cs. Мы можем регистрировать ленивые зависимости так:
public void ConfigureServices(IServiceCollection services) { // ...Omitted services .AddScoped<IGetRocketCommand, GetRocketCommand>() .AddScoped(x => new Lazy( () => x.GetRequiredService())); }
HttpContext и ActionContext
Теперь Вы можете подумать, как получить доступ к HttpContext или ActionContext, для, например, установки HTTP заголовка? Для этой цели мы можем использовать интерфейсы IHttpContextAccessor и IActionContextAccessor
public void ConfigureServices(IServiceCollection services) { // ...Omitted services .AddSingleton<IHttpContextAccessor, HttpContextAccessor>() .AddSingleton<IActionContextAccessor,ActionContextAccessor>();; }
Обратите, что они могут быть зарегистрированы, как singleton. Затем, Вы можете ииспользовать их для получения объектов HttpContext или ActionContext для текущего HTTP-запроса. Вот простой пример:
public class SetHttpHeaderCommand : ISetHttpHeaderCommand { private readonly IHttpContextAccessor httpContextAccessor; public SetHttpHeaderCommand (IHttpContextAccessor httpContextAccessor) => this.httpContextAccessor = httpContextAccessor; public async Task<IActionResult> ExecuteAsync() { this.httpContextAccessor.HttpContext.Response.Headers.Add("X-Rocket", "Saturn V"); return new OkResult(); } }
Unit тестирование
Ещё одно преимущество данного паттерна состоит в том, что тестирование каждой команды становится супер простым. Вам больше не нужно будет настраивать контроллер с большим количеством зависимостей, которые Вам не нужны. Вам нужно только написать тестовый код для этой единственной функции.
Также рекомендую ознакомиться со статьей Паттерн проектирования Посредник (Mediator) на C#. А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.