Back to: ASP.NET Core Web API Tutorials
Dependency Injection in ASP.NET Core Web API
In this article, I will explain the Dependency Injection Design Pattern in ASP.NET Core Web API Application with examples. Please read our previous article discussing 5XX HTTP Status Codes in ASP.NET Core Web API Applications.
What is Dependency Injection (DI)?
Dependency Injection (DI) is a design pattern used to achieve loose coupling between classes and their dependencies. In simple terms, DI allows an object (class) to receive its dependencies (like services or objects it needs to perform its tasks) from an external source (usually the DI container) rather than creating and managing them internally. Here, we need to understand two terms, Dependency and Injection:
- Dependency: An object that another object depends on.
- Injection: The process of providing the dependencies to a class.
Dependency Injection (DI) helps decouple a system’s components so that they can be more easily replaced and tested. DI allows us to inject dependencies (services, repositories, etc.) into a class rather than having that class construct or manage them, i.e., removes hard-coded dependencies between classes. This promotes loose coupling and makes the application easier to test, maintain, and extend.
Example Without DI:
Example With DI:
Why We Need to Use Dependency Injection in ASP.NET Core Web API?
- Loose Coupling: Classes do not need to create or manage the lifetime of their dependencies. They simply declare what they need, and the container will create and manage the lifetime.
- Easier Testing: Since dependencies are injected, you can easily substitute real dependencies with mocks or stubs when writing unit tests.
- Increased Flexibility: Components can be replaced with alternative implementations without modifying the classes that use them.
How Dependency Injection Design Pattern Works:
Dependency Injection involves three key components:
- The Client: The object that depends on the service. This is also called the dependent object.
- The Injector: Configures which service to inject and when to inject it, often implemented by a DI container.
- The Service: The object being used by the client. This is also called the dependency object.
For a better understanding, please have a look at the following diagram:
As you can see in the above diagram, the Injector (i.e., DI Container) creates an object of the Service Class and injects that object into the Client Class. The Client Class then uses the Injected Object of the Service Class to call the Methods of the Service Class. So, in this way, the Dependency Injection Design Pattern separates the responsibility of creating and managing an object of the service class out of the Client Class.
How Dependency Injection works in ASP.NET Core Web API:
ASP.NET Core has a built-in DI container that automatically resolves dependencies at runtime. We need to register our classes (services) with a specific lifetime (Singleton, Scoped, or Transient) in the DI container. DI container resolves these dependencies when needed by injecting them into the class constructor (most recommended approach), properties, or methods.
Typical DI setup in ASP.NET Core:
- Register Services: Register services (classes) in the Program class using methods like AddTransient(), AddScoped(), or AddSingleton().
- Inject Dependencies: Provide dependencies to classes via constructors or other means. That means in your controllers or other classes, add a parameter in the constructor that matches the interface or class you registered.
- Resolve Dependencies: The DI container resolves and injects the required dependencies when needed.
Service Lifetimes
ASP.NET Core DI container supports three primary service lifetimes:
Singleton
A single instance is created and shared throughout the application’s lifetime. It is ideal for stateless services that hold global configuration, caching, or any data that should be shared and remains constant across all requests. For example, a service reads from a config file once, and all consumers get the same data. The real-time scenarios include Logging services and configuration services. The syntax to register a service as Singleton is given below.
- Syntax-1: Services.AddSingleton<IService, ServiceImplementation>(); //Interface and its concrete Implementation
- Syntax-2: Services.AddSingleton<ServiceImplementation>(); //Without Interface
Scoped
A new instance is created once per client request (HTTP request). It is ideal for services that maintain state throughout a single request but not across multiple requests. For example, database context in web applications is typically scoped, so each request has its own DbContext. The syntax to register a service as Scoped is given below.
- Syntax-1: Services.AddScoped<IService, ServiceImplementation>(); //Interface and its concrete Implementation
- Syntax-2: Services.AddScoped<ServiceImplementation>(); //Without Interface
Transient
A new instance is created every time the service is requested. It is ideal for lightweight, stateless services or services that need distinct instances each time. For example, a service that performs a short-lived operation, such as generating unique IDs or sending emails. The syntax to register a service as Scoped is given below.
- Syntax-1: Services.AddTransient<IService, ServiceImplementation>(); //Interface and its concrete Implementation
- Syntax-2: Services.AddTransient<ServiceImplementation>(); //Without Interface
Implementing Dependency Injection Design Pattern in ASP.NET Core Web API:
Let’s create an ASP.NET Core Web API application that demonstrates the differences between Singleton, Scoped, and Transient services with one real-time example. To showcase their behavior, we will build a simple e-commerce system where different services are registered with different lifetimes.
- Singleton: We will use a service to manage application-wide settings or cache. For example, a GlobalDiscountService that holds discount rates for the entire application. This will remain constant (or infrequently changed) over the application’s lifetime.
- Scoped: We will use a ShoppingCartService tied to a particular request. Each request gets its own instance of the service, ensuring that items added to the cart in one request do not interfere with another user’s request.
- Transient: We will use an OrderIdGenerator that might produce a unique identifier for each operation on demand. A new instance is needed for each consumption, ensuring complete independence.
Creating ASP.NET Core Web API Project:
First, create a new ASP.NET Core Web API Project called ECommerceDIExample. Once you have created the project, please install the Entity Framework Core packages by executing the following commands in the Package Manager Console:
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
Creating Models:
First, create a folder named Models at the project root directory where we will create all our Models:
User.cs
The User entity represents a user in the system. Create a class file named User.cs within the Models folder and then copy and paste the following code:
using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace ECommerceDIExample.Models { [Index(nameof(Email), Name = "IX_Email_Unique", IsUnique =true)] public class User { // Unique Identifier public int Id { get; set; } // Common user details [Required(ErrorMessage = "Email is required.")] public string Email { get; set; } [Required(ErrorMessage = "Name is required.")] [StringLength(100, MinimumLength = 2, ErrorMessage = "Name must be between 2 and 100 characters.")] public string Name { get; set; } [StringLength(20, ErrorMessage = "UserType cannot exceed 20 characters.")] public string? UserType { get; set; } // VIP, Premium, Standard, Guest // Timestamps public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; // Soft delete indicator (true => user is active) public bool IsActive { get; set; } = true; // Navigation property: A user can have many carts public ICollection<Cart>? Carts { get; set; } } }
Product.cs
The Product entity represents items available for purchase. Create a class file named Product.cs within the Models folder and then copy and paste the following code:
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ECommerceDIExample.Models { public class Product { public int Id { get; set; } // Common product details [Required(ErrorMessage = "Product name is required.")] [StringLength(100, ErrorMessage = "Product name cannot exceed 100 characters.")] public string? Name { get; set; } [Column(TypeName = "decimal(18,2)")] [Range(0.0, double.MaxValue, ErrorMessage = "Price must be non-negative.")] public decimal Price { get; set; } [StringLength(500, ErrorMessage = "Description cannot exceed 500 characters.")] public string? Description { get; set; } [Range(0, 1000, ErrorMessage = "Stock cannot be negative.")] public int Stock { get; set; } // Timestamps public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; // Soft delete indicator public bool IsAvailable { get; set; } = true; } }
Cart.cs
Represents a shopping cart associated with a user. Create a class file named Cart.cs within the Models folder and then copy and paste the following code:
using System.ComponentModel.DataAnnotations; namespace ECommerceDIExample.Models { public class Cart { public int Id { get; set; } [Required] public int UserId { get; set; } // Foreign Key public User User { get; set; } // Navigation Property public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public bool IsCheckedOut { get; set; } = false; // Navigation property for cart items public ICollection<CartItem> CartItems { get; set; } } }
CartItem.cs
Represents individual items in a cart, each referencing a Product and specifying a Quantity. Create a class file named CartItem.cs within the Models folder, and then copy and paste the following code:
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ECommerceDIExample.Models { public class CartItem { public int Id { get; set; } [Required] public int CartId { get; set; } public Cart? Cart { get; set; } [Required] public int ProductId { get; set; } public Product? Product { get; set; } [Range(1, int.MaxValue, ErrorMessage = "Quantity should be at least 1.")] public int Quantity { get; set; } [Column(TypeName = "decimal(18,2)")] [Range(0.0, double.MaxValue, ErrorMessage = "UnitPrice cannot be negative.")] public decimal UnitPrice { get; set; } [Column(TypeName = "decimal(18,2)")] [Range(0.0, double.MaxValue, ErrorMessage = "TotalPrice cannot be negative.")] public decimal TotalPrice { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } }
ECommerceDbContext
Create a folder named Data in the project root directory. Then, create a new file, ECommerceDbContext.cs, under the Data folder and copy and paste the following code. We have also provided initial seed data within the ECommerceDbContext using the HasData method. This facilitates testing and ensures the database has the necessary default entries upon creation.
using ECommerceDIExample.Models; using Microsoft.EntityFrameworkCore; namespace ECommerceDIExample.Data { /// The application's database context with DbSet properties and seed data. public class ECommerceDbContext : DbContext { public ECommerceDbContext(DbContextOptions<ECommerceDbContext> options) : base(options) { } public DbSet<User> Users { get; set; } public DbSet<Product> Products { get; set; } public DbSet<Cart> Carts { get; set; } public DbSet<CartItem> CartItems { get; set; } // Configures the model with seed data. protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Define static dates for seed data var staticCreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); var staticUpdatedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); // Seed Users modelBuilder.Entity<User>().HasData( new User { Id = 1, Email = "john.doe@example.com", Name = "John Doe", CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt, IsActive = true, UserType = "VIP" }, new User { Id = 2, Email = "jane.smith@example.com", Name = "Jane Smith", CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt, IsActive = true, UserType = "Premium" } ); // Seed Products modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "Wireless Mouse", Description = "Ergonomic wireless mouse with adjustable DPI.", Price = 25.99m, Stock = 150, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt, IsAvailable = true }, new Product { Id = 2, Name = "Mechanical Keyboard", Description = "RGB backlit mechanical keyboard with blue switches.", Price = 89.99m, Stock = 80, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt, IsAvailable = true }, new Product { Id = 3, Name = "HD Monitor", Description = "24-inch full HD monitor with IPS panel.", Price = 149.99m, Stock = 60, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt, IsAvailable = true } ); // Seed Carts modelBuilder.Entity<Cart>().HasData( new Cart { Id = 1, UserId = 1, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt, IsCheckedOut = false }, new Cart { Id = 2, UserId = 2, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt, IsCheckedOut = false } ); // Seed CartItems modelBuilder.Entity<CartItem>().HasData( new CartItem { Id = 1, CartId = 1, ProductId = 1, Quantity = 2, UnitPrice = 25.99m, TotalPrice = 51.98m, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt }, new CartItem { Id = 2, CartId = 1, ProductId = 2, Quantity = 1, UnitPrice = 89.99m, TotalPrice = 89.99m, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt }, new CartItem { Id = 3, CartId = 2, ProductId = 3, Quantity = 1, UnitPrice = 149.99m, TotalPrice = 149.99m, CreatedAt = staticCreatedAt, UpdatedAt = staticUpdatedAt } ); } } }
Code Explanation
- Users: Three default users are seeded for testing purposes.
- Products: Three products are seeded with detailed information, including descriptions and stock levels.
- Carts: Each user has an associated cart.
- CartItems: Initial items are added to the carts to simulate existing shopping activities.
Creating DTOs for Cart Controller
We will create separate DTOs for input (requests) and output (responses) related to cart operations. The Input DTOs represent the data structure the client expects when performing actions like adding or updating items in the cart. The Output DTOs represent the data structure sent back to the client, encapsulating the cart details and items. First, create a folder named DTOs in the project root directory, where we will create all our DTOs.
AddItemRequestDTO:
For adding an item to the cart. Create a class file named AddItemRequestDTO.cs within the DTOs folder and copy and paste the following:
using System.ComponentModel.DataAnnotations; namespace ECommerceDIExample.DTOs { public class AddItemRequestDTO { [Required(ErrorMessage = "UserId is required.")] public int UserId { get; set; } [Required(ErrorMessage = "ProductId is required.")] [Range(1, int.MaxValue, ErrorMessage = "ProductId must be a positive integer.")] public int ProductId { get; set; } [Required(ErrorMessage = "Quantity is required.")] [Range(1, int.MaxValue, ErrorMessage = "Quantity must be at least 1.")] public int Quantity { get; set; } = 1; } }
UpdateQuantityRequestDTO:
This DTO is used to update the quantity of an existing cart item. Create a class file named UpdateQuantityRequestDTO.cs within the DTOs folder and copy and paste the following:
using System.ComponentModel.DataAnnotations; namespace ECommerceDIExample.DTOs { public class UpdateQuantityRequestDTO { [Required(ErrorMessage = "UserId is required.")] public int UserId { get; set; } [Required(ErrorMessage = "CartItemId is required.")] public int CartItemId { get; set; } [Required(ErrorMessage = "NewQuantity is required.")] [Range(1, int.MaxValue, ErrorMessage = "NewQuantity must be at least 1.")] public int NewQuantity { get; set; } } }
RemoveItemRequestDTO:
For adding an item to the cart. Create a class file named RemoveItemRequestDTO.cs within the DTOs folder and copy and paste the following:
using System.ComponentModel.DataAnnotations; namespace ECommerceDIExample.DTOs { public class RemoveItemRequestDTO { [Required(ErrorMessage = "UserId is required.")] public int UserId { get; set; } [Required(ErrorMessage = "CartItemId is required.")] public int CartItemId { get; set; } } }
CartItemResponseDTO:
Represents individual items within the cart. Create a class file named CartItemResponseDTO.cs within the DTOs folder and copy and paste the following:
namespace ECommerceDIExample.DTOs { //DTO representing an individual item in the cart. public class CartItemResponseDTO { // ID of the cart item. public int CartItemId { get; set; } // ID of the product. public int ProductId { get; set; } // Name of the product. public string? ProductName { get; set; } // Description of the product. public string? Description { get; set; } // Unit price of the product. public decimal UnitPrice { get; set; } // Total price for this cart item (UnitPrice * Quantity). public decimal TotalPrice { get; set; } // Quantity of the product in the cart. public int Quantity { get; set; } // Price after applying discounts. public decimal DiscountedPrice { get; set; } } }
CartResponseDTO:
Represents the entire cart with all its items. Create a class file named CartResponseDTO.cs within the DTOs folder and copy and paste the following:
namespace ECommerceDIExample.DTOs { //DTO representing the entire cart. public class CartResponseDTO { // ID of the cart. public int CartId { get; set; } // ID of the user who owns the cart. public int UserId { get; set; } // Indicates whether the cart has been checked out. public bool IsCheckedOut { get; set; } // Timestamp when the cart was created. public DateTime CreatedAt { get; set; } // Timestamp when the cart was last updated. public DateTime UpdatedAt { get; set; } // Collection of items in the cart. public IEnumerable<CartItemResponseDTO>? Items { get; set; } // Total amount before discounts. public decimal TotalAmount { get; set; } // Discount rate applied based on user type. public decimal DiscountRate { get; set; } // Total amount after applying discounts. public decimal DiscountedTotal { get; set; } } }
Services
First, create a folder named Services, where we will create all our service interfaces and their implementor classes:
ILoggerService (Singleton)
Create a class file named ILoggerService.cs within the Services folder, and then copy and paste the following code:
namespace ECommerceDIExample.Services { public interface ILoggerService { void Log(string message); } }
LoggerService (Singleton)
Create a class file named LoggerService.cs within the Services folder, and then copy and paste the following code. The LoggerService will be registered as a Singleton to ensure only one instance handles all logging, maintaining a single log file.
namespace ECommerceDIExample.Services { // A singleton service responsible for logging messages to a text file. public class LoggerService : ILoggerService { private readonly string _logFilePath; public LoggerService() { // Log file path (ensure "logs" folder exists or adjust as needed) _logFilePath = Path.Combine(Directory.GetCurrentDirectory(), "logs", "app_log.txt"); // Create logs directory if it doesn't exist var directory = Path.GetDirectoryName(_logFilePath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } } // Logs a message with a timestamp to the log file. public void Log(string message) { var logMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} - {message}{Environment.NewLine}"; File.AppendAllText(_logFilePath, logMessage); } } }
This logger simply appends a timestamped message to a logs/app_log.txt file. It is declared as Singleton so that one instance handles all logging throughout the app. Whenever we create a new service or perform certain operations, we call Log(…) to record it for debugging or tracking.
IGlobalDiscountService (Singleton)
Create a class file named IGlobalDiscountService.cs within the Services folder, and then copy and paste the following code:
namespace ECommerceDIExample.Services { public interface IGlobalDiscountService { // Returns a discount rate (0.0 to 1.0) for the specified user type. decimal GetDiscountRate(string? userType = null); // Sets a default discount rate for fallback or general use. void SetDefaultDiscountRate(decimal newRate); } }
GlobalDiscountService (Singleton)
Create a class file named GlobalDiscountService.cs within the Services folder, and then copy and paste the following code:
namespace ECommerceDIExample.Services { // Manages application-wide discounts. // Registered as a Singleton, so one instance is shared across the entire application lifetime. public class GlobalDiscountService : IGlobalDiscountService { private readonly ILoggerService _logger; // Example: We keep a default discount and specialized discount rules by user role. private decimal _defaultDiscountRate; private Dictionary<string, decimal> _userTypeBasedDiscounts; public GlobalDiscountService(ILoggerService logger) { _logger = logger; _logger.Log("Creating GlobalDiscountService (Singleton)."); // Default discount _defaultDiscountRate = 0.05m; // Simulate role-based discount _userTypeBasedDiscounts = new Dictionary<string, decimal> { { "VIP", 0.10m }, { "Premium", 0.08m }, { "Standard", 0.05m }, { "Guest", 0.02m } }; } // Returns the discount rate for a given user role. public decimal GetDiscountRate(string? userType = null) { if (!string.IsNullOrEmpty(userType) && _userTypeBasedDiscounts.ContainsKey(userType)) { return _userTypeBasedDiscounts[userType]; } return _defaultDiscountRate; // fallback if no User Type is found } // Updates the default discount rate for the entire application. public void SetDefaultDiscountRate(decimal newRate) { _defaultDiscountRate = newRate; //_logger.Log($"Global discount updated to {newRate:P2}"); } } }
Code Explanation:
- Maintains discount rates for different user types.
- Provides a default discount if the user type is not recognized.
- It is Singleton, so any controller or service that needs discount info gets the same consistent data.
IOrderIdGenerator (Transient)
Create a class file named IOrderIdGenerator.cs within the Services folder, and then copy and paste the following code:
namespace ECommerceDIExample.Services { // Defines the contract for generating new order IDs. public interface IOrderIdGenerator { string GenerateOrderId(); } }
OrderIdGenerator (Transient)
Create a class file named OrderIdGenerator.cs within the Services folder, and then copy and paste the following code:
namespace ECommerceDIExample.Services { // Generates order IDs in a transient fashion (new instance for each request). // Useful for scenarios where each usage should be fully isolated. public class OrderIdGenerator : IOrderIdGenerator { private readonly ILoggerService _logger; public OrderIdGenerator(ILoggerService logger) { _logger = logger; _logger.Log("Creating OrderIdGenerator (Transient)."); } // Generate a "realistic" order ID, e.g., ORD202501231234567. // This uses the current datetime plus a short random suffix. public string GenerateOrderId() { // Example format: ORD-[Year][Month][Day][HHmmss]-[random 4 digits] var now = DateTime.Now; var randomSuffix = new Random().Next(1000, 9999).ToString(); var orderId = $"ORD-{now:yyyyMMddHHmmss}-{randomSuffix}"; return orderId; } } }
Code Explanation:
- Each time the application needs to generate a new order ID, a fresh instance of this class is created.
- It is transient to ensure no shared state; each usage is completely independent.
- Logs creation in its constructor so we can see how often a new instance is made.
IShoppingCartService (Scoped)
Create a class file named IShoppingCartService.cs within the Services folder, and then copy and paste the following code:
using ECommerceDIExample.Models; namespace ECommerceDIExample.Services { public interface IShoppingCartService { // Retrieves the active cart for a user or creates one if it doesn't exist. Cart GetOrCreateCart(int userId); // Adds an item to the user's cart. void AddItemToCart(int userId, int productId, int quantity = 1); // Removes an item from the user's cart. void RemoveItemFromCart(int userId, int cartItemId); // Updates the quantity of an item in the user's cart. void UpdateItemQuantity(int userId, int cartItemId, int newQuantity); // Retrieves the user's cart with all items and product details. Cart? GetUserCartWithItems(int userId); // Clears all items from the user's cart. void ClearCart(int userId); // Checks out the user's cart, marking it as completed. void Checkout(int userId); } }
ShoppingCartService (Scoped)
Create a class file named ShoppingCartService.cs within the Services folder, and then copy and paste the following code:
using ECommerceDIExample.Data; using ECommerceDIExample.Models; using Microsoft.EntityFrameworkCore; namespace ECommerceDIExample.Services { // Scoped service managing shopping cart operations for a specific user. public class ShoppingCartService : IShoppingCartService { private readonly ECommerceDbContext _dbContext; private readonly ILoggerService _logger; public ShoppingCartService(ECommerceDbContext dbContext, ILoggerService logger) { _dbContext = dbContext; _logger = logger; _logger.Log("ShoppingCartService instance created."); } public Cart GetOrCreateCart(int userId) { var cart = _dbContext.Carts .Include(c => c.CartItems) .FirstOrDefault(c => c.UserId == userId && !c.IsCheckedOut); if (cart == null) { cart = new Cart { UserId = userId, CartItems = new List<CartItem>(), CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, IsCheckedOut = false }; _dbContext.Carts.Add(cart); _dbContext.SaveChanges(); //_logger.Log($"Created new cart for User ID: {userId}"); } else { //_logger.Log($"Retrieved existing cart (ID: {cart.Id}) for User ID: {userId}"); } return cart; } public void AddItemToCart(int userId, int productId, int quantity = 1) { var cart = GetOrCreateCart(userId); var product = _dbContext.Products.Find(productId); if (product == null || !product.IsAvailable) { // _logger.Log($"Attempted to add unavailable product (ID: {productId}) to cart."); throw new Exception("Product not available."); } if (product.Stock < quantity) { // _logger.Log($"Insufficient stock for product (ID: {productId}). Requested: {quantity}, Available: {product.Stock}"); throw new Exception("Insufficient stock for the product."); } var existingCartItem = _dbContext.CartItems .FirstOrDefault(ci => ci.CartId == cart.Id && ci.ProductId == productId); if (existingCartItem == null) { var newCartItem = new CartItem { CartId = cart.Id, ProductId = product.Id, Quantity = quantity, UnitPrice = product.Price, TotalPrice = product.Price * quantity, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _dbContext.CartItems.Add(newCartItem); //_logger.Log($"Added new item (Product ID: {productId}, Quantity: {quantity}) to Cart ID: {cart.Id}"); } else { existingCartItem.Quantity += quantity; existingCartItem.TotalPrice = existingCartItem.UnitPrice * existingCartItem.Quantity; existingCartItem.UpdatedAt = DateTime.UtcNow; _dbContext.CartItems.Update(existingCartItem); //_logger.Log($"Updated item (CartItem ID: {existingCartItem.Id}) quantity to {existingCartItem.Quantity} in Cart ID: {cart.Id}"); } // Reduce product stock product.Stock -= quantity; _dbContext.Products.Update(product); _dbContext.SaveChanges(); } public void RemoveItemFromCart(int userId, int cartItemId) { var cart = GetOrCreateCart(userId); var cartItem = _dbContext.CartItems .Include(ci => ci.Product) .FirstOrDefault(ci => ci.Id == cartItemId && ci.CartId == cart.Id); if (cartItem == null) { //_logger.Log($"Attempted to remove non-existent CartItem ID: {cartItemId} from Cart ID: {cart.Id}"); throw new Exception("Cart item not found."); } // Restore product stock if (cartItem.Product != null) { cartItem.Product.Stock += cartItem.Quantity; _dbContext.Products.Update(cartItem.Product); } _dbContext.CartItems.Remove(cartItem); _dbContext.SaveChanges(); // _logger.Log($"Removed CartItem ID: {cartItemId} from Cart ID: {cart.Id}"); } public void UpdateItemQuantity(int userId, int cartItemId, int newQuantity) { var cart = GetOrCreateCart(userId); var cartItem = _dbContext.CartItems .Include(ci => ci.Product) .FirstOrDefault(ci => ci.Id == cartItemId && ci.CartId == cart.Id); if (cartItem == null) { //_logger.Log($"Attempted to update non-existent CartItem ID: {cartItemId} in Cart ID: {cart.Id}"); throw new Exception("Cart item not found."); } if (newQuantity <= 0) { RemoveItemFromCart(userId, cartItemId); return; } var difference = newQuantity - cartItem.Quantity; if (cartItem.Product == null) { //_logger.Log($"Product not found for CartItem ID: {cartItem.Id}"); throw new Exception("Associated product not found."); } if (difference > 0 && cartItem.Product.Stock < difference) { //_logger.Log($"Insufficient stock to increase quantity for Product ID: {cartItem.ProductId}"); throw new Exception("Insufficient stock to increase quantity."); } cartItem.Quantity = newQuantity; cartItem.TotalPrice = cartItem.UnitPrice * newQuantity; cartItem.UpdatedAt = DateTime.UtcNow; // Adjust product stock cartItem.Product.Stock -= difference; _dbContext.Products.Update(cartItem.Product); _dbContext.CartItems.Update(cartItem); _dbContext.SaveChanges(); //_logger.Log($"Updated CartItem ID: {cartItemId} to Quantity: {newQuantity} in Cart ID: {cart.Id}"); } public Cart? GetUserCartWithItems(int userId) { var cart = _dbContext.Carts .Include(c => c.CartItems) .ThenInclude(ci => ci.Product) .FirstOrDefault(c => c.UserId == userId && !c.IsCheckedOut); if (cart != null) { //_logger.Log($"Retrieved Cart ID: {cart.Id} for User ID: {userId}"); } else { //_logger.Log($"No active cart found for User ID: {userId}"); } return cart; } public void ClearCart(int userId) { var cart = GetOrCreateCart(userId); var cartItems = _dbContext.CartItems .Where(ci => ci.CartId == cart.Id) .Include(ci => ci.Product) .ToList(); foreach (var item in cartItems) { if (item.Product != null) { item.Product.Stock += item.Quantity; _dbContext.Products.Update(item.Product); } _dbContext.CartItems.Remove(item); } _dbContext.SaveChanges(); // _logger.Log($"Cleared all items from Cart ID: {cart.Id} for User ID: {userId}"); } public void Checkout(int userId) { var cart = GetOrCreateCart(userId); if (cart.CartItems == null || !cart.CartItems.Any()) { // _logger.Log($"Attempted checkout with empty Cart ID: {cart.Id} for User ID: {userId}"); throw new Exception("Cart is empty."); } cart.IsCheckedOut = true; cart.UpdatedAt = DateTime.UtcNow; _dbContext.Carts.Update(cart); _dbContext.SaveChanges(); //_logger.Log($"Checked out Cart ID: {cart.Id} for User ID: {userId}"); } } }
Code Explanations:
- Add Item: Adds a product to the cart, ensuring product availability and sufficient stock. Updates product stock accordingly.
- Remove Item: Removes an item from the cart and restores the product stock.
- Update Quantity: Adjusts the quantity of a cart item, handling stock changes and potential removal if the quantity is set to zero.
- Get Cart: Retrieves the active cart with all items and product details.
- Clear Cart: Empties the cart, restoring product stocks.
- Checkout: Marks the cart as checked out, preventing further modifications.
CartController:
Create an API Empty Controller named CartController within the Controllers folder and then copy and paste the following code:
using ECommerceDIExample.Data; using ECommerceDIExample.DTOs; using ECommerceDIExample.Services; using Microsoft.AspNetCore.Mvc; namespace ECommerceDIExample.Controllers { // Controller managing shopping cart operations. [ApiController] [Route("api/[controller]")] public class CartController : ControllerBase { private readonly IShoppingCartService _shoppingCartService; private readonly IGlobalDiscountService _globalDiscountService; private readonly IOrderIdGenerator _orderIdGenerator; private readonly ECommerceDbContext _context; public CartController( IShoppingCartService shoppingCartService, IGlobalDiscountService globalDiscountService, IOrderIdGenerator orderIdGenerator, ECommerceDbContext context) { _shoppingCartService = shoppingCartService; _globalDiscountService = globalDiscountService; _orderIdGenerator = orderIdGenerator; _context = context; } // Adds an item to the user's cart. [HttpPost("AddItem")] public IActionResult AddItem([FromBody] AddItemRequestDTO request) { try { _shoppingCartService.AddItemToCart(request.UserId, request.ProductId, request.Quantity); return Ok("Item added to cart."); } catch (Exception ex) { return BadRequest(ex.Message); } } // Removes an item from the user's cart. [HttpDelete("RemoveItem")] public IActionResult RemoveItem(RemoveItemRequestDTO requestDTO) { try { _shoppingCartService.RemoveItemFromCart(requestDTO.UserId, requestDTO.CartItemId); return Ok("Item removed from cart."); } catch (Exception ex) { return BadRequest(ex.Message); } } // Updates the quantity of an item in the user's cart. [HttpPut("UpdateItemQuantity")] public IActionResult UpdateItemQuantity([FromBody] UpdateQuantityRequestDTO request) { if(!ModelState.IsValid) { return BadRequest(ModelState); } try { _shoppingCartService.UpdateItemQuantity(request.UserId, request.CartItemId, request.NewQuantity); return Ok("Cart item quantity updated."); } catch (Exception ex) { return BadRequest(ex.Message); } } // Retrieves the user's cart with all items. [HttpGet("{userId}")] public IActionResult GetUserCart(int userId) { var cart = _shoppingCartService.GetUserCartWithItems(userId); if (cart == null) { return NotFound("Cart not found for this user."); } //Fetch the user type from Database var userType = _context.Users.Find(userId)?.UserType; // Apply global discount var discountRate = _globalDiscountService.GetDiscountRate(userType); // Map cart to CartResponseDTO var cartDto = new CartResponseDTO { CartId = cart.Id, UserId = cart.UserId, IsCheckedOut = cart.IsCheckedOut, CreatedAt = cart.CreatedAt, UpdatedAt = cart.UpdatedAt, Items = cart.CartItems?.Select(ci => new CartItemResponseDTO { CartItemId = ci.Id, ProductId = ci.ProductId, ProductName = ci.Product?.Name, Description = ci.Product?.Description, UnitPrice = ci.UnitPrice, TotalPrice = ci.TotalPrice, DiscountedPrice = (ci.UnitPrice * ci.Quantity) * (1 - discountRate), Quantity = ci.Quantity }), TotalAmount = cart.CartItems?.Sum(ci => ci.TotalPrice) ?? 0m, DiscountRate = discountRate, DiscountedTotal = (cart.CartItems?.Sum(ci => ci.TotalPrice) ?? 0m) * (1 - discountRate) }; return Ok(cartDto); } // Clears all items from the user's cart. [HttpPost("ClearCart/{userId}")] public IActionResult ClearCart(int userId) { try { _shoppingCartService.ClearCart(userId); return Ok("Cart cleared successfully."); } catch (Exception ex) { return BadRequest(ex.Message); } } // Checks out the user's cart. [HttpPost("checkout/{userId}")] public IActionResult Checkout(int userId) { try { _shoppingCartService.Checkout(userId); var orderId = _orderIdGenerator.GenerateOrderId(); return Ok(new { Message = "Checkout successful.", OrderId = orderId }); } catch (Exception ex) { return BadRequest(ex.Message); } } } }
Modify appsettings.json
Make sure to add a connection string to your appsettings.json file. So, please modify the appsettings.json file as follows.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Program Setup
Configure the Dependency Injection (DI) container in Program.cs to register all services with their respective lifetimes and ensure that the LoggerService is registered as a Singleton. So, please modify the Program class as follows:
using ECommerceDIExample.Data; using ECommerceDIExample.Services; using Microsoft.EntityFrameworkCore; namespace ECommerceDIExample { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() .AddJsonOptions(options => { // This will use the property names as defined in the C# model options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); //Configure the ConnectionString and DbContext class builder.Services.AddDbContext<ECommerceDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection")); }); // Register Services into DI Container builder.Services.AddSingleton<ILoggerService, LoggerService>(); // Singleton builder.Services.AddSingleton<IGlobalDiscountService, GlobalDiscountService>(); // Singleton builder.Services.AddScoped<IShoppingCartService, ShoppingCartService>(); // Scoped builder.Services.AddTransient<IOrderIdGenerator, OrderIdGenerator>(); // Transient var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Service Registrations:
- Singleton (ILoggerService, IGlobalDiscountService): These services are instantiated once and reused throughout the application’s lifetime.
- Scoped (IShoppingCartService): A new instance is created per HTTP request, ensuring request-specific data handling.
- Transient (IOrderIdGenerator): A new instance is created each time it’s requested and is suitable for lightweight operations like generating unique IDs.
Database Migration
Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows.
With this, our Database with Users tables should be created as shown in the below image:
Now, run the application and test the functionalities, and it should work as expected.
Action Method Injection in ASP.NET Core Application
Sometimes, we only need a dependency object in a single action method. In that case, we need to use the [FromServices] attribute. For a better understanding, please modify the Checkout method as follows. We are using the [FromServices] attribute within the Checkout action method. So, at runtime, the IoC Container will inject the dependency object into the IOrderIdGenerator orderIdGenerator variable. Injecting the dependency object through a method is called method dependency injection.
[HttpPost("checkout/{userId}")] public IActionResult Checkout([FromServices] IOrderIdGenerator _orderIdGenerator, int userId) { try { _shoppingCartService.Checkout(userId); var orderId = _orderIdGenerator.GenerateOrderId(); return Ok(new { Message = "Checkout successful.", OrderId = orderId }); } catch (Exception ex) { return BadRequest(ex.Message); } }
What is the FromServices in ASP.NET Core Dependency Injection?
The [FromServices] attribute in ASP.NET Core explicitly indicates that an action method parameter should be resolved from the Dependency Injection (DI) container. This attribute is useful when injecting services directly into action methods instead of through the constructor. When we decorate a parameter in an action method with the [FromServices] attribute, ASP.NET Core’s dependency injection system will resolve and inject the specified service directly into the action method.
Get Services Manually in ASP.NET Core
We can also manually access the services configured with built-in IoC containers using the RequestServices property of HttpContext. The GetService method in ASP.NET Core Dependency Injection (DI) is used to retrieve a service from the DI container. For a better understanding, please modify the Checkout method as follows.
[HttpPost("checkout/{userId}")] public IActionResult Checkout(int userId) { try { var services = HttpContext.RequestServices; IOrderIdGenerator? _orderIdGenerator = (IOrderIdGenerator?)services.GetService(typeof(IOrderIdGenerator)); _shoppingCartService.Checkout(userId); var orderId = _orderIdGenerator.GenerateOrderId(); return Ok(new { Message = "Checkout successful.", OrderId = orderId }); } catch (Exception ex) { return BadRequest(ex.Message); } }
With the above changes in place, run the application, and you should get the output as expected. It is recommended to use constructor injection instead of getting it using RequestServices.
So, Dependency Injection (DI) is a design pattern that promotes loose coupling and enhances the modularity and testability of applications. In the context of ASP.NET Core, Dependency Injection is a fundamental feature integrated into the framework, enabling developers to manage dependencies between classes efficiently.
In the next article, I will discuss ApiController Attribute in ASP.NET Core Web API applications with Examples. Here, I explain the Dependency Injection Design Pattern in ASP.NET Core Web API application with multiple Examples. I hope you enjoy this article on “Dependency Injection in ASP.NET Core Web API.”