Learn how YOU can convert your Objects between layers with Automapper, C#, .NET Core and VS Code

When we read/write data we need to transport our information from one layer to the next. Our medium of transportation is usually by storing our data in a POCO a plain old C# object. However, how this object looks like it might differ between layers. In the top layer, we might have a view model. In a domain layer, the data might have been altered to represent what the object might look like in a domain. In our data layer, it might be changed yet again to be easily inserted into a data storage like a DB. The point is - changing this object as it passes through the layer is a cumbersome, boring and time-consuming process. Surely there must be a better way? There is, with a library called Auto mapper.

TLDR; this article will focus on teaching the use of the library AutoMapper. AutoMapper is a whole host of supporting libraries so we have chosen a specific approach, namely to use the version of AutoMapper that uses Dependency Injection.

You can find a fully working repo here

https://github.com/softchris/automapper-demo/tree/master

References

WHY

As we mentioned initially our data needs to move from layer to layer. Changing the data we pass should be an automated process. We don't really have any flow control logic, we just need to map one or several columns to a new set of columns. As our application grows we get more and more of these models. Automate this now before you spend way too much time on this versus actual business logic. 😃

WHAT

So what can AutoMapper do for us?

  1. Map from one type of object to another type
  2. Handle Complex differences, sometimes we have cases that are more advanced than others. It might not be as simple as just a 1-1 replacement. We need to be able to handle 1-N changes too and AutoMapper can help us with that
  3. Default scenarios, AutoMapper has some sensible defaults built-in which we can use to resolve certain 1-N cases

DEMO

So what will we show?

  1. Install and Set up, let's look at how to get started by installing the needed libraries
  2. Configuration, after install let's see how we instruct AutoMapper. Here we will show to use Profiles to tell AutoMapper what objects can be converted to what other objects.
  3. Basic scenario, let's look at a basic scenario in which we transform from one type
  4. Advanced scenarios, Let's cover some more advanced scenarios including 1-N and defaults

Install and Set up

Solution and project

dotnet new sln
dotnet new webapi -o api
dotnet sln add api/api.csproj
1
2
3

Install NuGet package

dotnet add api package AutoMapper.Extensions.Microsoft.DependencyInjection
1

Configuration

First we need to add this line too Startup.cs and the method ConfigureServices():

services.AddAutoMapper(typeof(Startup));
1

Once you have that in place it's time to figure out how to map different classes to each other. For that, we will use a concept called Profiles. In the past, you would have one file where you entered all the mappings. While this works it might not make sense.

Wouldn't it be better if we could enter the mapping configuration closer to where it's used?

Of course, it would. We call this way of organizing our code to organize by topic or domain. Let me show you an example:

/User
  UserController.cs
  UserModel.cs
  UserProfile.cs
  UserViewModel.cs
1
2
3
4
5

Whether you want to organize your code this way or not the concept Profiles means we created dedicated mapping classes for our mapping configuration. So let's take a look at how we can create one of those. First, let's think out a domain. We usually need the concept User as our app usually have users using it. So let's create the file UserProfile.cs and give it the following content:

// UserProfile.cs

using AutoMapper;

namespace api.Profiles 
{
  public class UserProfile: Profile 
  {  
    public UserProfile()
    {
      CreateMap<User, UserViewModel>();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Above we are inheriting from the base class Profile and in the constructor, we set up our mapping:

CreateMap<User, UserViewModel>();
1

How shall we interpret the above though?

Well, it's 50/50 right? 😉 Jokes aside. What we mean by the above is that given a User object, how do we create a UserViewModel object from it. This means we have set it up in one direction.

What about the other direction?

I knew you'd ask that 😃

There are actually a few choices there depending on how each class looks, or rather how compatible they are.

Let's look at some different scenarios so you see how this affects the configuration.

Basic scenario

In this basic scenario owe want to convert between a ViewModel and a Model in our service layer. We have a class UserViewModel and a class UserModel. Let's have a look at the code for each:

// UserViewModel.cs

namespace api.Models 
{
  public class UserViewModel
  {
    public int Id { get; set; }
    public string FullName { get; set; }
  }
}
1
2
3
4
5
6
7
8
9
10

now the UserModel:

namespace api.Models 
{
  public class UserModel
  {
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
  }
}
1
2
3
4
5
6
7
8
9

Looking at the above we can see that there are similarities Id but also differences FullName vs FirstName and LastName. We need to do the following:

  • Figure out the difference between the two classes
  • Encode the difference as a configuration in a profile class
  • Ensure it works

Figure out the difference

We deduce that a FullName is the same thing as FirstName + LastName. This might not always be so easy to figure but in this case it seemed quite simple.

Encode the difference

At this point, we are ready to encode this in mapper configuration. So we create a file UserProfile.cs and give it the following content:

using AutoMapper;

namespace api.Models 
{
  public class UserProfile: Profile 
  {  
    public UserProfile()
    {
      CreateMap<User, UserViewModel>();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

What we are saying is, given a User we can create a UserViewModel.

But wait, we are not done. The field names don't match. This won't work or?

Yes you are correct, we need to update our code above to use the helper method ForMember(), like so:

CreateMap<User, UserViewModel>()
  .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.FirstName + src.LastName));
1
2

Here we are saying, given the field FullName, construct it given FirstName + LastName.

Ensure it works

How do we know it works? Well, we have chosen to create a Web API project so let's create a route where we can test it out.

Create the file UserController.cs and give it the following content:

// UserController.cs

using AutoMapper;
using api.Models;

using Microsoft.AspNetCore.Mvc;

namespace api.Controllers 
{
  [ApiController]
  [Route("[controller]")]
  public class UserController 
  {
    private IMapper _mapper;
    public UserController(IMapper mapper)
    {
        _mapper = mapper;
    }

    
    [Route("/[controller]/[action]")]
    public string Get() 
    {
      var result = _mapper.Map<UserViewModel>(new User() { FirstName="Chris", LastName="Noring", Id = 1 });
      return result.FullName;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Above we have two things of interest.

  1. The constructor, here we can see that we have injected an instance of IMapper called mapper.
private IMapper _mapper;
public DataController(IMapper mapper)
{
  _mapper = mapper;
}
1
2
3
4
5
  1. Controller action, here we are setting up our route to listen to controller/action which means the name of the Controller class and the name of the method. Because our controller is called UserController that means the route is <baseUrl/>user/get.
[Route("/[controller]/[action]")]
public string Get() 
{
  var result = _mapper.Map<UserViewModel>(new User() { FirstName="Chris", LastName="Noring", Id = 1 });
  return result.FullName;
}
1
2
3
4
5
6

Ok, let's try to run this with:

dotnet build
dotnet run
1
2

We should get the following:

As you can see we are able to hit our route https://localhost:5001/user/get and it hits the correct class and method above. We can also see that our _mapper is able to resolve the User object we pass it and turn that into a ViewModel.

It should be said though in a more realistic example we would probably fetch our User object using a service and then convert it into a ViewModel like the below code:

var user = _userService.get();
var viewModel = _mapper.Map<UserViewModel>(user);
1
2

Complex scenarios

Ok, we understand a basic scenario. What's a more complex scenario? Well, we might have nested classes where you have a class in a class like so:

public class Order {
  public Customer Customer { get; set; }
  public DateTime Created { get; set; }
}
1
2
3
4

Above we have Customer being a part of Order.

The target class might look like this:

public class OrderViewModel {
  public string CustomerName { get; set; }
  public DateTime Created { get; set; }
}
1
2
3
4

The above actually works without any specific mapping rules?

How is that possible?

Well, it depends on the shape Customer. Customer in this case just looks like this:

public class Customer 
{
  public string Name { get; set }
}
1
2
3
4

AutoMapper is by default able to solve nesting like this as long as the name matches so Customer -> Name can be seen as CustomerName, quite powerful.

Configure

You shouldn't take my word for it, so let me show you. We will need to do the following:

  • Configure our mapping, we do this by creating a class OrderProfile
  • Create our model and view model, we've already mentioned Order and OrderViewModel, we need to create those.
  • Add a router class, we need to create the class OrderController

Configure our mapping

Let's create our OrderProfile class. Start by creating the OrderProfile.cs and give it the following content:

using AutoMapper;
using api.Models;

namespace api.Profiles {
  public class OrderProfile: Profile
  {
    private IMapper _mapper;
    public OrderProfile(IMapper mapper)
    {
      _mapper = mapper;
      CreateMap<Order, OrderViewModel>();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Above we are setting up a simple mapping:

CreateMap<Order, OrderViewModel>();
1

Create our model and view model

Next let's create first our three model classes Customer, Order and OrderViewModel, like so:

// Customer.cs

namespace api.Models
  {
  public class Customer
  {
      public string Name { get; set; }
  }
}
1
2
3
4
5
6
7
8
9
// Order.cs

using System;

namespace api.Models
{
  public class Order
  {
    public Customer Customer { get; set; }
    public DateTime Created { get; set; }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

and lastly OrderViewModel:

// OrderViewModel.cs

namespace api.Models
{
  public class OrderViewModel
  {
    public string CustomerName { get; set; }
  }
}
1
2
3
4
5
6
7
8
9

Add a router class

Create a file OrderController.cs with the following content:

using AutoMapper;
using api.Models;
using System;

using Microsoft.AspNetCore.Mvc;

namespace api.Controllers
{
  [ApiController]
  [Route("[controller]")]
  public class OrderController
  {
    private IMapper _mapper;
    public OrderController(IMapper mapper)
    {
      _mapper = mapper;
    }

    [Route("/[controller]/[action]")]
    public string Get()
    {
      var orderViewModel = _mapper.Map<OrderViewModel>(new Order { 
        Customer = new Customer(){ Name = "Chris" }, 
        Created = DateTime.Now 
      });
      return orderViewModel.CustomerName;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Above we are constructing orderViewModel by passing it an Order instance.

Test it out

Running it, we get the following result:

As you can see above, this just works. It's able to take our Order class drill down into Order->Customer->Name and turn that into CustomerName.

Additional defaults

There are two additional defaults I wanted to show before wrapping up this article namely:

  • Reverse mapping, normally when we set up mappings we need to specify mapping in both directions, from model to view model and from the view model to model. If we have a case like the above we can actually use a method ReverseMap() that automatically adds configuration for mapping in the other direction
  • Method resolver, if we add methods to our class that name wise somewhat matches a field on the destination class then it will be invoked and will help us resolve our mapping

Reverse Mapping

Given the code from our complex case let's use the ReverseMap() method on our Order configuration. Let's open the file OrderProfile.cs and change the code to the following:

CreateMap<Order, OrderViewModel>()
  .ReverseMap();
1
2

Now open OrderController.cs and ensure the Get() method has the following code:

var orderViewModel = _mapper.Map<OrderViewModel>(new Order { 
  Customer = new Customer(){ Name = "Chris" }, 
  Created = DateTime.Now 
});

var order = _mapper.Map<Order>(orderViewModel);
var name = order.Customer.Name;

return orderViewModel.CustomerName;
1
2
3
4
5
6
7
8
9

Above we have added the row:

var order = _mapper.Map<Order>(orderViewModel);
var name = order.Customer.Name;
1
2

This converts our view model back to a model.

Method resolver

This is not the only default that works out of the box. We can help with the conversion of one object to another by adding helper methods. to the source object.

That sounds a bit cryptic, can you elaborate?

Sure. Say you have a User class with fields FirstName and LastName. Let's also say we have UserViewModel with a Fullname field. How would we resolve that in a conversion? Your first go-to is probably to set up a config for this using the method ForMember. There is another way though, using a method. By adding the following method to the User class:

public string GetFullname() {
  return string.Format("{0} {1}", this.FirstName, this.LastName);
}
1
2
3

We now have a way to map FirstName + LastName to a field FullName. Just think of the syntax is this way Get<Fieldname>().

A second example

This is powerful. Let's have a look at a new example. Imagine we have a Location class and a LocationViewModel class. Location looks like this:

namespace api.Models 
{
  public class Location
  {
    public string Street { get; set; }      
    public string City { get; set; }
    public string Country  { get; set; }
  }
}
1
2
3
4
5
6
7
8
9

by adding a method GetLocation() looking like this:

public string GetLocation() 
{
  return string.Format("{0} {1} {2}", Country, City, Street);
}
1
2
3
4

we don't actually have to define this mapping with the MemberFor() method.

What's really useful is that we can use the same approach to convert a LocationViewModel to a Location object by adding similar methods to LocationViewModel. Change LocationViewModel.cs to the following:

namespace api.Models
{
  public class LocationViewModel
  {
    public string Location { get; set; }

    public string GetCountry() 
    {
      return Location.Split(" ")[0];
    }

    public string GetCity()
    {
      return Location.Split(" ")[1];
    }

    public string GetStreet()
    {
      return Location.Split(" ")[2];
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Just like with our User/ UserViewModel scenario we can now write code like this, to convert a Location to a LocationViewModel and back again:

var locationViewmodel = _mapper.Map<LocationViewModel>(new Location() { 
  Street = "abc", 
  City= "Sthlm", 
  Country = "Sweden"  
});

var location = _mapper.Map<Location>(new LocationViewModel{   
  Location = "Country City Street" 
});
1
2
3
4
5
6
7
8
9

NOTE, Because we added methods on the LocationViewModel class, that we expect to be used converting from it to Location object - we need to add the ReverseMap() call to our LocationProfile configuration.

There is a more to learn about AutoMapper but hopefully, you've got a good idea of what to use it for and how it can help you and you can get started. Please check the reference section for more details.

Summary

So what did we learn? We learned that AutoMapper is a great library to convert between different types of objects. Additionally, we learned that we can spread out the configuration in different Profiles to make our code easier to maintain. Lastly, we've learned that AutoMapper comes with a lot of smart defaults that means we barely need to config if we name our fields in a clever way or add a helper method.