Automapper Complex Type Mapping in ASP.NET Core Web API

AutoMapper Complex Type Mapping in ASP.NET Core Web API

In this article, I will discuss How to Implement AutoMapper Complex Type Mapping in ASP.NET Core Web API Application with Examples. Please read our previous article discussing How to Use Automapper in ASP.NET Core Web API with Examples.

What is AutoMapper Complex Type Mapping?

AutoMapper is an object-to-object mapper that helps us map properties from one object to another, reducing the need for manual mapping logic. When working with entities in an application, especially when using Entity Framework Core, we might need to map between entities (which represent the database tables) and DTOs (Data Transfer Objects) that expose only the necessary information to API clients.

When both types (Source and Destination) involved in the mapping contain properties of the Complex Type, then in such scenarios, we need to use the AutoMapper Complex Mapping in ASP.NET Core Web API. So, Complex Mapping with AutoMapper occurs when:

  • We need to map objects whose properties don’t directly match in name or structure.
  • The target object has complex properties or collections requiring mapping by different sources.
  • We have special transformation rules (e.g., combining two fields into one).
Real-time Example to Understand AutoMapper Complex Type Mapping:

Let us understand AutoMapper Complex Mapping with one real-time example in an ASP.NET Core Web API Application using Entity Framework Core. Let’s implement a real-time e-commerce application using ASP.NET Core Web API and Entity Framework Core to manage Order, where we will apply AutoMapper Complex Mapping to map between the Order entity and various DTOs.

Setting Up the Project and Installing Entity Framework Core

First, create a new ASP.NET Core Web API Application named AutoMapperComplexMappingDemo. Once you have created 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.

Product.cs

Create a class file named Product.cs within the Models folder and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace AutoMapperComplexMappingDemo.Models
{
    public class Product
    {
        [Key]
        public int Id { get; set; }

        [Required, MaxLength(100)]
        public string Name { get; set; }

        [Required]
        [Column(TypeName = "decimal(18,2)")]
        public decimal Price { get; set; }

        [MaxLength(500)]
        public string Description { get; set; } // Optional description of the product
    }
}
Address.cs

Create a class file named Address.cs within the Models folder and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace AutoMapperComplexMappingDemo.Models
{
    public class Address
    {
        [Key]
        public int Id { get; set; }

        [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; }
        [ForeignKey("CustomerId")]
        public Customer Customer { get; set; } // Navigation property for the Customer
    }
}
Customer.cs

Create a class file named Customer.cs within the Models folder, and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;
namespace AutoMapperComplexMappingDemo.Models
{
    public class Customer
    {
        [Key]
        public int Id { get; set; }

        [Required, MaxLength(100)]
        public string FirstName { get; set; }

        [Required, MaxLength(100)]
        public string LastName { get; set; }

        [Required]
        public string Email { get; set; } // Email of the customer

        [Required]
        public string PhoneNumber { get; set; } // Contact number of the customer
        public Address Address { get; set; } // Navigation property for the Address
        public List<Order> Orders { get; set; } // Collection Navigation property for the Orders
    }
}
Order.cs

Create a class file named Order.cs within the Models folder and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AutoMapperComplexMappingDemo.Models
{
    public class Order
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public DateTime OrderDate { get; set; }

        [Required]
        [Column(TypeName = "decimal(18,2)")]
        public decimal Amount { get; set; }  // Total amount of the order

        [Required]
        public int CustomerId { get; set; }

        [ForeignKey("CustomerId")]
        public Customer Customer { get; set; } // Navigation Property

        public ICollection<OrderItem> OrderItems { get; set; }
    }
}
OrderItem.cs

Create a class file named OrderItem.cs within the Models folder, and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace AutoMapperComplexMappingDemo.Models
{
    public class OrderItem
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public int ProductId { get; set; }

        [ForeignKey("ProductId")]
        public Product Product { get; set; } // Navigation Property

        [Required]
        public int Quantity { get; set; } // Quantity of the product in the order

        [Required]
        [Column(TypeName = "decimal(18,2)")]
        public decimal ProductPrice { get; set; }

        [Required]
        public int OrderId { get; set; }

        [ForeignKey("OrderId")]
        public Order Order { get; set; } // Navigation Property
    }
}
OrderDTO.cs

Create a class file named OrderDTO.cs within the DTOs folder, and then copy and paste the following code.

namespace AutoMapperComplexMappingDemo.DTOs
{
    public class OrderDTO
    {
        public int OrderId { get; set; }
        public string OrderDate { get; set; } // Formatted Date
        public decimal Amount { get; set; } // Total amount of the order
        public string CustomerName { get; set; } // Full Name of the customer
        public string CustomerEmail { get; set; }  // Email of the customer
        public string CustomerPhoneNumber { get; set; }  // Contact number
        public AddressDTO Address { get; set; }
        public List<OrderItemDTO> Items { get; set; }
    }
}
OrderItemDTO.cs

Create a class file named OrderItemDTO.cs within the DTOs folder, and then copy and paste the following code.

namespace AutoMapperComplexMappingDemo.DTOs
{
    public class OrderItemDTO
    {
        public string ProductName { get; set; }
        public decimal ProductPrice { get; set; }
        public int Quantity { get; set; }
        public decimal TotalPrice { get; set; } // ProductPrice * Quantity
    }
}
AddressDTO.cs

Create a class file named AddressDTO.cs within the DTOs folder, and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;
namespace AutoMapperComplexMappingDemo.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; }
    }
}
OrderCreateDTO.cs

Create a class file named OrderCreateDTO.cs within the DTOs folder, and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;

namespace AutoMapperComplexMappingDemo.DTOs
{
    public class OrderCreateDTO
    {
        [Required]
        public int CustomerId { get; set; }

        [Required]
        public List<OrderItemCreateDTO> Items { get; set; }

        // The amount can be calculated on the server side based on the product prices and quantities.
        // However, if you prefer to include it, you can add it here.
    }
}
OrderItemCreateDTO.cs

Create a class file named OrderItemCreateDTO.cs within the DTOs folder, and then copy and paste the following code.

using System.ComponentModel.DataAnnotations;

namespace AutoMapperComplexMappingDemo.DTOs
{
    public class OrderItemCreateDTO
    {
        [Required]
        public int ProductId { get; set; }

        [Required]
        public int Quantity { get; set; }
    }
}
Create a DbContext Class

First, create a folder called Data. Inside the Data folder, create a class file named ECommerceDBContext.cs. Copy and paste the following code. We have also added some initial seed data for testing purposes.

using Microsoft.EntityFrameworkCore;

namespace AutoMapperComplexMappingDemo.Models
{
    public class ECommerceDBContext : DbContext
    {
        public ECommerceDBContext(DbContextOptions<ECommerceDBContext> options) : base(options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Seeding Customer data
            modelBuilder.Entity<Customer>().HasData(
                new Customer
                {
                    Id = 1,
                    FirstName = "Pranaya",
                    LastName = "Rout",
                    Email = "pranayarout@example.com",
                    PhoneNumber = "1234567890"
                }
            );

            // Seeding Address data
            modelBuilder.Entity<Address>().HasData(
                new Address { Id = 1, Street = "123 Main St", City = "Jajpur", ZipCode = "755019", CustomerId = 1 }
            );

            // Seeding Product data
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 1, Name = "Laptop", Price = 1500m, Description = "High-performance laptop" },
                new Product { Id = 2, Name = "Mouse", Price = 25m, Description = "Wireless mouse" },
                new Product { Id = 3, Name = "Keyboard", Price = 50m, Description = "Mechanical keyboard" }
            );

            // Seeding Order data
            modelBuilder.Entity<Order>().HasData(
                new Order { Id = 1, OrderDate = DateTime.Now, CustomerId = 1, Amount = 1550m }
            );

            // Seeding OrderItem data
            modelBuilder.Entity<OrderItem>().HasData(
                new OrderItem { Id = 1, ProductId = 1, Quantity = 1, ProductPrice= 1500m, OrderId = 1 },
                new OrderItem { Id = 2, ProductId = 2, Quantity = 2, ProductPrice= 25m, OrderId = 1 }
            );
        }

        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; }
    }
}
Configure the Database Connection

Next, please update the appsettings.json with the database connection string as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ECommerceDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Set Up Dependency Injection and Middleware:

Please modify the Program class as follows.

using AutoMapperComplexMappingDemo.Models;
using Microsoft.EntityFrameworkCore;

namespace AutoMapperComplexMappingDemo
{
    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 Complex Mapping in ASP.NET Core Web API with Examples

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 Complex Mapping in ASP.NET Core Web API

Create an Automapper Mapping Profile

Now, let’s configure AutoMapper to map between the Entities and DTOs. We want to map the Order object with the OrderDTO object to retrieve the Order data. Also, we need to map the OrderCreateDTO to the Order object to create the order. Here, the most important point you must remember is that both Order and OrderDTO contain some complex properties. Similarly, OrderCreateDTO to Order also contains some complex properties.

By default, the complex properties are not automatically mapped even though the complex property names are the same, and we need to map them manually. So, create a folder named MappingProfiles at 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 AutoMapperComplexMappingDemo.DTOs;
using AutoMapperComplexMappingDemo.Models;

namespace AutoMapperComplexMappingDemo.MappingProfiles
{
    public class OrderMappingProfile : Profile
    {
        public OrderMappingProfile()
        {
            // CreateMap: Defines a mapping between two types.
            // ForMember: Customizes the mapping for specific properties.

            // Mapping configuration for fetching order details
            // Maps the Order entity to OrderDTO for data transfer
            CreateMap<Order, OrderDTO>()
                // Map the Order.Id property to OrderDTO.OrderId
                .ForMember(dest => dest.OrderId, opt => opt.MapFrom(src => src.Id))

                // Map OrderDate from DateTime to a formatted string (yyyy-MM-dd)
                .ForMember(dest => dest.OrderDate, opt => opt.MapFrom(src => src.OrderDate.ToString("yyyy-MM-dd")))

                // Concatenate Customer.FirstName and Customer.LastName to form CustomerName in OrderDTO
                .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => $"{src.Customer.FirstName} {src.Customer.LastName}"))

                // Map the Customer.Email property to OrderDTO.CustomerEmail
                .ForMember(dest => dest.CustomerEmail, opt => opt.MapFrom(src => src.Customer.Email))

                // Map the Customer.PhoneNumber property to OrderDTO.CustomerPhoneNumber
                .ForMember(dest => dest.CustomerPhoneNumber, opt => opt.MapFrom(src => src.Customer.PhoneNumber))

                // Map the Customer.Address (complex type) to OrderDTO.Address (DTO for complex types)
                .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Customer.Address))

                // Map the collection of OrderItems to a list of OrderItemDTO in OrderDTO.Items
                .ForMember(dest => dest.Items, opt => opt.MapFrom(src => src.OrderItems));

            // Mapping configuration for order items
            // Maps the OrderItem entity to OrderItemDTO for data transfer
            CreateMap<OrderItem, OrderItemDTO>()
                // Map the Product.Name property to OrderItemDTO.ProductName
                .ForMember(dest => dest.ProductName, opt => opt.MapFrom(src => src.Product.Name))

                // Map the Product.Price property to OrderItemDTO.ProductPrice
                .ForMember(dest => dest.ProductPrice, opt => opt.MapFrom(src => src.Product.Price))

                // Calculate and map the total price (Product.Price * Quantity) to OrderItemDTO.TotalPrice
                .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.Product.Price * src.Quantity));

            // Mapping configuration for customer addresses
            // Maps the Address entity to AddressDTO for data transfer
            CreateMap<Address, AddressDTO>(); //Propery Mapping is not required as both contains the same property names

            // Mapping configuration for creating orders
            // Maps the OrderCreateDTO (data received from client) to the Order entity
            CreateMap<OrderCreateDTO, Order>()
                // Set the OrderDate to the current date and time (DateTime.Now) when creating an order
                .ForMember(dest => dest.OrderDate, opt => opt.MapFrom(src => DateTime.Now))

                // Ignore the Amount property during the mapping, as it will be calculated later
                .ForMember(dest => dest.Amount, opt => opt.Ignore())

                // Map the list of OrderItemCreateDTO to the OrderItems property in the Order entity
                .ForMember(dest => dest.OrderItems, opt => opt.MapFrom(src => src.Items));

            // Mapping configuration for creating order items
            // Maps the OrderItemCreateDTO (data received from client) to the OrderItem entity
            CreateMap<OrderItemCreateDTO, OrderItem>(); //Propery Mapping is not required as both contains the same property names
        }
    }
}
Create the Orders API Controller

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 AutoMapperComplexMappingDemo.DTOs;
using AutoMapperComplexMappingDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace AutoMapperComplexMappingDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class OrderController : ControllerBase
    {
        private readonly ECommerceDBContext _context;
        private readonly IMapper _mapper;

        public OrderController(ECommerceDBContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        //This method is responsible for creating a new order.
        //It accepts an OrderCreateDTO from the client,
        //validates the input data (e.g., checks if the customer exists and verifies the products),
        //maps the DTO to the Order entity,
        //calculates the total order amount based on the selected products and their quantities,
        //and saves the new order to the database.
        // POST: api/order
        [HttpPost]
        public async Task<ActionResult<OrderDTO>> CreateOrder([FromBody] OrderCreateDTO orderCreateDTO)
        {
            // Validate the incoming order data
            if (orderCreateDTO == null)
            {
                return BadRequest("Order data is null.");
            }

            try
            {
                // Check if the customer exists in the database
                var customerExists = await _context.Customers.AnyAsync(c => c.Id == orderCreateDTO.CustomerId);
                if (!customerExists)
                {
                    return NotFound($"Customer with ID {orderCreateDTO.CustomerId} not found.");
                }

                // Validate the products in the order
                // Extract product IDs from the order items
                var productIds = orderCreateDTO.Items.Select(i => i.ProductId).ToList();

                // Retrieve products from the database
                var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync();

                // Validate that all products exist
                if (products.Count != productIds.Count)
                {
                    return BadRequest("One or more products in the order are invalid.");
                }
               
                // Map OrderCreateDTO to Order entity
                var order = _mapper.Map<Order>(orderCreateDTO);

                // Calculate the total amount of the order based on product prices and quantities
                decimal totalAmount = 0;
                foreach (var item in order.OrderItems)
                {
                    var product = products.First(p => p.Id == item.ProductId);
                    
                    // Calculate total price for each order item and accumulate the total order amount
                    totalAmount += product.Price * item.Quantity;
                }
                order.Amount = totalAmount; // Set the calculated amount

                // Add the new order to the database
                _context.Orders.Add(order);
                await _context.SaveChangesAsync(); // Save changes asynchronously

                // Fetch the created order along with related data to return in the response
                var createdOrder = await _context.Orders
                    .Include(o => o.Customer)
                    .ThenInclude(c => c.Address)
                    .Include(o => o.OrderItems)
                    .ThenInclude(oi => oi.Product)
                    .FirstOrDefaultAsync(o => o.Id == order.Id);

                if (createdOrder == null)
                    return StatusCode(500, "An error occurred while creating the order.");

                // Map the created Order entity to OrderDTO
                var orderDTO = _mapper.Map<OrderDTO>(createdOrder);

                // Return the created order data in the response with a status code 201 (Created)
                return CreatedAtAction(nameof(GetOrderById), new { id = order.Id }, orderDTO);
            }
            catch (Exception ex)
            {
                // Return a 500 Internal Server Error with the exception message
                return StatusCode(500, $"An error occurred while processing the request: {ex.Message}");
            }
        }

        //This method is responsible for fetching the details of a specific order by its OrderId.
        //It retrieves the order from the database, including related data such as the customer, their address, and the products ordered.
        //If the order is found, it maps the order to an OrderDTO and returns it to the client.
        // GET: api/order/{id}
        [HttpGet("{id}")]
        public async Task<ActionResult<OrderDTO>> GetOrderById(int id)
        {
            try
            {
                // Fetch the order by its ID, including related data such as Customer, Address, and OrderItems
                var order = await _context.Orders
                    .Include(o => o.Customer)
                    .ThenInclude(c => c.Address)
                    .Include(o => o.OrderItems)
                    .ThenInclude(oi => oi.Product)
                    .FirstOrDefaultAsync(o => o.Id == id);

                if (order == null)
                    return NotFound($"Order with ID {id} not found.");

                // Map the Order entity to OrderDTO
                var orderDTO = _mapper.Map<OrderDTO>(order);

                // Return the order data in the response
                return Ok(orderDTO);
            }
            catch (Exception ex)
            {
                // Return a 500 Internal Server Error with the exception message
                return StatusCode(500, $"An error occurred while fetching the order: {ex.Message}");
            }
        }

        // This method is responsible for fetching all orders made by a specific customer, identified by their CustomerId.
        // It retrieves a list of orders associated with the customer from the database, along with related data
        // such as the customer, their address, and the products ordered in each order.
        // The method returns the list of orders in the form of OrderDTO objects.
        // GET: api/order/customer/{customerId}
        [HttpGet("customer/{customerId}")]
        public async Task<ActionResult<IEnumerable<OrderDTO>>> GetOrdersByCustomerId(int customerId)
        {
            try
            {
                // Fetch all orders associated with a specific customer ID, including related data
                var orders = await _context.Orders
                    .Where(o => o.CustomerId == customerId)
                    .Include(o => o.Customer)
                    .ThenInclude(c => c.Address)
                    .Include(o => o.OrderItems)
                    .ThenInclude(oi => oi.Product)
                    .ToListAsync();

                if (orders == null || orders.Count == 0)
                    return NotFound($"No orders found for customer with ID {customerId}.");

                // Map the list of Order entities to a list of OrderDTO
                var ordersDTO = _mapper.Map<IEnumerable<OrderDTO>>(orders);

                // Return the list of orders in the response
                return Ok(ordersDTO);
            }
            catch (Exception ex)
            {
                // Return a 500 Internal Server Error with the exception message
                return StatusCode(500, $"An error occurred while fetching orders for customer ID {customerId}: {ex.Message}");
            }
        }
    }
}

Now, run the application and try to access the Order information based on the Order ID. You will see the following result.

Automapper Complex Mapping in ASP.NET Core Web API

Note: Always check the Response data. Please verify the mapping and property names if some data is missing. If it is a complex type, specify the complex type mapping. If the property names differ, we also need to specify how to map the properties.

When to Use AutoMapper Complex Type Mapping?

  • Nested or Related Entities: When we need to convert an entity with nested objects or collections to a flat DTO or vice versa.
  • Custom Transformations: If we need specific data conversions, such as concatenating fields or transforming dates.
  • Data Aggregation: For transforming multiple entities into a single DTO, especially in response models with aggregated data.

AutoMapper Complex Mapping helps simplify the transformation of nested and related entities into DTOs better suited for API responses. This is useful when dealing with APIs that need to provide comprehensive data involving relationships or custom transformations. By using AutoMapper, we can maintain clean code, reduce redundancy, and ensure that DTOs remain easy to read and use.

In the next article, I will discuss Mapping Complex Types to Primitive Types using AutoMapper in ASP.NET Core Web API with Examples. In this article, I explain Automapper Complex Mapping in ASP.NET Core Web API with Examples. I hope you enjoy this article, “Automapper Complex Mapping in ASP.NET Core Web API.”

Leave a Reply

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