Lazy command C# | Паттерн Ленивая команда C#

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-канал. Там еще больше полезного и интересного для программистов.