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
Good blog post on AutoMapper A quite good article that captures the important aspects of the library.
Official docs for AutoMapper Official documentation
GitHub repo for AutoMapper The official GitHub repo
Docs pages for .NET If you are completely new to .NET, have a look here.
Getting started with .NET Core Good intro to .NET Core.
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?
- Map from one type of object to another type
- 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
- 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?
- Install and Set up, let's look at how to get started by installing the needed libraries
- 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.
- Basic scenario, let's look at a basic scenario in which we transform from one type
- 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
2
3
Install NuGet package
dotnet add api package AutoMapper.Extensions.Microsoft.DependencyInjection
Configuration
First we need to add this line too Startup.cs
and the method ConfigureServices()
:
services.AddAutoMapper(typeof(Startup));
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
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>();
}
}
}
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>();
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; }
}
}
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; }
}
}
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>();
}
}
}
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));
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;
}
}
}
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.
- The constructor, here we can see that we have injected an instance of
IMapper
calledmapper
.
private IMapper _mapper;
public DataController(IMapper mapper)
{
_mapper = mapper;
}
2
3
4
5
- 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 calledUserController
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;
}
2
3
4
5
6
Ok, let's try to run this with:
dotnet build
dotnet run
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);
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; }
}
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; }
}
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 }
}
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
andOrderViewModel
, 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>();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Above we are setting up a simple mapping:
CreateMap<Order, OrderViewModel>();
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; }
}
}
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; }
}
}
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; }
}
}
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;
}
}
}
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();
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;
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;
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);
}
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; }
}
}
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);
}
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];
}
}
}
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"
});
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 toLocation
object - we need to add theReverseMap()
call to ourLocationProfile
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.