Boutique en ligne côté client Blazor: Partie 6 - Création d'une commande et utilisation des actions compensatoires



Bonjour, Habr! Je continue de faire une boutique en ligne et j'apprends Blazor. Dans cette partie, je vais vous expliquer comment j'ai ajouté la possibilité de créer une commande, d'afficher des commandes et de travailler avec une séquence d'actions, dont l'une peut entraîner une erreur. Pour plus de détails, bienvenue au chat.

Contenu



Références


→  Sources
Images sur le registre Docker

Saga


Je me suis ennuyé et j'ai décidé d'imiter la situation lorsque nous avons deux microservices - pour le panier et pour passer des commandes. En règle générale, pour de telles situations, un orchestrateur de microservices est créé, qui contrôle la séquence d'actions et exécute des actions compensatoires. Par exemple, cela est intégré à MassTransit, NServiceBus, MS Orleans (transactions distribuées). Par exemple, j'ai décidé de simuler une petite situation où vous avez besoin de frapper sur deux services différents que vous n'avez pas la possibilité de changer. Google et FaceBook par exemple. Bien qu'il soit également préférable de le faire via le serveur et d'envoyer une demande au backend. Voici l'exemple le plus simple. Un peu plus compliqué serait d' enregistrer l'état incomplet dans le navigateur LocalStorage et d'essayer de revenir en arrière par minuterie, mais pas aussi maladroit que je l'ai fait ici.

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

Le code


1) Modèle


    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) Voir


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

Captures d'écran






Option sur Angular 9



All Articles