اگر قرار است یک فروشگاه آنلاین یا اپلیکیشن تجارت الکترونیک بسازید، سیستم مدیریت سبد خرید قلب تپندهی آن است. این سیستم مشخص میکند که کاربر چه محصولاتی انتخاب کرده، چه مقداری، چه قیمتی باید بپردازد و در نهایت فرایند خرید چگونه تکمیل میشود.
زبان C# به دلیل ساختار قوی، پشتیبانی از برنامهنویسی شیءگرا (OOP)، یکپارچگی با ASP.NET Core و اکوسیستم غنی مایکروسافت، یکی از بهترین انتخابها برای پیادهسازی چنین سیستمی است. در این مقاله، از صفر تا صد طراحی و پیادهسازی یک سیستم سبد خرید با C# را با هم مرور میکنیم.
قبل از نوشتن حتی یک خط کد، باید معماری سیستم را درک کنیم. یک سبد خرید حرفهای معمولاً از این اجزا تشکیل میشود:
اولین قدم، طراحی کلاسهای اصلی است. یک طراحی تمیز و قابل توسعه به این شکل خواهد بود:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public string ImageUrl { get; set; }
public string Category { get; set; }
}
public class CartItem
{
public int Id { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice => UnitPrice * Quantity;
}
public class ShoppingCart
{
public string CartId { get; set; }
public string UserId { get; set; }
public List Items { get; set; } = new List();
public DateTime CreatedAt { get; set; } = DateTime.Now;
public DateTime UpdatedAt { get; set; } = DateTime.Now;
public decimal SubTotal => Items.Sum(i => i.TotalPrice);
public decimal DiscountAmount { get; set; }
public decimal ShippingCost { get; set; }
public decimal TotalAmount => SubTotal - DiscountAmount + ShippingCost;
public int TotalItems => Items.Sum(i => i.Quantity);
}
حالا باید منطق اصلی برنامه را بنویسیم. اینترفیس ICartService را ابتدا تعریف میکنیم تا از اصل Dependency Inversion پیروی کنیم:
public interface ICartService
{
Task GetCartAsync(string cartId);
Task AddItemAsync(string cartId, int productId, int quantity);
Task UpdateItemAsync(string cartId, int cartItemId, int quantity);
Task RemoveItemAsync(string cartId, int cartItemId);
Task ClearCartAsync(string cartId);
Task ApplyDiscountAsync(string cartId, string couponCode);
}
و حالا پیادهسازی اصلی این سرویس:
public class CartService : ICartService
{
private readonly ICartRepository _cartRepository;
private readonly IProductRepository _productRepository;
public CartService(
ICartRepository cartRepository,
IProductRepository productRepository)
{
_cartRepository = cartRepository;
_productRepository = productRepository;
}
public async Task AddItemAsync(
string cartId, int productId, int quantity)
{
var cart = await _cartRepository.GetCartAsync(cartId)
?? new ShoppingCart { CartId = cartId };
var product = await _productRepository.GetByIdAsync(productId);
if (product == null)
throw new Exception(“محصول یافت نشد.”);
if (product.StockQuantity < quantity)
throw new Exception(“موجودی کافی نیست.”);
var existingItem = cart.Items
.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.Quantity += quantity;
}
else
{
cart.Items.Add(new CartItem
{
ProductId = productId,
Product = product,
Quantity = quantity,
UnitPrice = product.Price
});
}
cart.UpdatedAt = DateTime.Now;
await _cartRepository.SaveCartAsync(cart);
return cart;
}
public async Task RemoveItemAsync(
string cartId, int cartItemId)
{
var cart = await _cartRepository.GetCartAsync(cartId);
if (cart == null)
throw new Exception(“سبد خرید یافت نشد.”);
var item = cart.Items.FirstOrDefault(i => i.Id == cartItemId);
if (item != null)
cart.Items.Remove(item);
cart.UpdatedAt = DateTime.Now;
await _cartRepository.SaveCartAsync(cart);
return cart;
}
public async Task ApplyDiscountAsync(
string cartId, string couponCode)
{
var cart = await _cartRepository.GetCartAsync(cartId);
// منطق اعمال کد تخفیف
if (couponCode == “DISCOUNT20”)
cart.DiscountAmount = cart.SubTotal * 0.20m;
await _cartRepository.SaveCartAsync(cart);
return cart;
}
}
یکی از مهمترین تصمیمات معماری در طراحی سبد خرید، محل ذخیرهسازی دادهها است. دو رویکرد اصلی وجود دارد:
// ذخیره سبد در Session با JSON Serialization
public class SessionCartRepository : ICartRepository
{
private readonly IHttpContextAccessor _httpContextAccessor;
private const string CartSessionKey = “ShoppingCart”;
public SessionCartRepository(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Task GetCartAsync(string cartId)
{
var session = _httpContextAccessor.HttpContext.Session;
var cartJson = session.GetString(CartSessionKey);
if (string.IsNullOrEmpty(cartJson))
return Task.FromResult(null);
var cart = JsonSerializer.Deserialize(cartJson);
return Task.FromResult(cart);
}
public Task SaveCartAsync(ShoppingCart cart)
{
var session = _httpContextAccessor.HttpContext.Session;
var cartJson = JsonSerializer.Serialize(cart);
session.SetString(CartSessionKey, cartJson);
return Task.CompletedTask;
}
}
با استفاده از Entity Framework Core، میتوانیم سبد خرید را در SQL Server یا هر دیتابیس دیگری ذخیره کنیم. این روش برای کاربران ثبتنامشده ایدهآل است زیرا سبد خرید بین جلسات مختلف حفظ میشود:
public class ApplicationDbContext : DbContext
{
public DbSet ShoppingCarts { get; set; }
public DbSet CartItems { get; set; }
public DbSet Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasMany(c => c.Items)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity()
.Property(i => i.UnitPrice)
.HasColumnType(“decimal(18,2)”);
}
}
🚀 آیا میخواهید سایت شما هم مثل رقبا در صفحه اول گوگل باشد و زنگخورهایتان چند برابر شود؟
سئوی سایت خود را به متخصصان ما بسپارید. همین حالا برای مشاوره رایگان با ما تماس بگیرید:
📞 09190994063 - 09376846692
با داشتن سرویس و ریپازیتوری، حالا میتوانیم CartController را پیادهسازی کنیم:
[ApiController]
[Route(“api/[controller]”)]
public class CartController : ControllerBase
{
private readonly ICartService _cartService;
public CartController(ICartService cartService)
{
_cartService = cartService;
}
[HttpGet(“{cartId}”)]
public async Task GetCart(string cartId)
{
var cart = await _cartService.GetCartAsync(cartId);
if (cart == null)
return NotFound(new { message = “سبد خرید یافت نشد.” });
return Ok(cart);
}
[HttpPost(“{cartId}/items”)]
public async Task AddItem(
string cartId, [FromBody] AddItemRequest request)
{
try
{
var cart = await _cartService.AddItemAsync(
cartId, request.ProductId, request.Quantity);
return Ok(cart);
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpDelete(“{cartId}/items/{itemId}”)]
public async Task RemoveItem(
string cartId, int itemId)
{
var cart = await _cartService.RemoveItemAsync(cartId, itemId);
return Ok(cart);
}
[HttpPost(“{cartId}/discount”)]
public async Task ApplyDiscount(
string cartId, [FromBody] DiscountRequest request)
{
var cart = await _cartService.ApplyDiscountAsync(
cartId, request.CouponCode);
return Ok(cart);
}
}
در پروژههای بزرگ با ترافیک بالا، استفاده از Redis برای کش کردن سبد خرید یک بهترین تجربه (Best Practice) محسوب میشود. Redis یک پایگاه داده In-Memory است که سرعت بسیار بالایی دارد:
public class RedisCartRepository : ICartRepository
{
private readonly IDistributedCache _cache;
private readonly TimeSpan _expiry = TimeSpan.FromDays(30);
public RedisCartRepository(IDistributedCache cache)
{
_cache = cache;
}
public async Task GetCartAsync(string cartId)
{
var cartJson = await _cache.GetStringAsync(cartId);
if (string.IsNullOrEmpty(cartJson))
return null;
return JsonSerializer.Deserialize(cartJson);
}
public async Task SaveCartAsync(ShoppingCart cart)
{
var cartJson = JsonSerializer.Serialize(cart);
await _cache.SetStringAsync(cart.CartId, cartJson,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _expiry
});
}
public async Task DeleteCartAsync(string cartId)
{
await _cache.RemoveAsync(cartId);
}
}
برای اینکه همه چیز کار کند، باید وابستگیها را در Dependency Injection Container ثبت کنیم:
var builder = WebApplication.CreateBuilder(args);
// ثبت DbContext
builder.Services.AddDbContext(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString(“DefaultConnection”)));
// ثبت Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration[“Redis:ConnectionString”];
});
// ثبت سرویسها
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
// Session Configuration
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
var app = builder.Build();
app.UseSession();
app.MapControllers();
app.Run();
یک کد بدون تست، کد ناقص است. با استفاده از xUnit و Moq، میتوانیم سرویس را تست کنیم:
public class CartServiceTests
{
private readonly Mock _cartRepoMock;
private readonly Mock _productRepoMock;
private readonly CartService _cartService;
public CartServiceTests()
{
_cartRepoMock = new Mock();
_productRepoMock = new Mock();
_cartService = new CartService(
_cartRepoMock.Object,
_productRepoMock.Object);
}
[Fact]
public async Task AddItem_WhenProductExists_ShouldAddToCart()
{
// Arrange
var product = new Product
{
Id = 1,
Name = “لپتاپ”,
Price = 25000000m,
StockQuantity = 10
};
_productRepoMock
.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(product);
_cartRepoMock
.Setup(r => r.GetCartAsync(“cart-001”))
.ReturnsAsync((ShoppingCart)null);
// Act
var cart = await _cartService.AddItemAsync(“cart-001”, 1, 2);
// Assert
Assert.Single(cart.Items);
Assert.Equal(2, cart.Items[0].Quantity);
Assert.Equal(50000000m, cart.TotalAmount);
}
}
💡 یک نکته مهم برای شما:
داشتن کد خوب کافی نیست! اگر سایت یا فروشگاه آنلاین شما در گوگل دیده نشود، هیچکس آن را پیدا نمیکند. سئوی حرفهای یعنی تبدیل بازدیدکننده به مشتری واقعی.
آیا میخواهید سایت شما هم مثل رقبا در صفحه اول گوگل باشد و زنگخورهایتان چند برابر شود؟ سئوی سایت خود را به متخصصان ما بسپارید.
📞 همین حالا برای مشاوره رایگان تماس بگیرید: 09190994063 - 09376846692
Session برای کاربران مهمان (Guest) مناسب است و با بستن مرورگر از بین میرود. دیتابیس برای کاربران ثبتنامشده ایدهآل است چون سبد در جلسات مختلف حفظ میشود. بهترین روش، ترکیب هر دو است: کاربر مهمان از Session استفاده میکند و پس از لاگین، سبد با دیتابیس ادغام میشود.
در متد AddItemAsync باید قبل از افزودن محصول به سبد، مقدار StockQuantity را با تعداد درخواستی مقایسه کنید. همچنین در زمان نهاییسازی خرید (Checkout)، باید مجدداً موجودی را با استفاده از Optimistic Concurrency یا Transaction بررسی کنید تا از Race Condition جلوگیری شود.
خیر. Redis یک بهینهسازی اختیاری است. برای پروژههای کوچک و متوسط، Session یا دیتابیس کاملاً کافی است. اما اگر ترافیک بالایی دارید یا از چندین سرور (Load Balancing) استفاده میکنید، Redis سرعت و مقیاسپذیری را به شدت بهبود میدهد.
برای این کار یک جدول Coupon در دیتابیس بسازید که شامل کد، نوع تخفیف (درصدی یا مبلغ ثابت)، تاریخ انقضا و حداقل مبلغ خرید باشد. در متد ApplyDiscountAsync، کد وارد شده با جدول Coupon مقایسه میشود و در صورت معتبر بودن، مقدار DiscountAmount در سبد خرید بهروزرسانی میشود.
در ASP.NET Core MVC، میتوانید از یک ViewComponent به نام CartSummaryViewComponent استفاده کنید که در هر بار لود صفحه، تعداد آیتمهای سبد را از Session یا Cache میخواند و در Header نمایش میدهد. برای بهینگی بیشتر، از AJAX و SignalR میتوانید سبد را به صورت Real-Time بهروز کنید.
بله! در معماری Microservices، میتوانید یک سرویس مستقل Cart Service داشته باشید که از طریق gRPC یا REST API با سایر سرویسها (مثل Product Service و Order Service) ارتباط برقرار میکند. در این حالت، Redis به عنوان Storage اصلی سبد، انتخاب رایج است و Event Sourcing با RabbitMQ یا Kafka برای هماهنگی بین سرویسها استفاده میشود.
کنترلر API به خوبی طراحی شده بود. در سیستمهای بزرگتر که نیاز به ردیابی سبدهای رها شده (Abandoned Carts) داریم، چه تغییراتی لازم است؟
برای ردیابی سبدهای رها شده، ابتدا باید یک زمانبندی (Scheduler) تعریف کنید که به صورت دورهای سبدهای ذخیره شده در دیتابیس را بررسی کند. سپس سبدهایی که برای مدت مشخصی بهروز نشدهاند و تبدیل به سفارش نشدهاند را به عنوان رها شده علامتگذاری کنید. میتوانید با ارسال ایمیل یادآوری یا نوتیفیکیشن، کاربران را تشویق به تکمیل خرید کنید. این قابلیت نیازمند یک سرویس جداگانه (مثلاً Background Service) است. برای مشاوره بیشتر با ما تماس بگیرید: 09190994063 - 09376846692
مقاله فوقالعادهای بود، به خصوص بخش ذخیرهسازی با Redis. سوال من در مورد مدیریت موجودی است. اگر همزمان چند کاربر قصد خرید یک محصول محدود را داشته باشند، چطور میتوان از فروش بیش از موجودی جلوگیری کرد؟
برای جلوگیری از فروش بیش از موجودی در سناریوهای همزمان، باید از تکنیکهایی مانند Optimistic Concurrency یا استفاده از Transaction در دیتابیس در زمان نهاییسازی خرید (Checkout) بهره ببرید. همچنین میتوانید از صفهای پیام (Message Queues) برای پردازش سفارشها استفاده کنید. برای مشاوره و پیادهسازی این موارد با ما تماس بگیرید: 09190994063 - 09376846692
سپاس از توضیحات جامع. بخش تست واحد بسیار مفید بود. آیا برای مدیریت خطاهای پیشبینی نشده در سرویسها (مثل نبود محصول)، مکانیزم خاصی برای گزارشگیری و نمایش پیامهای کاربرپسند پیشنهاد میکنید؟
بله، برای مدیریت خطاها در سرویسها، توصیه میشود از سیستمهای Logging (مانند Serilog یا NLog) استفاده کنید تا خطاها را ثبت کنید. همچنین میتوانید از Middleware های سفارشی در ASP.NET Core برای گرفتن استثناها و برگرداندن پاسخهای استاندارد و کاربرپسند (مثلاً JSON با کد خطا و پیام مناسب) استفاده کنید. برای اطلاعات بیشتر با ما تماس بگیرید: 09190994063 - 09376846692
مقاله به قدری کامل بود که واقعا به دیدگاه من در مورد سبد خرید نظم داد. ممنونم. آیا برای سبد خرید قابلیتهایی مثل 'لیست علاقهمندیها' (Wishlist) هم میشود در همین معماری جای داد یا بهتر است یک ماژول جداگانه باشد؟
بسیار عالی، خوشحالیم که مفید بوده! 'لیست علاقهمندیها' (Wishlist) هرچند مفهوم مشابهی با سبد خرید دارد، اما معمولاً بهتر است به عنوان یک ماژول یا سرویس جداگانه پیادهسازی شود. دلیل آن این است که Wishlist منطق و هدف متفاوتی دارد (ذخیره برای خرید آتی، اشتراکگذاری با دیگران) و نیازمندیهای خاص خود را در دیتابیس و رابط کاربری دارد. با این حال، میتوان بین آنها ارتباطاتی برقرار کرد. برای مشاوره در طراحی با ما تماس بگیرید: 09190994063 - 09376846692
تفاوت بین ذخیرهسازی در Session و دیتابیس رو به خوبی توضیح دادید. میخواستم بپرسم وقتی کاربر مهمان لاگین میکنه، چطور سبد خرید Session با دیتابیس ادغام میشه؟ آیا منطق خاصی برای حل تداخلها وجود داره؟
بله، وقتی کاربر مهمان لاگین میکند، باید محتویات سبد خرید ذخیره شده در Session را بخوانید و سپس آن آیتمها را به سبد خرید کاربر در دیتابیس اضافه کنید. در صورت وجود تداخل (مثلاً یک محصول مشترک در هر دو سبد)، معمولاً تعداد موجود در سبد Session را به تعداد موجود در سبد دیتابیس اضافه میکنند. پس از ادغام، سبد Session باید پاک شود. برای مشاوره حرفهای با ما تماس بگیرید: 09190994063 - 09376846692
در مورد استفاده از Redis، آیا برای پیادهسازی آن نیاز به تنظیمات پیچیدهای در سرور داریم؟ آیا Redis میتواند در محیط ابری (مثل Azure یا AWS) هم به همین راحتی استفاده شود؟
Redis در محیطهای ابری (Azure Cache for Redis یا AWS ElastiCache) به راحتی قابل راهاندازی و استفاده است و بسیاری از پیچیدگیهای مدیریت سرور را پوشش میدهند. در سرورهای شخصی هم نصب و کانفیگ آن نسبتاً ساده است. استفاده از IDistributedCache در ASP.NET Core انتزاع خوبی برای کار با Redis فراهم میکند. برای جزئیات پیادهسازی با ما تماس بگیرید: 09190994063 - 09376846692
مقاله بسیار جامع و کاربردی بود. ممنون از توضیحات کامل. یک سوال: آیا امکان پیادهسازی بهروزرسانی لحظهای سبد خرید (مثلاً با SignalR) وجود دارد که بدون رفرش صفحه، تعداد آیتمها در هدر بهروز شود؟
خواهش میکنم، خوشحالیم که مقاله مفید بوده است. بله، قطعاً امکان پیادهسازی بهروزرسانی لحظهای با استفاده از SignalR وجود دارد. این روش تجربه کاربری را به شکل قابل توجهی بهبود میبخشد. برای جزئیات بیشتر یا مشاوره میتوانید تماس بگیرید: 09190994063 - 09376846692
بخش مدل داده و سرویسها عالی بود. در مورد اعمال کد تخفیف، آیا منطق پیچیدهتری مثل تخفیف برای محصولات خاص یا محدودیتهای استفاده (یک بار مصرف) هم قابل پیادهسازی است؟
بله، منطق تخفیف میتواند بسیار پیچیدهتر باشد. میتوانید یک جدول Coupon در دیتابیس با فیلدهایی برای نوع تخفیف، محصولات مجاز، تاریخ انقضا، حداقل مبلغ خرید و تعداد دفعات استفاده تعریف کنید. سپس در متد ApplyDiscountAsync تمامی این شرایط را بررسی کنید. برای راهنمایی بیشتر با ما تماس بگیرید: 09190994063 - 09376846692
یک سناریو خاص: اگر کاربر یک محصول را به سبد اضافه کند و بعداً قیمت آن محصول در فروشگاه تغییر کند، آیا قیمت در سبد خرید بهروز میشود یا همان قیمت زمان اضافه شدن حفظ میشود؟
نکته بسیار مهمی است! در طراحی که ارائه شد، قیمت (UnitPrice) در CartItem ذخیره میشود. این بدان معناست که اگر قیمت محصول اصلی تغییر کند، قیمت در سبد خرید همان قیمت زمان اضافه شدن به سبد باقی میماند. این یک Best Practice است تا کاربر تجربهی ثابتتری داشته باشد. البته میتوانید منطق بهروزرسانی را نیز پیادهسازی کنید. برای مشاوره فنی با ما در تماس باشید: 09190994063 - 09376846692