How to Use AutoMapper in .NET
Introduction
When designing an application, it is common to use a multi-layered architecture. Adhering to the principle of separation of concerns, one generally does not transmit the same DTO across multiple layers. Instead, the incoming DTO is converted into another DTO between layers before being passed to the next. For example, you might have a split like this: Entity ← (Domain Layer) → Service DTO ← (Application Layer) → ViewModel. When performing data updates, the Application Layer converts the ViewModel into a Service DTO and passes it as a parameter to the Domain Layer's Service. The Service then converts the Service DTO into an Entity and writes it to the database via a Repository (Entity Framework itself implements the Repository Pattern). Handling the property/field assignments for these DTO conversions is very cumbersome, so developers often write mapping (Reflection) APIs to simplify the operation. AutoMapper is a commonly used mapping package, and its documentation is very detailed and relatively easy to get started with.
How to Use AutoMapper
The usage of AutoMapper is as follows:
// Create a MapperConfiguration to define the mapping relationship between classes
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Order, OrderDto>();
});
// Validate the configuration. Members present in the Destination must exist in the Source,
// or be handled specifically; otherwise, an AutoMapperConfigurationException will be thrown.
config.AssertConfigurationIsValid();
// Create the Mapper
var mapper = config.CreateMapper();
// Create a Destination dest and map the source values to dest
Destination dest = mapper.Map<Destination>(source);
// Map the source values to an existing dest
mapper.Map(source, dest);Configuration
Normally, the mapping relationships between classes do not change dynamically, so you only need to create one instance of MapperConfiguration. This is typically written in Startup.cs. In .NET 6, due to new C# syntax, Startup.cs is not provided by default, so it is written in Program.cs.
Common Mapping Configuration Between Classes
var config = new MapperConfiguration(cfg => {
// Single type conversion setting, configuring that Source can be converted to Destination
cfg.CreateMap<Source, Destination>();
// Use ConvertUsing to define the conversion relationship between two types directly using Expression<Func<Source, Destination>>
// Here it is used to trim whitespace from string values
cfg.CreateMap<string?, string?>()
.ConvertUsing(x => x == null ? x : x.Trim());
// Referencing settings written in a Profile
cfg.AddProfile(OtherProfile);
});
//...
// Write settings in a separate class for others to reference
public class OtherProfile : Profile {
public OtherProfile() {
cfg.CreateMap<Source1, Destination1>();
}
}Common Mapping Configuration Between Properties
Prefixes/Postfixes
var config = new MapperConfiguration(cfg => {
// Single class setting, configuring that Source can be converted to Destination
cfg.CreateMap<Source, Destination>();
// Set source prefix
// For example: Both Source.Name -> Destination.Name and Source.PrefixName -> Destination.Name are supported
// If Source has both Name and PrefixName, the member defined first in Source takes precedence
cfg.RecognizePrefixes("Prefix");
// Set source postfix
cfg.RecognizePostfixes("Postfix");
// Set destination prefix
// For example: Both Source.Name -> Destination.Name and Source.Name -> Destination.PrefixName are supported
// If Destination has both Name and PrefixName, both members will receive the value of Source.Name
cfg.RecognizeDestinationPrefixes("Prefix");
// Set destination postfix
cfg.RecognizeDestinationPostfixes("Postfix");
// "Get" is added as a prefix by default; call this API if you do not want this prefix
cfg.ClearPrefixes();
});
//......
// RecognizePrefixes reference principle
class Destination {
public string Name { get; set; }
}
// If Source.Name is defined first
class Source {
public string Name { get; set; } // Destination.Name gets the value of Source.Name
public string PrefixName { get; set; }
}
// If Source.PrefixName is defined first
class Source {
public string PrefixName { get; set; } // Destination.Name gets the value of Source.PrefixName
public string Name { get; set; }
}Configuring for a Single Member
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Source, Destination>()
// Do not map values to Destination.Prop1
// Usually used when Destination has this member but Source does not
.ForMember(desc => desc.Prop1, opt => opt.Ignore())
// Source and Destination names are different, set the mapping relationship
.ForMember(dest => dest.DestProp2, opt => opt.MapFrom(src => src.SourceProp2))
// When mapping objects, do not take the value from Source, but set it explicitly
.ForMember(desc => desc.DateProp3, opt => opt.MapFrom(src => DateTime.Now))
// Only map to Destination.IntProp4 if Source.IntProp4 is greater than or equal to 0
.ForMember(dest => dest.IntProp4, opt => opt.Condition(src => (src.IntProp4 >= 0)))
// If Source.Prop5 is Null, set Destination.Prop5 to "Other Value"; otherwise, use the value from Source.Prop5
.ForMember(dest => dest.Prop5, opt => opt.NullSubstitute("Other Value")))
// If desc.CreatedTime is not default, set desc.ModifiedTime to the current time
// If desc.CreatedTime is default, set desc.CreatedTime to the current time
// Mainly used when writing to different fields for creation and modification;
// the CreatedTime used for checking existence must be mapped last
.ForMember(desc => desc.ModifiedTime, opt => {
opt.PreCondition((src, desc, context) => desc.CreatedTime != default);
opt.MapFrom(src => DateTime.Now);
})
.ForMember(desc => desc.CreatedTime, opt => {
opt.PreCondition((src, desc, context) => desc.CreatedTime == default);
opt.MapFrom(src => DateTime.Now);
});
});
class Source {
public string? SourceProp2 { get; set; }
public int IntProp4 { get; set; }
public string? Prop5 { get; set; }
}
public class Destination {
public string? Prop1 { get; set; }
public string? Prop2 { get; set; }
public DateTime DateProp3 { get; set; }
public int IntProp4 { get; set; }
public string? Prop5 { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime? ModifiedTime { get; set; }
}Reverse Mapping
If you want Source and Destination types to be mutually convertible, you can use the following two methods:
// Define the conversion relationship between Source and Destination separately
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Source, Destination>();
cfg.CreateMap<Destination, Source>();
});
//...
// Use reverse mapping
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Source, Destination>()
.ReverseMap();
});There are two things to note when using reverse mapping; evaluate them before use:
- For complex conversions, you need to set
ForPathto define the reverse conversion relationship. AssertConfigurationIsValid()does not work for reverse mapping.
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.Prop1))
.ForMember(dest => dest.Prop5, opt => opt.MapFrom(src => src.Prop3 + "," + src.Prop4))
.ReverseMap()
.ForPath(s => s.Prop3, opt => opt.MapFrom(src => src.Prop5.Split(new char[] { ',' })[0]))
.ForPath(s => s.Prop4, opt => opt.MapFrom(src => src.Prop5.Split(new char[] { ',' })[1]));
});
var source = new Destination {
Prop2 = "123",
Prop5 = "111,222"
};
Source dest = mapper.Map<Source>(source);
// dest.Prop1 = "123" This mapping relationship is simple, so it can be reverse-converted without ForPath
// dest.Prop3 = "111" If ForPath is not set, it will be null
// dest.Prop4 = "222" If ForPath is not set, it will be null
public class Source {
public string? Prop1 { get; set; }
public string? Prop3 { get; set; }
public string? Prop4 { get; set; }
}
public class Destination {
public string? Prop2 { get; set; }
public string? Prop5 { get; set; }
}Dependency Injection
You need to install the NuGet package AutoMapper.Extensions.Microsoft.DependencyInjection.
.NET Core 3.x Startup.cs
public void ConfigureServices(IServiceCollection services) {
// Register using Assembly
services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/);
// Register using the type belonging to the Assembly
services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/);
}.NET 6 Program.cs (Writing style when not using Startup by default)
// Register using Assembly
builder.Services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/);
// Register using the type belonging to the Assembly
builder.Services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/);Injecting AutoMapper into a Service or Controller
public class EmployeesController {
private readonly IMapper mapper;
public EmployeesController(IMapper mapper) => this.mapper = mapper;
}Changelog
- 2022-10-24 Initial version of the document created.
