Code Buckets

Buckets of code

.Net

Passing Parameters with Automapper

OK I admit it – sometimes I find automapper hard to work with. It’s fantastic when everything is well … auto, but some of the mappings I work with on a day to day basis are anything but automatic. There is not much auto about the automappings. But enough of my troubles – one of the lesser known features of automapper that I have found useful is the ability to pass parameters around. This allows you to implement mappings that are dependent on some other part of the object graph; a part that is not normally accessible in the context you are working with.

Example

For a demo I have a simple application that retrieves customer who have visited my lovely shop. I retrieve customer database entities and I want to map them to some DTOs. Just for the example we will display the object on a console application – I’m not concerned with the UI right now so this is good enough. I guess I missed the UI design parts of my university course. I must have slept in that day.

Database Entities

The customers object that we retrieve from the database are

public class Customer
{
 public int Id { get; set; }
 public string Name { get; set; }
 public DateTime DateOfBirth { get; set; }
 public virtual Address Address { get; set; }
 public CustomerType CustomerType { get; set; }

}

public class Address
{
 public int Id { get; set; }
 public int? HouseNumber { get; set; }
 public string HouseName { get; set; }
 public string StreetName { get; set; }
 public string Town { get; set; }
}

public enum CustomerType
{
 Unspecified = 0,
 Consumer,
 Business
}

This is just a straight representation of our database. It’s a customer of a given type with an address. Too easy.

DTO message

As is very standard we want to map the database entity to a set of DTOs. These are

public class CustomerDto
{
 public string Name { get; set; }
 public DateTime DateOfBirth { get; set; }
 public AddressDto Address { get; set; }
}

public class AddressDto
{
 public int? HouseNumber { get; set; }
 public string HouseName { get; set; }
 public string StreetName { get; set; }
 public string Town { get; set; }
 public bool Residential { get; set; }
}

The Problem

We want the residential property on the address DTO to be false if the customer type is business but true otherwise. So let’s try that.

Constructing the mappings

Mapping an object with no parameters

So our first mapping is very straightforward

Mapper.Initialize(cfg =>
 {
 cfg.CreateMap<Customer, CustomerDto>();

 cfg.CreateMap<Address, AddressDto>();
 
 });

We are letting the automapper magic do its thing. HouseName maps to HouseName, Street maps to Street and so forth. It all looks very good.

However if I retrieve a customer that I know is residential then the residential property of the customer object isn’t set. The output of my mapping is

map-no-parameters

The residential property is false. This makes sense – I haven’t mapped

public bool Residential { get; set; }

to anything. In fact my mappings aren’t really valid at all. If I check the mappings in code with

Mapper.AssertConfigurationIsValid();

It will throw an exception with the message

Unmapped members were found. Review the types and members below.

Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type

For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters

==============================================================

Address -> AddressDto (Destination member list)

Automapper.Entities.Address -> Automapper.Messages.AddressDto (Destination member list)

Unmapped properties:

Residential

This tells me exactly what my issue is. The valid mapping is

Mapper.Initialize(cfg =>
 {
 cfg.CreateMap<Customer, CustomerDto>();

 cfg.CreateMap<Address, AddressDto>()
 .ForMember(d => d.Residential, o => o.Ignore());
 });

And my full code for this is

 var customerRepository = new CustomerRepository();
 var customer = customerRepository.FindById(1);

 Mapper.Initialize(cfg =>
 {
 cfg.CreateMap<Customer, CustomerDto>();

 cfg.CreateMap<Address, AddressDto>()
 .ForMember(d => d.Residential, o => o.Ignore());
 });

 //..validate mappings
 Mapper.AssertConfigurationIsValid();

 var customerDto = Mapper.Map<CustomerDto>(customer);
 Console.WriteLine(customerDto.ToString());

Which has the same output as before but this time is actually valid.

map-no-parameters

How can I set the residential property? The problem is that I need information from the customer object when the address object is being mapped. However when I am mapping the address object I’m in the context of the address and I don’t have the customer information. I can’t do the mapping.

Mapping an object with parameters

What I want to do is pass information from the customer into the address mappings. I’m going to use parameters for that. The call to map has a ResolutionContext. This has dictionary object that I can use to pass through information i.e.

resolutionContext .Items["CustomerType"] = “information”;

So when I call the mapper I can pass what the customer is using the overload with ResolutionContext

var customerDto = Mapper.Map<CustomerDto>(customer,

opts => opts.Items["CustomerType"] = customer.CustomerType);

So now the context knows what type of customer this is. I need to grab the context in the address mappings. Happily there are several hooks into the resolution context I can use within the mappings. For this I’m going to use a custom value resolver

var customerRepository = new CustomerRepository();
var customer = customerRepository.FindById(1);

Mapper.Initialize(cfg =>
{
 cfg.CreateMap<Customer, CustomerDto>();

 cfg.CreateMap<Address, AddressDto>()
 .ForMember(d => d.Residential,
 o =>
 o.ResolveUsing(
 (src, dest, destMember, resContext) =>
 dest.Residential =
 (CustomerType)resContext.Items["CustomerType"] != CustomerType.Business));
});

//..validate mappings
Mapper.AssertConfigurationIsValid();

var customerDto = Mapper.Map<CustomerDto>(customer,
 opts => opts.Items["CustomerType"] = customer.CustomerType);

Console.WriteLine(customerDto.ToString());

The interesting part of the code is

.ForMember(d => d.Residential, o => o.ResolveUsing(
 (src, dest, destMember, resContext) =>
 dest.Residential =
 (CustomerType)resContext.Items["CustomerType"] != CustomerType.Business))

I am using a resolver to access the information in the resolution context and set the residential property using this. It works and the residential property is set correctly

map-with-parameters

However, I am relying on passing the information myself into the mappings. If I forget to pass in CustomerType then the mapping collapses with a null exception. Also I want these mappings to use the full automapper goodness and be able to map lists of customer entities into list of customer DTOs. It’s not going to do it.

Mapping lists of objects with parameters

To map lists of objects I want the mappings to set the context themselves. That going to be good as it will hide this away from consumers of the mappings and avoids mistakes. I’m going to use the BeforeMap function which also has an overload for ResolutionContext.

 cfg.CreateMap<Customer, CustomerDto>()
 .BeforeMap((customer, customerDto, resContext) =>
 {
 resContext.Items["CustomerType"] = customer.CustomerType;
 });

As the name implies it runs before the mapping takes place so it’s a good place to set up the context. So the call to the mapper becomes

var customerDtos = Mapper.Map<List<CustomerDto>>(customers);

End consumers no longer need to worry about the context; it just works. We can use the amended code to map lists of objects i.e.

var customers = customerRepository.ListAll();

Mapper.Initialize(cfg =>
{
 cfg.CreateMap<Customer, CustomerDto>()
 .BeforeMap((customer, customerDto, resContext) =>
 {
 resContext.Items["CustomerType"] = customer.CustomerType;
 });

 cfg.CreateMap<Address, AddressDto>()
 .ForMember(d => d.Residential,
 o =>
 o.ResolveUsing(
 (src, dest, destMember, resContext) =>
 dest.Residential =
 (CustomerType)resContext.Items["CustomerType"] != CustomerType.Business))
 ;
});

//..validate mappings 
Mapper.AssertConfigurationIsValid();

var customerDtos = Mapper.Map<List<CustomerDto>>(customers);
foreach (var customerDto in customerDtos)
{
 Console.WriteLine(customerDto.ToString());
 Console.WriteLine();
}

map-list-with-parameters

The residential property is set correctly for each object in the list. Job done!

Demo Project

Full source code for this post is on github at
https://github.com/timbrownls20/Demo/tree/master/AutoMapper

Useful Links

Automapper
https://github.com/AutoMapper/AutoMapper

Worked example of automapper custom value resolvers
https://automapper.codeplex.com/wikipage?title=Custom%20Value%20Resolvers

ToStringBuilder
To output a string representation of the object to the console I used a nifty bit of code on stack overflow to create a generic ToString method for objects. Nice!
http://stackoverflow.com/questions/2417647/is-there-an-equivalent-to-javas-tostringbuilder-for-c-what-would-a-good-c-sha

 

9 COMMENTS

  1. Is assignment to dest.Residential really required? Is not returned value from ResolveUsing() assigned to it?
    cfg.CreateMap()
    .ForMember(d => d.Residential,
    o =>
    o.ResolveUsing(
    (src, dest, destMember, resContext) =>
    // dest.Residential =
    (CustomerType)resContext.Items["CustomerType"] != CustomerType.Business))
    ;
    });

  2. Wouldn’t .ForMember(d => d.Residential, o => 0.MapFrom(src => src.CustomerType != CustomerType.Business)) work in this case?

  3. There’s any way to do this with more than one parameter? for example:

    opts>= opts.Items[“CustomerType”] = CustomerType, opts.Items[“CustomerType2”] = CustomerType2

  4. Passing in key-value to Mapper

    When calling map you can pass in extra objects by using key-value and using a custom resolver to get the object from context.

    mapper.Map(src, opt => opt.Items[“Foo”] = “Bar”);
    This is how to setup the mapping for this custom resolver

    cfg.CreateMap()
    .ForMember(dest => dest.Foo, opt => opt.MapFrom((src, dest, destMember, context) => context.Items[“Foo”]));

    From : https://docs.automapper.org/en/stable/Custom-value-resolvers.html

LEAVE A RESPONSE

Your email address will not be published. Required fields are marked *