Automapper Reverse Mapping in ASP.NET Core Web API

AutoMapper Reverse Mapping in ASP.NET Core Web API

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

AutoMapper Reverse Mapping in ASP.NET Core Web API

AutoMapper is a widely used object-to-object mapping library in .NET that helps streamline the process of mapping properties between different objects, such as entity models and Data Transfer Objects (DTOs). It significantly reduces the amount of repetitive code needed for manual property assignments. Reverse mapping is a feature in AutoMapper that enables two-way mapping between objects without requiring separate configurations for each direction. This is extremely useful in ASP.NET Core Web API applications where we often need to:

  • Map our EF Core entities to DTOs when sending data to the client.
  • When creating or updating data, map incoming DTOs back to our EF Core entities.

When we create a mapping between two classes (e.g., an Entity and a DTO), we typically define the mapping in one direction: CreateMap<Employee, EmployeeDTO>(); If we want to map back (i.e., from EmployeeDTO to Employee), we have two options:

  • Option1: Create another mapping configuration explicitly: CreateMap<EmployeeDTO, Employee>();
  • Option2: Use AutoMapper’s Reverse Mapping feature by appending .ReverseMap() to your mapping configuration: CreateMap<Employee, EmployeeDTO>().ReverseMap();

The ReverseMap() method automatically creates the reverse mapping for us. This simplifies our configuration, reduces code duplication, and ensures that both mappings remain consistent.

Example to Understand AutoMapper Reverse Mapping in ASP.NET Core Web API:

Let’s understand the AutoMapper Reverse Mapping in ASP.NET Core Web API with one real-time example. We will create a simple employee management system with the following classes:

  • Address Model: Represents the address details of an employee.
  • Employee Model: Represents the employee and includes personal information.
  • EmployeeDTO: Combining properties from the Employee and Address models for client-server data transfer.
Setting Up the Project and Installing Entity Framework Core and Automapper

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

Address Model

Create a class file named Address.cs within the Models folder and add the following code. This class represents the address details of an employee.

namespace AutomapperDemo.Models
{
    public class Address
    {
        public int Id { get; set; } // Unique identifier for the address
        public string City { get; set; } // City of the address
        public string State { get; set; } // State of the address
        public string Country { get; set; } // Country of the address
        public int EmployeeId { get; set; } // Foreign key to Employee
        public Employee Employee { get; set; } // Navigation property for Employee
    }
}
Employee Model:

Create a class file named Employee.cs within the Models folder and add the following code. This class represents the employee entity with personal and address details.

namespace AutomapperDemo.Models
{
    public class Employee
    {
        public int Id { get; set; } // Primary key for the employee

        public string FirstName { get; set; } // Employee's first name
        public string LastName { get; set; } // Employee's last name
        public string Email { get; set; } // Employee's email address
        public string Gender { get; set; } // Employee's gender

        public Address Address { get; set; } // Navigation property to the Address entity
    }
}
EmployeeDTO Model

Create a class file named EmployeeDTO.cs within the DTOs folder, then add the following code. This Data Transfer Object combines properties from both Employee and Address for client-server communication.

namespace AutomapperDemo.DTOs
{
    public class EmployeeDTO
    {
        public int EmployeeId { get; set; } // Mapped from Employee.Id
        public string FullName { get; set; } // Combined first and last names
        public string Email { get; set; }    // Employee's email address
        public string Gender { get; set; }   // Employee's gender

        // Address properties extracted from the Address model
        public string City { get; set; }
        public string State { get; set; }
        public string Country { get; set; }
    }
}
Create a DbContext Class

First, create a folder called Data, and then inside the Data folder, create a class file named EmployeeDBContext.cs, then copy and paste the following code. Here, we have also added some initial seed data for testing purposes.

using AutomapperDemo.Models;
using Microsoft.EntityFrameworkCore;

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

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Seed Employee data
            modelBuilder.Entity<Employee>().HasData(
                new Employee { Id = 1, FirstName = "Anurag", LastName = "Mohanty", Email = "anurag.mohanty@example.com", Gender = "Male" },
                new Employee { Id = 2, FirstName = "Pranaya", LastName = "Rout", Email = "pranaya.rout@example.com", Gender = "Male" }
            );

            // Seed Address data linked to employees
            modelBuilder.Entity<Address>().HasData(
                new Address { Id = 1, City = "Bhubaneswar", State = "Odisha", Country = "India", EmployeeId = 1 },
                new Address { Id = 2, City = "Mumbai", State = "Maharashtra", Country = "India", EmployeeId = 2 }
            );
        }

        // Define DbSets for Employee and Address
        public DbSet<Employee> Employees { 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": {
    "EmployeeDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=EmployeeDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Set Up AutoMapper, DbContext, 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);

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

            // 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 EmployeeDB database and required Employees and Addresses tables:

AutoMapper Reverse Mapping in ASP.NET Core Web API with Examples

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

Configure AutoMapper with Reverse Mapping in ASP.NET Core Web API

Configure AutoMapper with Reverse Mapping in ASP.NET Core Web API

Create a mapping profile that defines the transformation rules between Employee and EmployeeDTO and vice versa. This allows us to map an Employee object to EmployeeDTO and vice versa. First, create a folder named MappingProfiles, and inside this folder, create a class file named EmployeeMappingProfile.cs and add the following code:

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

namespace AutomapperDemo.MappingProfiles
{
    public class EmployeeMappingProfile : Profile
    {
        public EmployeeMappingProfile()
        {
            // Define two-way mapping between Employee and EmployeeDTO.
            // This maps Employee.Id to EmployeeDTO.EmployeeId, combines FirstName and LastName into FullName,
            // and maps the Address properties to their corresponding DTO fields.
            CreateMap<Employee, EmployeeDTO>()
                .ForMember(dest => dest.EmployeeId, opt => opt.MapFrom(src => src.Id))
                .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
                .ForMember(dest => dest.City, opt => opt.MapFrom(src => src.Address.City))
                .ForMember(dest => dest.State, opt => opt.MapFrom(src => src.Address.State))
                .ForMember(dest => dest.Country, opt => opt.MapFrom(src => src.Address.Country))
                
                // Enable reverse mapping: this automatically creates the mapping from EmployeeDTO to Employee
                .ReverseMap()

                // When mapping back, split the FullName into FirstName and LastName.
                .ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => GetFirstName(src.FullName)))
                .ForMember(dest => dest.LastName, opt => opt.MapFrom(src => GetLastName(src.FullName)));
        }

        // Extracts the first name from FullName.
        private static string GetFirstName(string fullName)
        {
            if (string.IsNullOrWhiteSpace(fullName))
                return string.Empty;
            var names = fullName.Split(' ');
            return names.FirstOrDefault() ?? string.Empty;
        }

        // Extracts the last name from FullName.
        private static string GetLastName(string fullName)
        {
            if (string.IsNullOrWhiteSpace(fullName))
                return string.Empty;
            var names = fullName.Split(' ');

            // If there is more than one word, combine the rest as the last name
            return names.Length > 1 ? string.Join(" ", names.Skip(1)) : string.Empty;
        }
    }
}
Code Explanations:
  • Forward Mapping: Converts Employee to EmployeeDTO, including combining first and last names.
  • Reverse Mapping: Converts EmployeeDTO back to Employee and splits the full name into first and last names using helper methods. Using ReverseMap() avoids defining a second, separate mapping.
Using Automapper Reverse Mapping in a Controller

Implement a controller that uses AutoMapper’s reverse mapping capabilities. This controller will handle CRUD operations for employees, demonstrating both forward and reverse mappings. So, create a controller named EmployeesController 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
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
        private readonly IMapper _mapper;         // AutoMapper instance for mapping objects
        private readonly EmployeeDBContext _context; // EF Core database context

        public EmployeesController(IMapper mapper, EmployeeDBContext context)
        {
            _mapper = mapper;
            _context = context;
        }

        // GET: api/Employees/GetEmployees
        // Retrieves all employees with their address details
        [HttpGet("GetEmployees")]
        public async Task<ActionResult<IEnumerable<EmployeeDTO>>> GetEmployees()
        {
            // Retrieve employees including their related Address data
            var employees = await _context.Employees
                .AsNoTracking()
                .Include(e => e.Address)
                .ToListAsync();

            // Map the list of Employee entities to a list of EmployeeDTOs
            var employeeDTOs = _mapper.Map<List<EmployeeDTO>>(employees);

            return Ok(employeeDTOs);
        }

        // GET: api/Employees/GetEmployee/1
        // Retrieves a single employee by ID with address details
        [HttpGet("GetEmployee/{id}")]
        public async Task<ActionResult<EmployeeDTO>> GetEmployee(int id)
        {
            // Find employee by ID and include Address data
            var employee = await _context.Employees
                                         .AsNoTracking()
                                         .Include(e => e.Address)
                                         .FirstOrDefaultAsync(e => e.Id == id);
            if (employee == null)
            {
                return NotFound($"Employee with ID {id} not found.");
            }

            // Map the found Employee entity to EmployeeDTO
            var employeeDTO = _mapper.Map<EmployeeDTO>(employee);

            return Ok(employeeDTO);
        }

        // POST: api/Employees/AddEmployee
        // Creates a new employee record from the provided EmployeeDTO
        [HttpPost("AddEmployee")]
        public async Task<ActionResult<EmployeeDTO>> AddEmployee(EmployeeDTO employeeDTO)
        {
            if (employeeDTO == null)
            {
                return BadRequest("Employee data is null.");
            }

            // Convert the incoming EmployeeDTO to an Employee entity
            var employee = _mapper.Map<Employee>(employeeDTO);

            // Add the new employee to the database
            _context.Employees.Add(employee);
            await _context.SaveChangesAsync();

            // Map the newly created Employee entity back to EmployeeDTO (includes generated ID)
            var createdEmployeeDTO = _mapper.Map<EmployeeDTO>(employee);
            return Ok(createdEmployeeDTO);
        }

        // PUT: api/Employees/UpdateEmployee/1
        // Updates an existing employee's details based on the provided EmployeeDTO
        [HttpPut("UpdateEmployee/{id}")]
        public async Task<ActionResult<EmployeeDTO>> UpdateEmployee(int id, EmployeeDTO employeeDTO)
        {
            if (id != employeeDTO.EmployeeId)
            {
                return BadRequest("Employee ID mismatch.");
            }

            // Retrieve the existing employee record including its Address
            var existingEmployee = await _context.Employees
                                                 .Include(e => e.Address)
                                                 .FirstOrDefaultAsync(e => e.Id == id);
            if (existingEmployee == null)
            {
                return NotFound($"Employee with ID {id} not found.");
            }

            // Map updated values from the DTO to the existing Employee entity
            _mapper.Map(employeeDTO, existingEmployee);

            try
            {
                // Save the updated record in the database
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException ex)
            {
                if (!EmployeeExists(id))
                {
                    return NotFound($"Employee with ID {id} no longer exists.");
                }
                else
                {
                    // Return a 500 status with an error message on exception
                    return StatusCode(500, $"An error occurred: {ex.Message}");
                }
            }

            // Map the updated Employee entity back to EmployeeDTO and return it
            var updatedEmployeeDTO = _mapper.Map<EmployeeDTO>(existingEmployee);
            return Ok(updatedEmployeeDTO);
        }

        // Helper method to check if an employee exists in the database
        private bool EmployeeExists(int id)
        {
            return _context.Employees.Any(e => e.Id == id);
        }
    }
}
Testing the API Endpoints Using Postman

Now, run the application and test the functionality, and it should work as expected. First, run the application, and then you can test the endpoints as follows:

GET All Employees:

Method: GET
URL: https://localhost:<PORT>/api/Employees/GetEmployees
Expected Response: You will receive a JSON array of employees with their address details.

AutoMapper Reverse Mapping in ASP.NET Core Web API

GET a Single Employee:

Method: GET
URL: https://localhost:<PORT>/api/Employees/GetEmployee/1
Expected Response: If the employee does not exist, you will receive a 404 response. If the employee exists, you will get 200 Ok Responses.

Example to Understand AutoMapper Reverse Mapping in ASP.NET Core Web API

POST a New Employee:

Method: POST
Headers: Content-Type: application/json
URL: https://localhost:<PORT>/api/Employees/AddEmployee
Body: (Raw JSON)

{
  "FullName": "John Doe",
  "Email": "john.doe@example.com",
  "Gender": "Male",
  "City": "New York",
  "State": "NY",
  "Country": "USA"
}

Expected Response: You should receive a response with status 200 (Ok) and the created employee data.

AutoMapper Reverse Mapping in ASP.NET Core Web API

PUT (Update) an Existing Employee:

Method: PUT
Headers: Content-Type: application/json
URL: https://localhost:<PORT>/api/Employees/UpdateEmployee/1
Body:(Raw JSON)

{
  "EmployeeId": 1,
  "FullName": "John A. Doe",
  "Email": "john.a.doe@example.com",
  "Gender": "Male",
  "City": "Los Angeles",
  "State": "CA",
  "Country": "USA"
}

Expected Response: The response should return the updated employee details with a 200 (OK) status.

When Should We Use AutoMapper Reverse Mapping in ASP.NET Core Web API?

When Should We Use AutoMapper Reverse Mapping in ASP.NET Core Web API?

We need to use AutoMapper Reverse Mapping for a two-way transformation between two models. Some common scenarios include:

  • Two-Way Data Flow: You want to expose data via a GET endpoint (mapping Entity → DTO) and Accept updates or new entity data via POST/PUT endpoints (mapping DTO → Entity).
  • Minimize Mapping Definitions: If your entity and DTO properties are almost the same (with some minor differences in naming or structure), ReverseMap() keeps the configuration in a single place instead of writing separate CreateMap<>() calls for both directions.
  • Consistency and Maintainability: When you add or remove properties from your entity, using ReverseMap() helps you quickly notice where changes might affect both directions of the map.

AutoMapper ReverseMap() allows you to maintain a single mapping configuration to handle Entity → DTO and DTO → Entity transformations. This approach simplifies your code, reduces duplication, and ensures that both directions stay in sync. It is particularly useful in scenarios where objects need to be transformed both ways (e.g., API input and output). Using ReverseMap() eliminates redundant mapping configurations and maintains consistency across transformations.

In the next article, I will discuss How to Implement AutoMapper Conditional Mapping in ASP.NET Core Web API with Examples. In this article, I explain AutoMapper Reverse Mapping in ASP.NET Core Web API with Examples. I hope you enjoy this article, “AutoMapper Reverse Mapping in ASP.NET Core Web API.”

Leave a Reply

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