Using Automapper for mapping entities in C#

Mapping entities in C# is a common task when dealing with object-relational mapping (ORM) frameworks, such as Entity Framework. This process involves transforming data between different types, often between domain models and data transfer objects (DTOs). AutoMapper is a powerful and widely-used library that simplifies this mapping process, reducing boilerplate code and increasing productivity. In this article, we will explore how to use AutoMapper to map entities in a C# application, focusing on best practices and common scenarios.

What is AutoMapper?

AutoMapper is an open-source library for .NET that simplifies the mapping between two object models. It is particularly useful in scenarios where the structure of your domain models differs from the structure of your database entities or when you need to project data into a different shape.

Key features of AutoMapper include:

  • Convention-based Mapping: AutoMapper can automatically map properties with matching names between source and destination types based on naming conventions.

  • Explicit Configuration: For cases where naming conventions are not sufficient, you can configure mappings explicitly using a fluent API.

  • Flattening and Unflattening: AutoMapper supports flattening and unflattening complex object structures, which is useful when dealing with nested or hierarchical data.

  • Custom Value Resolvers: You can define custom logic to resolve values during the mapping process, allowing for more complex scenarios.

Getting Started with AutoMapper

To begin using AutoMapper, follow these steps:

Step 1: Install AutoMapper via NuGet

Install the AutoMapper NuGet package in your project using the Package Manager Console or the Visual Studio Package Manager:

Install-Package AutoMapper

Step 2: Configure AutoMapper

In your application startup code, typically in the Startup.cs file for ASP.NET Core applications, configure AutoMapper by creating a profile and adding mappings:

using AutoMapper;

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<SourceEntity, DestinationEntity>();
        // Add more mappings as needed
    }
}

// In your startup code (e.g., Startup.cs)
public void ConfigureServices(IServiceCollection services)
{
    // Other configurations...

    services.AddAutoMapper(typeof(MappingProfile));

    // Other configurations...
}

Step 3: Inject IMapper where needed

In classes where you want to perform mapping, inject the IMapper interface:

public class MyService
{
    private readonly IMapper _mapper;

    public MyService(IMapper mapper)
    {
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    public void PerformMapping()
    {
        var sourceEntity = new SourceEntity { /* Initialize properties */ };
        var destinationEntity = _mapper.Map<DestinationEntity>(sourceEntity);

        // Use the mapped entity as needed
    }
}

Basic Mapping Scenarios

Let's explore some common mapping scenarios using AutoMapper.

1. Simple Property Mapping

public class SourceEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class DestinationEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

2. Nested Property Mapping

public class SourceEntity
{
    public int Id { get; set; }
    public NestedEntity Nested { get; set; }
}

public class NestedEntity
{
    public string Description { get; set; }
}

public class DestinationEntity
{
    public int Id { get; set; }
    public string NestedDescription { get; set; }
}

3. Ignore Property

public class SourceEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string IgnoreMe { get; set; }
}

public class DestinationEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

In the mapping profile:

CreateMap<SourceEntity, DestinationEntity>()
    .ForMember(dest => dest.IgnoreMe, opt => opt.Ignore());

4. Custom Value Resolver

public class SourceEntity
{
    public DateTime DateOfBirth { get; set; }
}

public class DestinationEntity
{
    public int Age { get; set; }
}

In the mapping profile:

CreateMap<SourceEntity, DestinationEntity>()
    .ForMember(dest => dest.Age, opt => opt.MapFrom<AgeResolver>());

And create the AgeResolver:

public class AgeResolver : IValueResolver<SourceEntity, DestinationEntity, int>
{
    public int Resolve(SourceEntity source, DestinationEntity destination, int destMember, ResolutionContext context)
    {
        var today = DateTime.Today;
        var age = today.Year - source.DateOfBirth.Year;

        // Adjust age if the birthday hasn't occurred yet this year
        if (source.DateOfBirth.Date > today.AddYears(-age))
            age--;

        return age;
    }
}

Advanced Mapping Configurations

AutoMapper provides advanced features to handle more complex scenarios.

1. Conditional Mapping

You can conditionally apply mappings based on specific criteria:

CreateMap<SourceEntity, DestinationEntity>()
    .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name == "John" ? "Special John" : src.Name));

2. Mapping Collections

Mapping collections involves configuring AutoMapper to map each element in the source collection to the corresponding element in the destination collection:

CreateMap<SourceEntity, DestinationEntity>()
    .ForMember(dest => dest.Collection, opt => opt.MapFrom(src => src.Collection.Select(item => _mapper.Map<SourceItem, DestinationItem>(item))));

3. Inheritance Mapping

When dealing with inheritance hierarchies, you can configure AutoMapper to handle the mapping based on the derived type:

CreateMap<BaseEntity, BaseDto>()
    .Include<DerivedEntity, DerivedDto>();

CreateMap<DerivedEntity, DerivedDto>();

4. Reverse Mapping

AutoMapper supports reverse mapping, allowing you to generate a mapping configuration in the opposite direction:

CreateMap<SourceEntity, DestinationEntity>();
CreateMap<DestinationEntity, SourceEntity>();

Conclusion

Using AutoMapper in your C# projects can significantly simplify the process of mapping entities, reducing the amount of boilerplate code and increasing maintainability. By following the steps outlined in this article and exploring the various mapping scenarios and configurations, you can leverage the full power of AutoMapper to handle complex mapping tasks efficiently.

Remember to check the official AutoMapper documentation for more in-depth information and advanced features. Happy mapping!