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


    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое