AutoMapper Advanced Concepts in ASP.NET Core Web API

AutoMapper Advanced Concepts in ASP.NET Core Web API

In this article, I will discuss AutoMapper Advanced Concepts in ASP.NET Core Web API Applications with Examples. Please read our previous article discussing Automapper Conditional Mapping in ASP.NET Core Web API with Examples.

AutoMapper Advanced Concepts in ASP.NET Core Web API

AutoMapper is a widely used library that simplifies the process of transforming one object type into another. This is particularly useful for mapping between our entity classes and Data Transfer Objects (DTOs) in ASP.NET Core Web API Application. In this article, you will learn about several advanced mapping techniques with AutoMapper, including:

  • Fixed Values: Always setting a property to a specific value.
  • Dynamic Values: Calculating a property’s value based on source data.
  • Null Substitution: Providing default values when the source property is null.
  • Ignoring Properties: Excluding certain properties from being mapped.

We will also demonstrate these concepts using a real-time example, a Product Management System that uses Entity Framework Core.

Storing Fixed Values in Destination Property

Sometimes, we want a destination property always to have the same value regardless of the source data. Fixed values are static values that do not change based on the source data. It is useful when a specific field must always have the same value in all mappings. This is useful if you need to store a constant piece of information (e.g., a hard-coded string or status). To set a fixed value, you can use ForMember() and specify a value directly in the mapping configuration. The syntax is given below:

CreateMap<Source, Destination>()
    .ForMember(dest => dest.FixedValueProperty, opt => opt.MapFrom(src => "FixedValue"));

In this mapping, every time an object is transformed, the FixedValueProperty in the destination object is set to “FixedValue”, even if the source has a different value.

Storing Dynamic Values in Destination Property

Dynamic values allow the destination property to be computed from the source data. Dynamic values provide flexibility, as they can vary depending on source values, runtime conditions, or any computed logic. For dynamic values, you can use ForMember() with a lambda expression to calculate or derive the value based on source properties or other conditions. The syntax is given below:

CreateMap<Source, Destination>()
    .ForMember(dest => dest.DynamicValueProperty, opt => opt.MapFrom(src => src.SomeProperty * 2));

With this configuration, the value of DynamicValueProperty in the Destination is computed by multiplying SomeProperty from the Source by 2. This allows the value to vary based on the data in the source.

Ignoring a Property Mapping

There may be cases where we do not want to map certain properties from source to destination entities, either because they are sensitive (like passwords) or do not make sense in the context of the destination object. Ignoring such properties ensures they retain their original values. To ignore a property, use the ForMember() with the Ignore() method. The syntax is given below:

CreateMap<Source, Destination>()
    .ForMember(dest => dest.PropertyToIgnore, opt => opt.Ignore());

This configuration tells AutoMapper to leave PropertyToIgnore unchanged during the mapping process.

Null Substitution

Null substitution is a helpful feature that provides a default value when a source property is null. This ensures that the destination object does not receive any null values that might lead to issues later on. To handle null values, use ForMember() with NullSubstitute() method. The syntax is given below:

CreateMap<Source, Destination>()
    .ForMember(dest => dest.NullSubstitutedProperty, opt => opt.NullSubstitute("Default Value"));

If the source property for NullSubstitutedProperty is null, AutoMapper assigns it the value “Default Value”.

Real-Time Example: Product Management System

Let’s see how these advanced automapper concepts come together in a Product Management System built with ASP.NET Core Web API, Entity Framework Core, and AutoMapper. Our system has the following entities and DTOs:

  • Product: Stores product details.
  • Category: Stores category details.
  • ProductDTO: Used for retrieving product data.
  • ProductCreateDTO: Used for creating a new product.

We will map these objects using AutoMapper to apply the four concepts above—fixed values, dynamic values, ignored properties, and null substitution.

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.

Category Model:

Create a class file named Category.cs within the Models folder and add the following code. The Category Model Represents the Category entity, containing properties like CategoryId and Name.

using System.ComponentModel.DataAnnotations;

namespace AutomapperDemo.Models
{
    public class Category
    {
        public int CategoryId { get; set; }

        [Required(ErrorMessage = "Category Name is required.")]
        [MaxLength(50, ErrorMessage = "Category Name can't exceed 50 characters.")]
        public string Name { get; set; }

        public List<Product> Products { get; set; }
    }
}
Product Model

Create a class file named Product.cs within the Models folder and add the following code. The Product Model Represents the Product entity, containing properties like ID, Name, Price, Stock, and CategoryId.

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

namespace AutomapperDemo.Models
{
    public class Product
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Product Name is required.")]
        [MaxLength(100, ErrorMessage = "Product Name can't exceed 100 characters.")]
        public string Name { get; set; }

        [MaxLength(500, ErrorMessage = "Description can't exceed 500 characters.")]
        public string? Description { get; set; } // Nullable for null substitution

        [Range(1, double.MaxValue, ErrorMessage = "Price must be greater than zero.")]
        [Column(TypeName = "decimal(18,2)")]
        public decimal Price { get; set; }

        public DateTime CreatedDate { get; set; } // Fixed value
        public string CreatedBy { get; set; } // Fixed value "Admin"
        public int Stock { get; set; }
        public string? ProductType { get; set; } // Dynamic (e.g., Premium or Standard)
        public bool IsAvailable { get; set; } // Dynamic based on Stock quantity
        public int? CategoryId { get; set; }
        public Category? Category { get; set; } // Navigation property
    }
}
ProductCreateDTO Model

Create a class file named ProductCreateDTO.cs within the DTOs folder and add the following code. The ProductCreateDTO is a data transfer object used to create a new product. It includes fields like Name, Description, Price, Stock, and CategoryId.

using System.ComponentModel.DataAnnotations;

namespace AutomapperDemo.DTOs
{
    public class ProductCreateDTO
    {
        [Required(ErrorMessage = "Product Name is required.")]
        [MaxLength(100, ErrorMessage = "Product Name can't exceed 100 characters.")]
        public string Name { get; set; }

        [MaxLength(500, ErrorMessage = "Description can't exceed 500 characters.")]
        public string? Description { get; set; } // Nullable for null substitution

        [Range(1, double.MaxValue, ErrorMessage = "Price must be greater than zero.")]
        public decimal Price { get; set; }
        public int Stock { get; set; }
        public int? CategoryId { get; set; } // Category assignment
    }
}
ProductDTO Model

Create a class file named ProductDTO.cs within the DTOs folder and copy and paste the following code. The ProductDTO is a Data Transfer Object used to retrieve product details. It includes properties such as Name, Price, ProductType, and CategoryName, which are helpful for the front-end display. This helps abstract the internal details of the product model.

namespace AutomapperDemo.DTOs
{
    public class ProductDTO
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string? Description { get; set; }
        public decimal Price { get; set; }
        public string ProductType { get; set; }
        public bool IsAvailable { get; set; }
        public string CategoryName { get; set; }
        public string CreatedDate { get; set; }
    }
}
Create a DbContext Class

First, create a folder called Data, and then inside the Data folder, create a class file named ProductDbContext.cs. then copy and paste the following code. The database context class manages the Product and Category entities in the database. It provides DbSet properties for each entity and handles initial seed data for testing purposes.

using AutomapperDemo.Models;
using Microsoft.EntityFrameworkCore;

namespace AutomapperDemo.Data
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext(DbContextOptions<ProductDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Seed Categories
            modelBuilder.Entity<Category>().HasData(
                new Category { CategoryId = 1, Name = "Electronics" },
                new Category { CategoryId = 2, Name = "Furniture" }
            );

            // Seed Products
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 1, Name = "Smartphone", Description = "High-end phone", Price = 800, CreatedDate = DateTime.Parse("2025-01-01"), CreatedBy = "Admin", Stock = 10, ProductType = "Premium", IsAvailable = true, CategoryId = 1 },
                new Product { Id = 2, Name = "Desk Lamp", Price = 50, CreatedDate = DateTime.Parse("2025-01-01"), CreatedBy = "Admin", Stock = 10, ProductType = "Premium", IsAvailable = true, CategoryId = 2 },
                new Product { Id = 3, Name = "Tablet", Description = "Compact tablet", Price = 300, CreatedDate = DateTime.Parse("2025-01-01"), CreatedBy = "Out-of-Stock", Stock = 0, ProductType = "Standard", IsAvailable = true, CategoryId = 1 },
                new Product { Id = 4, Name = "Chair", Price = 150, CreatedDate = DateTime.Parse("2025-01-01"), CreatedBy = "Admin", Stock = 0, ProductType = "Standard", IsAvailable = false, CategoryId = 2 }
            );
        }

        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { 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": {
    "ProductsDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ProductsDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Set Up AutoMapper, Dependency Injection and Middleware:

Please modify the Program class as follows. The Program class configures essential application services, such as database connections, AutoMapper registration, Swagger for API documentation, and other middleware settings for handling requests.

using AutomapperDemo.Data;
using Microsoft.EntityFrameworkCore;

namespace AutomapperDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Register AutoMapper and the mapping profile
            builder.Services.AddAutoMapper(typeof(Program).Assembly);

            // 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 ProductDbContext with dependency injection
            builder.Services.AddDbContext<ProductDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("ProductsDBConnection")));

            // 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 ProductsDB database and required Products table:

Advanced AutoMapper Concepts in ASP.NET Core Web API

Once you execute the above commands and verify the database, you should see the ProductsDB database with the required tables, as shown in the image below.

Advanced AutoMapper Concepts in ASP.NET Core Web API

AutoMapper Configuration

First, create a folder named MappingProfiles, and inside this folder, create a class file named ProductMappingProfile.cs and add the following code. The following class configures mappings between Product and its DTOs (ProductCreateDTO and ProductGetDTO) using AutoMapper, applying transformation rules, default values, and conditional mappings. It ensures that properties are correctly transferred between entities and DTOs during different operations (create, get).

using AutoMapper;
using AutomapperDemo.DTOs;
using AutomapperDemo.Models;

namespace AutomapperDemo.MappingProfiles
{
    public class ProductMappingProfile : Profile
    {
        public ProductMappingProfile()
        {
            // Mapping from ProductCreateDTO to Product (for creation)
            CreateMap<ProductCreateDTO, Product>()
                .ForMember(dest => dest.CreatedDate, opt => opt.MapFrom(src => DateTime.UtcNow)) // Fixed value for CreatedDate
                .ForMember(dest => dest.CreatedBy, opt => opt.MapFrom(src => "Admin")) // Fixed value for CreatedBy
                .ForMember(dest => dest.ProductType, opt => opt.MapFrom(src => src.Price > 500 ? "Premium" : "Standard")) // Dynamic value based on Price
                .ForMember(dest => dest.IsAvailable, opt => opt.MapFrom(src => src.Stock > 0)) // Dynamic value based on Stock
                .ForMember(dest => dest.Description, opt => opt.NullSubstitute("No Description Available")) // Null substitution for Description
                .ForMember(dest => dest.Category, opt => opt.Ignore()); // Ignore complex type mapping for Category

            // Mapping from Product to ProductGetDTO (for retrieving product details)
            CreateMap<Product, ProductDTO>()
                .ForMember(dest => dest.CategoryName, opt => opt.MapFrom(src => src.Category != null ? src.Category.Name : "Uncategorized"))
                .ForMember(dest => dest.CreatedDate, opt => opt.MapFrom(src => src.CreatedDate.ToString("MM-dd-yyyy")))
                .ForMember(dest => dest.Description, opt => opt.NullSubstitute("No Description Available")) // Null substitution for Description
                .ForMember(dest => dest.IsAvailable, opt => opt.MapFrom(src => src.Stock > 0)); // Availability based on Stock
        }
    }
}
Using Automapper in a Controller

Create an API Empty Controller named ProductsController within the Controllers folder and copy and paste the following code. The following controller manages HTTP requests for Product operations, including retrieving all products, retrieving a product by ID, and adding a new product. It uses ProductDbContext for database operations and AutoMapper to map between entities and DTOs.

using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using AutomapperDemo.Models;
using AutomapperDemo.Data;
using AutomapperDemo.DTOs;

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

        public ProductsController(ProductDbContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        // GET: api/Products
        [HttpGet]
        public async Task<ActionResult<List<ProductDTO>>> GetAllProducts()
        {
            try
            {
                var products = await _context.Products
                    .AsNoTracking()
                    .Include(p => p.Category)
                    .ToListAsync();

                var productsDto = _mapper.Map<List<ProductDTO>>(products);
                return Ok(productsDto);
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"Internal server error: {ex.Message}");
            }
        }

        // GET: api/Products/5
        [HttpGet("{id}")]
        public async Task<ActionResult<ProductDTO>> GetProductById(int id)
        {
            try
            {
                var product = await _context.Products
                    .AsNoTracking()
                    .Include(p => p.Category)
                    .FirstOrDefaultAsync(p => p.Id == id);

                if (product == null)
                    return NotFound("Product not found");

                var productDto = _mapper.Map<ProductDTO>(product);
                return Ok(productDto);
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"Internal server error: {ex.Message}");
            }
        }

        // POST: api/Products/AddProduct
        [HttpPost("AddProduct")]
        public async Task<ActionResult<ProductDTO>> AddProduct([FromBody] ProductCreateDTO productCreateDto)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            try
            {
                var product = _mapper.Map<Product>(productCreateDto);
                await _context.Products.AddAsync(product);
                await _context.SaveChangesAsync();

                // Retrieve and assign Category (if applicable)
                product.Category = await _context.Categories.FirstOrDefaultAsync(ctg => ctg.CategoryId == product.CategoryId);
                var productDto = _mapper.Map<ProductDTO>(product);

                return Ok(productDto);
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"Internal server error: {ex.Message}");
            }
        }
    }
}

Run your application and test the endpoints using Swagger or any API testing tool to verify that all mapping functionalities work as expected. We have built a Product and Category Management System demonstrating advanced AutoMapper features. Here, we learned how to:

  • Set fixed values during mapping (e.g., setting CreatedDate and CreatedBy).
  • Calculate dynamic values based on conditions (e.g., determining ProductType and IsAvailable).
  • Substitute null values with defaults (e.g., providing a default description).
  • Ignore complex or unnecessary mappings (e.g., excluding the Category property during product creation)

By combining fixed values, dynamic values, ignored properties, and null substitution with AutoMapper, we can handle various mapping scenarios in our ASP.NET Core Web API Applications. These techniques help reduce repetitive code, maintain consistent transformation logic, and ensure our data is mapped accurately between entities and DTOs.

In the next article, I will discuss Automapper with One Real-time ECommerce Application in ASP.NET Core Web API with Examples. In this article, I explain AutoMapper Advanced Concepts in ASP.NET Core Web API with Examples. I hope you enjoy this article, “AutoMapper Advanced Concepts in ASP.NET Core Web API.”

Leave a Reply

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