DEV Community

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

Posted on • Originally published at softchris.github.io

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 has a whole host of supporting libraries so we have chosen a specific approach, namely to use the version of AutoMapper that uses Dependency Injection. I should say that in the past I have not used AutoMapper with DI but straight up AutoMapper. This approach is meant to be used with ASP.NET Core where DI is a central part of your app. DI brings benefits of being able to test, as it is easy to mock out dependencies.

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
Enter fullscreen mode Exit fullscreen mode

Install NuGet package

dotnet add api package AutoMapper.Extensions.Microsoft.DependencyInjection
Enter fullscreen mode Exit fullscreen mode

 Configuration

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

services.AddAutoMapper(typeof(Startup));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

CreateMap<User, UserViewModel>();
Enter fullscreen mode Exit fullscreen mode

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; }
  }
}
Enter fullscreen mode Exit fullscreen mode

now the UserModel:

namespace api.Models 
{
  public class UserModel
  {
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
  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;
}
Enter fullscreen mode Exit fullscreen mode

Ok, let's try to run this with:

dotnet build
dotnet run
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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>();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Above we are setting up a simple mapping:

CreateMap<Order, OrderViewModel>();
Enter fullscreen mode Exit fullscreen mode

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; }
  }
}
Enter fullscreen mode Exit fullscreen mode
// Order.cs

using System;

namespace api.Models
{
  public class Order
  {
    public Customer Customer { get; set; }
    public DateTime Created { get; set; }
  }
}
Enter fullscreen mode Exit fullscreen mode

and lastly OrderViewModel:

// OrderViewModel.cs

namespace api.Models
{
  public class OrderViewModel
  {
    public string CustomerName { get; set; }
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Above we have added the row:

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

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);
}
Enter fullscreen mode Exit fullscreen mode

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; }
  }
}
Enter fullscreen mode Exit fullscreen mode

by adding a method GetLocation() looking like this:

public string GetLocation() 
{
  return string.Format("{0} {1} {2}", Country, City, Street);
}
Enter fullscreen mode Exit fullscreen mode

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];
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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" 
});
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (7)

Collapse
costinmanda profile image
Costin Manda

I had to work on a project that used AutoMapper and I hated it. Yes, it has a lot of features and as you move from similar models and entities to more complex scenarios it has support for most of them, but debugging becomes a nightmare. Here are the issues I have with AutoMapper:

  • it uses reflection, so you can't see which properties are being used and which are not
  • since it maps properties with the same names, it promotes isomorphism between models and entities, which is an antipattern
  • it's hard to determine the source of bugs and then to subsequently debug problems in the AutoMapper configuration
  • it hinders testability

My solution: use dependency injection and inject IMapper when you need mapping. Implement those mappers however you see fit (using, if you must, AutoMapper).

I changed all automapping with this kind of solution in my project and never looked back. It allowed for async mapping, injection of services when I needed extra information (like turning some id into a value or getting details from additional source) and everything was clearly defined in terms of intent and implementation as well as unit testable at every level.

Collapse
softchris profile image
Chris Noring Author

hi Costin. I appreciate you writing this down. I've never faced that much of a problem as you describe above. I think most libraries have a sweet spot where they shine vs don't shine

Collapse
lftrejo profile image
lftrejo

What’s the advantage of using this instead of implicit operator or explicit operator?

Collapse
victorioberra profile image
Victorio Berra • Edited

Automapper has more quality of life features like built in mapping validation and helpful errors and it protects you against forgetting to map properties. Also it makes testing your app much easier since it's got the built in DI stuff so you could technically inject a mock mapper.

It's whole purpose is to make your life easier when mapping objects. You should use whatever works best for you.

Collapse
thfsilvab profile image
Thadeu

Thank you for making this question, I am really curious about it too.

Collapse
binarypatrick profile image
Patrick M • Edited

I recently found automapper and am really sold on the ease of use and power it offers. It's a really great product.

I am a bit confused about the DI piece though. I don't get why you need to specify the startup class or why it works in that way? I've never seen DI like that. I've kinda gone my own way and created a Singleton I inject on my own instead of using their functionality.

My question is, what is the DI doing with startup class? I can't see a benefit as I still have to define my maps.