agileobjects / AgileMapper

A zero-configuration, highly-configurable, unopinionated object mapper with viewable execution plans. Flattens, unflattens, deep clones, merges, updates and projects queries. .NET 3.5+ and .NET Standard 1.0+.
MIT License
460 stars 27 forks source link

Exception using Map().Over() #216

Closed vsederburg closed 3 years ago

vsederburg commented 3 years ago

I use the Map().Over() all the time. The only thing I can see different here is that the dto being mapped implements an interface that has a Guid defined. Guid PlaceholderGuid { get; }

Code is:

protected void CloneChangesToMasterRecordList(T recordToClone) { var masterDto = MasterRecordList.FirstOrDefault(p => p.PlaceholderGuid == recordToClone.PlaceholderGuid); if (masterDto != null) { // Update the master list with this one recordToClone.Map().Over(masterDto); } }

An exception occurred mapping CustomFieldOptionListDto.OriginalPropertyValues -> CustomFieldOptionListDto.OriginalPropertyValues with rule set Overwrite."} | System.Exception {AgileObjects.AgileMapper.MappingException}

at AgileObjects.AgileMapper.ObjectPopulation.ObjectMapper2.Map(ObjectMappingData2 mappingData)\r\n at AgileObjects.AgileMapper.MappingExecutor1.PerformMapping[TTarget](TTarget target)\r\n at AgileObjects.AgileMapper.MappingExecutor1.PerformMapping[TTarget](MappingRuleSet ruleSet, TTarget target)\r\n at AgileObjects.AgileMapper.MappingExecutor1.Over[TTarget](TTarget existing)\r\n at mt.Shared.Base.MtListMaintParentRemoteViewModel1.CloneChangesToMasterRecordList(T recordToClone)

Files: [MapOverError.zip](https://github.com/agileobjects/AgileMapper/files/6587906/MapOverError.zip)
SteveWilkes commented 3 years ago

Hi,

Thanks for letting me know about this - the error looks to be happening cloning the OriginalPropertyValues dictionary. I'll take a look and report back.

Cheers,

Steve

SteveWilkes commented 3 years ago

Hello!

Sorry for the delay - I've been working on a code-generation project to enable build-time generation of Mapper source code, which I'm now going to test with this bug!

Unless I've missed something, I don't seem to have the sources for DtoClassMetadata and DtoPropertyMetadata - could you point me in the right direction?

Cheers,

Steve

SteveWilkes commented 3 years ago

Ok - I've mocked up the DtoMetadata classes and made a bit of progress - the generated mapping plan for CustomFieldOptionListDto looks like this:

cfoldToCfoldData =>
{
    Issue216.CustomFieldOptionListDto sourceCustomFieldOptionListDto;
    try
    {
        sourceCustomFieldOptionListDto = cfoldToCfoldData.Source;

        cfoldToCfoldData.Target.OptionId = sourceCustomFieldOptionListDto.OptionId;
        cfoldToCfoldData.Target.CustomColumnId = sourceCustomFieldOptionListDto.CustomColumnId;
        cfoldToCfoldData.Target.OptionValue = sourceCustomFieldOptionListDto.OptionValue;
        cfoldToCfoldData.Target.SortOrder = sourceCustomFieldOptionListDto.SortOrder;
        cfoldToCfoldData.Target.IsDefault = sourceCustomFieldOptionListDto.IsDefault;
        cfoldToCfoldData.Target.IsDeleted = sourceCustomFieldOptionListDto.IsDeleted;
        cfoldToCfoldData.Target.PlaceholderGuid = sourceCustomFieldOptionListDto.PlaceholderGuid;
        cfoldToCfoldData.Target.DataValueProperty = sourceCustomFieldOptionListDto.DataValueProperty;
        cfoldToCfoldData.Target.DisplayValueProperty = sourceCustomFieldOptionListDto.DisplayValueProperty;
        cfoldToCfoldData.Target.IsDefaultProperty = sourceCustomFieldOptionListDto.IsDefaultProperty;
        cfoldToCfoldData.Target.IsDirty = sourceCustomFieldOptionListDto.IsDirty;

        if (sourceCustomFieldOptionListDto.OriginalPropertyValues != null)
        {
            cfoldToCfoldData.Target.OriginalPropertyValues =
            {
                IObjectMappingData<Dictionary<string, object>, Dictionary<string, object>> sodToSodData;
                try
                {
                    sodToSodData = MappingDataFactory.ForChild(
                        sourceCustomFieldOptionListDto.OriginalPropertyValues,
                        cfoldToCfoldData.Target.OriginalPropertyValues,
                        cfoldToCfoldData.ElementIndex,
                        cfoldToCfoldData.ElementKey,
                        "OriginalPropertyValues",
                        0,
                        cfoldToCfoldData);

                    var stringObjectDictionary = sodToSodData.Target ?? new Dictionary<string, object>(sodToSodData.Source.Comparer);
                    var i = 0;
                    var enumerator = sodToSodData.Source.GetEnumerator();
                    try
                    {
                        while (true)
                        {
                            if (!enumerator.MoveNext())
                            {
                                break;
                            }

                            var originalPropertyValuesKey = enumerator.Current.Key;

                            if (enumerator.Current.Value == null)
                            {
                                stringObjectDictionary[originalPropertyValuesKey] = default(object);
                                ++i;
                                continue;
                            }

                            stringObjectDictionary[originalPropertyValuesKey] = enumerator.Current.Value.GetType().IsSimple()
                                ? enumerator.Current.Value
                                : sodToSodData.Map(
                                    enumerator.Current.Value,
                                    default(object),
                                    i,
                                    originalPropertyValuesKey);
                            ++i;
                        }
                    }
                    finally
                    {
                        enumerator.Dispose();
                    }

                    return stringObjectDictionary;
                }
                catch (Exception ex)
                {
                    throw MappingException.For(
                        "Overwrite",
                        "Issue216.CustomFieldOptionListDto.OriginalPropertyValues",
                        "Issue216.CustomFieldOptionListDto.OriginalPropertyValues",
                        ex);
                }
            }
        }
        else
        {
            cfoldToCfoldData.Target.OriginalPropertyValues = default(Dictionary<string, object>);
        }

        cfoldToCfoldData.Target.IsLinkedToDatabase = sourceCustomFieldOptionListDto.IsLinkedToDatabase;
        // No data sources for SetNotLinkedToDatabase

        return cfoldToCfoldData.Target;
    }
    catch (Exception ex)
    {
        throw MappingException.For(
            "Overwrite",
            "Issue216.CustomFieldOptionListDto",
            "Issue216.CustomFieldOptionListDto",
            ex);
    }
}

I can't see anything obvious which would throw an exception - the OriginalPropertyValues dictionary cloning is done item-by-item as the values are of type object, but they should all fall into the GetType().IsSimple() branch as the only properties included in CustomFieldOptionListDto are int, string and bool.

Do you get the error if you try ignore OriginalPropertyValues? Looks like it's self-populating in any case and probably doesn't need to be mapped...

vsederburg commented 3 years ago

Oh man, I think I get to be a bit embarassed. Yes, marking OriginalPropertyValues to not map does seem to fix it. However, after it wasn't erroring, I realized the save wasn't quite working as expected, and that's when I realized that I was actually calling the Map().Over() to map a record over itself.

The code I provided was:

protected void CloneChangesToMasterRecordList(T recordToClone) { var masterDto = MasterRecordList.FirstOrDefault(p => p.PlaceholderGuid == recordToClone.PlaceholderGuid); if (masterDto != null) { // Update the master list with this one recordToClone.Map().Over(masterDto); } }

but it SHOULD have been:

protected void CloneChangesToMasterRecordList(T recordToClone) { var dto = LocalRecordList.FirstOrDefault(p => p.PlaceholderGuid == recordToClone.PlaceholderGuid); if (dto != null) { // Update the master list with this one recordToClone.Map().Over(masterDto); } }

Basically, find the record in my local list, and then copy it over the corresponding master list record.

Once I fixed that, I removed the "do not map" attribute from OriginalPropertyValues, and it still works fine. So if anything, maybe the mapper could throw a warning if you're mapping an object over itself by mistake? Like if the source and target hash matches? Not sure. You're the expert there... but I think I probably wasted your time with my own error. Very sorry about that.

Thanks for a great program, I truly appreciate it.

SteveWilkes commented 3 years ago

Haha! Oh well - all's well that ends well :) Glad it works now - maybe I'll pop a reference equals check in there somewhere.

Glad you find the mapper useful!

All the best,

Steve