Following the first article on AutoMapper in ASP.NET Core, this one represents an extension to the mapping of complex objects, describing some additional features that can save us a lot of work. The tools which will help us achieve the desired goal are custom projections. That said, we will not go into details about how to set up the project with AutoMapper but rather build up the current implementation. This means that before diving into this article, base knowledge about AutoMapper is recommended if not necessary.
Let’s start.
AutoMapper Recap
As mentioned in the previous article, AutoMapper relies on its ability to map source properties to their corresponding destination properties as long as we respect the naming convention.
If AutoMapper cannot automatically detect the properties that we’re supposed to map to, there are two possible scenarios:
- We don’t know how to write the configuration,
- We want a custom configuration.
The naming convention can cover simpler examples where the source object has a property, method, or a method with a “Get” as a prefix with the same name as the property of a destination object. A more complex mapping set would include mapping source property of type object to a destination property of a simple type using the PascalCase convention. The property would have to be a combination of an object type + its property name for it to work.
But what if we have two child objects on the source and we want to map the properties which exist without checking each child object separately? Or if we have to do some calculations before mapping the values of the properties?
Continue on reading…
IncludeMembers
Moving on with the specific configuration, IncludeMembers deserves an honorable mention. It helps us decide from which child object we want to map the desired property without actually checking whether the child object has that property at all. However, it requires a mapping from child objects to destination objects, otherwise, the configuration will fail.
What is important to mention here is that, when using IncludeMembers, the order is relevant.
Where does that leave us?
It means when performing configuration, AutoMapper first checks whether it can perform any automatic mappings. If no matching properties or methods are found on the source object itself, it continues with the IncludeMembers in the order the child objects are added to the configuration. If it doesn’t find a match in the first child object, it moves on to the second and so on. The moment it reaches the desired match, AutoMapper considers the mapping successful and terminates the process for that particular property.
Implementation
With the aim of better understanding IncludeMembers and how it performs, we will use the implementation from the first part and adjust it to suit our needs.
In the current implementation, we have two classes – User:
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public Address Address { get; set; } public string GetFullName() { return $"{LastName}, {FirstName}"; } }
And Address:
public class Address { public int Id { get; set; } public string Door { get; set; } public string Street1 { get; set; } public string Street2 { get; set; } public string City { get; set; } public string State { get; set; } public string Country { get; set; } public string ZipCode { get; set; } }
And at the end, the destination object that will be shown in the UI:
public class UserViewModel { [Display(Name = "Full Name")] public string FullName { get; set; } [Display(Name = "Country")] public string AddressCountry { get; set; } public string Email { get; set; } }
As you can see, in the current view model, we are applying one of the naming conventions for the automatic mapping by naming the property AddressCountry
. That means that the AutoMapper will search for a child object named Address
and its property named Country
.
And voila, it works:
To show the true power of the IncludeMembers configuration, we will add another class named AdditionalInfo
:
public class AdditionalInfo { public string PhoneNumber { get; set; } }
And a reference to it in the User class:
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public Address Address { get; set; } public AdditionalInfo AdditionalInfo { get; set; } public string GetFullName() { return $"{LastName}, {FirstName}"; } }
And lastly, the property of the view model class is also modified by adding the new PhoneNumber
property:
public class UserViewModel { [Display(Name = "Full Name")] public string FullName { get; set; } public string Country { get; set; } public string Email { get; set; } [Display(Name = "Phone Number")] public string PhoneNumber { get; set; } }
Now that we’ve renamed the Country
property as well, we will explain in a bit how it contributes to our solution.
Updating Mapping Configuration
Since we’ve decided to use IncludeMembers
, we don’t need to rely on the naming conventions anymore between the child and destination object. We do, however, need to make sure that the names of the properties are the same. And that’s the only condition. Therefore, in this case, if one of the child objects has a property named Country
, it will be mapped. But what’s important to mention here is that we don’t need to specify which child object that would be, not by using some other mapping configurations nor by the naming conventions. The same goes for PhoneNumber
. We know we need it, but we don’t know from where. Luckily, AutoMapper does.
The only thing left to do is to update the mapping configuration. The configuration will be inside the UserProfile
class:
public class UserProfile : Profile { public UserProfile() { CreateMap<User, UserViewModel>().IncludeMembers(u => u.Address, u => u.AdditionalInfo); CreateMap<Address, UserViewModel>(MemberList.None); CreateMap<AdditionalInfo, UserViewModel>(MemberList.None); } }
Note: Don’t forget to add mappings from child objects to the destination object! Otherwise, the validation of the configuration will fail and the application will throw an exception. Additionally, MemberList.None
means that the validation of the mapping configuration will be skipped for this specific map.
Running the application after these changes, we can see the desired output:
The Country
property is successfully mapped and the PhoneNumber
as well.
If you thought this was easy-peasy and you’re feeling like you’re up for a challenge, make sure to scroll down a bit more because we’re talking about custom projections. And who doesn’t love (or need) custom things?
Custom Projections
Speaking of customization, AutoMapper gives a variety of possibilities. You can choose from simple conversions like applying a function or just manipulating source properties directly and more complicated ones like converting between types and values.
Regarding simplicity, the following approach relies on things we already know but goes one step further. Let’s take our UserViewModel.cs
and extend it with a new property:
public class UserViewModel { [Display(Name = "Full Name")] public string FullName { get; set; } public string Country { get; set; } public string Email { get; set; } [Display(Name = "Phone Number")] public string PhoneNumber { get; set; } [Display(Name = "Secondary Address")] public bool HasSecondaryAddress { get; set; } }
This property will tell us whether the User has both streets set up in the Address
child object. But how do we map it? There is no naming convention whatsoever so we cannot use the previously described approaches.
For this to be mapped correctly, we need to change our UserProfile class:
public class UserProfile : Profile { public UserProfile() { CreateMap<User, UserViewModel>() .ForMember(dest => dest.HasSecondaryAddress, opt => opt.MapFrom(src => string.IsNullOrEmpty(src.Address.Street2))) .IncludeMembers(u => u.Address, u => u.AdditionalInfo); CreateMap<Address, UserViewModel>(MemberList.None); CreateMap<AdditionalInfo, UserViewModel>(MemberList.None); } }
As we can see here, we have added a special configuration for the HasSecondaryAddress
property – by checking whether the second street property on the child object exists or is set. Here we are combining ForMember
, which tells us for which destination property we want to provide the configuration and MapFrom
, which is from where we want to map and how.
After all, the result of the User details page confirms it:
Relating to more complex mappings, we differ Value Resolvers and Type Converters. As their names suggest, they provide mappings between different values and types that cannot be automatically mapped.
Let’s show it in an example and dive a bit deeper.
Custom Value Resolvers
Firstly, we are going to explain value resolvers. Custom Value Resolvers come in handy when there are some calculations that we want to apply to the source values before mapping them to the destination ones.
Take our User class, for example. Let’s say we want to calculate User’s BMI and show it in the application. For us to do that, the User class must provide information about height and weight:
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public double Height { get; set; } public double Weight { get; set; } public Address Address { get; set; } public AdditionalInfo AdditionalInfo { get; set; } public string GetFullName() { return $"{LastName}, {FirstName}"; } }
Speaking of, we need to change the UserViewModel.cs
too to display the calculated BMI:
public class UserViewModel { [Display(Name = "Full Name")] public string FullName { get; set; } public string Country { get; set; } public string Email { get; set; } [Display(Name = "BMI")] public double Bmi { get; set; } [Display(Name = "Phone Number")] public string PhoneNumber { get; set; } [Display(Name = "Secondary Address")] public bool HasSecondaryAddress { get; set; } }
To create a Value Resolver, we need to create a class that implements the IValueResolver
interface of the AutoMapper library:
public class BmiValueResolver : IValueResolver<User, UserViewModel, double> { public double Resolve(User source, UserViewModel destination, double destMember, ResolutionContext context) { return Math.Round(source.Weight / Math.Pow(source.Height, 2)); } }
The interface accepts three parameters: source type, destination type, and the type of destination member that will represent the result of the mapping. It is necessary to implement the Resolve
method which holds the desired logic. In this case, we are using the Math
library to help us calculate the BMI.
NOTE: For a more generic approach, we can replace specific types in the Resolver configuration with the type object. That way the Resolver is not dependent on the types of source and destination objects.
After finishing this part, we are left with three ways to apply the Resolver: using generics, typeof
, or passing an instance directly. In the following example, we will use the first version, which makes the mapping easy and readable:
public class UserProfile : Profile { public UserProfile() { CreateMap<User, UserViewModel>() .ForMember(dest => dest.HasSecondaryAddress, opt => opt.MapFrom(src => string.IsNullOrEmpty(src.Address.Street2))) .ForMember(dest => dest.Bmi, opt => opt.MapFrom<BmiValueResolver>()) .IncludeMembers(u => u.Address, u => u.AdditionalInfo); CreateMap<Address, UserViewModel>(MemberList.None); CreateMap<AdditionalInfo, UserViewModel>(MemberList.None); } }
NOTE: Passing an instance of the Resolver works like a charm when the Resolver has a non-empty constructor.
And the result of following the previous steps:
Suppose we can say that the User is quite healthy, having a BMI of 21. Either way, we have it successfully calculated, mapped, and shown on the UI.
Custom Type Converters
Last, but definitely not the least, are Custom Type Converters. We know that when mapping a type to a string
, AutoMapper automatically calls its ToString()
method. But what happens when we are mapping from a string
or to a different type?
Thinking about a scenario where we have to map between two different types, it is obvious that AutoMapper provides a fair number of options. We can use a simple function that will do the conversion for us, so no need for Type Converters. Then why are they so special?
What differs Custom Type Converters from other custom projections is that they are global. Once a Type Converter for two types is created, the conversion between properties with those types will always go through the Type Converter, regardless of the objects they belong to.
Just imagine how much time did Type Converter save us! There’s only one thing left to do – learn how to write it.
For this, we are expanding the User class with one more property, BirthDate
:
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public double Height { get; set; } public double Weight { get; set; } public string BirthDate { get; set; } public Address Address { get; set; } public AdditionalInfo AdditionalInfo { get; set; } public string GetFullName() { return $"{LastName}, {FirstName}"; } }
For whatever reason, this property is of type string
and in the format “dd.MM.yyyy”. But, in the view model, we would like it to be of type DateTime
:
public class UserViewModel { [Display(Name = "Full Name")] public string FullName { get; set; } public string Country { get; set; } public string Email { get; set; } [Display(Name = "Birth Date")] public DateTime BirthDate { get; set; } [Display(Name = "BMI")] public double Bmi { get; set; } [Display(Name = "Phone Number")] public string PhoneNumber { get; set; } [Display(Name = "Secondary Address")] public bool HasSecondaryAddress { get; set; } }
If we’re to let AutoMapper do its magic, it will, unfortunately, crash. As we said before, it only knows conversions to string
but not from it.
For creating a Custom Type Converter, we require a class that implements ITypeConverter
in which we will parse the child object to match the destination member:
public class DateTimeTypeConverter : ITypeConverter<string, DateTime> { public DateTime Convert(string source, DateTime destination, ResolutionContext context) { var dates = source.Split("."); return new DateTime(int.Parse(dates[2]), int.Parse(dates[1]), int.Parse(dates[0])); } }
The interface accepts two parameters: child and destination member types and needs Convert
method implementation.
Applying the Custom Type Converter is very similar to the Custom Value Resolver: it can be generic or by instantiating. However, it is important to mention that in the case of Type Converter, we are using the ConvertUsing
method of AutoMapper to which we will pass the converter in a way that fits us best. Once again, we are going with generics:
public class UserProfile : Profile { public UserProfile() { CreateMap<string, DateTime>().ConvertUsing<DateTimeTypeConverter>(); CreateMap<User, UserViewModel>() .ForMember(dest => dest.HasSecondaryAddress, opt => opt.MapFrom(src => string.IsNullOrEmpty(src.Address.Street2))) .ForMember(dest => dest.Bmi, opt => opt.MapFrom<BmiValueResolver>()) .IncludeMembers(u => u.Address, u => u.AdditionalInfo); CreateMap<Address, UserViewModel>(MemberList.None); CreateMap<AdditionalInfo, UserViewModel>(MemberList.None); } }
And the final output:
Note: The birth date is shown like this because we’ve used DateTime
‘s overload ToLongDateString()
on the UI.
Excellent!
Conclusion
So, this concludes the second part of the AutoMapper articles.
In this article, we’ve learned that AutoMapper is very versatile, intuitive, and easy to use tool that provides a wide variety of options to solve our problems.
We’ve seen how we can avoid writing converter classes with copy-paste code by providing different types of converters as well as the chance to apply custom functions.