Search code examples
c#automapperautomapper-9

AutoMapper: Issues mapping an ImmutableHashSet


I'm trying to Map and ReverseMap an ImmutableHashSet property to an ICollection using AutoMapper.

The Automapper successfully maps the ImmutableHashSet property to ICollection but it fails to map the ICollection back to ImmutableHashSet

Here is the minimal reproducible example:

Consider I have Order and OrderItem class as below:

public class Order
{
    private HashSet<OrderItem> _orderItems;

    public Order()
    {
        _orderItems = new HashSet<OrderItem>();
    }

    public ImmutableHashSet<OrderItem> OrderItems
    {
        get => _orderItems.ToImmutableHashSet();
        private set => _orderItems = value.ToHashSet();
    }

    public void AddOrderItem(OrderItem orderItem)
    {
        _orderItems.Add(orderItem);
    }

    public void RemoveOrderItem(OrderItem orderItem)
    {
        _orderItems.Add(orderItem);
    }
}

public class OrderItem
{
    public OrderItem(int orderId, string orderName)
    {
        OrderId = orderId;
        OrderName = orderName;
    }

    public int OrderId { get; private set; }


    public string OrderName { get; private set; }
}

The Order and OrderItem classes need to be mapped to below OrderDto and classes

public class OrderDto
{
    public ICollection<OrderItem> OrderItems { get; set; }
}

public class OrderItemDto
{

    public int OrderId { get; set; }


    public string OrderName { get; set; }
}

The OrderProfile class below defines the Automapper Profile to map Order to OrderDto and visa-versa.

public class OrderProfile : Profile
{
    public OrderProfile()
    {
        CreateMap<Order, OrderDto>()
            .ReverseMap();
        CreateMap<OrderItem, OrderItemDto>()
            .ReverseMap();
    }
}

public class OrderItemDto
{

    public int OrderId { get; set; }


    public string OrderName { get; set; }
}

Code to map and reverse map the Order and OrderDto classes:

private static void Map()
{
    var mapper = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile(new OrderProfile());
    }).CreateMapper();

    var order = new Order();
    order.AddOrderItem(new OrderItem(1, "Laptop"));
    order.AddOrderItem(new OrderItem(2, "Keyboard"));

    // This code maps correctly
    var orderDto = mapper.Map<OrderDto>(order);

    // This is where I get an exception
    var orderMappedBack = mapper.Map<Order>(orderDto);
}

On mapping the Order object from OrderDto, I get the following exception:

System.ArgumentException: System.Collections.Immutable.ImmutableHashSet`1[ReadOnlyCollectionDemo.OrderItem]
needs to have a constructor with 0 args or only optional args.
(Parameter 'type') at lambda_method(Closure , OrderDto , Order , ResolutionContext )

Any pointers to help fix this issue is highly appreciated.

Update

I somehow got it working through custom Converter. But I really do not know how it worked. I updated my mapper configuration as follows:

 var mapper = new MapperConfiguration(cfg =>
        {
            cfg.AddProfile(new OrderProfile());
            cfg.CreateMap(typeof(ICollection<>), typeof(ImmutableHashSet<>)).ConvertUsing(typeof(HashMapConverter<,>));
        }).CreateMapper();

My HashMapConverter class:

public class HashMapConverter<TCollection, TImmutableHashSet> 
        : ITypeConverter<ICollection<TCollection>, ImmutableHashSet< TImmutableHashSet>>
    {
        public ImmutableHashSet< TImmutableHashSet> Convert(
            ICollection<TCollection> source,
            ImmutableHashSet< TImmutableHashSet> destination,
            ResolutionContext context)
        {
            return ImmutableHashSet<TImmutableHashSet>.Empty;
        }
    }

As you can see I'm just returning an empty ImmutableHashSet, however, to my surprise the OrderDto is now successfully mapped back to Order with the correct number of OrderItems Count. I would have expected the Count to be 0 since I'm returning an empty HashSet. I suspect it is working because OrderItem and OrderItemDto are also mapped through AutoMapper.

I would like to confirm my hypothesis and know if this is a correct approach.


Solution

  • As it turns out mapping an ImmutableHashSet with the collection was not as straightforward as it seems. However, this made me realize that maybe I was not solving the correct problem. All I wanted was to ensure that my OrderItem HashSet cannot be modified outside the Order class. I could easily achieve this by returning an IReadOnlyCollection.

    Here's my updated Order class:

    public class Order
    {
        private HashSet<OrderItem> _orderItems;
    
        public Order()
        {
            _orderItems = new HashSet<OrderItem>();
        }
    
        public IReadOnlyCollection<OrderItem> OrderItems
        {
            get => _orderItems.ToImmutableHashSet();
            private set => _orderItems = value.ToHashSet();
        }
    
        public void AddOrderItem(OrderItem orderItem)
        {
            _orderItems.Add(orderItem);
        }
    
        public void RemoveOrderItem(OrderItem orderItem)
        {
            _orderItems.Add(orderItem);
        }
    }
    

    IReadOnlyCollection not only helps me to resolve the Automapper issue but in addition to this, it does not expose the Add method. Hence, a user cannot accidentally call order.OrderItems.Add(orderItem) method outside the Order class.