1

My question is kind of hard to explain:

I'm calling an API, this API can return a list of different objects, therefore the type of the object cannot be used in the LINQ query. Whatever the type of the returned objects are, in every objects, or subobjects, theres a property called "FleetId".

What I'm trying to achieve is to iterate throught the list returned and obtain the value of Property "FleetId", one of the API call results look like this:

enter image description here

As u can see the object has a property UserRole, in this case the "FleetId" is located in that list:

enter image description here

So what I'm trying to achieve:

Wherever "FleetId" is located, I need an iteration untill its found. So iteration needs to go throught subobjects aswell(if theres one).

How can i achieve this in the best way using LINQ?

Niels
  • 145
  • 1
  • 11
  • What's the depth, you want to parse? Assuming the following `A1.A2.A3.A4.A5.A6.A7.Ax.FleetId` - instance A1, has a member of A2, A2 has a member of A3, A3 has a member of A4 and so on, and Ax finally has the FleetId member you are looking for and now it should return the instance of A1? – Rand Random Aug 21 '20 at 11:14
  • I'm calling an API, whatever call this makes this API returns a list of non specified objects, these objects CAN contain subobjects depending on what is returned from the API. What I want is to iterate throught every object(and subobjects if present) returned in that list and find the property "FleetId". What i need is something like this: foreach through properties, check if property is FleetId or check if property has subchildren and iterate through these subchildren untill "FleetId" is found. – Niels Aug 21 '20 at 11:18
  • Was that meant to be an answer to my question? – Rand Random Aug 21 '20 at 11:21
  • I dont really understood ur question but check the comments under Joacho's answer. Its kind of clear there. – Niels Aug 21 '20 at 11:23
  • WolfgangJacques had the same question as I had, he refered to it as how many "levels", I refered to it as how "depth" you want to look into the objects – Rand Random Aug 21 '20 at 11:26
  • Oke, yes it can be nested into more than 2 levels. In my program i got another variable that is assigned as FleetId, I want to compare this FleetId to the FleetId found anywhere in my object to filter the returned list. – Niels Aug 21 '20 at 11:31

5 Answers5

2

I guess this is what you're looking for, using Reflection.

public static void FindProperty(Type currentType)
{
    if (!types.Contains(currentType)) {
        types.Add(currentType);

        foreach (var info in currentType.GetProperties()) {

            if (info.Name.Equals("FleetId")) {
                // found
            }

            if (!info.PropertyType.IsPrimitive) {
                FindProperty(info.PropertyType);
            }
        }
    }
}

You can get the value of FleetId using PropertyInfo.GetValue.

To prevent an infinite loop, keep a list of the already known types

HashSet<Type> knownTypes = new HashSet<Type>();

That method can then simply be invoked using FindProperty(apiResukt.GetType()).

The basic idea is to recursively loop through the properties and check their name. If they don't match - keep going.


I think your hierarchy is not going to be deep, otherwise using a Stack<T> instead of recursively calling that method might be the better choice.

devsmn
  • 1,031
  • 12
  • 21
  • Where do i need to put: PropertyInfo.GetValue and where do I need to put HashSet knownTypes = new HashSet(); since its not used in the FindProperty method – Niels Aug 21 '20 at 11:45
  • This can filter my list based on the FleetId? – Niels Aug 21 '20 at 11:46
  • How to implement this so the list is filtered on FleetId = 1? I dont really understand whats going on here – Niels Aug 21 '20 at 11:48
  • Well, your goal is to get the value of a property named ```FleetId```, correct? You'll have to put the .GetValue where the ```FleetId``` has been found – devsmn Aug 21 '20 at 11:52
  • I want to filter this list by Where FleetId == 1: Results = ApiProvider.GetAll(typeof(SettingsSubPage)); – Niels Aug 21 '20 at 11:55
0

You can use a switch statement to cast the objects to their respective type.

var fleetId = 1;
var fleetWhereIdIs1 = Results.Where(obj => 
{
    switch(obj)
    {
        case User user:
            return user.UserRoles.Any(role => role.FleetId == fleetId);
        case OtherObj obj:
            // Get fleet id from other object
            return <other_condition_here>;
        default:
            // default condition if no other condition met
            return default;
    }
}).ToList();

Or the (IMO) more elegant pattern matching switch from C# 8

var fleetWhereIdIs1 = Results.Where(obj => 
{
    return obj switch
    {
        User user => user.UserRoles.Any(role => role.FleetId == fleetId),
        OtherObj obj =>  <other_condition_here>,
        _ => default
    };
}).ToList();
joacho
  • 111
  • 6
  • Hi, isnt there a check in LINQ that checks if property has children and than foreach through these children untill FleetId is found? – Niels Aug 21 '20 at 10:54
  • What I need if to filter the result list Where FleetId is "1" for example. FleetId can be located anywhere. I need to iterate dynamically through every property (and children properties) in the result list – Niels Aug 21 '20 at 10:59
  • No, forgot User class, it can be any class, we dont know whats getting returned from the API. Look the answer from "Jdweng" above, in this case my classes look like that. In this case the FleetId is hidden in property UserRole that is located in User. The list that im getting returned is of type User in this case. I want to iterate throught this result untill I find "FleetId". Basically what i need is: iterate throught the returned list and check if theres FleetId, if theres not than FleetId is probally hidden in another object like "UserRole" in the case above. I want to obtain that property. – Niels Aug 21 '20 at 11:10
  • In the case above the Linq query can not specify the expected object like User and UserRole. – Niels Aug 21 '20 at 11:13
  • You can add the other classes to the switch condition. – joacho Aug 21 '20 at 11:17
  • 2
    @joacho - but that's not want OP wants, OP doesn't want to handle each class seperatly/manueally, OP wants "magic" to handle all possible cases eg. if a class unknown to OP had a member "FleetId" it should be handled - atleast that is how I understood it – Rand Random Aug 21 '20 at 11:20
  • a) If there is no `FleetId` in the object itself, would it be nested one level deeper or could it be more levels? So we have to decide if we need to work recursively or can just differentiate two cases? b) Can you be sure there is FleetId somewhere or could it possibly be missing completely? – Wolfgang Jacques Aug 21 '20 at 11:20
  • @RandRandom ur right. I just want to find FleetId in every object returned in the list, even if FleetId is located in subobject in the list. – Niels Aug 21 '20 at 11:21
  • That is not possible without using reflection, it is probably easier to handle each case separately. – joacho Aug 21 '20 at 11:21
  • @WolfgangJacques can be nested more than 2 levels and yes, FleetId is always somewhere in these objects. – Niels Aug 21 '20 at 11:22
  • 1
    If you want to iterate through children of different objects without handling them separately then you need some reflection that checks for List properties on each object and traverses all of them recursively and checks for a FleetId property. I would not do this unless there are very many different classes. A switch case should be perfectly fine depending on the amount of cases you need. – joacho Aug 21 '20 at 11:30
  • @Niels did you look at my recursive code? Did you really pick this solution? How does it fit your question? – Wolfgang Jacques Aug 24 '20 at 15:26
0

Try following :

    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static void Main(string[] args)
        {
            int FleetId = 123;
            List<User> users = new List<User>();

            List<User> fleetId = users.Where(x => x.UserRoles.FleetId == FleetId).ToList();
 
        }
    }
    public class User
    {
        public string Email { get; set; }
        public Boolean Enabled { get; set; }
        public string FirstName { get; set; }
        public int Id { get; set; }
        public DateTime LastLogin { get; set; }
        public string LastName { get; set; }
        public string Password { get; set; }
        public UserRoles UserRoles { get; set; }
    }
    public class UserRoles
    {
        public string Fleet { get; set; }
        public int FleetId { get; set; }
        public string Installation { get; set; }
        public int InstallationId { get; set; }
        public string Role { get; set; }
        public int RoleId { get; set; }
        public string User { get; set; }
        public int UserId { get; set; }
    }
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • This is not dynamic enough, the list can contain any object, user is just one of them. I need to foreach through any list and find the FleetId, even if its hidden in another property which has children in this case. – Niels Aug 21 '20 at 10:55
  • I do not know what your complete model looks like. – jdweng Aug 21 '20 at 11:25
0

Something like this

private static IEnumerable<(object, object)> FindFleetId(object rootObj, int fleetId)
{
    //if the rootObj is null, return an empty list
    if (rootObj == null)
        Enumerable.Empty<(object, object)>();

    //call a local method to keep track of the rootObj and the currentObj
    return Parse(rootObj, rootObj, fleetId);
}

private static IEnumerable<(object, object)> Parse(object rootObj, object currentObj, int fleetId)
{
    //break the method if currentObj is null
    if (currentObj == null)
        yield break;

    var type = currentObj.GetType();
    //check if the type of the object is IEnumerable if it is enumerate the children, a string is an IEnumerable of char, so ignore a string
    var enumerable = currentObj as IEnumerable;
    if (enumerable != null && type != typeof(string))
    {
        //enumerate the list
        foreach (var item in enumerable)
        {
            //incase the rootObj is an IEnumerable, change the rootObj to the item inside the IEnumerable
           var newRootObj = object.ReferenceEquals(rootObj, currentObj) ? item : rootObj;
            foreach (var nestedObj in Parse(newRootObj, item, fleetId))
                yield return nestedObj;
        }

        yield break;
    }

    //get all properties
    var properties = type.GetProperties();
    foreach (var propertyInfo in properties)
    {
        //get the value of the property
        var propValue = propertyInfo.GetValue(currentObj);

        //check if the name is FleetId
        if (propertyInfo.Name == "FleetId")
        {
            //cast the value to int, and compare it with the pased parameter, return if true                    
            if (propValue is int fId && fId == fleetId)
                yield return (rootObj, currentObj);

            break;
        }
    
        //call method recursivly
        foreach (var nestedObj in Parse(rootObj, propValue, fleetId))
            yield return nestedObj;    
    }
}

The return type for this idea is IEnumerable<(object, object)> where the Item1 of the tuple is the root object (Level 0, ground zero) of the chain, and Item2 of the tuple is the object that actually had the member FleetId.

The code uses recursion to parse the levels/depths of the object.

To find the member and get the value it uses reflection, the member has to be public.

Complete code, with test cases:

class Program
{
    static void Main(string[] args)
    {
        int fleetId = 1;

        var a = FindFleetId(TestA(), fleetId);
        foreach (var x in a)
            Console.WriteLine(x.Item1.ToString() + " | " + x.Item2.ToString());

        var b = FindFleetId(TestB(), fleetId);
        foreach (var x in b)
            Console.WriteLine(x.Item1.ToString() + " | " + x.Item2.ToString());

        var c = FindFleetId(TestC(), fleetId);
        foreach (var x in c)
            Console.WriteLine(x.Item1.ToString() + " | " + x.Item2.ToString());

        var d = FindFleetId(TestD(), fleetId);
        foreach (var x in d)
            Console.WriteLine(x.Item1.ToString() + " | " + x.Item2.ToString());

        Console.ReadKey();
    }

    private static IEnumerable<(object, object)> FindFleetId(object rootObj, int fleetId)
    {
        if (rootObj == null)
            Enumerable.Empty<(object, object)>();

        return Parse(rootObj, rootObj, fleetId);
    }

    private static IEnumerable<(object, object)> Parse(object rootObj, object currentObj, int fleetId)
    {
        if (currentObj == null)
            yield break;

        var type = currentObj.GetType();
        var enumerable = currentObj as IEnumerable;
        if (enumerable != null && type != typeof(string))
        {
            foreach (var item in enumerable)
            {
                var newRootObj = object.ReferenceEquals(rootObj, currentObj) ? item : rootObj;
                foreach (var nestedObj in Parse(newRootObj, item, fleetId))
                    yield return nestedObj;
            }

            yield break;
        }

        var properties = type.GetProperties();
        foreach (var propertyInfo in properties)
        {
            var propValue = propertyInfo.GetValue(currentObj);

            if (propertyInfo.Name == "FleetId")
            {
                if (propValue is int fId && fId == fleetId)
                    yield return (rootObj, currentObj);

                break;
            }
        
            foreach (var nestedObj in Parse(rootObj, propValue, fleetId))
                yield return nestedObj;    
        }
    }

    private static A1[] TestA()
    {
        return new[]
        {
            new A1() {A2 = new A2() {A3 = new A3() {FleetId = 1}}},
            new A1() {A2 = new A2() {A3 = new A3() {FleetId = 2}}},
        };
    }
    private static B1[] TestB()
    {
        return new[]
        {
            new B1() {FleetId = 1},
            new B1() {FleetId = 2},
        };
    }
    private static C1[] TestC()
    {
        return new[]
        {
            new C1() { C2 = new List<C2>() { new C2() { C3 = new C3(){FleetId = 1}}}},
            new C1() { C2 = new List<C2>() { new C2() { C3 = new C3(){FleetId = 2}}}},
        };
    }
    private static D1[] TestD()
    {
        return new[]
        {
            new D1() { D2 = new List<D2>() {new D2(){FleetId = 1}}},
            new D1() { D2 = new List<D2>() {new D2(){FleetId = 2}}},
        };
    }
}

public class A1
{
    public A2 A2 { get; set; }
    public override string ToString()
    {
        return "A1." + A2;
    }
}
public class A2
{
    public A3 A3 { get; set; }
    public override string ToString()
    {
        return "A2." + A3;
    }
}
public class A3
{
    public int FleetId { get; set; }
    public override string ToString()
    {
        return "A3." + FleetId;
    }
}

public class B1
{
    public int FleetId { get; set; }
    public override string ToString()
    {
        return "B1." + FleetId;
    }
}

public class C1
{
    public List<C2> C2 { get; set; }
    public override string ToString()
    {
        return "C1." + string.Join("|", C2);
    }
}
public class C2
{
    public C3 C3 { get; set; }
    public override string ToString()
    {
        return "C2." + C3;
    }
}
public class C3
{
    public int FleetId { get; set; }
    public override string ToString()
    {
        return "C3." + FleetId;
    }
}

public class D1
{
    public List<D2> D2 { get; set; }
    public override string ToString()
    {
        return "D1." + string.Join("|", D2);
    }
}
public class D2
{
    public int FleetId { get; set; }
    public override string ToString()
    {
        return "D2." + FleetId;
    }
}

See it in action: https://dotnetfiddle.net/mqWCVk

Rand Random
  • 7,300
  • 10
  • 40
  • 88
  • Impressive!:) How can i use this code to filter my list by for example FleetId = 1? – Niels Aug 21 '20 at 12:13
  • Usage is demonstrated in the `static void Main(string[] args)` – Rand Random Aug 21 '20 at 12:19
  • Updated the with explanation and an url to a running example. – Rand Random Aug 21 '20 at 12:21
  • I'm getting error on this line: if (currentObj is IEnumerable enumerable && type != typeof(string)) saying "requires 1 argument type" and on this line: foreach (var nestedObj in Parse(rootObj, item, fleetId)) saying "a static local function cannot contain this or base" – Niels Aug 21 '20 at 12:22
  • Updated the code changed the `IEnumerable` check to `as IEnumerable` and a `null` check and made the local method to a normal method. – Rand Random Aug 21 '20 at 12:32
0

Here's my solution to find a value of a property by the property name somewhere deep in the object. I walk recursively over each item and I descend into a property whenever it is not a simple type. null is returned if the property is ultimately not found.

I used this as found here to check for simple type.

Here's my complete code, written in LINQPad.

void Main()
{
    var Things = new List<object>();
    Things.Add(new Car()
    {
        Make = "BMW",
        Color = "red",
        FleetId = "1A"
    });
    Things.Add(new Box()
    {
        Shape = "oblong",
        FleetId = "2B",
        Volume = 20
    });
    Things.Add(new Pretzel()
    {
        Baker = "Smith",
        Container = new Box()
        {
            Shape = "round",
            FleetId = "1A",
            Volume = 18
        },
        Weight = 300
    });
    Things.Add(new Whatever()
    {
        Something = "Dinglehopper",
        NotFleet = "XXX"
    });

    var wantedFleetId = "1A";
    var result = Things.Where(x => (string)GetNestedProperty(x, "FleetId") == wantedFleetId);

    Console.WriteLine($"Elements with FleetId {wantedFleetId}:");
    foreach (var element in result)
        Console.WriteLine(element.ToString());
}

object GetNestedProperty(object obj, string PropName)
{
    foreach (var prop in obj.GetType().GetProperties())
    {
        if (prop.Name == PropName)
            return prop.GetValue(obj);
        else
        {
            if (!IsSimpleType(prop.PropertyType))
            {
                var result = GetNestedProperty(prop.GetValue(obj), PropName);
                if (result != null)
                    return result;
            }
        }
    }
    return null;
}

// source: https://gist.github.com/jonathanconway/3330614
public bool IsSimpleType(Type type)
{
    return
        type.IsValueType ||
        type.IsPrimitive ||
        new Type[] {
                typeof(String),
                typeof(Decimal),
                typeof(DateTime),
                typeof(DateTimeOffset),
                typeof(TimeSpan),
                typeof(Guid)
        }.Contains(type) ||
        Convert.GetTypeCode(type) != TypeCode.Object;
}

class Car
{
    public string Make { get; set; }
    public string FleetId { get; set; }
    public string Color { get; set; }
    public override string ToString()
    {
        return $"Car: {Make} {FleetId} {Color}";
    }
}

class Box
{
    public string Shape { get; set; }
    public string FleetId { get; set; }
    public int Volume { get; set; }
    public override string ToString()
    {
        return $"Box: {Shape} {FleetId} {Volume}";
    }
}

class Pretzel
{
    public string Baker { get; set; }
    public Box Container { get; set; }
    public int Weight { get; set; }
    public override string ToString()
    {
        return $"Pretzel: {Baker} {Weight} {Container.ToString()}";
    }
}

class Whatever
{
    public string Something { get; set; }
    public string NotFleet { get; set; }
    public override string ToString()
    {
        return $"Whatever: {Something} {NotFleet}";
    }
}
Wolfgang Jacques
  • 769
  • 6
  • 15