AutoMapper Real-time Example in ASP.NET Core Web API

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:

AutoMapper Real-time Example in ASP.NET Core Web API

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.

AutoMapper Real-time Example in ASP.NET Core Web API

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.”

1 thought on “AutoMapper Real-time Example in ASP.NET Core Web API”

  1. 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

Leave a Reply

Your email address will not be published. Required fields are marked *