Blazor Client Side Online Store: Part 6 - Creating an Order and Working with Compensatory Actions



Hello, Habr! I continue to make an online store and learn Blazor. In this part I’ll talk about how I added the ability to create an order, view orders and work with a sequence of actions, one of which may result in an error. For details, welcome to cat.

Content



References


β†’  Sources
β†’ Images on the Docker Registry

Saga


I got bored and I decided to imitate the situation when we have two microservices - for the basket and placing orders. Typically, for such situations, a microservice Orchestrator is created, which controls the sequence of actions and performs compensatory actions. For example, this is built into MassTransit, NServiceBus, MS Orleans (distributed transactions). For example, I decided to simulate a little situation when you need to knock on two different services that you do not have the opportunity to change. Google and FaceBook for example. Although it’s also better to do this through the server, and send one request to the backend. Here is the simplest example. A little more complicated it would be necessary to save the incomplete state in the LocalStorage browser and try to roll back by timer, but not as clumsy as I did here.

OrdersViewModel Code:

        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);
                }
            }
        }

The code


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();
    }
}

Screenshots






Option on Angular 9



All Articles