Back to: ASP.NET Core Web API Tutorials
AutoMapper Real-time Example in ASP.NET Core Web API:
In this article, I will develop one Real-time Example in ASP.NET Core Web API using Automapper. In this example, we will build a small e-commerce application using ASP.NET Core Web API, Entity Framework Core (Code-First approach), SQL Server, and AutoMapper. The primary goal is to show how AutoMapper can help map data between entities (representing our database tables) and DTOs (Data Transfer Objects, which are used for API communication).
Real-time E-commerce Application using AutoMapper in ASP.NET Core Web API:
The main goal of our e-commerce application is to manage customer orders efficiently and support key functions such as product management, customer account management, membership benefits, order placement, and shipment tracking. The application supports the following key features:
- Product Management: The application provides the ability to define, categorize, and manage products. Each product can have attributes like price, discount, SKU, stock quantity, and a reference to its category.
- Customer Management: It allows for managing customer information, including personal details, membership tiers, and associated addresses for shipping. Customers are assigned membership tiers (e.g., Gold, Silver, Bronze), which determine the discount they receive on purchases and also manage multiple addresses per customer.
- Order Creation and Processing: The application manages orders placed by customers, including tracking ordered items, calculating product-level and order-level discounts, handling dynamic charges like delivery fees, and computing total order costs. It also allows tracking the status of an order.
- Discount and Membership Tier Management: The application incorporates a flexible discount management system using both product-level discounts and membership-tier-specific discounts, improving customer retention.
- Shipping and Tracking: Customers can store multiple addresses for shipping purposes. The application supports managing orders with specific shipping addresses, delivery charges, and tracking information for delivery, such as estimated delivery dates and tracking numbers.
- Dynamic Delivery Charges: Allows dynamic control over delivery charges based on configurations, providing free delivery on eligible orders, which can be managed centrally through the configuration file (appsettings.json).
Setting Up the Project and Installing Entity Framework Core and AutoMapper
First, create a new ASP.NET Core Web API Application named ECommerceAPI. Once you create the project, please install the Entity Framework Core and Automapper Packages by executing the following command in the Package Manager Console.
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
- Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection
Defining Entities and DTOs:
First, create two folders named Models and DTOs in the project root directory, where we will create all our Entities and DTOs.
Creating Models:
Each model represents a table in the database with properties corresponding to columns.
Creating Category Model
Create a class file named Category.cs within the Models folder, and copy and paste the following code. This model defines categories for products, containing properties like Name and Description. It has a one-to-many relationship with Product, allowing multiple products in a single category. This entity represents product categories (e.g., Electronics, Accessories, etc.).
using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.Models { public class Category { public int Id { get; set; } // Primary Key [Required, MaxLength(100)] public string Name { get; set; } public string? Description { get; set; } public List<Product> Products { get; set; } } }
Creating Product Model
Create a class file named Product.cs within the Models folder and then copy and paste the following code. This entity represents individual products, including properties like Price, SKU, Stock Quantity, and Discount Percentage. It is linked to a Category to classify the product type.
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.Models { public class Product { public int Id { get; set; } // Primary Key [Required, MaxLength(100)] public string Name { get; set; } [Required] [Column(TypeName = "decimal(18,2)")] public decimal Price { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal? DiscountPercentage { get; set; } //Product Level Discount [MaxLength(500)] public string? Description { get; set; } [MaxLength(50)] public string SKU { get; set; } // Stock Keeping Unit [Required] public int StockQuantity { get; set; } public int CategoryId { get; set; } // Foreign Key [ForeignKey("CategoryId")] public Category Category { get; set; } } }
Creating MembershipTier Model:
Create a class file named MembershipTier.cs within the Models folder, and copy and paste the following code. This model represents different membership levels (e.g., Gold, Silver, and Bronze) with a Discount Percentage property. This discount is applied at the order level based on the customer’s membership type.
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.Models { public class MembershipTier { [Key] public int Id { get; set; } //Primary Key [Required, MaxLength(50)] public string TierName { get; set; } // e.g., Gold, Silver, Bronze [Column(TypeName = "decimal(5,2)")] public decimal DiscountPercentage { get; set; } // Discount percentage for this tier } }
Creating Customer Model
Create a class file named Customer.cs within the Models folder, and copy and paste the following code. This model stores customer information, including details like Email, Phone Number, and membership (Membership Tier). It holds lists of Addresses (a customer can have multiple addresses) and Orders related to the customer.
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Net; namespace ECommerceAPI.Models { public class Customer { public int Id { get; set; } //Primary Key [Required, MaxLength(100)] public string FirstName { get; set; } [Required, MaxLength(100)] public string LastName { get; set; } [Required] public string Email { get; set; } [Required] public string PhoneNumber { get; set; } public DateTime DateOfBirth { get; set; } public bool IsActive { get; set; } public int MembershipTierId { get; set; } // Foreign key to MembershipTier [ForeignKey("MembershipTierId")] public MembershipTier MembershipTier { get; set; } public List<Address> Addresses { get; set; } public List<Order> Orders { get; set; } } }
Creating Address Model
Create a class file named Address.cs within the Models folder and then copy and paste the following code. This entity represents a physical address for a customer. Each address record links to a single Customer, but a customer can have many addresses.
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.Models { public class Address { public int Id { get; set; } //Primary Key [Required, MaxLength(200)] public string Street { get; set; } [Required, MaxLength(100)] public string City { get; set; } [Required, MaxLength(10)] public string ZipCode { get; set; } public int CustomerId { get; set; } //Foreign Key [ForeignKey("CustomerId")] public Customer Customer { get; set; } // Navigation property for the Customer } }
Creating Order Model
Create a class file named Order.cs within the Models folder and then copy and paste the following code. This entity stores information about an order placed by a customer. It tracks the base amount, discount, total amount, status (Processing, Shipped, etc.), shipping address, and tracking detail.
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.Models { public class Order { public int Id { get; set; } //Primary Key [Required] public DateTime OrderDate { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal Amount { get; set; } //Base Amount [Column(TypeName = "decimal(18,2)")] public decimal OrderDiscount { get; set; } //Order Level Discount [Column(TypeName = "decimal(18,2)")] public decimal DeliveryCharge { get; set; } //Delivery Charge [Column(TypeName = "decimal(18,2)")] public decimal TotalAmount { get; set; } //Total Amount After Apply Discount and Delivery Charge [MaxLength(20)] public string Status { get; set; } public DateTime? ShippedDate { get; set; } [Required] public int? CustomerId { get; set; } //Foreign Key [ForeignKey("CustomerId")] public Customer Customer { get; set; } public int? ShippingAddressId { get; set; } //Foreign Key [ForeignKey("ShippingAddressId")] public Address ShippingAddres { get; set; } public List<OrderItem> OrderItems { get; set; } public TrackingDetail TrackingDetail { get; set; } } }
Creating OrderItem Model
Create a class file named OrderItem.cs within the Models folder, and copy and paste the following code. This model represents each item in an order, including properties like Product Price, Discount, and Total Price. This is linked to Product and Order.
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.Models { public class OrderItem { public int Id { get; set; } //Primary Key [Required] public int OrderId { get; set; } //Foreign Key [ForeignKey("OrderId")] public Order Order { get; set; } [Required] public int ProductId { get; set; } //Foreign Key [ForeignKey("ProductId")] public Product Product { get; set; } [Required] public int Quantity { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal ProductPrice { get; set; } //Product Prioce [Column(TypeName = "decimal(18,2)")] public decimal Discount { get; set; } // Product Level Discount [Column(TypeName = "decimal(18,2)")] public decimal TotalPrice { get; set; } //Product Level Total Price after Applying Discoun } }
Creating TrackingDetail Model
Create a class file named TrackingDetail.cs within the Models folder, and copy and paste the following code. This entity stores shipment tracking details for an order, including Carrier, Estimated Delivery Date, and Tracking Number. It is related to Order to track delivery status.
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.Models { public class TrackingDetail { public int Id { get; set; } //Primary Key public int OrderId { get; set; } //Foreign Key [ForeignKey("OrderId")] public Order Order { get; set; } [Required] public string Carrier { get; set; } public DateTime EstimatedDeliveryDate { get; set; } [MaxLength(500)] public string TrackingNumber { get; set; } } }
Creating DTOs:
Data Transfer Objects (DTOs) transfer data between the clients and servers. This ensures that only relevant information is exposed, enhancing security and better controlling what is communicated.
Creating OrderDTO
Create a class file named OrderDTO.cs within the DTOs folder, and copy and paste the following code. This DTO represents a simplified view of Order for API responses, including essential properties like Order Date, Total Amount, and customer contact details. It also includes the Shipping Address, Order Items, and Tracking Details.
namespace ECommerceAPI.DTOs { public class OrderDTO { public int OrderId { get; set; } public string OrderDate { get; set; } public decimal Amount { get; set; } public decimal OrderDiscount { get; set; } public decimal DeliveryCharge { get; set; } public decimal TotalAmount { get; set; } public string CustomerName { get; set; } public string CustomerEmail { get; set; } public string CustomerPhoneNumber { get; set; } public string Status { get; set; } public string ShippedDate { get; set; } public AddressDTO ShippingAddress { get; set; } public List<OrderItemDTO> OrderItems { get; set; } public TrackingDetailDTO TrackingDetail { get; set; } } }
Creating OrderItemDTO
Create a class file named OrderItemDTO.cs within the DTOs folder, and then copy and paste the following code. This DTO represents individual items within an order, containing product details (Product Name, Product Price) and calculated properties like Total Price and Discount.
namespace ECommerceAPI.DTOs { public class OrderItemDTO { public string ProductName { get; set; } public decimal ProductPrice { get; set; } public int Quantity { get; set; } public decimal Discount { get; set; } public decimal TotalPrice { get; set; } } }
Creating AddressDTO
Create a class file named AddressDTO.cs within the DTOs folder, and then copy and paste the following code. This DTO represents a minimal representation of the Address model, showing only the relevant address fields (Street, City, ZipCode).
using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.DTOs { public class AddressDTO { [Required, MaxLength(200)] public string Street { get; set; } [Required, MaxLength(100)] public string City { get; set; } [Required, MaxLength(10)] public string ZipCode { get; set; } } }
Creating TrackingDetailDTO
Create a class file named TrackingDetailDTO.cs within the DTOs folder and then copy and paste the following code. This DTO contains tracking information for an order, including Carrier, Estimated Delivery Date, and Tracking Number, without exposing additional properties.
namespace ECommerceAPI.DTOs { public class TrackingDetailDTO { public string Carrier { get; set; } public DateTime EstimatedDeliveryDate { get; set; } public string TrackingNumber { get; set; } } }
Creating OrderCreateDTO
Create a class file named OrderCreateDTO.cs within the DTOs folder, and then copy and paste the following code. This DTO creates an order, containing CustomerId, Shipping Address ID, and a list of OrderItemCreateDTO items.
using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.DTOs { public class OrderCreateDTO { [Required] public int CustomerId { get; set; } [Required] public int ShippingAddressId { get; set; } [Required] public List<OrderItemCreateDTO> Items { get; set; } } }
Creating OrderItemCreateDTO
Create a class file named OrderItemCreateDTO.cs within the DTOs folder, and then copy and paste the following code. This DTO is used within OrderCreateDTO, representing product ID and quantity information required to create an Order Item.
using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.DTOs { public class OrderItemCreateDTO { [Required] public int ProductId { get; set; } [Required] public int Quantity { get; set; } } }
Creating TrackingCreateDTO
Create a class file named TrackingCreateDTO.cs within the DTOs folder, and then copy and paste the following code. This DTO contains tracking information for an order, including Carrier, Estimated Delivery Date, and Tracking Number, which is required to update an order’s tracking details.
using System.ComponentModel.DataAnnotations; namespace ECommerceAPI.DTOs { public class TrackingCreateDTO { [Required] public int OrderId { get; set; } [Required] public string Carrier { get; set; } [Required] public DateTime EstimatedDeliveryDate { get; set; } [Required] public string TrackingNumber { get; set; } } }
Create a DbContext Class
The database context file represents the connection to the database and defines the DbSets for each model, such as Customers, Products, and Orders. This file configures relationships, including seeding initial data for Membership Tier, Customer, Address, Category, Product, Order, and Tracking Detail. The OnModelCreating method sets up initial data.
First, create a folder called Data, and then inside the Data folder, create a class file named ECommerceDBContext.cs, then copy and paste the following code.
using ECommerceAPI.Models; using Microsoft.EntityFrameworkCore; namespace ECommerceAPI.Data { public class ECommerceDBContext : DbContext { public ECommerceDBContext(DbContextOptions<ECommerceDBContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Seed Membership Tiers modelBuilder.Entity<MembershipTier>().HasData( new MembershipTier { Id = 1, TierName = "Gold", DiscountPercentage = 15.0m }, new MembershipTier { Id = 2, TierName = "Silver", DiscountPercentage = 10.0m }, new MembershipTier { Id = 3, TierName = "Bronze", DiscountPercentage = 5.0m }, new MembershipTier { Id = 4, TierName = "Standard", DiscountPercentage = 0.0m } ); // Seed Customer Data modelBuilder.Entity<Customer>().HasData( new Customer { Id = 1, FirstName = "Pranaya", LastName = "Rout", Email = "pranayarout@example.com", PhoneNumber = "1234567890", DateOfBirth = new DateTime(1985, 5, 20), IsActive = true, MembershipTierId = 1 // Gold Member }, new Customer { Id = 2, FirstName = "Hina", LastName = "Sharma", Email = "hinasharma@example.com", PhoneNumber = "234567", DateOfBirth = new DateTime(1988, 8, 15), IsActive = true, MembershipTierId = 2 // Silver Member } ); // Seed Address Data modelBuilder.Entity<Address>().HasData( new Address { Id = 1, Street = "123 Main St", City = "Jajpur", ZipCode = "755019", CustomerId = 1 }, new Address { Id = 2, Street = "456 Main St", City = "Cuttack", ZipCode = "755123", CustomerId = 2 }, new Address { Id = 3, Street = "789 Main St", City = "BBSR", ZipCode = "755456", CustomerId = 1 } ); // Seed Category Data modelBuilder.Entity<Category>().HasData( new Category { Id = 1, Name = "Electronics", Description = "Electronic Products Description" }, new Category { Id = 2, Name = "Accessories", Description = "Accessories Products Description" } ); // Seed Product Data modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "Laptop", Price = 1500m, Description = "High-performance laptop", SKU = "LPT-100", StockQuantity = 50, CategoryId = 1, DiscountPercentage = 10 }, new Product { Id = 2, Name = "Mouse", Price = 25m, Description = "Wireless mouse", SKU = "MSE-200", StockQuantity = 200, CategoryId = 2, DiscountPercentage = 5 }, new Product { Id = 3, Name = "Keyboard", Price = 50m, Description = "Mechanical keyboard", SKU = "KBD-300", StockQuantity = 150, CategoryId = 1, DiscountPercentage = 15 }, new Product { Id = 4, Name = "Mobile", Price = 2550m, Description = "iPhone 15 pro", SKU = "MOB-123", StockQuantity = 100, CategoryId = 1, DiscountPercentage = 0 } ); // Seed Order Data modelBuilder.Entity<Order>().HasData( new Order { Id = 1, OrderDate = new DateTime(2025, 02, 06), CustomerId = 1, ShippingAddressId = 1, Status = "Processing", Amount = 1397.50m, // Total amount before applying membership discount OrderDiscount = 209.63m, // Membership discount based on Gold membership (15%) DeliveryCharge = 0m, // Delivery charge waived TotalAmount = 1187.87m // Final total after all discounts }, new Order { Id = 2, OrderDate = new DateTime(2025, 02, 05), CustomerId = 1, ShippingAddressId = 1, Status = "Processing", Amount = 900m, // Total amount before discounts and charges OrderDiscount = 135m, // 15% Gold membership discount DeliveryCharge = 50.0m, // Delivery charge applied TotalAmount = 900m - 135m + 50.0m, // Total after discount and delivery charge } ); // Seed OrderItem Data modelBuilder.Entity<OrderItem>().HasData( new OrderItem { Id = 1, OrderId = 1, ProductId = 1, Quantity = 1, ProductPrice = 1500m, //Actual Product Price Discount = 150m, // Product-level discount for Laptop (10%) TotalPrice = 1350m // Price after product discount }, new OrderItem { Id = 2, OrderId = 1, ProductId = 2, Quantity = 2, ProductPrice = 25m, //Actual Product Price Discount = 2.50m, // Product-level discount for 2 Mice (5% each, total 2.5) TotalPrice = 47.50m // Price after product discount }, new OrderItem { Id = 3, OrderId = 2, ProductId = 2, Quantity = 10, ProductPrice = 25m, //Actual Product Price Discount = 12.5m, // Product-level discount (5% of 25 * 10) TotalPrice = (25m * 10) - 12.5m // Total price after discount } ); // Seed ShippingDetail Data modelBuilder.Entity<TrackingDetail>().HasData( new TrackingDetail { Id = 1, OrderId = 1, Carrier = "FedEx", EstimatedDeliveryDate = new DateTime(2025, 02, 15), TrackingNumber = "123456789" }, new TrackingDetail { Id = 2, OrderId = 2, Carrier = "FedEx", EstimatedDeliveryDate = new DateTime(1988, 02, 15), TrackingNumber = "123789456" } ); } public DbSet<MembershipTier> MembershipTiers { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<Customer> Customers { get; set; } public DbSet<Product> Products { get; set; } public DbSet<OrderItem> OrderItems { get; set; } public DbSet<Address> Addresses { get; set; } public DbSet<Category> Categories { get; set; } public DbSet<TrackingDetail> TrackingDetails { get; set; } } }
Configure the Database Connection
Next, please update the appsettings.json as follows. Here, we store application settings, including the connection string for the database (ECommerceDBConnection), delivery charge settings (DeliveryChargeSettings), and logging configuration (LogLevel). This file allows configuration changes without modifying code.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "DeliveryChargeSettings": { "DeliveryChargeAmount": 50.0, "ApplyDeliveryCharge": true, "FreeDeliveryThreshold": 1000.0 }, "ConnectionStrings": { "ECommerceDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Here,
- ConnectionStrings: Holds database connection details, allowing the application to connect to the ECommerceDB database.
- DeliveryChargeSettings: Provides configuration for delivery charges, such as the DeliveryChargeAmount, whether to ApplyDeliveryCharge, and the FreeDeliveryThreshold for orders eligible for free delivery.
- Logging and AllowedHosts: Configures logging settings and specifies allowed hosts for the application, respectively.
Set Up Dependency Injection and Middleware:
The Main method of the Program class is the entry point for the ASP.NET Core application. It sets up the application configuration and dependencies, including:
- Adding and configuring services like DbContext, AutoMapper, and Controllers.
- Configuring Swagger for API documentation.
- Setting up middleware for HTTPS redirection, authorization, and routing.
- Running the web server to listen for requests.
So, please modify the Program class as follows.
using ECommerceAPI.Data; using Microsoft.EntityFrameworkCore; namespace ECommerceAPI { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() // Optionally, configure JSON options or other formatter settings .AddJsonOptions(options => { // Configure JSON serializer settings to keep the original names in serialization and deserialization options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Register the AutoMapper with dependency injection builder.Services.AddAutoMapper(typeof(Program).Assembly); // Register the ProductDbContext with dependency injection builder.Services.AddDbContext<ECommerceDBContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("ECommerceDBConnection"))); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); 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(); } } }
Creating and Applying Database Migration:
Open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows to generate the Migration file and then apply the Migration file to create the ECommerceDB database and required tables:
Once you execute the above commands and verify the database, you should see the ECommerceDB database with the required tables, as shown in the image below.
Creating Mapping Profile
Next, we need to define the AutoMapper Mapping Profile that configures how entities (Order, OrderItem, etc.) are mapped to their corresponding DTOs (OrderDTO, OrderItemDTO, etc.). This helps automate object-object mapping and ensures that data is transferred between different application layers.
So, first, create a folder named MappingProfiles in the project root directory, and then inside this folder, add a class file named OrderMappingProfile.cs and then copy and paste the following code.
using AutoMapper; using ECommerceAPI.DTOs; using ECommerceAPI.Models; namespace ECommerceAPI.MappingProfiles { public class OrderMappingProfile : Profile { public OrderMappingProfile() { //----------------------------------------------------------------- // 1. Order -> OrderDTO //----------------------------------------------------------------- CreateMap<Order, OrderDTO>() // Different property names: "Id" -> "OrderId" .ForMember(dest => dest.OrderId, opt => opt.MapFrom(src => src.Id)) // Format "OrderDate" from DateTime to a string (e.g., '2025-02-06 13:15:00') .ForMember(dest => dest.OrderDate, opt => opt.MapFrom(src => src.OrderDate.ToString("yyyy-MM-dd HH:mm:ss"))) // Combine two properties into one .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => $"{src.Customer.FirstName} {src.Customer.LastName}")) // Map "Customer.Email" -> "CustomerEmail" (different property names) .ForMember(dest => dest.CustomerEmail, opt => opt.MapFrom(src => src.Customer.Email)) // Map "Customer.PhoneNumber" -> "CustomerPhoneNumber" .ForMember(dest => dest.CustomerPhoneNumber, opt => opt.MapFrom(src => src.Customer.PhoneNumber)) // Map "ShippingAddres" (typo in entity) -> "ShippingAddress" (DTO) .ForMember(dest => dest.ShippingAddress, opt => opt.MapFrom(src => src.ShippingAddres)) // Format "ShippedDate" if not null .ForMember(dest => dest.ShippedDate, opt => opt.MapFrom(src => src.ShippedDate.HasValue ? src.ShippedDate.Value.ToString("yyyy-MM-dd HH:mm:ss") : null)); // Note: We do NOT explicitly map .ForMember(dest => dest.TrackingDetail, ...) // or .ForMember(dest => dest.OrderItems, ...) here because: // - The property names are the same in source and destination // - We have separate mappings for TrackingDetail -> TrackingDetailDTO // and OrderItem -> OrderItemDTO (see below) // AutoMapper will automatically use those mappings, as long as // the source and destination property names and types align. //----------------------------------------------------------------- // 2. OrderItem -> OrderItemDTO //----------------------------------------------------------------- CreateMap<OrderItem, OrderItemDTO>() // We only specify a custom mapping where source != destination. // "Product.Name" -> "ProductName" is a custom transformation. .ForMember(dest => dest.ProductName, opt => opt.MapFrom(src => src.Product.Name)); // We do NOT specify mappings for ProductPrice or TotalPrice // because the property names match exactly in both the source // and the destination (and there's no special logic needed): // Source: OrderItem.ProductPrice -> Destination: OrderItemDTO.ProductPrice // Source: OrderItem.TotalPrice -> Destination: OrderItemDTO.TotalPrice // AutoMapper will do these by convention. //----------------------------------------------------------------- // 3. Address -> AddressDTO //----------------------------------------------------------------- // All property names match, and no special transform is needed, // so we don't need ForMember. This single CreateMap is enough. CreateMap<Address, AddressDTO>(); //----------------------------------------------------------------- // 4. TrackingDetail -> TrackingDetailDTO //----------------------------------------------------------------- CreateMap<TrackingDetail, TrackingDetailDTO>() // We apply a null substitute for TrackingNumber if null. .ForMember(dest => dest.TrackingNumber, opt => opt.NullSubstitute("Tracking not available")); //----------------------------------------------------------------- // 5. OrderCreateDTO -> Order //----------------------------------------------------------------- CreateMap<OrderCreateDTO, Order>() // Set the OrderDate to the current time when creating a new order .ForMember(dest => dest.OrderDate, opt => opt.MapFrom(src => DateTime.Now)) // Initialize a default status (e.g., "Pending") .ForMember(dest => dest.Status, opt => opt.MapFrom(src => "Pending")) // Map "OrderCreateDTO.OrderItems" -> "Order.Items" (different property names) .ForMember(dest => dest.OrderItems, opt => opt.MapFrom(src => src.Items)); // We do NOT specify .ForMember for "Amount", "OrderDiscount", or "TotalAmount" // because they might be calculated logic in the controller/service layer // rather than mapped directly from the DTO. //----------------------------------------------------------------- // 6. OrderItemCreateDTO -> OrderItem //----------------------------------------------------------------- // Because the property names match ("ProductId", "Quantity") and // there's no special transformation, a default CreateMap is sufficient. CreateMap<OrderItemCreateDTO, OrderItem>(); } } }
Create the Orders API Controller
The API controller is responsible for managing orders in the system. It includes endpoints to:
- Get an order by its ID.
- Fetch orders for a specific customer.
- Create a new order.
- Add or update tracking details for an order.
- Fetch order tracking details.
The controller uses the database context to interact with the database, AutoMapper to map entities to DTOs, and dependency injection to access configuration values and services. So, create an API Empty Controller named OrdersController within the Controllers folder and copy and paste the following code. The following code is self-explained, so please read the comment lines for a better understanding.
using AutoMapper; using AutoMapper.QueryableExtensions; // Needed for .ProjectTo<T>() using ECommerceAPI.Data; using ECommerceAPI.DTOs; using ECommerceAPI.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace ECommerceAPI.Controllers { [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly ECommerceDBContext _context; private readonly IMapper _mapper; private readonly IConfiguration _configuration; // The constructor injects: // 1. ECommerceDBContext for database access // 2. IMapper for AutoMapper // 3. IConfiguration for reading settings from appsettings.json public OrdersController(ECommerceDBContext context, IMapper mapper, IConfiguration configuration) { _context = context; _mapper = mapper; _configuration = configuration; } // --------------------------- // GET: api/Orders/GetOrderById/{id} // --------------------------- // This action method retrieves a single order by its ID and maps it // directly to an OrderDTO. We use AsNoTracking() since no updates are needed, // and ProjectTo<OrderDTO> to fetch only the columns required by the DTO. [HttpGet("GetOrderById/{id}")] public async Task<ActionResult<OrderDTO>> GetOrderById(int id) { try { // Use AsNoTracking for a read-only query. // Then, use ProjectTo<OrderDTO> to map from the Orders entity to // our OrderDTO without manually specifying .Include() calls. var orderDTO = await _context.Orders .AsNoTracking() .Where(o => o.Id == id) .ProjectTo<OrderDTO>(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); // If the order does not exist, respond with 404 if (orderDTO == null) return NotFound($"Order with ID {id} not found."); // If found, return a 200 OK along with the mapped OrderDTO return Ok(orderDTO); } catch (Exception ex) { // In case of any unanticipated errors, return a 500 status with the exception message return StatusCode(500, $"An error occurred while fetching the order: {ex.Message}"); } } // --------------------------- // GET: api/Orders/GetOrdersByCustomerId/{customerId} // --------------------------- // This action method retrieves all orders for a specific customer. // Again, we use AsNoTracking() for a read-only scenario and ProjectTo // to map directly to our DTO. [HttpGet("GetOrdersByCustomerId/{customerId}")] public async Task<ActionResult<IEnumerable<OrderDTO>>> GetOrdersByCustomerId(int customerId) { try { // We filter by CustomerId, then project to OrderDTO and fetch the results var ordersDTO = await _context.Orders .AsNoTracking() .Where(o => o.CustomerId == customerId) .ProjectTo<OrderDTO>(_mapper.ConfigurationProvider) .ToListAsync(); // If there are no matching orders, return 404 if (!ordersDTO.Any()) return NotFound($"No orders found for customer with ID {customerId}."); // Otherwise, return them with a 200 OK return Ok(ordersDTO); } catch (Exception ex) { // A catch-all for any runtime issues return StatusCode(500, $"An error occurred while fetching orders for customer {customerId}: {ex.Message}"); } } // --------------------------- // POST: api/Orders/CreateOrder // --------------------------- // This endpoint creates a new order. It uses a transaction for atomicity // and includes performance enhancements to fetch only necessary data. [HttpPost("CreateOrder")] public async Task<ActionResult<OrderDTO>> CreateOrder([FromBody] OrderCreateDTO orderCreateDTO) { // Validate incoming data quickly if (orderCreateDTO == null) return BadRequest("Order data cannot be null."); // Start a transaction to ensure either all operations succeed or none do using var transaction = await _context.Database.BeginTransactionAsync(); try { // 1. Fetch the customer (and related membership + addresses) needed for discount & address validation // We use AsNoTracking here because we don't want to update the customer's data var customer = await _context.Customers .AsNoTracking() .Include(c => c.MembershipTier) .Include(c => c.Addresses) .FirstOrDefaultAsync(c => c.Id == orderCreateDTO.CustomerId && c.IsActive); // If the customer is invalid or not active, we stop here if (customer == null) return NotFound($"Customer with ID {orderCreateDTO.CustomerId} not found or inactive."); // 2. Verify the shipping address belongs to this customer var shippingAddress = customer.Addresses .FirstOrDefault(a => a.Id == orderCreateDTO.ShippingAddressId); // If no matching address, we return a BadRequest if (shippingAddress == null) return BadRequest("The specified shipping address is invalid or does not belong to the customer."); // 3. Extract the product IDs from the incoming DTO var productIds = orderCreateDTO.Items.Select(i => i.ProductId).ToList(); // 4. Performance optimization: only fetch columns we actually need // (Price, DiscountPercentage, StockQuantity). var productsData = await _context.Products .Where(p => productIds.Contains(p.Id)) .Select(p => new { p.Id, p.Price, p.DiscountPercentage, p.StockQuantity }) .ToListAsync(); // If the requested productIDs don't match what's in the DB, some are invalid if (productsData.Count != productIds.Count) return BadRequest("One or more products in the order are invalid."); // 5. Ensure we have enough stock for each requested product foreach (var item in orderCreateDTO.Items) { var productInfo = productsData.FirstOrDefault(p => p.Id == item.ProductId); if (productInfo == null || productInfo.StockQuantity < item.Quantity) { return BadRequest($"Insufficient stock for product ID {item.ProductId}. " + $"Available: {productInfo?.StockQuantity ?? 0}"); } } // 6. Convert the OrderCreateDTO into an Order entity // (this sets basic fields like Status = "Pending" and Orderdate to DateTime.Now by default). var order = _mapper.Map<Order>(orderCreateDTO); // 7. Calculate the total amount of this order decimal totalAmount = 0; // We will need to update each order item's pricing details, and also // reduce the product's stock in the database. foreach (var item in order.OrderItems) { var productInfo = productsData.First(p => p.Id == item.ProductId); // Product-level discount decimal productDiscount = productInfo.DiscountPercentage.HasValue ? productInfo.Price * productInfo.DiscountPercentage.Value / 100 : 0; // Fill out the item details (price, discount, etc.) item.ProductPrice = productInfo.Price; item.Discount = productDiscount * item.Quantity; item.TotalPrice = (productInfo.Price - productDiscount) * item.Quantity; // Accumulate into totalAmount which will be sored in the order level totalAmount += item.TotalPrice; // Reduce stock in the actual tracked Product entity // We do a separate fetch by ID so we can update the real entity var trackedProduct = await _context.Products.FindAsync(item.ProductId); if (trackedProduct == null) return BadRequest($"Product with ID {item.ProductId} no longer exists."); trackedProduct.StockQuantity -= item.Quantity; } // 8. Calculate membership discount for the entire order decimal membershipDiscountPercentage = customer.MembershipTier?.DiscountPercentage ?? 0m; decimal orderDiscount = totalAmount * (membershipDiscountPercentage / 100); // 9. Determine delivery charge based on config settings and totalAmount bool applyDeliveryCharge = _configuration.GetValue<bool>("DeliveryChargeSettings:ApplyDeliveryCharge"); decimal deliveryChargeAmount = _configuration.GetValue<decimal>("DeliveryChargeSettings:DeliveryChargeAmount"); decimal freeDeliveryThreshold = _configuration.GetValue<decimal>("DeliveryChargeSettings:FreeDeliveryThreshold"); decimal deliveryCharge = 0; if (applyDeliveryCharge && totalAmount < freeDeliveryThreshold) { deliveryCharge = deliveryChargeAmount; } // 10. Final calculation for Amount, OrderDiscount, DeliveryCharge, and TotalAmount for the Product Level order.Amount = totalAmount; order.OrderDiscount = orderDiscount; order.DeliveryCharge = deliveryCharge; order.TotalAmount = totalAmount - orderDiscount + deliveryCharge; // 11. Add the new order to the DB context for insertion _context.Orders.Add(order); // Save changes: includes new order and updated stock await _context.SaveChangesAsync(); // Commit the transaction so everything is permanent await transaction.CommitAsync(); // 12. Retrieve the newly created order in a read-only manner using AsNoTracking, // then ProjectTo<OrderDTO> to create the final response object. var createdOrderDTO = await _context.Orders .AsNoTracking() .Where(o => o.Id == order.Id) .ProjectTo<OrderDTO>(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); // If we can't find the newly-created order for some reason, throw 500 if (createdOrderDTO == null) return StatusCode(500, "An error occurred while creating the order."); // 13. Return a 200 OK response with the final OrderDTO return Ok(createdOrderDTO); } catch (Exception ex) { // Roll back the transaction if anything goes wrong await transaction.RollbackAsync(); return StatusCode(500, $"An error occurred while creating the order: {ex.Message}"); } } // --------------------------- // POST: api/Orders/AddTrackingDetails // --------------------------- // This endpoint adds or updates the tracking details for a given order. // We allow updates only if the order status is "Pending" or "Shipped." [HttpPost("AddTrackingDetails")] public async Task<ActionResult<TrackingDetailDTO>> AddTrackingDetails([FromBody] TrackingCreateDTO trackingDetailDTO) { // Validate the input DTO if (trackingDetailDTO == null) return BadRequest("Invalid Tracking Details"); try { // 1. Retrieve the order (tracked), including any existing TrackingDetail. var order = await _context.Orders .Include(o => o.TrackingDetail) .FirstOrDefaultAsync(o => o.Id == trackingDetailDTO.OrderId); // 2. If the order doesn't exist, return 404 if (order == null) return NotFound($"Order with ID {trackingDetailDTO.OrderId} not found."); // 3. Only allow updates if status is "Pending", "Processing", or "Shipped" // - "Pending"/"Processing" transitions to "Shipped" upon adding tracking // - If the order is already "Shipped", we allow further updates // - Otherwise, return a 400 error. if (order.Status == "Pending" || order.Status == "Processing") { order.Status = "Shipped"; } else if (order.Status != "Shipped") { return BadRequest($"Cannot add or update tracking for an order in status '{order.Status}'."); } // 4. If the order has no tracking detail, create one; otherwise, update it if (order.TrackingDetail == null) { var trackingDetail = new TrackingDetail { OrderId = order.Id, Carrier = trackingDetailDTO.Carrier, EstimatedDeliveryDate = trackingDetailDTO.EstimatedDeliveryDate, TrackingNumber = trackingDetailDTO.TrackingNumber }; _context.TrackingDetails.Add(trackingDetail); } else { order.TrackingDetail.Carrier = trackingDetailDTO.Carrier; order.TrackingDetail.EstimatedDeliveryDate = trackingDetailDTO.EstimatedDeliveryDate; order.TrackingDetail.TrackingNumber = trackingDetailDTO.TrackingNumber; } // 5. Save changes to the database await _context.SaveChangesAsync(); // 6. Map the newly created or updated TrackingDetail to a TrackingDetailDTO // Now that the database update is done, 'order.TrackingDetail' holds the latest data. var updatedTrackingDTO = _mapper.Map<TrackingDetailDTO>(order.TrackingDetail); // 7. Return 200 OK with the updated tracking info return Ok(updatedTrackingDTO); } catch (Exception ex) { // If anything goes wrong, return 500 with an error message return StatusCode(500, $"An error occurred while updating tracking details: {ex.Message}"); } } // --------------------------- // GET: api/Orders/GetTrackingDetailByOrderId/{orderId} // --------------------------- // This method retrieves only the tracking details for a given order ID. // We use AsNoTracking because it's a read-only scenario. // We also only fetch the existing TrackingDetail if not null. [HttpGet("GetTrackingDetailByOrderId/{orderId}")] public async Task<ActionResult<TrackingDetailDTO>> GetTrackingDetailByOrderId(int orderId) { try { // Again, we only want the tracking details, so we filter to orders that // have a matching orderId AND a non-null TrackingDetail. Then we project // that detail to TrackingDetailDTO. var trackingDetailDTO = await _context.Orders .AsNoTracking() .Where(o => o.Id == orderId && o.TrackingDetail != null) .Select(o => o.TrackingDetail) .ProjectTo<TrackingDetailDTO>(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); // If not found, return 404 if (trackingDetailDTO == null) return NotFound($"No tracking details found for order with ID {orderId}."); // Otherwise, return the mapped DTO return Ok(trackingDetailDTO); } catch (Exception ex) { // If something goes awry, respond with 500 return StatusCode(500, $"An error occurred: {ex.Message}"); } } } }
Testing the Endpoints:
Let us test each OrdersController endpoint.
GetOrderById:
Retrieve a single Order (plus related details) by Order ID.
Method: GET
URL: https://localhost:5001/api/Orders/GetOrderById/1
Replace 1 with a valid Order ID in your database.
GetOrdersByCustomerId
Purpose: Fetch all orders for a specific customer.
Method: GET
URL: https://localhost:5001/api/Orders/GetOrdersByCustomerId/1
Replace 1 with a valid Customer ID in your database.
CreateOrder
Create a new order for a given customer, calculate product- and membership-level discounts, and adjust product stock.
Method: POST
URL: https://localhost:5001/api/Orders/CreateOrder
Body: JSON representing an OrderCreateDTO.
{ "CustomerId": 1, "ShippingAddressId": 1, "Items": [ { "ProductId": 2, "Quantity": 2 }, { "ProductId": 3, "Quantity": 1 } ] }
Here,
- CustomerId: Must reference an active customer (e.g., ID=1).
- ShippingAddressId: Must belong to that customer.
- Items: A list of Product ID and Quantity pairs. Each product must exist and have sufficient stock.
AddTrackingDetails
Add or update tracking information (carrier, tracking number, estimated delivery date) for an existing order. Automatically updates the order’s status from Pending to Shipped if applicable.
Method: POST
URL: https://localhost:5001/api/Orders/AddTrackingDetails
Body: JSON representing a TrackingCreateDTO.
{ "OrderId": 3, "Carrier": "UPS", "EstimatedDeliveryDate": "2025-03-01T00:00:00", "TrackingNumber": "UPS-TRACK-123" }
GetTrackingDetailByOrderId
Retrieve the tracking detail for a specific order if it exists.
Method: GET
URL: https://localhost:5001/api/Orders/GetTrackingDetailByOrderId/1
Replace 1 with the desired Order ID.
In the next article, I will discuss HTTP Methods in ASP.NET Core Web API with Examples. In this article, I explain Automapper with One Real-time e-commerce application in ASP.NET Core Web API with Examples. I hope you enjoy this article, “AutoMapper Real-time Example in ASP.NET Core Web API.”
i have already read the overview features of project in this article and i have to say WOW. you are such a dedicated and
meticulous person. thank you so much