[HttpPost("order")] // Оставляем название экшена, но имя метода меняем, чтобы соответствовало naming convention public async Task <ActionResult> OrderAsync(Cart cart) { // Заказ с единственным полем Positions, который при этом равен null, нам точно не нужен, сразу возвращаем Bad Request (400) if (cart?.Positions == null) { // Перед тем как вернуть Bad Request (400), логируем Warning, желательно ещё и передать в лог сам запрос _logger.Log(LogLevel.Warning, $"Bad Request {nameof(cart.Positions)} is null"); return(new BadRequestResult()); } // ПОМЕНЯЛ МЕСТАМИ проверку доступности позиций для заказа и преобразование заказа из API моделей в доменные модели + ВАЛИДАЦИЮ, // так как это всё происходит в коде, а значит быстро (должно быть быстрее, чем обращение к сервису FiguresStorage -> IRedisStorage). // Если что-то пойдёт не так или сработает валидация, то мы вернём отрицательный ответ пользователю, не обращаясь и не нагружая FiguresStorage -> IRedisStorage. // А в задании было сказано про несколько инстансов этого сервиса (API), а значит Redis (или что-то другое, что запроксировали как Redis) будут закидывать запросами сразу несколько инстансов (может быть их штук 100 запустят потом). // В любом случае, если есть валидация, то она должна сработать раньше чем обращения к другим сервисам // Вынесем логику по созданию и маппингу конкретных типов в отдельный экстеншен и избавимся от конструкции switch case (с оговорками) var order = cart.ToOrder(out var errorMessages); if (errorMessages?.Any() == true) { var errorMessage = $"Bad Request. Validation errors: {string.Join(Environment.NewLine, errorMessages)}"; // Перед тем как вернуть Bad Request (400) response, логируем Warning _logger.Log(LogLevel.Warning, errorMessage); // Пользователю тоже даём обратную связь return(BadRequest(errorMessage)); } // И только теперь мы начинаем проверять доступность позиции для заказа (после преобразования API моделей и валидации) // TODO: выбрать один из вариантов проверки, в зависимости от среднего количества позиций и скорости работы сервиса IFiguresStorage // ВАРИАНТ 1: Если проверка работает приемлемо быстро и позиций обычно не бывает много, то if (!AreAllPositionsAvailableSlow(cart, out var badPosition)) // Иначе, если проверка выше занимает много времени (зависит от реализации IFiguresStorage), то можно попробовать делать такую проверку и параллельно: // ВАРИАНТ 2: //if (!AreAllPositionsAvailableFast(cart, out var badPosition)) { // Перед тем как вернуть Bad Request (400) response, логируем Warning _logger.Log(LogLevel.Warning, $"Bad Request for type '{badPosition?.Type}' with count '{badPosition?.Count}'"); return(new BadRequestResult()); } foreach (var position in cart.Positions) { _figuresStorage.Reserve(position.Type, position.Count); } decimal result; try { // Так как, реализации IOrderStorage я не увидел, то могу лишь предполагать, что в качестве результата она возвращает стоимость, которая теперь будет вычисляться примерно так: // result = order.GetTotalPrice(); // и, за счёт реализации паттерна Компоновщик, стоимость легко посчитается и не получится добавить фигуры без реализации подсчёта цены // Используя await мы получим конкретное исключение вместо AggregateException result = await _orderStorage.SaveAsync(order); } catch (Exception e) { // Перед тем как пробросить эксепшен - логируем его _logger.Log(LogLevel.Error, e, e.Message); // Именно так, чтобы сохранить полный stack trace throw; } return(new OkObjectResult(result)); // Используя result.Result мы получим AggregateException (с сообщением о том, что произошла одна или более ошибок). // И получить ошибку можно будет только из InnerException или InnerExceptions, а полный StackTrace придётся собирать из полей StackTrace AggregateException и InnerException //var result = _orderStorage.SaveAsync(order); //return new OkObjectResult(result.Result); }