Intro
In this post I will focus on two things:
- Why should we map our models (database, external api dtos...) to a different object before exposing the data in a API response?
- How do we map our models?
Background
Imagine that we're building an API that returns orders. Our domain object looks like this, one abstract Order
class and then multiple concrete implementations of it, we will focus on BusinessOrder
(B2B).
Order
public abstract class Order<TBillingAddress, TShippingAddress, TCustomer>
{
protected Order(
int id,
TBillingAddress billing,
TShippingAddress shipping,
TCustomer customer,
OrderDetails orderDetails)
{
Id = id;
Billing = billing;
Shipping = shipping;
Customer = customer;
OrderDetails = orderDetails;
}
public int Id { get; }
public abstract string Type { get; }
public TBillingAddress Billing { get; }
public TShippingAddress Shipping { get; }
public TCustomer Customer { get; }
public OrderDetails OrderDetails { get; }
}
public class BusinessOrder : Order<BusinessBillingAddress, BusinessShippingAddress, BusinessCustomer>
{
public BusinessOrder(
int id,
BusinessBillingAddress billing,
BusinessShippingAddress shipping,
BusinessCustomer customer,
OrderDetails orderDetails) : base(
id,
billing,
shipping,
customer,
orderDetails)
{
}
public override string Type => "BUSINESS";
}
You don't need to know how the BusinessBillingAddress
, BusinessShippingAddress
or OrderDetails
look like, we will focus on BusinessCustomer
in this post.
BusinessCustomer
public abstract class Customer
{
protected Customer(string id)
{
Id = id;
}
public string Id { get; }
}
public class BusinessCustomer : Customer
{
public BusinessCustomer(
string id,
string companyName,
string organizationalNumber,
string description) : base(id)
{
CompanyName = companyName;
OrganizationalNumber = organizationalNumber;
Description = description;
}
public string CompanyName { get; }
public string OrganizationalNumber { get; }
public string Description {get; }
}
Why should we map our models?
Now, imagine that we have the following customer:
var businessCustomer = new BusinessCustomer(
"bc-1234",
"Dunder Mifflin Paper",
"12345678",
"Sells paper, very annoying boss"
);
Then they want to use V1 of our API that looks like this:
public async Task<IActionResult> Get(int orderId)
{
var order = await _getOrderQuery.Execute(orderId);
return new ObjectResult(order);
}
The response would look something like this:
{
"orderId": 1234,
"type": "BUSINESS",
"billing": {......},
"shipping": {......},
"orderDetails": {......},
"customer": {
"id": "bc-1234",
"companyName": "Dunder Mifflin Paper",
"organizationalNumber": "12345678",
"description": "Sells paper, very annoying boss"
}
}
A few hours later you receive a phone call from someone called Michael Scott who yells like crazy and screams something about a hitman named Dwight.
So, what happened here?
We exposed secret internal data about our customer in the API response. Not good.
How can we prevent this from happening again?
How do we map our models?
"just put a
JsonIgnore
attribute on the description property and get on with your life"
— Non-caring developer
Sure, that would work, but I don't want to clutter my domain models with attributes that are specific for serialization.
And what if I want to rename the companyName
to just name
in the response? Should I just rename the property in my domain model or what?
Or what if I want to group the Billing
and Shipping
address together in the response? Should I remodel my domain model to make it work?
Of course not, that's why we're going to map it.
Meet OrderResponseDto
.
public abstract class OrderResponseDto
{
public int OrderId { get; }
public abstract string Type { get; }
public OrderDetailsResponseDto OrderDetails { get; }
protected OrderResponseDto(int orderId, OrderDetailsResponseDto orderDetails)
{
OrderId = orderId;
OrderDetails = orderDetails;
}
}
public class BusinessOrderResponseDto : OrderResponseDto
{
public BusinessOrderResponseDto(
int orderId,
BusinessAddressResponseDto address,
BusinessCustomerResponseDto customer,
OrderDetailsResponseDto orderDetails) : base(
orderId,
orderDetails)
{
Address = address;
Customer = customer;
}
public override string Type => "BUSINESS";
public BusinessAddressResponseDto Address { get; }
public BusinessCustomerResponseDto Customer { get; }
}
The BusinessCustomerResponseDto
looks like this:
public class BusinessCustomerResponseDto
{
public BusinessCustomerResponseDto(
string id,
string name,
string organizationalNumber)
{
Id = id;
Name = name;
OrganizationalNumber = organizationalNumber;
}
public string Id { get; }
public string Name { get; }
public string OrganizationalNumber { get; }
}
Now, if we were to populate our BusinessOrderResponseDto
and return that one instead of the domain object the response would look something like this:
{
"orderId": 1234,
"type": "BUSINESS",
"address": {
"billing": {.....},
"shipping": {....}
},
"orderDetails": {......},
"customer": {
"id": "bc-1234",
"name": "Dunder Mifflin Paper",
"organizationalNumber": "12345678"
}
}
Look! We have now managed to rename companyName
to name
and we are no longer leaking the description
, NICE!
But...how do we create the response object?
Right now our controller looks like this, we are new:ing it up and all our mapping takes place in the controller.
public async Task<IActionResult> Get(int orderId)
{
var order = await _getOrderQuery.Execute(orderId);
var response = new BusinessOrderResponseDto(.....);
return new ObjectResult(response);
}
That works, but it's not optimal, let's fix it!
Before showing the different approaches (you can stop screaming "just use AutoMapper!!"), think of the following:
- Do we really need to introduce a new dependency just for this?
- All models are immutable, both domain and repsonse.
Option 1 - OrderResponseDtoFactory
This has been my prefered way of doing mappings for a long time. I create a factory class that I register as a singleton and then inject it wherever I need it.
OrderResponseDtoFactory
public class OrderResponseDtoFactory
{
public BusinessOrderResponseDto Create(BusinessOrder order)
{
var billingAddress = new BusinessAddressBillingResponseDto(
order.Billing.Receiver.CompanyName,
order.Billing.Receiver.Reference,
order.Billing.CareOf,
order.Billing.City,
order.Billing.Street,
order.Billing.Zip,
order.Billing.Country);
var shippingAddress = order.Shipping != null
? new BusinessAddressShippingResponseDto(
order.Shipping.Receiver.CompanyName,
order.Shipping.Receiver.Reference,
order.Shipping.CareOf,
order.Shipping.City,
order.Shipping.Street,
order.Shipping.Zip,
order.Shipping.Country)
: null;
var orderDetailsResponseDto = new OrderDetailsResponseDto(
order.OrderDetails.TotalPrice,
order.OrderDetails.OrderRows.Select(x => new OrderRowResponseDto(
x.Name,
x.ProductId,
x.Quantity,
x.Price,
x.Vat,
x.VatPercentage
))
);
return new BusinessOrderResponseDto(
order.Id,
new BusinessAddressResponseDto(
billingAddress,
shippingAddress
),
new BusinessCustomerResponseDto(
order.Customer.Id,
order.Customer.CompanyName,
order.Customer.OrganizationalNumber),
orderDetailsResponseDto);
}
}
Usage
public async Task<IActionResult> Get(int orderId)
{
var order = await _getOrderQuery.Execute(orderId);
var mapped = _orderResponseDtoFactory.Create(order);
return new ObjectResult(mapped);
}
Pros
- All mapping logic in one place
- Testable
- Easy to inject other dependencies (other factories for example)
Cons
- You need to take a new dependency on the factory wherever you want to map. In our case this will just be in our controller so it's not that big of a deal, but could be in other cases.
Option 2 - AutoMapper
First, HUGE DISCLAIMER:
AutoMapper is a really really really nice library IF it fits your use case. I have never used AutoMapper like this before (with immutable models) so take all the config/benchmarking with a huge grain of salt.
I recommend everyone to read this article written by Jimmy Bogard (the creator of AutoMapper) regarding AutoMapper's design philosophy.
And this: Usage guidelines
The "Auto" is for "automatic" and if it's not "Auto" then don't use this library, it will make things more, not less complicated.
-- Jimmy Bogard
I chose to add AutoMapper here just because of how widely used it is and it seems to be the go-to for a lot of developers as soon as any mapping needs to be done. I've worked in projects where the mapping config has been several hundred lines of code with a bunch of hacks here and there and it has been quite obvious that AutoMapper should not have been used.
I believe in choosing the right tool for the job and in this case, as you will see, AutoMapper might not be the right tool.
AutoMapper works something like this:
var config = new MapperConfiguration(cfg => cfg.CreateMap<BusinessOrder, BusinessOrderResponseDto>());
var mapper = config.CreateMapper();
var businessOrderResponseDto = mapper.Map<BusinessOrderResponseDto>(order);
So we need to configure AutoMapper before using it, seems easy enough.
Usage
public async Task<IActionResult> Get(int orderId)
{
var order = await _getOrderQuery.Execute(orderId);
var mapped = Mapper.Map<BusinessOrderResponseDto>.Map(order);
return new ObjectResult(mapped);
}
There's only one problem though. The above code does not work. Since my models are immutable, AutoMapper can't automatically map them. So we need to modify our config a bit.
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<BusinessOrder, BusinessOrderResponseDto>()
.ConstructUsing(x => new BusinessOrderResponseDto(
x.Id,
new BusinessAddressResponseDto(
new BusinessAddressBillingResponseDto(
x.Billing.Receiver.CompanyName,
x.Billing.Receiver.Reference,
x.Billing.CareOf,
x.Billing.City,
x.Billing.Street,
x.Billing.Zip,
x.Billing.Country),
new BusinessAddressShippingResponseDto(
x.Shipping.Receiver.CompanyName,
x.Shipping.Receiver.Reference,
x.Shipping.CareOf,
x.Shipping.City,
x.Shipping.Street,
x.Shipping.Zip,
x.Shipping.Country)),
new BusinessCustomerResponseDto(
x.Customer.Id,
x.Customer.CompanyName,
x.Customer.OrganizationalNumber),
new OrderDetailsResponseDto(
x.OrderDetails.TotalPrice,
x.OrderDetails.OrderRows.Select(r => new OrderRowResponseDto(
r.Name,
r.ProductId,
r.Quantity,
r.Price,
r.Vat,
r.VatPercentage)))));
Im not an expert on AutoMapper so there is most certainly a better/simpler way of creating the mapper but right now, there isn't that much auto in the above mapping.
Now we can use AutoMapper to map from BusinessOrder
to a BusinessOrderResponseDto
Usage
public async Task<IActionResult> Get(int orderId)
{
var order = await _getOrderQuery.Execute(orderId);
var mapped = _mapper.Map<BusinessOrderResponseDto>.Map(order);
return new ObjectResult(mapped);
}
Pros
- All mapping logic in one place
- Testable
- ...we are using AutoMapper, like everyone else?
Cons
- There isn't anything auto going on, we still need to create all the mapping configuration ourselves since we are using immutable objects.
- Performance (as we will see in the benchmarks section)
Option 3 - Implicit/Explicit mapping
If you don't need any external dependencies (inject other services in your factory for example) to do your mappings, then Implicit/Explicit mappings could be a great option.
OrderResponseDto
public static implicit operator BusinessOrderResponseDto(BusinessOrder order)
{
var billing = (BusinessAddressBillingResponseDto) order.Billing;
var shipping = order.Shipping == null
? null
: (BusinessAddressShippingResponseDto) order.Shipping;
var customer = (BusinessCustomerResponseDto) order.Customer;
var orderDetails = (OrderDetailsResponseDto) order.OrderDetails;
return new BusinessOrderResponseDto(
order.Id,
new BusinessAddressResponseDto(billing, shipping),
customer,
orderDetails);
}
By adding the following operator to my code, I can now do my mappings like this:
(Note, I have implemented the implicit operator in the billing, shipping, customer and orderDetails classes as well but left it out for clarity).
Usage
public async Task<IActionResult> Get(int orderId)
{
var order = await _getOrderQuery.Execute(orderId);
BusinessOrderResponseDto mapped = _businessOrder;
return new ObjectResult(mapped);
}
public async Task<IActionResult> Get(int orderId)
{
var order = await _getOrderQuery.Execute(orderId);
var mapped = (BusinessOrderResponseDto)_businessOrder;
return new ObjectResult(mapped);
}
public async Task<BusinessOrderResponseDto> Get(int orderId)
{
return await _getOrderQuery.Execute(orderId);
}
Pretty neat right?
Pros
- All mapping logic in one place
- Testable
Cons
- You can't inject dependencies.
Benchmarks
I like to meassure things so I ran a quick benchmark and here's the result.
There is more or less no difference between using a factory or explicit/implicit mapping. Use what works best for your use case, if you need external dependencies, use a factory and inject them.
If not, go for implicit/explicit mapping.
And, as I said before, AutoMapper is not the right tool for the use case in this post, ~6.5 times slower (and allocates more as well).
All code for this post can be found here, complete with benchmark code and tests.