Back to: ASP.NET Core Web API Tutorials
Automapper Conditional Mapping in ASP.NET Core Web API
In this article, I will discuss How to Implement Automapper Conditional Mapping in ASP.NET Core Web API Application with Examples. Please read our previous article discussing Automapper Reverse Mapping in ASP.NET Core Web API with Examples.
AutoMapper Conditional Mapping in ASP.NET Core Web API
In modern ASP.NET Core Web API projects, AutoMapper is a widely used library that helps map our domain (EF Core) models and Data Transfer Objects (DTOs). Often, we need to control when and how individual members get mapped. AutoMapper provides two built-in mechanisms for this: Pre-Conditions and Conditions. Although there isn’t a dedicated Post-Condition method, similar functionality can be achieved via the AfterMap method. In this article, I will discuss the following pointers:
- What are AutoMapper Pre-Condition, Condition, and (simulated) Post-Condition?
- When to use each in an ASP.NET Core Web API?
- Their syntax and execution order.
- A real-time example using ASP.NET Core Web API and EF Core Code First approach.
- The differences between AutoMapper Pre-Condition, Condition, and Post-Condition.
What Are AutoMapper Pre-Condition, Condition, and Post-Condition?
AutoMapper Pre-Condition:
A Pre-Condition is a predicate evaluated before the source member is even read (i.e., before reading the value by automapper). Its purpose is to decide whether or not the mapping for that particular member should occur at all. If the pre-condition returns false, AutoMapper skips reading the source member’s value, bypassing the mapping for that member.
AutoMapper Pre-Condition Syntax:
.ForMember(dest => dest.SomeProperty, opt => { // Check a condition before reading the source value. opt.PreCondition(src => src.AnotherProperty != null); opt.MapFrom(src => src.AnotherProperty); });
Pre-Condition Evaluation:
AutoMapper first checks the PreCondition predicate.
- If false: The mapping for that member is skipped entirely. The source value isn’t even read.
- If true (or not set): AutoMapper proceeds to the next step, i.e., Source Value Resolution (or auto mapper reading the source value).
When to Use: You need to use AutoMapper Pre-Condition when you want to avoid the cost of retrieving or processing a value that you know won’t be mapped (for example, if the source object does not satisfy a specific business rule).
AutoMapper Condition:
A Condition is a predicate evaluated after the source member has been resolved (i.e., its value is read by the automapper) but before the destination member is assigned. It determines whether the resolved source value should be assigned to the destination property. If the condition is evaluated as false, AutoMapper assigns the default value to the destination member (or leaves it unchanged, depending on your configuration).
AutoMapper Condition Syntax:
.ForMember(dest => dest.SomeProperty, opt => { // Check a condition after reading the source value. opt.Condition((src, dest, srcMember, destMember) => srcMember is int value && value > 0); opt.MapFrom(src => src.SomeValue); });
Source Value Resolution and Condition Evaluation:
AutoMapper retrieves the source member’s value. It then evaluates the Condition predicate using the resolved source value.
- If false: The mapping is skipped.
- If true: The value is mapped to the destination.
When to Use: We need to use AutoMapper Condition when the decision to map should be based on the value of the source property. For instance, you might want to map a property only if its value is valid (e.g., a positive number, a non-empty string, etc.).
Post-Condition (Simulated with AfterMap):
AutoMapper does not provide a built-in .PostCondition() extension like .PreCondition() or .Condition(), but you can simulate it using the AfterMap method to enforce additional rules after the mapping has been performed.
Simulating AutoMapper Post-Condition with AfterMap Syntax:
.AfterMap((src, dest) => { // Execute logic after all mappings are completed. if (dest.SomeProperty != null && dest.SomeProperty.Length > 50) { dest.SomeProperty = dest.SomeProperty.Substring(0, 50); } });
AfterMap Execution (Simulated Post-Condition):
- Once all member mappings are complete, any configured AfterMap actions run.
- This is your opportunity to perform post-mapping modifications, validations, or logging.
When to Use: We need to use AutoMapper Post-Condition (AfterMap) when we need to validate the destination object after all the member mappings have occurred. This is useful if you need to run business rules that depend on the fully mapped object.
The order is therefore: Pre-Condition → Read Source Value → Condition → Map Value → AfterMap (Post-Condition)
Real-Time Example to Understand AutoMapper Conditional Mapping in ASP.NET Core Web API
Let us build a simple e‑commerce application using ASP.NET Core Web API, EF Core Code First Approach, and AutoMapper. In this example, we have four entities: Customer, Product, Order, and OrderIte,m and we will use AutoMapper’s mapping features to enforce our business rules. In our application:
- Customer: A customer can be active or inactive. It has a flag IsActive indicating if they are active.
- Product: Products can be available or unavailable. It has a flag, IsAvailable, indicating if the product is still in stock.
- Order: Each order belongs to a customer and can have many items. It may or may not be shipped.
- OrderItem: References one product with a Quantity, price, and optional Discount.
We will demonstrate:
- Pre-Condition: Skip mapping of certain properties if a customer is not active or if a product is not available.
- Condition: Only map certain properties if a rule is met. For example, shipping cost is mapped only if the order is shipped, and subtotal (calculated as (Quantity * UnitPrice) – Discount) is calculated only if the quantity is greater than zero.
- AfterMap (Post-Condition): Perform extra logic after mapping (e.g., recalculate order total from order items, mark a customer as VIP if total spending is above a threshold). Adjust the shipping cost if needed (for example, reverting it to zero if it ends up negative).
Setting Up the Project and Installing Entity Framework Core
First, create a new ASP.NET Core Web API Application named AutomapperDemo. Once you create the project, please install the Entity Framework Core and Automapper Packages by executing the following commands in Visual Studio 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.
Customer Entity
Create a class file named Customer.cs within the Models folder and add the following code.
namespace AutomapperDemo.Models { public class Customer { public int Id { get; set; } public string Name { get; set; } public bool IsActive { get; set; } // Navigation public ICollection<Order> Orders { get; set; } = new List<Order>(); } }
Product Entity
Create a class file named Product.cs within the Models folder and add the following code.
using System.ComponentModel.DataAnnotations.Schema; namespace AutomapperDemo.Models { public class Product { public int Id { get; set; } public string Name { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal Price { get; set; } public bool IsAvailable { get; set; } } }
Order Entity
Create a class file named Order.cs within the Models folder and add the following code.
using System.ComponentModel.DataAnnotations.Schema; namespace AutomapperDemo.Models { public class Order { public int Id { get; set; } public DateTime OrderDate { get; set; } public bool IsShipped { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal ShippingCost { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal OrderTotal { get; set; } // Relationship public int CustomerId { get; set; } public Customer Customer { get; set; } = default!; public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>(); } }
OrderItem Entity
Create a class file named OrderItem.cs within the Models folder and add the following code.
using System.ComponentModel.DataAnnotations.Schema; namespace AutomapperDemo.Models { public class OrderItem { public int Id { get; set; } public int Quantity { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal UnitPrice { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal Discount { get; set; } // Navigation to Product public int ProductId { get; set; } public Product Product { get; set; } // Navigation to Order public int OrderId { get; set; } public Order Order { get; set; } // Optional: Computed SubTotal [Column(TypeName = "decimal(18,2)")] public decimal SubTotal => Quantity * UnitPrice - Discount; } }
Relationship Summary
- Customer ↔ Order: One-to-many (a customer has many orders).
- Order ↔ OrderItem: One-to-many (an order has many items).
- OrderItem → Product: Many-to-one (each item references one product).
DTO Classes
We create DTO classes to avoid exposing our EF entities directly. These DTOs shape the data that travels between the client and server.
CustomerDTO
Create a class file named CustomerDTO.cs within the DTOs folder, then add the following code.
namespace AutomapperDemo.DTOs { public class CustomerDTO { public int Id { get; set; } public string? Name { get; set; } // Possibly we include the Orders for demonstration public List<OrderDTO> Orders { get; set; } = new(); } }
ProductDTO
Create a class file named ProductDTO.cs within the DTOs folder, then add the following code.
namespace AutomapperDemo.DTOs { public class ProductDTO { public int Id { get; set; } public string? Name { get; set; } public decimal Price { get; set; } } }
OrderItemDTO
Create a class file named OrderItemDTO.cs within the DTOs folder, then add the following code.
namespace AutomapperDemo.DTOs { public class OrderItemDTO { public int Id { get; set; } public string? ProductName { get; set; } // We will map from Product.Name public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal Discount { get; set; } // SubTotal might be computed or mapped public decimal SubTotal { get; set; } } }
OrderDTO
Create a class file named OrderDTO.cs within the DTOs folder, then add the following code.
namespace AutomapperDemo.DTOs { public class OrderDTO { public int Id { get; set; } public DateTime OrderDate { get; set; } public bool IsShipped { get; set; } public decimal ShippingCost { get; set; } // Computed or re-verified after mapping public decimal OrderTotal { get; set; } public List<OrderItemDTO> OrderItems { get; set; } } }
Create a DbContext Class
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. Here, we have also added some initial seed data for testing purposes.
using AutomapperDemo.Models; namespace AutomapperDemo.Data { // Data/ECommerceDbContext.cs using Microsoft.EntityFrameworkCore; public class ECommerceDbContext : DbContext { public ECommerceDbContext(DbContextOptions<ECommerceDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // SEEDING DUMMY DATA // 1) Customers modelBuilder.Entity<Customer>().HasData( new Customer { Id = 1, Name = "John Doe", IsActive = true }, new Customer { Id = 2, Name = "Alice Wonderland", IsActive = false } ); // 2) Products modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "Gaming Laptop", Price = 1500m, IsAvailable = true }, new Product { Id = 2, Name = "Headphones", Price = 200m, IsAvailable = true }, new Product { Id = 3, Name = "Old Monitor", Price = 300m, IsAvailable = false } ); // 3) Orders modelBuilder.Entity<Order>().HasData( new Order { Id = 1, CustomerId = 1, OrderDate = new DateTime(2024, 01, 10), IsShipped = true, ShippingCost = 25m, OrderTotal = 0m }, new Order { Id = 2, CustomerId = 1, OrderDate = new DateTime(2024, 01, 15), IsShipped = false, ShippingCost = 0m, OrderTotal = 0m }, new Order { Id = 3, CustomerId = 2, OrderDate = new DateTime(2024, 02, 01), IsShipped = true, ShippingCost = 15m, OrderTotal = 0m } ); // 4) OrderItems modelBuilder.Entity<OrderItem>().HasData( new OrderItem { Id = 1, OrderId = 1, ProductId = 1, Quantity = 1, UnitPrice = 1500m, Discount = 0m }, new OrderItem { Id = 2, OrderId = 1, ProductId = 2, Quantity = 2, UnitPrice = 200m, Discount = 0m }, new OrderItem { Id = 3, OrderId = 2, ProductId = 1, Quantity = 1, UnitPrice = 1500m, Discount = 100m }, new OrderItem { Id = 4, OrderId = 3, ProductId = 3, Quantity = 1, UnitPrice = 300m, Discount = 50m } ); } public DbSet<Customer> Customers { get; set; } public DbSet<Product> Products { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderItem> OrderItems { 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=MyStoreDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Set Up AutoMapper, DbContext, Dependency Injection and Middleware:
Please modify the Program class as follows.
using AutomapperDemo.Data; using Microsoft.EntityFrameworkCore; namespace AutomapperDemo { 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; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Register DbContext builder.Services.AddDbContext<ECommerceDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("ECommerceDBConnection"))); // Register AutoMapper (scans the assembly for Profile) // This scans the assembly containing Program class for any classes inheriting Profile // and registers them automatically. builder.Services.AddAutoMapper(typeof(Program).Assembly); 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 MyStoreDB database and required tables:
Once you execute the above commands and verify the database, you should see the MyStoreDB database with the required tables, as shown in the image below.
AutoMapper Configuration with Conditional Mapping
First, create a folder named MappingProfiles in the project root directory, and inside this folder, create a class file named CustomerOrderMappingProfile.cs and add the following code:
using AutomapperDemo.DTOs; using AutomapperDemo.Models; using AutoMapper; namespace AutomapperDemo.MappingProfiles { public class CustomerOrderMappingProfile : Profile { public CustomerOrderMappingProfile() { // 1) Customer -> CustomerDto CreateMap<Customer, CustomerDTO>() // Pre-Condition: Only map Orders if customer is active .ForMember( dest => dest.Orders, opt => { opt.PreCondition(src => src.IsActive); // If not active, the Orders won't be mapped and remain empty in the DTO opt.MapFrom(src => src.Orders); } ) // AfterMap: If total spending across all orders > 1000, append (VIP) to name .AfterMap((src, dest) => { decimal totalSpending = dest.Orders.Sum(o => o.OrderTotal); if (totalSpending > 1000) { dest.Name += " (VIP)"; } }); // 2) Product -> ProductDto CreateMap<Product, ProductDTO>(); // 3) OrderItem -> OrderItemDto CreateMap<OrderItem, OrderItemDTO>() .ForMember( dest => dest.ProductName, opt => { // Pre-Condition: Only map ProductName if product is available opt.PreCondition(src => src.Product.IsAvailable); // If not available, dest.ProductName remains null opt.MapFrom(src => src.Product.Name); } ) .ForMember( dest => dest.SubTotal, opt => { // Condition: Only map SubTotal if Quantity > 0 opt.Condition((src, dest, srcValue, destValue) => src.Quantity > 0); // If condition is true, map from entity's computed SubTotal opt.MapFrom(src => src.SubTotal); } ); // 4) Order -> OrderDTO CreateMap<Order, OrderDTO>() .ForMember( dest => dest.ShippingCost, opt => { // Condition: Only map shipping cost if order is shipped opt.Condition((src, dest, srcValue, destValue) => src.IsShipped); // If not shipped, ShippingCost remains default (0) opt.MapFrom(src => src.ShippingCost); } ) // AfterMap: Recalculate OrderTotal from the item subtotals, also fix negative shipping cost if needed .AfterMap((src, dest) => { // If shipping cost is negative for some reason, revert to 0 if (dest.ShippingCost < 0) { dest.ShippingCost = 0; } // Recalculate from item subtotals decimal itemsTotal = dest.OrderItems.Sum(i => i.SubTotal); dest.OrderTotal = itemsTotal < 0 ? 0 : itemsTotal; }); } } }
Pre-Condition Usage
- Customer → CustomerDTO: We only map the Orders collection if the Customer is IsActive. Otherwise, the Orders in the DTO are empty.
- OrderItem → OrderItemDTO: We only map ProductName if the Product is IsAvailable. Otherwise, ProductName remains null.
Condition Usage
- Order → OrderDTO: We only assign ShippingCost if the order is marked as shipped (IsShipped). If false, the destination ShippingCost remains at its default value (e.g., 0m).
- OrderItem → OrderItemDTO: We only assign SubTotal if Quantity > 0. Otherwise, the SubTotal remains 0.
AfterMap (Post-Condition) Usage
- Customer: After mapping, we sum all order totals. If the total is above 1000, we append (VIP) to the customer’s name.
- Order: We also fix the negative shipping cost to 0 if it ends below zero. We also recalculate the OrderTotal from the sum of the SubTotal values in the mapped OrderItems. If that sum is negative (unlikely, but possible if discounts are mishandled), we set it to 0.
Using Automapper Conditional Mapping in a Controller
Implement a controller that uses AutoMapper’s conditional mapping capabilities. So, create a controller named CustomersController within the Controllers folder and add the following code:
using AutoMapper; using AutomapperDemo.Data; using AutomapperDemo.DTOs; using AutomapperDemo.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace AutomapperDemo.Controllers { [ApiController] [Route("api/[controller]")] public class CustomersController : ControllerBase { private readonly ECommerceDbContext _context; private readonly IMapper _mapper; public CustomersController(ECommerceDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } // GET: api/customers [HttpGet("GetCustomers")] public async Task<ActionResult<IEnumerable<CustomerDTO>>> GetCustomers() { // Include Orders -> OrderItems -> Product var customers = await _context.Customers .Include(c => c.Orders) .ThenInclude(o => o.OrderItems) .ThenInclude(i => i.Product) .ToListAsync(); var customerDtos = _mapper.Map<IEnumerable<CustomerDTO>>(customers); return Ok(customerDtos); } // POST: api/customers [HttpGet("GetCustomerById/{id}")] public async Task<ActionResult<CustomerDTO>> GetCustomerById(int Id) { // Include Orders -> OrderItems -> Product var customer = await _context.Customers .Include(c => c.Orders) .ThenInclude(o => o.OrderItems) .ThenInclude(i => i.Product) .FirstOrDefaultAsync(cust => cust.Id == Id); var customerDtos = _mapper.Map<CustomerDTO>(customer); return Ok(customerDtos); } // POST: api/customers [HttpPost] public async Task<ActionResult<CustomerDTO>> CreateCustomer(CustomerDTO customerDto) { var customer = new Customer { Name = customerDto.Name ?? "New Customer", IsActive = true // default to active }; _context.Customers.Add(customer); await _context.SaveChangesAsync(); var createdDto = _mapper.Map<CustomerDTO>(customer); return Ok(createdDto); } } }
Test the Endpoint at GET https://localhost:<port>/api/customers/GetCustomers.
- Customer #1 (John Doe) is active and should have two orders in the JSON.
- Customer #2 (Alice Wonderland) is inactive, so the Orders collection should be empty.
- Check that ProductName is only populated for available products (Product #3 is not available, so we skip the mapping).
- Shipping Cost is only mapped if IsShipped == true.
- SubTotal is only assigned if Quantity > 0.
- AfterMap logic ensures that the shipping cost isn’t negative and recalculates the total order for the items.
Differences Between Automapper Pre-Condition, Condition, and Post-Condition in ASP.NET Core Web API
Using AutoMapper’s Pre-Condition and Condition methods, we can control the mapping process at the member level in our ASP.NET Core Web API Application. Although there isn’t a dedicated Post-Condition method, the AfterMap method allows us to perform post-mapping logic effectively. These features are especially useful in real-world applications such as those using the EF Core Code First approach, where conditional mapping helps ensure that our DTOs accurately represent our domain models only when valid, saving processing time and avoiding potential errors.
In the next article, I will discuss the Ignore Property Mapping using Automapper in ASP.NET Core Web API with Examples. In this article, I explain How to Implement Conditional Mapping using AutoMapper in ASP.NET Core Web API with Examples. I hope you enjoy this article, “Conditional Mapping using AutoMapper in ASP.NET Core Web API.”