Question

I am mapping a Linq-to-SQL object to a domain model using AutoMapper. This Linq-to-SQL object has child objects. After mapping the Linq-to-SQL object to it's domain model all values are correct. However after mapping the domain model back to it's Linq-to-SQL counterpart all child objects lose their parent id.

I have narrowed it down to the SetElementValue() method in AutoMapper's EnumerableMapper class. The child object reference (an argument named mappedValue in SetElementValue()) keeps it's parent id up until it is added to the enumerable which in this case is an implementation of IBindingList. After being added to said implementation it's parent id is set to 0. I have compared all values in both the Linq-to-SQL object and domain model object, and all values are equal to their original state except for the parent id.

I am inclined to believe that this is related to the usage of IBindingList, does it make any sense at all that the parent id should be removed here? Am I overlooking something? I'm not sure about what source code you would require to provide any useful advice, just ask and you shall receive whatever you require.

Activity (parent) domain model definition:

public class Activity : IActivityEntity
{
    public int Id { get; set; }
    public string NameOrDescription { get; set; }
    public int ClientId;
    public int ActivityTypeId;
    public int PerformedByEmployeeId;
    public bool IsBillable;
    public DateTime PerformedDate { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime StopTime { get; set; }
    public string Description { get; set; }

    public int ProjectId;
    public int WorkOrderId;

    public IList<ProductLine> ProductLines;

    public Activity()
    {
        this.ProductLines = new List<ProductLine>();
    }

    public Activity(ActivityType activityType, int clientId, int employeeId, int workOrderId, DateTime startTime, DateTime stopTime, DateTime performedDate, string description)
    {
        this.ClientId = clientId;
        this.ActivityTypeId = activityType.Id;
        this.PerformedByEmployeeId = employeeId;
        this.Description = description ?? "";
        this.IsBillable = activityType.IsBillable;
        this.StartTime = startTime;
        this.StopTime = stopTime;
        this.PerformedDate = performedDate;
        this.WorkOrderId = workOrderId;
    }
}

ProductLine (child) domain model definition:

public class ProductLine : IEntity
{
    public int Id;
    public int ClientId;
    public int ProductId;
    public int ProductGroupId;
    public string ProductNumber;
    public int SupplierId;
    public string Name;
    public int ActivityId;
    public int WorkOrderId;

    public float Amount;
    public decimal InPriceWithoutVAT;
    public decimal OutPriceWithoutVAT;
    public bool IsBillable;
    public double? DiscountPercentage;
    public string Denomination;

    public bool CreatedBySystem;
    public bool StructureProductLine;

    public int SellerEmployeeId;
    public int ConsultantEmployeeId;

    public ProductLine()
    {
    }
}

Activity domain to DTO rules:

Mapper.CreateMap<Activity, D.Activity>()
            .ForMember(dto => dto.ActivityID, options => options.MapFrom(activity => activity.Id))
            .ForMember(dto => dto.ClientID, options => options.MapFrom(activity => activity.ClientId))
            .ForMember(dto => dto.ActivityTypeID, options => options.MapFrom(activity => activity.ActivityTypeId))
            .ForMember(dto => dto.Description, options => options.MapFrom(activity => activity.Description))
            .ForMember(dto => dto.StartTime, options => options.MapFrom(activity => activity.StartTime))
            .ForMember(dto => dto.StopTime, options => options.MapFrom(activity => activity.StopTime))
            .ForMember(dto => dto.IsBillable, options => options.MapFrom(activity => activity.IsBillable))
            .ForMember(dto => dto.WorkOrderID, options => options.MapFrom(activity => activity.WorkOrderId))
            .ForMember(dto => dto.PerformedByEmployeeID, options => options.MapFrom(activity => activity.PerformedByEmployeeId));

ProductLine domain to DTO rules:

Mapper.CreateMap<ProductLine, D.ProductLine>()
            .ForMember(dto => dto.ProductLineID, options => options.MapFrom(productLine => productLine.Id))
            .ForMember(dto => dto.ActivityID, options => options.MapFrom(productLine => productLine.ActivityId))
            .ForMember(dto => dto.Amount, options => options.MapFrom(productLine => productLine.Amount))
            .ForMember(dto => dto.Billable, options => options.MapFrom(productLine => productLine.IsBillable))
            .ForMember(dto => dto.ClientID, options => options.MapFrom(productLine => productLine.ClientId))
            .ForMember(dto => dto.Name, options => options.MapFrom(productLine => productLine.Name))
            .ForMember(dto => dto.OutPriceWithoutWAT, options => options.MapFrom(productLine => productLine.OutPriceWithoutVAT))
            .ForMember(dto => dto.ProductGroupID, options => options.MapFrom(productLine => productLine.ProductGroupId))
            .ForMember(dto => dto.ProductID, options => options.MapFrom(productLine => productLine.ProductId))
            .ForMember(dto => dto.ProductNumber, options => options.MapFrom(productLine => productLine.ProductNumber))
            .ForMember(dto => dto.StructureProductLine, options => options.MapFrom(productLine => productLine.StructureProductLine))
            .ForMember(dto => dto.SupplierID, options => options.MapFrom(productLine => productLine.SupplierId))
            .ForMember(dto => dto.InPriceWithoutWAT, options => options.MapFrom(productLine => productLine.InPriceWithoutVAT))
            .ForMember(dto => dto.Denomination, options => options.MapFrom(productLine => productLine.Denomination))
            .ForMember(dto => dto.SoldByEmployeeID, options => options.MapFrom(productLine => productLine.SellerEmployeeId))
            .ForMember(dto => dto.ConsultantEmployeeID, options => options.MapFrom(productLine => productLine.ConsultantEmployeeId))
            .ForMember(dto => dto.WorkOrderID, options => options.MapFrom(productLine => productLine.WorkOrderId));
Was it helpful?

Solution

I finally found a solution to my troubles. I don't have an answer for why this behaviour occurs, but I do have a way of circumventing it now.

Code:

Mapper.CreateMap<Activity, D.Activity>()
            .ForMember(dto => dto.ActivityID, options => options.MapFrom(activity => activity.Id))
            .ForMember(dto => dto.ClientID, options => options.MapFrom(activity => activity.ClientId))
            .ForMember(dto => dto.ActivityTypeID, options => options.MapFrom(activity => activity.ActivityTypeId))
            .ForMember(dto => dto.Description, options => options.MapFrom(activity => activity.Description))
            .ForMember(dto => dto.StartTime, options => options.MapFrom(activity => activity.StartTime))
            .ForMember(dto => dto.StopTime, options => options.MapFrom(activity => activity.StopTime))
            .ForMember(dto => dto.IsBillable, options => options.MapFrom(activity => activity.IsBillable))
            .ForMember(dto => dto.WorkOrderID, options => options.MapFrom(activity => activity.WorkOrderId))
            .ForMember(dto => dto.PerformedByEmployeeID, options => options.MapFrom(activity => activity.PerformedByEmployeeId))
            .ForMember(dto => dto.ProductLines, options => options.Ignore())
            .AfterMap((src, dest) =>
                {
                    foreach (var productLine in src.ProductLines)
                    {
                        dest.ProductLines.Add(Mapper.Map<D.ProductLine>(productLine));
                    }
                });

Explanation

The field ActivityID of D.ProductLine would always be set to zero with the previous AutoMapper rules for a reason I cannot explain, but only if D.ProductLine was a child of D.Activity or any other object. If I mapped only D.ProductLine on it's own the ActivityID would be correct. After 8 hours of headbashing and research I found Merenzo's answer over at Automapper overwrites missing source property on list with child objects which I could adapt to help me with my situation.

By telling AutoMapper to ignore Activity.ProductLines I could, like Merenzo, employ AfterMap() to do the mapping to D.ProductLine after all the other mapping had been finished. Thus mapping D.ProductLine without AutoMapper "realising" that it is a child object of D.Activity, denying it setting D.ProductLine.ActivityID to zero.

As mentioned I cannot explain this behaviour by AutoMapper, but I can now circumvent it for my needs. If anyone has a better solution or an explanation for this behaviour, please do not hesitate to post :)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top