Blazor Client Side Интернет Магазин: Часть 6 — Создание заказа и работа с компенсирующими действиями

от автора

Привет, Хабр! Продолжаю делать интернет магазин и изучать Blazor. В этой части расскажу о том как добавил в него возможность создания заказа, просмотра заказов и работу с последовательностью действий одно из которых может завершиться ошибкой. За подробностями добро пожаловать под кат.

Содержание

Ссылки

→ Исходники
Образы на Docker Registry

Saga

Мне стало скучно и я решил имитировать ситуацию когда у нас есть два микросервиса — для корзины и оформления заказов. Обычно для таких ситуаций создается микросервис Оркестратор, который управляет последовательностью действий и выполняет компенсирующие действия. Например такое встроено в MassTransit, NServiceBus, MS Orleans (распределенные транзакции). Для примера я решил немного смоделировать ситуацию, когда нужно постучаться в два разных сервиса которые у вас нет возможности изменить. Google и FaceBook например. Хотя тут тоже лучше сделать это через сервер, а на бекенд послать один запрос. Тут максимально простой пример. Чуть посложнее надо было бы в LocalStorage браузера сохранять незавершенное состояние и по таймеру пытаться откатить, а не так топорно как я тут сделал.

Код OrdersViewModel:

        public async Task Create()         {           //Загружаем состояние корзины с сервера.             await _basket.Load();             if (!string.IsNullOrWhiteSpace(_basket.Error))                 return;             //Глубокое клонирование лучше сделать автомаппером.             // Для учебного проекта и так сойдет.            //Сохранять прежнее состояние корзины лучше в локалстораже.             var lines = _basket.Model.Select(l => new LineModel()             {                 Product = new ProductModel()                 {                     Id = l.Product.Id,                     Price = l.Product.Price,                     Title = l.Product.Title,                     Version = l.Product.Version                 },                 Quantity = l.Quantity             }).ToList();           //Посылаем на сервер команду - очистить корзину.             await _basket.Clear();             if (!string.IsNullOrWhiteSpace(_basket.Error))                 return;            //Пытаемся создать заказ и если не получается то восстанавливаем прежнее состояние корзины.             try             {                 await _order.Create(lines, Address);             }             catch             {                 //Компенсирующие операции чтобы восстанавливаем прежнее состояние.                  await Restore(lines);                 throw;             }             if (!string.IsNullOrWhiteSpace(_order.Error))             {                 await Restore(lines);             }             State = OrderVmState.List;         }          private async Task Restore(IEnumerable<LineModel> lines)         {             //Тут надо бы сделать один метод на сервере который сразу коллекцию предметов корзины             //принимает но мне для учебного проекта лень.            // Поэтому в цикле поочередно добавляю предметы обратно в корзину.             foreach (var line in lines)             {                 for (int i = 0; i < line.Quantity; i++)                 {                     await _basket.Add(line.Product);                 }             }         } 

Код

1)Model

    public class LineModel     {         public uint Quantity { get; set; }         public ProductModel Product { get; set; }     }      public enum OrderStatus     {         Created = 10,         Delivered,     }      public class OrderModel     {         public Guid Id { get; set; }         public string Buyer { get; set; }         public OrderStatus Status { get; set; }         public List<LineModel> Lines { get; set; } = new List<LineModel>();         public string Address { get; set; }     } 

2)Service

    public sealed class OrderService : IOrderService     {         private readonly IApiRepository _api;         private List<OrderModel> _orders;          public OrderService(IApiRepository api)         {             _api = api;             _orders = new List<OrderModel>();         }          public string Error { get; private set; }         public IReadOnlyList<OrderModel> Orders => _orders?.AsReadOnly();          public async Task Create(IEnumerable<LineModel> lines, string address)         {             var (_, e) = await _api.CreateOrder(lines, address);             Error = e;             if (!string.IsNullOrWhiteSpace(e))                 return;             await Load();         }          public async Task Load()         {             var (r, e) = await _api.GetOrders();             _orders = r;             Error = e;         }     } 

3)ViewModel

    public class OrdersViewModel     {         private readonly IOrderService _order;         private readonly IBasketService _basket;          public OrdersViewModel(IOrderService order, IBasketService basket)         {             _order = order;             _basket = basket;             OrderFormContext = new EditContext(this);         }          public bool CanCreateOrder => _basket.ItemsCount > 0;         public string Error => _order.Error + _basket.Error;         public IReadOnlyList<OrderModel> Model => _order.Orders;         public decimal Sum => _basket.Model.Sum(m => m.Quantity * m.Product.Price);         public EditContext OrderFormContext { get; }         public OrderVmState State { get; set; }         [Required]         [StringLength(255, MinimumLength = 3)]         public string Address { get; set; }          public void ChangeState(string value)         {             State = OrderVmState.List;             if (string.IsNullOrWhiteSpace(value))                 return;             if (Enum.TryParse(value, true, out OrderVmState state))                 State = state;             if (_basket.ItemsCount == 0 && State == OrderVmState.Create)                 State = OrderVmState.List;         }          public async Task OnInitializedAsync()         {             await _order.Load();             await _basket.Load();         }          public async Task Create()         {             if (!OrderFormContext.Validate())                 return;             await _basket.Load();             if (!string.IsNullOrWhiteSpace(_basket.Error))                 return;             var lines = _basket.Model.Select(l => new LineModel()             {                 Product = new ProductModel()                 {                     Id = l.Product.Id,                     Price = l.Product.Price,                     Title = l.Product.Title,                     Version = l.Product.Version                 },                 Quantity = l.Quantity             }).ToList();             await _basket.Clear();             if (!string.IsNullOrWhiteSpace(_basket.Error))                 return;             try             {                 await _order.Create(lines, Address);             }             catch             {                 await Restore(lines);                 throw;             }             if (!string.IsNullOrWhiteSpace(_order.Error))             {                 await Restore(lines);             }             State = OrderVmState.List;         }          private async Task Restore(IEnumerable<LineModel> lines)         {             foreach (var line in lines)             {                 for (int i = 0; i < line.Quantity; i++)                 {                     await _basket.Add(line.Product);                 }             }         }     } 

4)View

@page "/orders" @page "/orders/{operation}"  @attribute [Authorize]  @inject OrdersViewModel ViewModel   <h3>Orders</h3> <div>     <Error Model="@ViewModel.Error" /> </div> @if (ViewModel.State == OrderVmState.Create) {     <EditForm EditContext="@ViewModel.OrderFormContext" OnValidSubmit="@ViewModel.Create">         <DataAnnotationsValidator />         <div class="form-group"><label class="form-label"> Sum: @ViewModel.Sum</label></div>         <div class="form-group">             <label class="form-label" for="address">Address</label>             <InputTextArea id="address" name="address" class="form-control" @bind-Value="@ViewModel.Address" />             <ValidationMessage For="@(() => ViewModel.Address)" />         </div>         <button type="submit" class="btn btn-primary" disabled="@(!context.Validate())">Save</button>         <button class="btn btn-default" @onclick="@(x => ViewModel.State = OrderVmState.List)">Cancel</button>     </EditForm> } else {     @if (ViewModel.CanCreateOrder)     {         <input type="button" class="btn btn-primary" value="Create Order" @onclick="@(x=>ViewModel.State = OrderVmState.Create)" />     }     <div class="table-responsive">         <table class="table">             <thead>                 <tr>                     <AuthorizeView Roles="admin">                         <Authorized>                             <th>Id</th>                             <th>Buyer Id</th>                         </Authorized>                     </AuthorizeView>                     <th>Status</th>                     <th>Products Count</th>                     <th>Sum</th>                     <th>Address</th>                 </tr>             </thead>             <tbody>                 @if (ViewModel.Model == null)                 {                     <tr>                         <td>                             <em>Loading...</em>                         </td>                     </tr>                 }                 else                 {                     foreach (var order in ViewModel.Model)                     {                          <tr>                             <AuthorizeView Roles="admin">                                 <Authorized>                                     <td>@order.Id</td>                                     <td>@order.Buyer</td>                                 </Authorized>                             </AuthorizeView>                             <td>@order.Status.ToString("G")</td>                             <td>@order.Lines.Sum(l => l.Quantity)</td>                             <td>@order.Lines.Sum(l => l.Quantity * l.Product.Price)</td>                             <td>@order.Address</td>                         </tr>                     }                 }             </tbody>         </table>     </div> }  @functions {      [Parameter]     public string Operation     {         get => ViewModel.State.ToString("G");         set => ViewModel.ChangeState(value);     }      protected override async Task OnInitializedAsync()     {         await ViewModel.OnInitializedAsync();     } } 

Скриншоты

Вариант на Angular 9

ссылка на оригинал статьи https://habr.com/ru/post/496084/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *