Pexels Divinetechygirl 1181676

Demystifying DTOs, Mappers, and When to Use AutoMapper, C# and TypeScript

April 25, 2024 By pH7x Systems

In modern software development, especially in projects following the principles of clean architecture and domain-driven design, the proper management of data transfer objects (DTOs) becomes crucial. DTOs help in decoupling the internal domain model from the external world, ensuring flexibility, scalability, and maintainability of the application. However, managing DTOs manually can become tedious, especially in large-scale projects. This is where mappers and tools like AutoMapper come into play, simplifying the mapping process and enhancing developer productivity. Let’s delve into the concepts of DTOs, mappers, and when to employ AutoMapper in your projects.

Understanding DTOs

Data Transfer Objects, or DTOs, are simple objects that carry data between different layers of an application. They are commonly used to encapsulate data exchanged between the presentation layer, service layer, and data access layer. DTOs help in reducing the coupling between these layers by providing a clear contract for data exchange, shielding the internal domain model from external changes.

DTOs typically contain only properties and no behavior. They represent a specific view of the data tailored to the requirements of a particular use case or client. For instance, in a web application, a DTO might represent the data displayed on a user profile page, containing fields like username, email, and profile picture URL.

The Role of Mappers

Mappers facilitate the conversion of data between different representations, such as between DTOs and domain entities. They eliminate the need for manual mapping code, reducing boilerplate and enhancing code maintainability. Mappers often come in the form of utility classes or libraries, offering methods to map data from one object to another based on predefined rules.

By employing mappers, developers can focus on writing business logic without worrying about the intricacies of data transformation. Mappers encapsulate the mapping logic, making it easier to update and refactor when requirements change.

When to Use AutoMapper

AutoMapper is a popular library available in various programming languages, including C#. It provides a simple and fluent API for mapping objects of one type to objects of another type. AutoMapper utilizes conventions and configuration to automatically map properties with similar names, reducing the need for explicit mapping code.

You should consider using AutoMapper in your project under the following circumstances:

  1. Routine Mapping Tasks: If your application involves frequent mapping between DTOs and domain entities, AutoMapper can significantly reduce the amount of manual mapping code you need to write.
  2. Consistency and Maintainability: AutoMapper promotes consistency in mapping conventions across your project. By centralizing mapping configurations, it ensures that all mappings adhere to the same rules, enhancing code maintainability.
  3. Complex Mapping Scenarios: While AutoMapper excels at simple property-to-property mappings, it also supports more complex scenarios involving custom mapping logic, value resolvers, and type converters. If your project requires such functionality, AutoMapper provides the flexibility to handle it.
  4. Productivity: By eliminating the need for repetitive mapping code, AutoMapper allows developers to focus on implementing business logic and delivering value to the application. It boosts productivity by reducing development time and minimizing the chance of errors in mapping code.
Image 5

Let’s start with setting up the project structure:

Project Structure:

  • UserManagement.API: ASP.NET Core Web API project
  • UserManagement.Core: Class library for domain entities, DTOs, and business logic
  • UserManagement.Infrastructure: Class library for data access and repository implementation

User Entity:

// UserManagement.Core
public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
    public byte[] ProfilePicture { get; set; }
}

User DTO:

// UserManagement.Core
public class UserDto
{
    public string Username { get; set; }
    public string Email { get; set; }
    public string ProfilePictureUrl { get; set; }
}

UserRepository Interface:

// UserManagement.Core
public interface IUserRepository
{
    Task<User> GetByIdAsync(int id);
    Task<IEnumerable<User>> GetAllAsync();
    Task<int> AddAsync(User user);
    Task UpdateAsync(int id, User user);
    Task DeleteAsync(int id);
}

UserRepository Implementation:

// UserManagement.Infrastructure
public class UserRepository : IUserRepository
{
    private readonly List<User> _users = new List<User>(); // In-memory storage for demonstration

    public async Task<User> GetByIdAsync(int id)
    {
        return await Task.FromResult(_users.FirstOrDefault(u => u.Id == id));
    }

    public async Task<IEnumerable<User>> GetAllAsync()
    {
        return await Task.FromResult(_users);
    }

    public async Task<int> AddAsync(User user)
    {
        user.Id = _users.Count + 1; // Mocking auto-increment of ID
        _users.Add(user);
        return await Task.FromResult(user.Id);
    }

    public async Task UpdateAsync(int id, User user)
    {
        var existingUser = _users.FirstOrDefault(u => u.Id == id);
        if (existingUser != null)
        {
            existingUser.Username = user.Username;
            existingUser.Email = user.Email;
            existingUser.ProfilePicture = user.ProfilePicture;
        }
        await Task.CompletedTask;
    }

    public async Task DeleteAsync(int id)
    {
        var userToDelete = _users.FirstOrDefault(u => u.Id == id);
        if (userToDelete != null)
        {
            _users.Remove(userToDelete);
        }
        await Task.CompletedTask;
    }
}

AutoMapper Profile:

// UserManagement.Core
public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<User, UserDto>()
            .ForMember(dest => dest.ProfilePictureUrl, opt => opt.MapFrom(src => ConvertProfilePictureToUrl(src.ProfilePicture)));
    }

    private string ConvertProfilePictureToUrl(byte[] profilePicture)
    {
        // Logic to convert profile picture to URL
        return "http://example.com/profiles/" + Guid.NewGuid();
    }
}

UserController (API Controller):

// UserManagement.API.Controllers
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly IUserRepository _userRepository;
    private readonly IMapper _mapper;

    public UserController(IUserRepository userRepository, IMapper mapper)
    {
        _userRepository = userRepository;
        _mapper = mapper;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<UserDto>>> GetAllUsers()
    {
        var users = await _userRepository.GetAllAsync();
        var userDtos = _mapper.Map<IEnumerable<UserDto>>(users);
        return Ok(userDtos);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUserById(int id)
    {
        var user = await _userRepository.GetByIdAsync(id);
        if (user == null)
        {
            return NotFound();
        }
        var userDto = _mapper.Map<UserDto>(user);
        return Ok(userDto);
    }

    [HttpPost]
    public async Task<ActionResult<int>> CreateUser(UserDto userDto)
    {
        var user = _mapper.Map<User>(userDto);
        var userId = await _userRepository.AddAsync(user);
        return CreatedAtAction(nameof(GetUserById), new { id = userId }, userId);
    }

    [HttpPut("{id}")]
    public async Task<ActionResult> UpdateUser(int id, UserDto userDto)
    {
        var existingUser = await _userRepository.GetByIdAsync(id);
        if (existingUser == null)
        {
            return NotFound();
        }

        var user = _mapper.Map<User>(userDto);
        await _userRepository.UpdateAsync(id, user);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<ActionResult> DeleteUser(int id)
    {
        var existingUser = await _userRepository.GetByIdAsync(id);
        if (existingUser == null)
        {
            return NotFound();
        }

        await _userRepository.DeleteAsync(id);
        return NoContent();
    }
}

Startup Configuration:

// UserManagement.API
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Other service configurations...

        services.AddScoped<IUserRepository, UserRepository>();
        services.AddAutoMapper(typeof(MappingProfile)); // Register AutoMapper

        // Other configurations...
    }
}

This example demonstrates a simple CRUD API for managing users using .NET Core, DTOs, AutoMapper, and a mock repository. You can further enhance this code by integrating a real database and adding validation, error handling, authentication, and authorization mechanisms as needed.

WAIT… but I’m using Node, TypeScript is really a thing for me, is there something similar?

Here’s an implementation of the same scenario using TypeScript, Node.js, Express.js, and a mock repository for managing user data.

Image 5

Project Structure:

  • src/
    • controllers/
      • UserController.ts: Controller for handling user-related routes
    • models/
      • User.ts: User entity
      • UserDto.ts: User DTO
    • repositories/
      • UserRepository.ts: Repository for managing user data
    • services/
      • UserService.ts: Service for business logic related to users
    • app.ts: Express application setup

User Entity:

// src/models/User.ts
export interface User {
  id: number;
  username: string;
  email: string;
  profilePicture?: Buffer;
}

User DTO:

// src/models/UserDto.ts
export interface UserDto {
  username: string;
  email: string;
  profilePictureUrl?: string;
}

UserRepository:

// src/repositories/UserRepository.ts
import { User } from '../models/User';

let users: User[] = []; // Mock database for demonstration

export const UserRepository = {
  getAllUsers: (): User[] => users,
  getUserById: (id: number): User | undefined => users.find(user => user.id === id),
  createUser: (user: User): void => {
    user.id = users.length + 1; // Mock auto-increment of ID
    users.push(user);
  },
  updateUser: (id: number, updatedUser: User): void => {
    const index = users.findIndex(user => user.id === id);
    if (index !== -1) {
      users[index] = { ...updatedUser, id };
    }
  },
  deleteUser: (id: number): void => {
    users = users.filter(user => user.id !== id);
  }
};

UserService:

// src/services/UserService.ts
import { User } from '../models/User';
import { UserRepository } from '../repositories/UserRepository';

export const UserService = {
  getAllUsers: (): User[] => UserRepository.getAllUsers(),
  getUserById: (id: number): User | undefined => UserRepository.getUserById(id),
  createUser: (user: User): void => UserRepository.createUser(user),
  updateUser: (id: number, updatedUser: User): void => UserRepository.updateUser(id, updatedUser),
  deleteUser: (id: number): void => UserRepository.deleteUser(id)
};

UserController:

// src/controllers/UserController.ts
import { Request, Response } from 'express';
import { UserDto } from '../models/UserDto';
import { UserService } from '../services/UserService';

export const UserController = {
  getAllUsers: (req: Request, res: Response): void => {
    const users = UserService.getAllUsers();
    const userDtos: UserDto[] = users.map(user => ({
      username: user.username,
      email: user.email,
      profilePictureUrl: user.profilePicture ? `http://example.com/profiles/${user.id}` : undefined
    }));
    res.json(userDtos);
  },

  getUserById: (req: Request, res: Response): void => {
    const userId = parseInt(req.params.id);
    const user = UserService.getUserById(userId);
    if (!user) {
      res.status(404).json({ message: 'User not found' });
    } else {
      const userDto: UserDto = {
        username: user.username,
        email: user.email,
        profilePictureUrl: user.profilePicture ? `http://example.com/profiles/${user.id}` : undefined
      };
      res.json(userDto);
    }
  },

  createUser: (req: Request, res: Response): void => {
    const newUser: UserDto = req.body;
    UserService.createUser(newUser);
    res.status(201).json({ message: 'User created successfully' });
  },

  updateUser: (req: Request, res: Response): void => {
    const userId = parseInt(req.params.id);
    const updatedUser: UserDto = req.body;
    UserService.updateUser(userId, updatedUser);
    res.json({ message: 'User updated successfully' });
  },

  deleteUser: (req: Request, res: Response): void => {
    const userId = parseInt(req.params.id);
    UserService.deleteUser(userId);
    res.json({ message: 'User deleted successfully' });
  }
};

Express Application Setup:

// app.ts
import express, { Request, Response, NextFunction } from 'express';
import { UserController } from './controllers/UserController';

const app = express();
app.use(express.json());

// Routes
app.get('/api/users', UserController.getAllUsers);
app.get('/api/users/:id', UserController.getUserById);
app.post('/api/users', UserController.createUser);
app.put('/api/users/:id', UserController.updateUser);
app.delete('/api/users/:id', UserController.deleteUser);

// Error handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err);
  res.status(500).json({ message: 'Internal server error' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));

Running the Application:

  1. Ensure you have Node.js and npm installed on your system.
  2. Install dependencies by running npm install.
  3. Run the application with npm start.

This TypeScript implementation demonstrates a simple CRUD API for managing users using Node.js, Express.js, DTOs, and a mock repository. You can further enhance this code by integrating a real database and adding validation, error handling, authentication, and authorization mechanisms as needed.

But where is the AutoMapper??!!

AutoMapper for TypeScript called ts-auto-mapper. It allows you to define mapping configurations between objects in a type-safe manner, similar to AutoMapper in C#. However, it’s important to note that ts-auto-mapper doesn’t have as extensive functionality as AutoMapper in C#, but it serves the purpose of simplifying object mapping in TypeScript.

Here’s how you can use ts-auto-mapper in the context of your TypeScript project:

npm install ts-auto-mapper

Usage:

  1. Define Mapping Configurations:

Create mapping configurations between your source and destination objects.

// src/mapping-profile.ts
import { AutoMapper, mapFrom, ProfileBase } from 'ts-auto-mapper';
import { User } from './models/User';
import { UserDto } from './models/UserDto';

export class MappingProfile extends ProfileBase {
  configure(): void {
    AutoMapper.createMap(User, UserDto)
      .forMember(dest => dest.profilePictureUrl, mapFrom(src => src.profilePicture ? `http://example.com/profiles/${src.id}` : undefined));
  }
}
  1. Initialize AutoMapper:

Initialize AutoMapper with your mapping profile.

// app.ts
import { AutoMapper } from 'ts-auto-mapper';
import { MappingProfile } from './mapping-profile';

AutoMapper.createMap(User, UserDto)
  .forMember(dest => dest.profilePictureUrl, mapFrom(src => src.profilePicture ? `http://example.com/profiles/${src.id}` : undefined));

// You can also initialize AutoMapper with mapping profiles like this:
// AutoMapper.initialize(MappingProfile);
  1. Perform Mapping:

Use AutoMapper to map objects.

import { AutoMapper } from 'ts-auto-mapper';
import { User } from './models/User';
import { UserDto } from './models/UserDto';

// Initialize AutoMapper (if not already initialized)
AutoMapper.createMap(User, UserDto)
  .forMember(dest => dest.profilePictureUrl, mapFrom(src => src.profilePicture ? `http://example.com/profiles/${src.id}` : undefined));

// Map objects
const user: User = getUserFromDatabase();
const userDto: UserDto = AutoMapper.map(User, UserDto, user);

Conclusion

In conclusion, DTOs and mappers play a crucial role in modern software development, enabling clean architecture and separation of concerns. DTOs facilitate data exchange between different layers of an application, while mappers streamline the mapping process, enhancing code maintainability and developer productivity. AutoMapper, in particular, offers a convenient solution for automating mapping tasks, especially in projects with routine mapping requirements or complex mapping scenarios. By understanding the principles behind DTOs, mappers, and when to employ AutoMapper, developers can build robust and maintainable applications efficiently.

Be aware ts-auto-mapper simplifies the process of object mapping in TypeScript by providing a fluent API for defining mapping configurations. While it may not have all the features of AutoMapper in C#, it’s a useful tool for handling object mapping in TypeScript projects.

ts-auto-mapper is a great tool for simplifying object mapping in TypeScript projects, but it’s not as feature-rich as AutoMapper in C#. Some of the features that are missing or not as extensive in ts-auto-mapper compared to AutoMapper in C# include:

  1. Complex Mapping Scenarios: While ts-auto-mapper supports basic property-to-property mappings and simple custom transformations, it may not handle complex mapping scenarios as comprehensively as AutoMapper. Features like nested mappings, conditional mappings, value resolvers, and type converters are more limited in ts-auto-mapper.
  2. Convention-based Mapping: AutoMapper in C# allows defining mapping conventions to automatically map properties based on naming conventions. ts-auto-mapper doesn’t have built-in support for such conventions.
  3. Flattening and Unflattening: AutoMapper in C# supports flattening and unflattening complex object graphs into flat DTOs and vice versa. ts-auto-mapper doesn’t have built-in support for this feature.
  4. Custom Resolvers and Converters: While ts-auto-mapper allows defining custom transformations using mapFrom and convertUsing methods, the flexibility and extensibility provided by custom resolvers and converters in AutoMapper are more advanced.
  5. Mapping Inheritance Hierarchies: AutoMapper in C# supports mapping inheritance hierarchies by automatically mapping properties from base classes to derived classes and vice versa. ts-auto-mapper may require more manual configuration for such scenarios.
  6. LINQ-like Queries for Collections: AutoMapper in C# provides LINQ-like querying capabilities for mapping collections of objects, allowing filtering, sorting, and projecting collections during mapping. ts-auto-mapper doesn’t have built-in support for this feature.
  7. Mapping Profiles: While ts-auto-mapper supports defining mapping profiles for organizing mapping configurations, the capabilities and flexibility provided by AutoMapper’s profiles are more extensive.

Despite these differences, ts-auto-mapper still serves as a valuable tool for simplifying object mapping in TypeScript projects. It provides a fluent API for defining mapping configurations, supports basic custom transformations, and helps reduce boilerplate code associated with manual mapping. Depending on the complexity of your mapping requirements, ts-auto-mapper may fulfill your needs or you may need to resort to manual mapping for more advanced scenarios.