Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

AutoMapper Runtime Mapping Control via IMappingOperationOptions

4.00/5 (3 votes)
20 Dec 2021CPOL3 min read 9.1K  
A short look into the possibilities of AutoMapper's operation options
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:

C#
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:

C#
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.

C#
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:

C#
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 Orders are only mapped if the option is enabled during runtime.

Consider this simplified example of a database access-layer using NHibernate:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)