In this article, you will learn more about a good feature of Automapper - a fairy underrated property named Items which is only briefly described in the documentation.
Introduction
AutoMapper is a great tool to easily map between two classes. However, it can become dangerous if the mapping is between e.g., NHibernate Entities and DTOs (Data-Transfer-Objects). With NHibernate's lazy-loading feature, this can easily result in unwanted database selects. AutoMapper has some good features, providing the developer control over what it should and should not map during runtime via Queryable Extensions.
But there is also a farily underrated feature, hidden inside its ResolutionContext
-> Options
, which is only briefly mentioned in the documentation: Passing in key-value to Mapper.
I am talking about the little gem of a property named "Items
" which is simply a IDictionary<string, object>
.
With this, you can directly control back-to-back what data you want to receive from your database, which I will show you with a small example.
Background
Having Entities with References and Lists on one hand, and DTOs on the other hand, you might find that, AutoMapper will always try to map everything what it can. So you either have to create different DTOs for different use-cases, or, just use the magic of NHibernates lazy-loading and direct control via flags the front-end can pass directly down the remote-call.
Let's see how this works!
Using the Code
Consider this simple example of two classes: Customer
and Orders
.
(This is just a basic example, not to confuse with real company applications.)
On the one side, you got these NHibernate Entities:
public class Customer
{
public virtual long Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Order> Orders { get; set; }
}
public class Order
{
public virtual long Id { get; set; }
public virtual string ProductNumber { get; set; }
public virtual int Amount { get; set; }
}
And here are the DTOs:
public class CustomerDto
{
public long Id { get; set; }
public string Name { get; set; }
public IList<OrderDto> Orders { get; set; }
}
public class OrderDto
{
public long Id { get; set; }
public string ProductNumber { get; set; }
public int Amount { get; set; }
}
Now, let's say you have two use-cases:
- Retrieve only the User
- Retrieve the User with its orders
For this to work, we will need an extension-class for the IMappingOperationOptions
.
This extension will store and retrieve the flags from Options
-> Items
.
public static class OperationOptionExtensions
{
private const string ShouldIncludeOrdersForCustomerKey = "ShouldIncludeOrdersForCustomer";
private static bool GetBoolValue(IMappingOperationOptions options, string key)
{
if (options.Items.ContainsKey(key) && options.Items[key] is bool value)
{
return value;
}
return false;
}
public static void IncludeOrdersForCustomer(
this IMappingOperationOptions options,
bool include = true)
{
options.Items[ShouldIncludeOrdersForCustomerKey] = include;
}
public static bool ShouldIncludeOrdersForCustomer(this IMappingOperationOptions options)
{
return GetBoolValue(options, ShouldIncludeOrdersForCustomerKey);
}
}
Now we can configure our mapping profile by telling AutoMapper
to only map the Orders
if the flag is set using the PreCondition
operation:
public class AutoMapperConfig : Profile
{
public AutoMapperConfig()
{
CreateMap<Customer, CustomerDto>()
.ForMember(
customer => customer.Orders,
config => config.PreCondition(
context => context.Options.ShouldIncludeOrdersForCustomer()));
CreateMap<Order, OrderDto>();
}
}
The condition returns false
by default, so the Order
s are only mapped if the option is enabled during runtime.
Consider this simplified example of a database access-layer using NHibernate:
public class DBAccess
{
public ISession Session { get; set; }
private IMapper Mapper { get; }
public DBAccess()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<AutoMapperConfig>();
});
Mapper = config.CreateMapper();
}
public CustomerDto GetCustomerById(long customerId, bool includeOrders)
{
var customer = Session.Get<Customer>(customerId);
return Mapper.Map<CustomerDto>(customer, options =>
{
options.IncludeOrdersForCustomer(includeOrders);
});
}
}
If includeOrders
is set to true
, AutoMapper
will map the Orders
by triggering NHibernates lazy-loading.
If it is set to false
, AutoMapper
will not map the Orders
and the Order
list inside the DTO stays empty. NHibernate will not be lazy-loading the Orders
.
Of course, this can be done with any other Property and the possibillities of storing anything inside the Dictionary
of String
-Object
are endless. In this case, they are only flags to have a better control of what should be mapped.
Conclusion
Using AutoMapper's Operation-Options can give you direct control of the runtime-mapping, from front to back, resulting in fewer classes and shorter code.
Note
I am aware that you can also simply check for the includeOrders
by code, execute another query for the Orders
and fill them in manually. But this is just a basic example to raise awareness. I'm sure that other developers can use the Dictionary
for a lot of other things to control and manipulate AutoMapper
's behaviour.
Points of Interest
Honestly, when I first stumbled upon this simple Dictionary
, it blew my mind on how powerful this really is.
The first thing that came into my mind was this Extension, to better control AutoMappers behaviour without having to maintain too many different DTOs. I hope this will get the attention of other developers back to AutoMapper
, who didn't like it or switched to some other mapper instead.
Control is everything!
History
- 21st December, 2021: Initial version