tvirch
tvirch

Reputation: 67

C# - Merging List of Objects into One Object

I get passed a list of small objects:

var smalls = new List<Small>();
smalls.AddRange( new Small[] { new Small{Name = "Aa", Id = 1, Value = "v1"},
                               new Small{Name = "Bb", Id = 1, Value = "v2"},
                               new Small{Name = "Cc", Id = 1, Value = "v3"},
                               new Small{Name = "Dd", Id = 1, Value = "v4"},
                               new Small{Name = "Ee", Id = 1, Value = "v5"},
                               new Small{Name = "Ff", Id = 1, Value = "v6"},
                               new Small{Name = "Gg", Id = 1, Value = "v7"} } );

From the above list I would like to populate an object that looks like this:

var large = new Large
    {
        Id = 1,
        Aa = "v1",
        Bb = "v2",
        Cc = "v3",
        Dd = "v4",
        Ee = "v5",
        Ff = "v6",
        Gg = "v7"
    }

The current code relies on the order of the list to populate the Large object however this does not feel secure enough and am looking for a more reliable way to map the list into the object.

Current code:

Large large = new Large
{
    Id = smalls[0].Id,
    Aa = smalls[0].Value,
    Bb = smalls[1].Value,
    Cc = smalls[2].Value,
    Dd = smalls[3].Value,
    Ee = smalls[4].Value,
    Ff = smalls[5].Value,
    Gg = smalls[6].Value
}

So I am looking to eliminate the assumption that they are in the correct order and populate the new fields based off of the Name string in the Small object into the corresponding field in the Large object.

Thanks for any input!!

Upvotes: 5

Views: 1958

Answers (5)

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

You can group values by Id, and make a few methods to extract values based on the Name field:

private static string GetValueByName(IDictionary<string,string> data, string name) {
    string res;
    return data.TryGetValue(name, out res) ? res : null;
}
private static Large MakeFromAttributes(IEnumerable<Small> data, int id) {
    var dictByName = data.ToDictionary(s => s.Name, s => s.Value);
    return new Large {
        Id = id
    ,   Aa = GetValueByName(dictByName, "Aa")
    ,   Bb = GetValueByName(dictByName, "Bb")
    ,   Cc = GetValueByName(dictByName, "Cc")
    ,   Dd = GetValueByName(dictByName, "Dd")
    ,   Ee = GetValueByName(dictByName, "Ee")
    ,   Ff = GetValueByName(dictByName, "Ff")
    ,   Gg = GetValueByName(dictByName, "Gg")
    };
}

With these helper methods you can construct a LINQ query as follows:

var largeList = smalls
    .GroupBy(s => s.Id)
    .Select(g => MakeFromAttributes(g, g.Key))
    .ToList();

Upvotes: 3

Dustin Kingen
Dustin Kingen

Reputation: 21245

A Dictionary(TKey, TValue) may be a more appropriate data structure for the use case since the Keys can be dynamic and any solution going from IEnumerable(Small) to Large will need to make assumptions on the composition of the collection or the aggregate object.

A simple dynamic solution is to use reflection, but this will have more overhead compared to the static lookup already proposed.

public static Large CreateLargeFromSmalls(int id, IEnumerable<Small> smalls)
{
    var largeType = typeof(Large);
    var large = new Large { Id = id };

    foreach (var small in smalls)
    {
        var prop = largeType.GetProperty(small.Name);
        if (prop != null)
        {
            prop.SetValue(large, small.Value);
        }
    }

    return large;
}

Assumptions

  • The Name of a Small will exactly match the corresponding Large property.
  • The Name of a Small is not unique and order of iteration matters.
  • If the property corresponding to the Name of a Small does not existing in the Large then it is not mapped.

Pros

  • Contract changes to Large do not affect the mapping logic.

Cons

  • Reflection overhead

Example:

var smalls = new List<Small>
{
    new Small{Name = "Aa", Id = 1, Value = "v1"},
    new Small{Name = "Bb", Id = 1, Value = "v2"},
    new Small{Name = "Cc", Id = 1, Value = "v3"},
    new Small{Name = "Dd", Id = 1, Value = "v4"},
    new Small{Name = "Ee", Id = 1, Value = "v5"},
    new Small{Name = "Ff", Id = 1, Value = "v6"},
    new Small{Name = "Gg", Id = 1, Value = "v7"}
};

var bigs =
    smalls
        .GroupBy(x => x.Id)
        .Select(g => CreateLargeFromSmalls(g.Key, g))
        .ToList();

Upvotes: 2

degant
degant

Reputation: 4981

If I understood your question correctly, you can use reflection to set all properties in the Large class without having to worry about order of Small collection:

Large large = new Large();
foreach (var propertyInfo in large.GetType().GetProperties())
{
    var sm = smalls.FirstOrDefault(small => string.Equals(small.Name, propertyInfo.Name, StringComparison.InvariantCultureIgnoreCase));
    if (sm != null)
        propertyInfo.SetValue(large, Convert.ChangeType(sm.Value, propertyInfo.PropertyType), null);
}

Important: Please note that for this solution to work, all properties of Large that need to be updated MUST be marked with getters and setters. eg. public string Aa { get; set; }

We first get all properties of Large by using large.GetType().GetProperties() which gets all properties. Then we compare the names with the .Name property in the Small class collection and if we find a match then we set the value of the property. You can read more about reflection here.

Screenshot of Large after trying it out:

Upvotes: 0

Ruben Vardanyan
Ruben Vardanyan

Reputation: 1308

You can group your list of Small's, then set Id as the Key of the group and other properties using reflection, will look like this

var large = smalls.GroupBy(small => small.Id)
    .Select(group =>
    {
        var result = new Large();
        result.Id = group.Key;
        var largeType = result.GetType();
        foreach (var small in group)
        {
                largeType.GetProperty(small.Name).SetValue(result, small.Value);
        }
        return result;
    }).First();

Upvotes: 0

David
David

Reputation: 218798

So I am looking to eliminate the assumption that they are in the correct order

Something like this perhaps?:

Aa = smalls.Single(s => s.Name == "Aa").Value

This at least still relies on the assumption that the record will be there at all, though it doesn't care about the order of the records. If you want to drop that assumption as well, you can add some error checking. Something like this perhaps:

Aa = smalls.Any(s => s.Name == "Aa") ? smalls.First(s => s.Name == "Aa") : string.Empty

It's not the most efficient thing in the world, but at least remains on one line as in your current usage. Separating into multiple lines will make it longer, but potentially more performant (if performance is even an issue... in the very small example provided it really isn't).

Those multiple lines could potentially then be re-factored into a custom extension method to put it back onto a single line? The sky's the limit, really.

Upvotes: 2

Related Questions