Search code examples
c#asp.netentity-frameworkdesign-patternse-commerce

Cart Implementation - is it ok to do a server call on each CRUD operation?


I am watching a tutorial about ECommerce Website with .Net Core - and the project is of type Web Assembly Blazor and I checked the .NET CORE Hosted, so the project spited to Client, Server and Sheared.

And the Cart model is consists of CartId, ProductId and UserId.

    public class CartItem
    {
        public int CartId { get; set; }
        public int UserId { get; set; }
        public int ProductId { get; set; }
        public int Quantity { get; set; } = 1;
    }

When the instructor implemented the Cart Model he was using the local storage only on the client side and he gets the Product details with a one server Call

Server service

    public interface ICartService
    {
        Task<ServiceResponse<List<CartProductResponse>>> GetCartProducts(List<CartItem> cartItems);
    }

Client service

    public interface ICartService
    {
        event Action OnChange;
        Task AddToCart(CartItem cartItem);
        Task<List<CartItem>> GetCartItems();
        Task<List<CartProductResponse>> GetCartProducts(); 
        Task RemoveProductFromCart(int productId, int productTypeId);
        Task UpdateQuantity(CartProductResponse product);
    }

So until now every thing make sense in my brian, all CURD operations are mainly on the Client side. But after he did the migration of local storage to the database. The CRUD operations are on both client and server side

Server service

    public interface ICartService
    {
        Task<ServiceResponse<List<CartProductResponse>>> GetCartProducts(List<CartItem> cartItems);
        Task<ServiceResponse<List<CartProductResponse>>> StoreCartItems(List<CartItem> cartItems);
        Task<ServiceResponse<int>> GetCartItemsCount();
        Task<ServiceResponse<List<CartProductResponse>>> GetDbCartProducts();
        Task<ServiceResponse<bool>> AddToCart(CartItem cartItem);
        Task<ServiceResponse<bool>> UpdateQuantity(CartItem cartItem);
        Task<ServiceResponse<bool>> RemoveItemFromCart(int productId, int productTypeId);
    }

Client service

public interface ICartService
{
   event Action OnChange;
   Task AddToCart(CartItem cartItem);
   Task<List<CartProductResponse>> GetCartProducts();
   Task RemoveProductFromCart(int productId, int productTypeId);
   Task UpdateQuantity(CartProductResponse product);
   Task StoreCartItems(bool emptyLocalCart);
   Task GetCartItemsCount();
}

My questions is:

Is that Ok/practical? having a call to the server on each cart change/update?

My suggestion:

if we could update the server side on section ends or on checkout only, but i have no idea if this possible or not


Solution

  • As a general rule with EF, is there any issue with going to the database for each operation? No. The database is a source of truth and it can be dangerous to overly assume that we can avoid what are perceived as "expensive" database calls then dump some accumulated state into the database all at once. The biggest issue with that approach is that systems with databases are rarely modified in isolation. Data state can change between the time when a particular user fetches current state to the time they go to submit those changes. The more work/time that passes between those two events, the more chance you will be dealing with data state conflicts (provided you actually look for them) or the chance that you overwrite state and state changes are lost. The goal is to be efficient about state and structuring operations to be only as big as they need to be. So yes, I would advocate on the side of having several atomic calls like you had outlined rather than writing a service that does something like:

    // IMO bad design...
    public interface ICartService
    {
        Cart GetCart(cartId);
        void SaveCart(Cart cart);
    }
    

    ... where we rely on client-side code / time to manage modifying a cart to minimize potential DB calls. So much can go wrong with designs like this.

    The other goal to consider is when passing data between server and client and back is structuring calls to pass minimum viable data. So calls like this:

    Task RemoveProductFromCart(int productId, int productTypeId);
    

    Are really good. We pass just two ints to the server. The server validates or ignores invalid requests.

    Stuff like this:

    Task UpdateQuantity(CartProductResponse product);
    

    not so much, that looks like a Form POST given the CartProductResponse was returned by an earlier operation. If all a user can typically do when it comes to editing a product in a cart is update the quantity, then:

    Task UpdateQuantity(int cartProductId, int quantity);
    

    Where the server call validates the quantity is a valid value (I.e. non-negative) retrieves the cart product, verifies the stock level, and updates the cart product's quantity. This would likely be what the original method would have done, but it doesn't need an entire model to do that, and the temptation could be there to update values other than the quantity.