Kevin Kalitowski
Kevin Kalitowski

Reputation: 6989

Create two Automapper maps between the same two object types

I am using AutoMapper in a WCF service to return User objects. User has properties such as AccountTeams which itself has child objects. All of the classes have AutoMapper maps.

Depending on the WCF OperationContract that is called, I want to return different amounts of data. I want one OperationContract to return the User object without its AccountTeams property (and their children) populated and another OperationContract to return the User with the whole chain of properties filled out.

Is there a way to have two different maps between the same two objects or do I need to perform the full mapping and null out the properties I don't want to return from the service?

Upvotes: 43

Views: 45777

Answers (4)

dulithm
dulithm

Reputation: 33

I found a different way to resolve such a scenario using a "named wrapper class" or a "type alias". Models:

// db model
public User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<AccountTeam> AccountTeams { get; set; } 
}

// view model
public UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public AccountTeam[] AccountTeams { get; set; } 
}
// type alias for UserDto
public UserTeamsDto : UserDto
{
}

Mappings:

public class UserProfile : Profile
{
    // register this in global mappings
    public UserProfile()
    {
        // ignore AccountTeams
        CreateMap<User, UserDto>()
            .ForMember(dest => dest.AccountTeams, opt => opt.Ignore());
        // include AccountTeams
        CreateMap<User, UserTeamsDto>()
            .ForMember(dest => dest.AccountTeams,  opt => opt.MapFrom(u => u.AccountTeams));
        // TODO: add `AccountTeam` mapping as well
    }
}

And Usage:

var usersWithoutTeams = await _dbContext.Users
    .Where(u => u.Id == 1)
    .ProjectTo<UserDto>(_mapper.ConfigurationProvider)
    .ToArrayAsync(cancellationToken);

var usersWithTeams = await _dbContext.Users
    .Where(u => u.Id == 1)
    .ProjectTo<UserTeamsDto>(_mapper.ConfigurationProvider)
    .ToArrayAsync(cancellationToken);

// you can also do this since parent class is `UserDto`
UserDto[] parentClassList = usersWithTeams;

Hope this helps! Thanks!

Upvotes: 1

Simon Elms
Simon Elms

Reputation: 19668

Kevin Kalitowski raised a good point about wal's answer: If we need two configurations to deal with a mapping that needs to be different, then don't we have to duplicate all the other mappings that are common?

I think I've found a way around this using profiles: Have one profile for each of the unique mappings, and a third profile for the common mappings. Then create two configurations, one for each unique profile but also add the common profile to each configuration as well.

Example, in AutoMapper 4.2:

The classes to be mapped: User and Vehicle:

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class Vehicle
{
    public int FleetNumber { get; set; }
    public string Registration { get; set; }
}

The profiles:

public class Profile1 : Profile
{
    protected override void Configure()
    {
        base.CreateMap<User, User>();
    }
}

public class Profile2 : Profile
{
    protected override void Configure()
    {
        base.CreateMap<User, User>().ForMember(dest => dest.Age, opt => opt.Ignore());
    }
}

public class CommonProfile : Profile
{
    protected override void Configure()
    {
        base.CreateMap<Vehicle, Vehicle>();
    }
}

Then create the configurations and map the objects:

[TestMethod]
public void TestMethod()
{
    var user = new User() { Name = "John", Age = 42 };
    var vehicle = new Vehicle {FleetNumber = 36, Registration = "XYZ123"};

    var configuration1 = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile<CommonProfile>();
        cfg.AddProfile<Profile1>();
    });

    var mapper1 = configuration1.CreateMapper();
    var mappedUser1 = mapper1.Map<User, User>(user);//maps both Name and Age
    var mappedVehicle1 = mapper1.Map<Vehicle, Vehicle>(vehicle);//Maps both FleetNumber 
                                                                //and Registration.

    var configuration2 = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile<CommonProfile>();
        cfg.AddProfile<Profile2>();
    });

    var mapper2 = configuration2.CreateMapper();
    var mappedUser2 = mapper2.Map<User, User>(user);//maps only Name
    var mappedVehicle2 = mapper2.Map<Vehicle, Vehicle>(vehicle);//Same as mappedVehicle1.
}

I tried this out and it works.

Upvotes: 22

wal
wal

Reputation: 17729

I am assuming you are mapping from User to User (if not then just change the destination type)

Assume this class for the following example:

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

You can then use separate AutoMapper.Configuration to define 2 maps:

[TestMethod]
public void TestMethod()
{
    var configuration1 = new Configuration(new TypeMapFactory(), MapperRegistry.AllMappers());
    var mapper1 = new MappingEngine(configuration1);
    configuration1.CreateMap<User, User>();

    var user = new User() { Name = "John", Age = 42 };
    var mappedUser1 = mapper1.Map<User, User>(user);//maps both Name and Age

    var configuration2 = new Configuration(new TypeMapFactory(), MapperRegistry.AllMappers());
    configuration2.CreateMap<User, User>().ForMember(dest => dest.Age, opt => opt.Ignore());
    var mapper2 = new MappingEngine(configuration2);

    var mappedUser2 = mapper2.Map<User, User>(user);
    Assert.AreEqual(0, mappedUser2.Age);//maps only Name
}

To avoid mapping every other Type twice you could add a common method that takes a Configuration which maps everything that can be reached from User and call this on both configuration1 and configuration2 after the calls to CreateMap.

Update

For Automapper 4.x use the following:

var configuration1 = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<User, User>();
});

var mapper1 = configuration1.CreateMapper();
var user = new User() { Name = "John", Age = 42 };
var mappedUser1 = mapper1.Map<User, User>(user);//maps both Name and Age

var configuration2 = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<User, User>().ForMember(dest => dest.Age, opt => opt.Ignore());
});

var mapper2 = configuration2.CreateMapper();
var mappedUser2 = mapper2.Map<User, User>(user);   //maps only Name

Upvotes: 15

mrtentje
mrtentje

Reputation: 1422

I think you can solve this problem with different Configuration objects as described here, you can find an example of this here

Upvotes: 2

Related Questions