2

My Problem

"Source" object Properties of the same class that do not exist in the View, are overwritting the same properties in the "Target" object with nulls. How do I prevent this? In affect how do I ensure only populated(not null) properties are merged into the "Target" object. I have also tried this with Automapper and failed, but I would be happy with an Automapper solution as an alternative.

I do appreciate that this "Null Mapping" question has appeared before, but I fear my situation is more involved since there are nested objects. Well I tried the suggested options and I could not get them to work. So here I am.

Huge gratitude for any help.

I appreciate this is a complex problem, and really, really appreciate any help with this, particularly if someone can produce a code sample for me. I have pulling my hair out over this for a few days :(

What I have attempted

I have 2 objects, one is the original("Target"), one("Source") is populated by a form ie a View.

The Original "Target" Object(myOrigDoc) looks like this:

enter image description here

The Form "Source" Object(myDoc) looks like this:

enter image description here

I then do the mapping:

            myOrigDoc.Introduction.InjectFrom<StrNotNull>(myDoc.Introduction);

using the following injector:

    public class StrNotNull: ConventionInjection
{
    bool blnNotNull = false;
    bool blnMatch = false;
    protected override bool Match(ConventionInfo c)
    {
        blnNotNull = false;
        blnMatch = false;

        //if ((c.SourceProp.Type == typeof(string)) && (c.SourceProp.Value != null))
        //    blnAssignable = true;

        if (c.SourceProp.Value != null)
            blnNotNull = true;

        if ((c.SourceProp.Name == c.TargetProp.Name) && (blnNotNull)) 
            blnMatch = true;

        return blnMatch;
    }
}

and I end up with:

enter image description here

The Form has no "DateOfBirth" field on it, therefore I suspect Model Binding is creating a null value for the "DataOfBirth" property, on the new "MyDoc" object, when I call:

        public ActionResult Index(Document myDoc)

Many thanks, Ed.

EDIT1: I believe this is a nested mapping problem due to the subclasses. Not sure how I sort this in ValueInjector.

EDIT2: Possible Automapper Solution from documentation for nested mappings, but I could not get it to work. I still get my nulls going across into the target.:

Mapper.CreateMap<XSD_Smart2.Document, XSD_Smart2.Document> 
().ForAllMembers(opt => opt.Condition(srs => !srs.IsSourceValueNull));

Mapper.CreateMap<XSD_Smart2.DocumentIntroduction, XSD_Smart2.DocumentIntroduction>  
().ForAllMembers(opt => opt.Condition(srs => !srs.IsSourceValueNull));

Mapper.CreateMap<XSD_Smart2.Client, XSD_Smart2.Client>().ForAllMembers(opt => 
opt.Condition(srs => !srs.IsSourceValueNull));
EdB
  • 449
  • 6
  • 21

2 Answers2

3

Update for ValueInjecter 3

public class IgnoreNulls : LoopInjection
{
    protected override void SetValue(object source, object target, PropertyInfo sp, PropertyInfo tp)
    {
        var val = sp.GetValue(source);
        if (val != null)
        {
            tp.SetValue(target, val);
        }
    }
}

previous version

create a custom injection that will have this behaviour:

    public class IgnoreNulls : ConventionInjection
    {
        protected override bool Match(ConventionInfo c)
        {
            return c.SourceProp.Name == c.TargetProp.Name
                  && c.SourceProp.Value != null;
        }
    }

and use it:

    target.InjectFrom<IgnoreNulls>(source);
Omu
  • 69,856
  • 92
  • 277
  • 407
  • Chuck, Thanks for this, but it does not seem to work. You will see from my debugger screens that I am dealing with properties that contain classes ie "Client". I was wondering whether the issue was to do with the fact that the form had a subset of the Class Properties, but the Model Binder would just set these properties to null anyway, and the ValueInjector would process the object and not the forms collection. I really appreciate your help. Thanks. Ed – EdB Nov 29 '12 at 23:34
  • After more research, I am convinced that the "IgnoreNulls" "ConventionInjection" is not being triggered as the parent Classes are being found which are not null. "Match" is used twice only for each property of the "Document" Class, ie "Introduction" and "InvestmentReview" which are themselves containers for subclasses. "Introduction" contains "Clients" which contains a list of "Client" where the nulls exist. So I guess it is a nesting problem. The Debug Screen shots illustrate my point. Thanks. – EdB Nov 30 '12 at 00:44
  • Hi, I think I need something like the "Deep Cloning" approach, as described on the VI CodePlex site, but does this take care of nested classes? Obviously there are the standard types like arrays, lists etc. I think I am getting warmer..... Ed – EdB Nov 30 '12 at 01:00
  • @EdB I recommend you to do it manually, you may use InjectFrom in your manual mapping code just for simple scenarios (no nesting) – Omu Nov 30 '12 at 19:10
  • Thanks for this. I was about to contact you :) Am I correct in thinking that I would need a far more involved version of "CloneInjection"? From the above example I am going down 2 levels of custom classes ie Document-> Introduction -> Client. – EdB Nov 30 '12 at 22:31
  • Also does "Unflattening" or "Flattening" help me in any way, since the above "scarcely" populated object represents the result of a posted form (view)? – EdB Nov 30 '12 at 22:36
  • A bit more info, the object is coming from a serialized XML document. It is this XML document that contains all the structure and indentation ie Document>Introduction>Clients. This has lots of advantages for me in the rest of the app, but possibly not when mapping between ViewModels and the original Document model. The concept seems simple since the objects are of identical type. It is just about updating the Original Object where "String" values exist in the source. Perhaps this is the clue, just "SetValue" where SourceProp.Value is a string and not null? The other Target values to be left. – EdB Nov 30 '12 at 22:50
  • 1
    Not sure if it'll help, but since you're playing with a serializable object you might be able to modify this to fit your needs: http://stackoverflow.com/a/78612/1505426 – Mightymuke Nov 30 '12 at 23:13
  • 1
    @Edb CloneInjection is to be used for source and target of the exact same type or at least exact same definition (same properties names&types), if this is what you need just try it (modify it and make ignore nulls (debug it and understand how it works)) , you canc also try Mightymuke's suggestion which is a very easy way to clone objects – Omu Nov 30 '12 at 23:18
  • @MightyMuke, thanks for your suggestion, but I do not think I need to clone an object. I just need to update the "Master" object with data(properties) from a "Form/View" Object of the same Class, ignoring Nulls. – EdB Dec 01 '12 at 03:15
  • The mapping should "update" the original object not "replace" it. – EdB Dec 01 '12 at 11:41
  • I am using this as the answer since I ended up going with VI with some customisation of the "CloneInjector" to handle updates. Huge thanks to all, and especially Chuck as he wrote VI. – EdB Dec 05 '12 at 12:41
  • @EdB I am having the exact same problem at the moment. Could you let me know if you succeeded using CloneInjector and maybe also elaborate which changes were necessary? Thanks! – leepfrog May 03 '14 at 21:09
  • Can you update example? In new versions of this nuget v3.x+, you should implement `PropertyInjection` class as `ConventionInjection` has been deprecated. – Korayem May 04 '16 at 11:57
  • 1
    @Korayem it's done, you can see more examples here: https://github.com/omuleanu/ValueInjecter/wiki/custom-injections-examples – Omu May 04 '16 at 13:41
1

This simple AutoMapper test works for me:

Classes

public class Client
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

AutoMapperConfiguration

public class MyProfile : Profile
{
    protected override void Configure()
    {
        CreateMap<Client, Client>()
            .ForAllMembers(opt => opt.Condition(src => !src.IsSourceValueNull));
    }
}

Unit Tests

[TestFixture]
public class MappingTests
{
    [Test]
    public void AutoMapper_Configuration_IsValid()
    {
        Mapper.Initialize(m => m.AddProfile<MyProfile>());
        Mapper.AssertConfigurationIsValid();
    }

    [Test]
    public void AutoMapper_ClientMapping_IsValid()
    {
        Mapper.Initialize(m => m.AddProfile<MyProfile>());
        Mapper.AssertConfigurationIsValid();

        var source = new Client
            {
                FirstName = "SourceFirstName1",
                LastName = null
            };

        var destination = new Client
            {
                FirstName = "DestinationFirstName1",
                LastName = "DestinationLastName1"
            };

        destination = Mapper.Map(source, destination);

        Assert.That(destination, Is.Not.Null);
        Assert.That(destination.FirstName, Is.EqualTo("SourceFirstName1"));
        Assert.That(destination.LastName, Is.EqualTo("DestinationLastName1"));
    }
}

UPDATE

Interestingly, when you use this mapping to map a list, it fails. IE - this test fails:

[Test]
public void AutoMapper_ClientListMapping_IsValid()
{
    Mapper.Initialize(m => m.AddProfile<MyProfile>());
    Mapper.AssertConfigurationIsValid();

    var source = new List<Client>
        {
            new Client
                {
                    FirstName = "SourceFirstName1",
                    LastName = null
                },
            new Client
                {
                    FirstName = null,
                    LastName = "SourceLastName2"
                }
        };

    var destination = new List<Client>
        {
            new Client
                {
                    FirstName = "DestinationFirstName1",
                    LastName = "DestinationLastName1"
                },
            new Client
                {
                    FirstName = "DestinationFirstName2",
                    LastName = "DestinationLastName2"
                }
        };

    destination = Mapper.Map(source, destination);

    Assert.That(destination, Is.Not.Null);
    Assert.That(destination.Count, Is.EqualTo(2));
    Assert.That(destination[0].FirstName, Is.EqualTo("SourceFirstName1"));
    Assert.That(destination[0].LastName, Is.EqualTo("DestinationLastName1"));
    //  /\  Line above went BANG!  /\
    Assert.That(destination[1].FirstName, Is.EqualTo("DestinationFirstName2"));
    Assert.That(destination[1].LastName, Is.EqualTo("SourceLastName2"));
}

This looks like a bug in AutoMapper (in 2.2.0 and 2.2.1-ci9000)

Mightymuke
  • 5,094
  • 2
  • 31
  • 42
  • Huge thanks for this. So it seems I am on the right track. Can you confirm that my code in EDIT2 reflects what I need from my debug screens ie Document, DocumentIntroduction and Client Classes? Huge thanks. Also do I have to deal with the "List of Clients" in some way? – EdB Nov 30 '12 at 02:20
  • Yes, your test reflects a simple class, and my "Clients" property contains a "List of Type Client". – EdB Nov 30 '12 at 02:27
  • Just realised there is no nesting of subclasses in your test, and I think this is the crux of my problem, hence the multiple "CreateMaps" in EDIT2. Thanks. – EdB Nov 30 '12 at 02:34
  • Interesting, not me being slow then.... Really appreciate your help here. Are they actively fixing bugs on AM? I have started an issue on AM On GitHub on this that points to this page. So we will see what happens. Could I use another type like array I wonder or what? I essentially trying to gather a group of class objects. – EdB Nov 30 '12 at 03:00
  • Yes, very active (check out the dates on their [nuget page](http://nuget.org/packages/automapper)), and Jimmy is very good at responding. – Mightymuke Nov 30 '12 at 03:02
  • The mapping should "update" the original object not "replace" it. – EdB Dec 01 '12 at 11:42
  • Thanks @Mightymuke, I really appreciate your help. I went with VI, as the still seems an issue with how AutoMapper handles nested lists. However should this situation change then I would be keep to relook at AM, since it is an excellent tool. Thanks again. – EdB Dec 05 '12 at 12:43