AutoMapper Runtime Mapping Control via IMappingOperationOptions
A short look into the possibilities of AutoMapper's operation options
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
{
// List of different keys for the Items Dictionary
private const string ShouldIncludeOrdersForCustomerKey = "ShouldIncludeOrdersForCustomer";
// ----
/// <summary>
/// Retreives a bool value from the Items Dictionary by key
/// </summary>
private static bool GetBoolValue(IMappingOperationOptions options, string key)
{
if (options.Items.ContainsKey(key) && options.Items[key] is bool value)
{
return value;
}
return false;
}
/// <summary>
/// Saves the bool value, whenever or not the mapping should include
/// the Orders List for Customer
/// </summary>
public static void IncludeOrdersForCustomer(
this IMappingOperationOptions options,
bool include = true)
{
options.Items[ShouldIncludeOrdersForCustomerKey] = include;
}
/// <summary>
/// Mapping in Profile requests, whenever or not it should include
/// the Orders List for Customer
/// </summary>
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>()
// The PreCondition retreives the stored value
// from the Items Dictionary inside the mapping options
.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
{
// Assume Dependency-Injection of NHibernate Session here
public ISession Session { get; set; }
private IMapper Mapper { get; }
public DBAccess()
{
// Profile and configuration
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<AutoMapperConfig>();
});
// Also typically by Dependency-Injection
Mapper = config.CreateMapper();
}
/// <summary>
/// Data-Access method: includeOrders will be requested by the front-end
/// </summary>
public CustomerDto GetCustomerById(long customerId, bool includeOrders)
{
// Retreive the Customer from database
var customer = Session.Get<Customer>(customerId);
// We directly inject the clients request for the Orders
// into this mapping operation option
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