Является ли выполнение запроса из команды нарушением разделения команд и запросов?

При наличии реальной анонимной корзины покупок рабочий процесс AddToCart должен выполнить следующие шаги:

  1. Поиск текущего продукта из базы данных. Получите цену от продукта или воспользуйтесь услугой для расчета цены по выбору пользователя и другим свойствам продукта. (Запрос)
  2. Поиск текущей корзины покупок из базы данных. (Запрос)
  3. Если текущая корзина покупок не существует в базе данных, создайте новую сущность корзины покупок (в памяти).
  4. Добавьте новый товар (продукт) в корзину (в памяти) вместе с его ценой.
  5. Выполните любые расчеты скидок на всю корзину покупок. (зависит от запроса)
  6. Запустите любой расчет налога с продаж в корзине. (зависит от запроса)
  7. Выполните любые расчеты доставки в корзине. (зависит от запроса)
  8. Если это новая корзина покупок, добавьте объект в базу данных, в противном случае обновите корзину покупок в базе данных. (Команда)

Таким образом, хотя "AddToCart" звучит так, как будто это должна быть команда (поскольку она обновляет состояние системы), на практике это зависит от многих запросов.

Мой вопрос

Каков общепринятый способ обработки таких рабочих процессов?

  1. Сделать AddToCartCommandHandler это зависит от других служб, которые могут выполнять запросы.
  2. Сделать фасад CartService он управляет рабочим процессом, который сначала выполняет запросы, а затем команды.
  3. Сделайте метод действия контроллера сначала запустите запросы, а затем выполните любые команды. Похоже, что некоторые этапы запроса могут быть пропущены, если их необходимо использовать повторно.
  4. Другой?

Является ли причиной, по которой я не могу найти ответ по этому поводу, потому что это "зависит от дизайна", и это одно из исключений, где его нельзя применять?

Если команды и запросы разделены, я бы передал свой класс сущности каркаса реальной сущности команде, которая добавляет / обновляет корзину (чтобы EF мог решить, присоединена она или нет)? Кажется, что DTO не будет в этом случае.

ПРИМЕЧАНИЕ: я неявно предполагаю, что системы, которые реализуют CQS сделать это с целью, чтобы в конечном итоге они могли стать полноценным CQRS система. Если так, то этот рабочий процесс, по-видимому, не сможет осуществить переход - отсюда мой вопрос.

Фон

Я беру свой первый удар в CQS.

Из документации, которую я прочитал об этом шаблоне, ясно, что запрос не должен изменять состояние системы.

Однако неясно, считается ли это нормальным для выполнения запроса из команды (кажется, я не могу найти информацию где-либо).

Есть несколько реальных случаев, которые я могу придумать, где это должно произойти. Но, учитывая отсутствие реальных примеров такой модели в Интернете, я не уверен, как поступить. В Интернете есть много теории, но единственный код, который я могу найти, здесь и здесь.

2 ответа

Решение

Ответ на эту проблему пришел в форме комментария от Qujck.

Решение состоит в том, чтобы разбить приложение на различные типы запросов и типов команд. Точное назначение каждого типа остается загадкой (поскольку в блоге не рассматриваются причины, по которым он сделал это различие), но в нем ясно показано, как команды верхнего и среднего уровня могут зависеть от запросов к базе данных.

Типы команд

  1. Команда (верхний уровень)
  2. Командная стратегия (средний уровень)
  3. Команда данных (прямой доступ к данным)

Типы запросов

  1. Запрос (на высшем уровне)
  2. Стратегия запросов (средний уровень)
  3. Data Query (прямой доступ к данным)

Реализация командного запроса

// Commands

public interface ICommand
{
}

public interface IDataCommand
{
}

/// <summary>
/// A holistic abstraction, an abstraction that acts as the whole of each transaction 
/// </summary>
/// <typeparam name="TCommand"></typeparam>
public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

public interface ICommandStrategyHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

/// <summary>
/// Direct database update
/// </summary>
/// <typeparam name="TCommand"></typeparam>
public interface IDataCommandHandler<TCommand> where TCommand : IDataCommand
{
    void Handle(TCommand command);
}



// Queries

public interface IQuery<TResult>
{
}

public interface IDataQuery<TResult>
{
}

/// <summary>
/// A holistic abstraction, an abstraction that acts as the whole of each transaction 
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TResult"></typeparam>
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

public interface IQueryStrategyHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

/// <summary>
/// Direct database query
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TResult"></typeparam>
public interface IDataQueryHandler<TQuery, TResult> where TQuery : IDataQuery<TResult>
{
    TResult Handle(TQuery query);
}


/// <summary>
/// Generic processor that can run any query
/// </summary>
public interface IQueryProcessor
{
    TResult Execute<TResult>(IQuery<TResult> query);

    // NOTE: Stephen recommends against using Async. He may be right that it is not
    // worth the aggrevation of bugs that may be introduced.
    //Task<TResult> Execute<TResult>(IQuery<TResult> query);

    TResult Execute<TResult>(IDataQuery<TResult> query);
}

График зависимостей AddToCart

Используя вышеописанную реализацию, структура графа зависимостей рабочего процесса AddToCart выглядит следующим образом.

  • AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
    • GetShoppingCartDetailsQueryHandler : IQueryHandler<GetShoppingCartDetailsQuery, ShoppingCartDetails>
      • GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
        • GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
          • ApplicationDbContext
        • CreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
          • ApplicationDbContext
    • UpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>
    • SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
      • GetProductDetailsDataQueryHandler : IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails>
        • ApplicationDbContext
    • SetTotalsCommandStrategyHandler : ICommandStrategyHandler<SetTotalsCommandStrategy>
      • SetDiscountsCommandStrategyHandler : ICommandStrategyHandler<SetDiscountsCommandStrategy>
        • ?
      • SetSalesTaxCommandStrategyHandler : ICommandStrategyHandler<SetSalesTaxCommandStrategy>

Реализация

DTOS

public class ShoppingCartDetails : IOrder
{
    private IEnumerable<IOrderItem> items = new List<ShoppingCartItem>();

    public Guid Id { get; set; }
    public decimal SubtotalDiscounts { get; set; }
    public string ShippingPostalCode { get; set; }
    public decimal Shipping { get; set; }
    public decimal ShippingDiscounts { get; set; }
    public decimal SalesTax { get; set; }
    public decimal SalesTaxDiscounts { get; set; }

    // Declared twice - once for the IOrder interface
    // and once so we can get the realized concrete type.
    // See: https://stackru.com/questions/15490633/why-cant-i-use-a-compatible-concrete-type-when-implementing-an-interface
    public IEnumerable<ShoppingCartItem> Items
    {
        get { return this.items as IEnumerable<ShoppingCartItem>; }
        set { this.items = value; }
    }
    IEnumerable<IOrderItem> IOrder.Items
    {
        get { return this.items; }
        set { this.items = value; }
    }

    //public IEnumerable<ShoppingCartNotification> Notifications { get; set; }
    //public IEnumerable<ShoppingCartCoupon> Coupons { get; set; } // TODO: Add this to IOrder
}

public class ShoppingCartItem : IOrderItem
{
    public ShoppingCartItem()
    {
        this.Id = Guid.NewGuid();
        this.Selections = new Dictionary<string, object>();
    }

    public Guid Id { get; set; }
    public Guid ShoppingCartId { get; set; }
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public decimal PriceDiscount { get; set; }
    public IDictionary<string, object> Selections { get; set; }
}

public class ProductDetails 
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public decimal Discount { get; set; }
}

Расчет итоговых заказов

Вместо того, чтобы полагаться на строку служб для выполнения простой (и обязательной) арифметики, я решил использовать это поведение в методах расширения, чтобы оно выполнялось на лету с фактическими данными. Поскольку эту логику нужно будет разделить между корзиной покупок, заказом и предложением, расчет выполняется по IOrder а также IOrderItem а не конкретные типы моделей.

// Contract to share simple cacluation and other business logic between shopping cart, order, and quote
public interface IOrder
{
    decimal SubtotalDiscounts { get; set; }
    decimal Shipping { get; set; }
    decimal ShippingDiscounts { get; set; }
    decimal SalesTax { get; set; }
    decimal SalesTaxDiscounts { get; set; }
    IEnumerable<IOrderItem> Items { get; set; }
}

public interface IOrderItem
{
    Guid ProductId { get; set; }
    int Quantity { get; set; }
    decimal Price { get; set; }
    decimal PriceDiscount { get; set; }
    IDictionary<string, object> Selections { get; set; }
}

public static class OrderExtensions
{
    public static decimal GetSubtotal(this IOrder order)
    {
        return order.Items.Sum(x => x.GetTotal());
    }

    public static decimal GetSubtotalBeforeDiscounts(this IOrder order)
    {
        return order.Items.Sum(x => x.GetTotalBeforeDiscounts());
    }

    public static decimal GetTotal(this IOrder order)
    {
        var subtotal = (order.GetSubtotal() - order.SubtotalDiscounts);
        var shipping = (order.Shipping - order.ShippingDiscounts);
        var salesTax = (order.SalesTax - order.SalesTaxDiscounts);
        return (subtotal + shipping + salesTax);
    }
}

public static class OrderItemExtensions
{
    public static decimal GetTotalBeforeDiscounts(this IOrderItem item)
    {
        return (item.Price * item.Quantity);
    }

    public static decimal GetTotal(this IOrderItem item)
    {
        return (GetTotalBeforeDiscounts(item) - item.PriceDiscount);
    }

    public static decimal GetDiscountedUnitPrice(this IOrderItem item)
    {
        return (item.Quantity > 0) ? (GetTotal(item) / item.Quantity) : 0;
    }
}

ShoppingCartController

Для краткости мы показываем только действие AddToCart, но именно здесь будут выполняться и другие действия с корзиной покупок (например, удаление из корзины).

public class ShoppingCartController : Controller
{
    private readonly IQueryProcessor queryProcessor;
    private readonly IAnonymousIdAccessor anonymousIdAccessor;
    private readonly ICommandHandler<AddToCartCommand> addToCartHandler;

    public ShoppingCartController(
        IQueryProcessor queryProcessor, 
        IAnonymousIdAccessor anonymousIdAccessor, 
        ICommandHandler<AddToCartCommand> addToCartHandler)
    {
        if (queryProcessor == null)
            throw new ArgumentNullException("queryProcessor");
        if (anonymousIdAccessor == null)
            throw new ArgumentNullException("anonymousIdAccessor");
        if (addToCartHandler == null)
            throw new ArgumentNullException("addToCartHandler");

        this.queryProcessor = queryProcessor;
        this.anonymousIdAccessor = anonymousIdAccessor;
        this.addToCartHandler = addToCartHandler;
    }

    public ActionResult Index()
    {
        var command = new GetShoppingCartDetailsQuery
        {
            ShoppingCartId = this.anonymousIdAccessor.AnonymousID
        };

        ShoppingCartDetails cart = this.queryProcessor.Execute(command);

        return View(cart);
    }

    public ActionResult AddToCart(ItemViewModel model)
    {
        var command = new AddToCartCommand
        {
            ProductId = model.Id,
            Quantity = model.Qty,
            Selections = model.Selections,
            ShoppingCartId = this.anonymousIdAccessor.AnonymousID
        };

        this.addToCartHandler.Handle(command);

        // If we execute server side, it should go to the cart page
        return RedirectToAction("Index");
    }
}

AddToCartCommandHandler

Здесь выполняется основная часть рабочего процесса. Эта команда будет вызываться непосредственно из AddToCart действие контроллера.

public class AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
{
    private readonly IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails> getShoppingCartQuery;
    private readonly IDataCommandHandler<UpdateShoppingCartDataCommand> updateShoppingCartCommand;
    private readonly ICommandStrategyHandler<SetItemPriceCommandStrategy> setItemPriceCommand;
    private readonly ICommandStrategyHandler<SetTotalsCommandStrategy> setTotalsCommand;

    public AddToCartCommandHandler(
        IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails> getShoppingCartCommand,
        IDataCommandHandler<UpdateShoppingCartDataCommand> updateShoppingCartCommand,
        ICommandStrategyHandler<SetItemPriceCommandStrategy> setItemPriceCommand,
        ICommandStrategyHandler<SetTotalsCommandStrategy> setTotalsCommand
        )
    {
        if (getShoppingCartCommand == null)
            throw new ArgumentNullException("getShoppingCartCommand");
        if (setItemPriceCommand == null)
            throw new ArgumentNullException("setItemPriceCommand");
        if (updateShoppingCartCommand == null)
            throw new ArgumentNullException("updateShoppingCartCommand");
        if (setTotalsCommand == null)
            throw new ArgumentNullException("setTotalsCommand");

        this.getShoppingCartQuery = getShoppingCartCommand;
        this.updateShoppingCartCommand = updateShoppingCartCommand;
        this.setItemPriceCommand = setItemPriceCommand;
        this.setTotalsCommand = setTotalsCommand;
    }

    public void Handle(AddToCartCommand command)
    {
        // Get the shopping cart (aggregate root) from the database
        var shoppingCart = getShoppingCartQuery.Handle(new GetShoppingCartQueryStrategy { ShoppingCartId = command.ShoppingCartId });

        // Create a new shopping cart item
        var item = new Contract.DTOs.ShoppingCartItem
        {
            ShoppingCartId = command.ShoppingCartId,
            ProductId = command.ProductId,
            Quantity = command.Quantity,

            // Dictionary representing the option selections the user made on the UI
            Selections = command.Selections
        };

        // Set the item's price (calculated/retrieved from database query)
        setItemPriceCommand.Handle(new SetItemPriceCommandStrategy { ShoppingCartItem = item });

        // Add the item to the cart
        var items = new List<Contract.DTOs.ShoppingCartItem>(shoppingCart.Items);
        items.Add(item);
        shoppingCart.Items = items;

        // Set the shopping cart totals (sales tax, discounts)
        setTotalsCommand.Handle(new SetTotalsCommandStrategy { ShoppingCart = shoppingCart });

        // Update the shopping cart details in the database
        updateShoppingCartCommand.Handle(new UpdateShoppingCartDataCommand { ShoppingCart = shoppingCart });
    }
}

GetShoppingCartQueryStrategyHandler

public class GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
{
    private readonly IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails> getShoppingCartDataQuery;
    private readonly IDataCommandHandler<CreateShoppingCartDataCommand> createShoppingCartDataCommand;

    public GetShoppingCartQueryStrategyHandler(
        IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails> getShoppingCartDataQuery,
        IDataCommandHandler<CreateShoppingCartDataCommand> createShoppingCartDataCommand)
    {
        if (getShoppingCartDataQuery == null)
            throw new ArgumentNullException("getShoppingCartDataQuery");
        if (createShoppingCartDataCommand == null)
            throw new ArgumentNullException("createShoppingCartDataCommand");

        this.getShoppingCartDataQuery = getShoppingCartDataQuery;
        this.createShoppingCartDataCommand = createShoppingCartDataCommand;
    }

    public ShoppingCartDetails Handle(GetShoppingCartQueryStrategy query)
    {
        var result = this.getShoppingCartDataQuery.Handle(new GetShoppingCartDataQuery { ShoppingCartId = query.ShoppingCartId });

        // If there is no shopping cart, create one.
        if (result == null)
        {
            this.createShoppingCartDataCommand.Handle(new CreateShoppingCartDataCommand { ShoppingCartId = query.ShoppingCartId });

            result = new ShoppingCartDetails
            {
                Id = query.ShoppingCartId
            };
        }

        return result;
    }
}

GetShoppingCartDataQueryHandler

/// <summary>
/// Data handler to get the shopping cart data (if it exists)
/// </summary>
public class GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
{
    private readonly IAppContext context;

    public GetShoppingCartDataQueryHandler(IAppContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public ShoppingCartDetails Handle(GetShoppingCartDataQuery query)
    {
        return (from shoppingCart in context.ShoppingCarts
                where shoppingCart.Id == query.ShoppingCartId
                select new ShoppingCartDetails
                {
                    Id = shoppingCart.Id,
                    SubtotalDiscounts = shoppingCart.SubtotalDiscounts,
                    ShippingPostalCode = shoppingCart.ShippingPostalCode,
                    Shipping = shoppingCart.Shipping,
                    ShippingDiscounts = shoppingCart.ShippingDiscounts,
                    SalesTax = shoppingCart.SalesTax,
                    SalesTaxDiscounts = shoppingCart.SalesTaxDiscounts,

                    Items = shoppingCart.Items.Select(i =>
                        new Contract.DTOs.ShoppingCartItem
                        {
                            Id = i.Id,
                            ShoppingCartId = i.ShoppingCartId,
                            ProductId = i.ProductId,
                            Quantity = i.Quantity,
                            Price = i.Price,
                            PriceDiscount = i.PriceDiscount
                            // TODO: Selections...
                        })
                }).FirstOrDefault();
    }
}

CreateShoppingCartDataCommandHandler

public class CreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
{
    private readonly IAppContext context;

    public CreateShoppingCartDataCommandHandler(IAppContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public void Handle(CreateShoppingCartDataCommand command)
    {
        var cart = new ShoppingCart
        {
            Id = command.ShoppingCartId
        };

        this.context.ShoppingCarts.Add(cart);
        this.context.SaveChanges();
    }
}

UpdateShoppingCartDataCommandHandler

Это обновляет корзину покупок всеми изменениями, примененными бизнес-уровнем.

В настоящее время эта "команда" выполняет запрос, чтобы согласовать различия между базой данных и копией памяти. Однако это, очевидно, нарушение паттерна CQS. Я планирую задать дополнительный вопрос, чтобы определить, как лучше всего отслеживать изменения, поскольку отслеживание изменений и CQS тесно связаны между собой.

public class UpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>
{
    private readonly IAppContext context;

    public UpdateShoppingCartDataCommandHandler(IAppContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public void Handle(UpdateShoppingCartDataCommand command)
    {
        var cart = context.ShoppingCarts
            .Include(x => x.Items)
            .Single(x => x.Id == command.ShoppingCart.Id);


        cart.Id = command.ShoppingCart.Id;
        cart.SubtotalDiscounts = command.ShoppingCart.SubtotalDiscounts;
        cart.ShippingPostalCode = command.ShoppingCart.ShippingPostalCode;
        cart.Shipping = command.ShoppingCart.Shipping;
        cart.ShippingDiscounts = command.ShoppingCart.ShippingDiscounts;
        cart.SalesTax = command.ShoppingCart.SalesTax;
        cart.SalesTaxDiscounts = command.ShoppingCart.SalesTaxDiscounts;

        ReconcileShoppingCartItems(cart.Items, command.ShoppingCart.Items, command.ShoppingCart.Id);

        // Update the cart with new data

        context.SaveChanges();
    }

    private void ReconcileShoppingCartItems(ICollection<ShoppingCartItem> items, IEnumerable<Contract.DTOs.ShoppingCartItem> itemDtos, Guid shoppingCartId)
    {
        // remove deleted items
        var items2 = new List<ShoppingCartItem>(items);
        foreach (var item in items2)
        {
            if (!itemDtos.Any(x => x.Id == item.Id))
            {
                context.Entry(item).State = EntityState.Deleted;
            }
        }


        // Add/update items
        foreach (var dto in itemDtos)
        {
            var item = items.FirstOrDefault(x => x.Id == dto.Id);
            if (item == null)
            {
                items.Add(new ShoppingCartItem
                {
                    Id = Guid.NewGuid(),
                    ShoppingCartId = shoppingCartId,
                    ProductId = dto.ProductId,
                    Quantity = dto.Quantity,
                    Price = dto.Price,
                    PriceDiscount = dto.PriceDiscount
                });
            }
            else
            {
                item.ProductId = dto.ProductId;
                item.Quantity = dto.Quantity;
                item.Price = dto.Price;
                item.PriceDiscount = dto.PriceDiscount;
            }
        }
    }
}

SetItemPriceCommandStrategyHandler

public class SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
{
    private readonly IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails> getProductDetailsQuery;

    public SetItemPriceCommandStrategyHandler(
        IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails> getProductDetailsQuery)
    {
        if (getProductDetailsQuery == null)
            throw new ArgumentNullException("getProductDetailsQuery");

        this.getProductDetailsQuery = getProductDetailsQuery;
    }

    public void Handle(SetItemPriceCommandStrategy command)
    {
        var shoppingCartItem = command.ShoppingCartItem;

        var product = getProductDetailsQuery.Handle(new GetProductDetailsDataQuery { ProductId = shoppingCartItem.ProductId });

        // TODO: For products with custom calculations, need to use selections on shopping cart item
        // as well as custom formula and pricing points from product to calculate the item price.

        shoppingCartItem.Price = product.Price;
    }
}

GetProductDetailsDataQueryHandler

public class GetProductDetailsDataQueryHandler : IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails>
{
    private readonly IAppContext context;

    public GetProductDetailsDataQueryHandler(IAppContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public ProductDetails Handle(GetProductDetailsDataQuery query)
    {
        return (from product in context.Products
                where product.Id == query.ProductId
                select new ProductDetails
                {
                    Id = product.Id,
                    Name = product.Name,
                    Price = product.Price
                }).FirstOrDefault(); 
    }
}

SetTotalsCommandStrategyHandler

public class SetTotalsCommandStrategyHandler : ICommandStrategyHandler<SetTotalsCommandStrategy>
{
    private readonly ICommandStrategyHandler<SetDiscountsCommandStrategy> setDiscountsCommand;
    private readonly ICommandStrategyHandler<SetSalesTaxCommandStrategy> setSalesTaxCommand;

    public SetTotalsCommandStrategyHandler(
        ICommandStrategyHandler<SetDiscountsCommandStrategy> setDiscountsCommand,
        ICommandStrategyHandler<SetSalesTaxCommandStrategy> setSalesTaxCommand
        )
    {
        if (setDiscountsCommand == null)
            throw new ArgumentNullException("setDiscountsCommand");
        if (setSalesTaxCommand == null)
            throw new ArgumentNullException("setSalesTaxCommand");

        this.setDiscountsCommand = setDiscountsCommand;
        this.setSalesTaxCommand = setSalesTaxCommand;
    }

    public void Handle(SetTotalsCommandStrategy command)
    {
        var shoppingCart = command.ShoppingCart;

        // Important: Discounts must be calculated before sales tax to ensure the discount is applied
        // to the subtotal before tax is calculated.
        setDiscountsCommand.Handle(new SetDiscountsCommandStrategy { ShoppingCart = shoppingCart });
        setSalesTaxCommand.Handle(new SetSalesTaxCommandStrategy { ShoppingCart = shoppingCart });
    }
}

SetDiscountsCommandStrategyHandler

public class SetDiscountsCommandStrategyHandler : ICommandStrategyHandler<SetDiscountsCommandStrategy>
{
    public void Handle(SetDiscountsCommandStrategy command)
    {
        var shoppingCart = command.ShoppingCart;

        // TODO: Set discounts according to business rules

        foreach (var item in shoppingCart.Items)
        {
            item.PriceDiscount = 0;
        }

        shoppingCart.SubtotalDiscounts = 0;
        shoppingCart.SalesTaxDiscounts = 0;
        shoppingCart.ShippingDiscounts = 0;
    }
}

SetSalesTaxCommandStrategyHandler

public class SetSalesTaxCommandStrategyHandler : ICommandStrategyHandler<SetSalesTaxCommandStrategy>
{
    public void Handle(SetSalesTaxCommandStrategy command)
    {
        var shoppingCart = command.ShoppingCart;
        var postalCode = command.ShoppingCart.ShippingPostalCode;

        bool isInCalifornia = !string.IsNullOrEmpty(postalCode) ?
            // Matches 90000 to 96200
            Regex.IsMatch(postalCode, @"^9(?:[0-5]\d{3}|6[0-1]\d{2}|6200)(?:-?(?:\d{4}))?$") :
            false;

        if (isInCalifornia)
        {
            var subtotal = shoppingCart.GetSubtotal();

            // Rule for California - charge a flat 7.75% if the zip code is in California
            var salesTax = subtotal * 0.0775M;

            shoppingCart.SalesTax = salesTax;
        }
    }
}

Обратите внимание, что в этом рабочем процессе нет расчета доставки. Это связано, прежде всего, с тем, что расчет доставки может зависеть от внешних API, и возврат может занять некоторое время. Поэтому я планирую сделать AddToCart рабочий процесс шаг, который выполняется мгновенно, когда элемент добавляется и делает CalculateShippingAndTax рабочий процесс, который происходит после того, как пользователь обновляет интерфейс снова после того, как итоги были получены из их (возможно, внешних) источников, что может занять некоторое время.

Решает ли это проблему? Да, это устраняет реальные проблемы, с которыми я сталкивался, когда команды должны зависеть от запросов.

Однако создается впечатление, что это действительно только концептуально отделяет запросы от команд. Физически они все еще зависят друг от друга, если только вы не посмотрите на IDataCommand а также IDataQuery абстракции, которые зависят только от ApplicationDbContext, Я не уверен, является ли это намерением Qujck или нет. Я также не уверен, решит ли это большую проблему передачи проекта в CQRS или нет, но так как это не то, что я планирую, меня это не беспокоит.

Всегда есть компромисс между противоречивыми принципами проектирования. Способ решить эту проблему - посмотреть на основные причины, лежащие в основе принципов. В этом случае невозможность выполнить запрос без выполнения команды является проблематичной, но невозможность выполнить команду без выполнения запроса, как правило, безвредна. Пока есть способ выполнить запрос автономно, я не вижу причин не добавлять результат запроса в команду, особенно если сделано что-то вроде этого:

QueryResult command()
{
   // do command stuff
   return query();
}
Другие вопросы по тегам