Back to: ASP.NET Core Web API Tutorials
Mapperly in ASP.NET Core Web API with Examples
In this article, I will explain how to use Mapperly in an ASP.NET Core Web API with Examples. In ASP.NET Core Web API development, we frequently convert Domain/Entity models (our internal data structures) into DTOs (our API’s public contract) and vice-versa. Doing this mapping manually across multiple controllers and services quickly becomes repetitive and error-prone, especially when our models evolve over time.
Older mapping tools like AutoMapper rely heavily on Runtime Reflection. While convenient, this introduces runtime overhead and delays error detection until execution. Also, the newer version of AutoMapper required Paid License.
Mapperly is built to solve these limitations. It uses the .NET compiler to generate mapping code at build time, producing clean, strongly typed C# methods without any runtime reflection. This not only results in extremely fast mappings, but also makes the mapping process transparent, predictable, and easier to maintain, ideal for modern high-performance Web APIs.
What is Mapperly?
Mapperly (Riok.Mapperly) is a Compile-Time Object Mapper for .NET that uses C# Source Generators to automatically generate highly optimized mapping code. Instead of relying on runtime conventions or reflection, Mapperly inspects our Partial Mapper Class at compile time and generates a set of strongly typed mapping methods for converting between Entities, DTOs, and other object types.
Why Mapperly stands out
- It uses .NET Source Generators introduced in .NET 5+, enabling Compile-Time Code Creation.
- It produces actual C# mapping code that becomes part of your assembly.
- It Avoids Reflection Entirely, resulting in extremely fast execution.
- It provides Build-Time Validation, ensuring that broken or incomplete mappings fail the build.
- The generated code is fully inspectable, improving debuggability and transparency.
Important Note: Mapperly is a next-generation mapper that delivers speed, safety, and clarity by generating lightweight mapping code at compile time.
Why do we need Mapperly in ASP.NET Core Web API?
Returning EF Core entities directly from controllers is rarely ideal, as entities often contain fields that should not be exposed publicly. DTOs help separate internal data structures from API output, ensuring:
- Security: Sensitive or internal fields (e.g., PasswordHash, IsDeleted, timestamps) remain internal.
- Stable API Contracts: DTOs shield clients from schema changes in your database.
- Optimized Payloads: You expose only the necessary data, improving performance and bandwidth usage.
- Cleaner Validation: Create/Update DTOs can be tailored for input validation without affecting domain objects.
Mapperly strengthens this approach by eliminating manual mapping code and ensuring:
- Consistency: All mappings are centralized instead of scattered across controllers.
- Reduced Repetitive Code: Repeated property-to-property assignments disappear.
- Compile-time Safety: If a DTO or model changes, Mapperly forces you to fix mappings at build time, helping maintain reliability in CI/CD pipelines.
Why is Mapperly Preferred over AutoMapper nowadays in .NET?
AutoMapper has been widely used for over a decade, but its reliance on runtime configuration and reflection makes it less aligned with today’s performance-focused and AOT-friendly development environment.
The fundamental difference:
- Mapperly → Generates concrete mapping code during compilation.
- AutoMapper → Configures mappings at runtime using Profiles, Conventions, and Reflection.
Why many teams now favor Mapperly
- Zero Runtime Cost: Because mapping methods are already available at build time, applications start faster and run more efficiently.
- Immediate Feedback: If a property is missing or incompatible, the compiler reports it, no runtime surprises.
- Better Visibility: The generated code is real C#, easy to inspect and debug.
- No Reflection: This aligns with .NET’s trimming and AOT directions, making Mapperly ideal for cloud-native, microservice, and containerized apps.
Note: AOT stands for Ahead-Of-Time compilation. In simple terms, it means your .NET application is compiled into native machine code before it runs.
AOT vs JIT
JIT (Just-In-Time compilation) – Traditional .NET Behaviour
- Your C# code is compiled into IL (Intermediate Language).
- When the app starts, the JIT compiler converts IL into machine code at runtime.
- Compilation happens while the application is executing.
AOT (Ahead-Of-Time Compilation)
- Your C# code is compiled into native machine code at build/publish time.
- The application runs natively.
- No JIT compilation occurs at runtime.
How to Implement Mapperly in ASP.NET Core Web API?
We need to follow the steps below to implement Mapperly in ASP.NET Core Web API.
Step 1: Create an ASP.NET Core Web API Project
Create a new ASP.NET Core Web API project and name it MapperlyDemo. This project will expose a few REST endpoints for Product and will use Mapperly to convert:
- Entity → Response DTO (what API returns)
- Request DTO → Entity (what API accepts)
The goal is to keep controllers/services clean and avoid manual property-by-property copying.
Step 2: Install Mapperly NuGet Package
Install the following package through the NuGet Package Manager Console:
- Install-Package Riok.Mapperly
Once installed, Mapperly becomes part of the compilation process. Every time we build the project, it will scan our mapper class, generate the required mapping code, and include it in our assembly without requiring any runtime configuration or initialization. In simple word, when we build the project, Mapperly will generate mapping code automatically based on our mapper definitions.
Step 3: Create Entities and DTOs
In a typical Web API architecture, we don’t expose our database entities directly to the client. Instead, we create DTOs (Data Transfer Objects) that represent the exact shape of the data we want to send or receive.
A common pattern is:
- Product → Entity stored in the system
- ProductResponseDTO → Returned to the client
- CreateProductRequestDTO → Handles “create” requests
- UpdateProductRequestDTO → Handles “update” requests
Creating Entities:
An entity represents internal application data (domain/storage model). First, create a folder named Entities at the project root, where we will store all our Entities.
Product Entity
Create a class file named Product.cs within the Entities folder, and then copy-paste the following code. This entity reflects how the application internally understands a product, not necessarily how it should be exposed to external clients. The entity includes all fields the system needs, including internal metadata like CreatedOn.
namespace MapperlyDemo.Entities
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
public DateTime CreatedOn { get; set; }
}
}
Creating DTOs:
DTOs represent API contract (request/response payloads). These are the objects that your API exposes externally. First, create a folder named DTOs at the project root, where we will store all our Request and Response DTOs.
ProductResponseDTO
Create a class file named ProductResponseDTO.cs within the DTOs folder, and then copy-paste the following code. It is used solely to return product data to clients. It intentionally excludes internal fields such as CreatedOn.
namespace MapperlyDemo.DTOs
{
public class ProductResponseDTO
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
}
}
CreateProductRequestDTO
Create a class file named CreateProductRequestDTO.cs within the DTOs folder, and then copy-paste the following code. It contains only the fields required when creating a product. Clients cannot set the Id, which is controlled by the server.
namespace MapperlyDemo.DTOs
{
public class CreateProductRequestDTO
{
public string Name { get; set; } = null!;
public decimal Price { get; set; }
}
}
UpdateProductRequestDTO
Create a class file named UpdateProductRequestDTO.cs within the DTOs folder, and then copy-paste the following code. It contains the fields required to update an existing product, including the ID to identify which record to modify.
namespace MapperlyDemo.DTOs
{
public class UpdateProductRequestDTO
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
}
}
DTOs enforce a clean separation between internal data and public API contracts. This separation makes API behavior predictable and avoids accidental over-posting or data leakage.
Step 4: Create a Mapper Class (Partial) with [Mapper]
This step introduces Mapperly into the project. Mapperly works when we define a partial class and mark it with the [Mapper] attribute. Mapperly then generates the mapping implementations during compilation.
First, create a folder named Mappers at the project root. Then, inside the Mappers folder, create a class file named ProductMapper.cs and copy-paste the following code. This class acts as a mapping contract, not an implementation.
using MapperlyDemo.DTOs;
using MapperlyDemo.Entities;
using Riok.Mapperly.Abstractions;
namespace MapperlyDemo.Mappers
{
[Mapper]
public partial class ProductMapper
{
// Entity → DTO
public partial ProductResponseDTO ProductToProductDto(Product product);
public partial List<ProductResponseDTO> ProductsToProductDtos(List<Product> products);
// Create DTO → Entity
public partial Product CreateProductRequestDTOToProduct(CreateProductRequestDTO dto);
// Update DTO → existing Entity
public partial void UpdateProductFromDTO(UpdateProductRequestDTO dto, Product product);
}
}
Key points about this mapper class:
- It is marked with the [Mapper] attribute so Mapperly can detect it.
- It is declared as partial, allowing Mapperly to generate the implementation.
- It contains only method signatures, not method bodies.
Each method clearly describes a mapping intention, such as:
- Entity → DTO
- DTO → Entity
- Updating an existing entity from a DTO
What does Mapperly do during build?
When we build the project, Mapperly:
- Creates a .g.cs file containing implementations for all partial mapping methods
- Maps values using matching property names and compatible types
- Generates direct assignment code for update mapping (copying values into the existing entity)
This means we get mapping performance like handwritten code, but without writing and maintaining it manually.
Step 5: Use the mapper in Services
Rather than performing mapping inside controllers, we follow a service-based architecture. This keeps controllers thin and focused on HTTP concerns. The service uses Mapperly to transform entities and DTOs without writing manual mapping logic.
IProductService
The IProductService interface defines the business operations available for products. It communicates exclusively using DTOs, ensuring that entities never leak outside the service layer. This abstraction:
- Improves testability
- Keeps controllers decoupled from implementation details
- Makes future changes (like adding a database) easier
Create a class file named IProductService.cs within the Services folder, and copy-paste the following code:
using MapperlyDemo.DTOs;
namespace MapperlyDemo.Services
{
public interface IProductService
{
Task<List<ProductResponseDTO>> GetAllAsync();
Task<ProductResponseDTO?> GetByIdAsync(int id);
Task<ProductResponseDTO> CreateAsync(CreateProductRequestDTO dto);
Task<ProductResponseDTO?> UpdateAsync(UpdateProductRequestDTO dto);
}
}
ProductService
The ProductService class provides the concrete implementation.
Important design points:
- It depends on ProductMapper, not on manual mapping logic.
- It uses an in-memory collection to simulate persistence.
- All transformations between entities and DTOs are delegated to Mapperly.
This demonstrates how Mapperly fits naturally into a service layer without adding complexity. Create a class file named ProductService.cs within the Services folder, and copy-paste the following code. Here, Mapperly eliminates redundant mapping lines, resulting in a clean, maintainable service.
using MapperlyDemo.DTOs;
using MapperlyDemo.Entities;
using MapperlyDemo.Mappers;
namespace MapperlyDemo.Services
{
public class ProductService : IProductService
{
private readonly ProductMapper _mapper;
// Static in-memory store (shared across requests)
private static readonly List<Product> _products = new();
public ProductService(ProductMapper mapper)
{
_mapper = mapper;
}
public async Task<List<ProductResponseDTO>> GetAllAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
var result = _mapper.ProductsToProductDtos(_products);
return result;
}
public async Task<ProductResponseDTO?> GetByIdAsync(int id)
{
await Task.Delay(TimeSpan.FromSeconds(1));
var product = _products.FirstOrDefault(p => p.Id == id);
return product is null ? null : _mapper.ProductToProductDto(product);
}
public async Task<ProductResponseDTO> CreateAsync(CreateProductRequestDTO dto)
{
await Task.Delay(TimeSpan.FromSeconds(1));
var product = _mapper.CreateProductRequestDTOToProduct(dto);
product.Id = _products.Count + 1;
_products.Add(product);
return _mapper.ProductToProductDto(product);
}
public async Task<ProductResponseDTO?> UpdateAsync(UpdateProductRequestDTO dto)
{
await Task.Delay(TimeSpan.FromSeconds(1));
var product = _products.FirstOrDefault(p => p.Id == dto.Id);
if (product is null)
return null;
_mapper.UpdateProductFromDTO(dto, product);
return _mapper.ProductToProductDto(product);
}
}
}
Step 6: Register the service in Program.cs:
Mapperly-generated mappers are plain C# classes after compilation. They have no runtime state and no special lifecycle requirements. Because of this:
- They can be registered using standard dependency injection.
- Singleton or scoped lifetimes are both safe choices.
Registering both the mapper and the service ensures they are available throughout the application and keeps the architecture clean and test-friendly. So, please modify the Program.cs class file as follows:
using MapperlyDemo.Mappers;
using MapperlyDemo.Services;
namespace MapperlyDemo
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
// Keep original property names during serialization/deserialization.
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// Mapperly mappers are normal classes after generation,
// so register like any other dependency:
builder.Services.AddSingleton<ProductMapper>();
//Register Product Service
builder.Services.AddSingleton<IProductService, ProductService>();
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();
}
}
}
Note: You can also keep mappers as plain new ProductMapper() inside services if you prefer not to register them, but DI is cleaner and more testable.
Step 7: Creating ProductsController
The controller acts as the API entry point.
Key characteristics:
- It depends only on IProductService, not on the mapper directly.
- It does not contain any mapping logic.
- Each endpoint corresponds to a clear business operation.
This separation ensures that:
- Controllers remain simple and readable.
- Mapping and business rules are handled in appropriate layers.
Create a controller named ProductsController.cs within the Controllers folder, and copy-paste the following code:
using MapperlyDemo.DTOs;
using MapperlyDemo.Services;
using Microsoft.AspNetCore.Mvc;
namespace MapperlyDemo.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service)
{
_service = service;
}
[HttpGet]
public async Task<ActionResult<List<ProductResponseDTO>>> GetAll()
{
var products = await _service.GetAllAsync();
return Ok(products);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ProductResponseDTO>> GetById(int id)
{
var product = await _service.GetByIdAsync(id);
if (product is null)
return NotFound();
return Ok(product);
}
[HttpPost]
public async Task<ActionResult<ProductResponseDTO>> Create([FromBody] CreateProductRequestDTO dto)
{
var created = await _service.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
[HttpPut]
public async Task<ActionResult<ProductResponseDTO>> Update([FromBody] UpdateProductRequestDTO dto)
{
var updated = await _service.UpdateAsync(dto);
if (updated is null)
return NotFound();
return Ok(updated);
}
}
}
Step 5: Build the Project and View Generated Mapperly Code
Mapperly runs only during the project build process. To view the generated mapping code, instruct the compiler to emit it to disk by adding this to your .csproj:
<PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> </PropertyGroup>
After rebuilding the solution:
- Open your project folder in File Explorer.
- Navigate to the obj directory.
- Drill down to: obj\Debug\net8.0\generated\{YourAssemblyName}\Riok.Mapperly\
- Here you’ll find .g.cs files containing Mapperly-generated C# mapping code.
These files include the actual mapping logic that Mapperly injects at compile time, such as property assignments and update logic. This gives full visibility into what Mapperly is doing behind the scenes. You will see the following auto-generated mapping code.
// <auto-generated />
#nullable enable
namespace MapperlyDemo.Mappers
{
public partial class ProductMapper
{
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
public partial global::MapperlyDemo.DTOs.ProductResponseDTO ProductToProductDto(global::MapperlyDemo.Entities.Product product)
{
var target = new global::MapperlyDemo.DTOs.ProductResponseDTO();
target.Id = product.Id;
target.Name = product.Name;
target.Price = product.Price;
return target;
}
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
public partial global::System.Collections.Generic.List<global::MapperlyDemo.DTOs.ProductResponseDTO> ProductsToProductDtos(global::System.Collections.Generic.List<global::MapperlyDemo.Entities.Product> products)
{
var target = new global::System.Collections.Generic.List<global::MapperlyDemo.DTOs.ProductResponseDTO>(products.Count);
foreach (var item in products)
{
target.Add(ProductToProductDto(item));
}
return target;
}
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
public partial global::MapperlyDemo.Entities.Product CreateProductRequestDTOToProduct(global::MapperlyDemo.DTOs.CreateProductRequestDTO dto)
{
var target = new global::MapperlyDemo.Entities.Product();
target.Name = dto.Name;
target.Price = dto.Price;
return target;
}
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
public partial void UpdateProductFromDTO(global::MapperlyDemo.DTOs.UpdateProductRequestDTO dto, global::MapperlyDemo.Entities.Product product)
{
product.Id = dto.Id;
product.Name = dto.Name;
product.Price = dto.Price;
}
}
}
Understanding the auto-generated code:
The generated output shows exactly what Mapperly creates:
- A ProductResponseDTO object is created
- Each property is assigned directly from the source
- List mapping uses a loop and calls the single-item mapper
- Update mapping assigns fields to the existing entity
This is why Mapperly feels fast: it’s basically producing the same code we would write manually, but in a consistent way.
Testing the Endpoints:
POST – Create Product
Endpoint: POST /api/products
Request Body:
{
"Name": "Laptop",
"Price": 75000
}
Response Body:
{
"Id": 1,
"Name": "Laptop",
"Price": 75000
}
PUT – Update Product
Endpoint: PUT /api/products
Request Body:
{
"Id": 1,
"Name": "Laptop Pro",
"Price": 85000
}
Response Body:
{
"Id": 1,
"Name": "Laptop Pro",
"Price": 85000
}
GET – Get All Products
Endpoint: GET /api/products
Response Body:
[
{
"Id": 1,
"Name": "Laptop Pro",
"Price": 85000
}
]
GET – Get Product By ID
Endpoint: GET /api/products/1
Response Body:
{
"Id": 1,
"Name": "Laptop Pro",
"Price": 85000
}
So, Mapperly brings together the convenience of an object-mapping library and the power of modern .NET source generators. For ASP.NET Core Web API projects, it helps us centralize mapping logic, reduce repetitive code, and gain compile-time safety with high performance and no runtime reflection.
Compared to AutoMapper, Mapperly often provides:
- Faster and more memory-efficient mappings,
- Earlier feedback on mapping problems (at compile time),
- Better transparency and debuggability,
- Safer deployments in trimming and AOT scenarios.
For new ASP.NET Core applications, especially real-time, high-throughput Web APIs, Mapperly is a strong choice and is quickly becoming the preferred alternative.
In the next article, I will discuss Mapperly Real-time Example in ASP.NET Core Web API. In this article, I explain how to use Mapperly in an ASP.NET Core Web API with Examples.
