如何用C# Observer Pattern优化MVC+EF订单缺货通知功能?
Nice question! Let's walk through how to implement the C# Observer Pattern alongside Entity Framework's EntityState to make your out-of-stock notification workflow more modular, maintainable, and efficient. Here's a step-by-step breakdown with code examples tailored to your MVC+EF+React setup:
First, let's align on why this combination works:
- Observer Pattern: Decouples the "trigger" (marking an item as unavailable) from the "action" (notifying users). This lets you add new notification types (like SMS or in-app alerts) later without touching existing code.
- EntityState: EF's ChangeTracker tracks when entities are modified, added, or deleted. We'll use this to detect exactly which OrderItems were marked as unavailable, avoiding redundant checks or notifications.
Start with abstractions to keep your code flexible:
// Observer interface: Defines the contract for any service that needs to react to unavailable items public interface IOrderItemObserver { void OnItemUnavailable(OrderItem item); } // Subject interface: Manages observers and triggers notifications when state changes public interface IOrderItemSubject { void Attach(IOrderItemObserver observer); void Detach(IOrderItemObserver observer); void NotifyUnavailableItems(); }
The subject will use EF's ChangeTracker to find modified OrderItems where IsUnavailable was set to true:
public class OrderItemSubject : IOrderItemSubject { private readonly List<IOrderItemObserver> _observers = new(); private readonly AppDbContext _dbContext; public OrderItemSubject(AppDbContext dbContext) { _dbContext = dbContext; } public void Attach(IOrderItemObserver observer) { if (!_observers.Contains(observer)) { _observers.Add(observer); } } public void Detach(IOrderItemObserver observer) { _observers.Remove(observer); } public void NotifyUnavailableItems() { // Use EF's ChangeTracker to filter only items just marked as unavailable var unavailableItems = _dbContext.ChangeTracker.Entries<OrderItem>() .Where(entry => entry.State == EntityState.Modified && entry.Property(p => p.IsUnavailable).CurrentValue == true && entry.Property(p => p.IsUnavailable).OriginalValue == false) // Ensure it's a new change .Select(entry => entry.Entity) .ToList(); // Notify all registered observers for each affected item foreach (var item in unavailableItems) { foreach (var observer in _observers) { observer.OnItemUnavailable(item); } } } }
Create an observer that handles sending emails to users. This keeps your notification logic separate from the MVC controller:
public class EmailNotificationObserver : IOrderItemObserver { private readonly IEmailService _emailService; // Inject your existing email service for better testability public EmailNotificationObserver(IEmailService emailService) { _emailService = emailService; } public void OnItemUnavailable(OrderItem item) { // Fetch the user from the associated Order (adjust based on your entity relationships) var user = item.Order.User; var productName = item.Product.Name; var subject = "Your Ordered Item is Unavailable"; var body = $"Hi {user.FullName},\n\nThe item \"{productName}\" from your recent order is currently out of stock. Please log in to our website to select a replacement or update your order.\n\nThanks,\nThe Team"; // Add error handling/retry logic here if needed _emailService.SendEmail(user.Email, subject, body); } } // Example email service interface (replace with your actual implementation) public interface IEmailService { void SendEmail(string to, string subject, string body); }
Now wire everything up in your MVC controller and DbContext to automate notifications:
Step 5.1: Register Services in Dependency Injection
In your Program.cs (or Startup.cs), register the subject and observer:
builder.Services.AddScoped<IOrderItemSubject, OrderItemSubject>(); builder.Services.AddScoped<IOrderItemObserver, EmailNotificationObserver>(); builder.Services.AddScoped<IEmailService, YourExistingEmailService>(); // Use your real email service
Step 5.2: Update the DbContext to Trigger Notifications Automatically
Modify your DbContext to call NotifyUnavailableItems after saving changes, so you don't have to trigger it manually in every controller:
public class AppDbContext : DbContext { private readonly IOrderItemSubject _orderItemSubject; public AppDbContext(DbContextOptions<AppDbContext> options, IOrderItemSubject orderItemSubject) : base(options) { _orderItemSubject = orderItemSubject; // Attach the observer (DI handles lifecycle management) var emailObserver = this.GetService<IOrderItemObserver>(); if (emailObserver != null) { _orderItemSubject.Attach(emailObserver); } } public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { // Save changes first to persist the unavailable state var result = await base.SaveChangesAsync(cancellationToken); // Trigger notifications for items just marked as unavailable _orderItemSubject.NotifyUnavailableItems(); return result; } }
Step 5.3: Update Your MVC Controller Method
Your existing React-triggered API endpoint becomes simpler—just mark the item as unavailable and let EF handle the rest:
[ApiController] [Route("api/orders/items")] public class OrderItemController : ControllerBase { private readonly AppDbContext _dbContext; public OrderItemController(AppDbContext dbContext) { _dbContext = dbContext; } [HttpPost("{id}/mark-unavailable")] public async Task<IActionResult> MarkAsUnavailable(int id) { var orderItem = await _dbContext.OrderItems .Include(item => item.Order) .Include(item => item.Order.User) .Include(item => item.Product) .FirstOrDefaultAsync(item => item.Id == id); if (orderItem == null) { return NotFound("Order item not found"); } // Mark the item as unavailable orderItem.IsUnavailable = true; // Save changes—this triggers the DbContext's SaveChangesAsync, which fires notifications await _dbContext.SaveChangesAsync(); return Ok("Item marked as unavailable, notification sent"); } }
- Decoupling: If you later want to add SMS notifications or in-app alerts, just create a new
IOrderItemObserverimplementation and register it—no changes to existing code. - Efficiency: Uses EF's EntityState to only process items that were actually just marked as unavailable, avoiding redundant checks.
- Maintainability: Clear separation of concerns: the controller handles HTTP requests, the subject manages state changes, and observers handle actions.
- Testability: You can mock the
IEmailServiceorIOrderItemObserverto test without sending real emails.
内容的提问来源于stack exchange,提问作者valheru

