0

I have the following classes,

User:

public class User:Domain
    {
        [Sortable]
        public string FirstName { get; set; }

        [Sortable]
        public string LastName { get; set; }

        [NestedSortable]
        public Profile Profile { get; set; }
    }

Profile:

public class Profile : Domain
    {
        [Sortable]
        public string BrandName { get; set; }

        [NestedSortable]
        public Client Client { get; set; }

        [NestedSortable]
        public ProfileContact ProfileContact { get; set; }
    }

Client:

public class Client : Domain
    {
        [Sortable]
        public string Name { get; set; }
    }

Profile Contact:

public class ProfileContact : Domain
    {
        [Sortable]
        public string Email { get; set; }
    }

I'm writing a recursive function to get all the properties decorated with [Sortable] attribute. This works well when I have a single [NestedSortableProperty] but fails when I have more than one.

Here is my recursive function:

private static IEnumerable<SortTerm> GetTermsFromModel(
            Type parentSortClass,
            List<SortTerm> sortTerms,
            string parentsName = null,
            bool hasNavigation = false)
        {
            if (sortTerms is null)
            {
                sortTerms = new List<SortTerm>();
            }

            sortTerms.AddRange(parentSortClass.GetTypeInfo()
                       .DeclaredProperties
                       .Where(p => p.GetCustomAttributes<SortableAttribute>().Any())
                       .Select(p => new SortTerm
                       {
                           ParentName = parentSortClass.Name,
                           Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
                           EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
                           Default = p.GetCustomAttribute<SortableAttribute>().Default,
                           HasNavigation = hasNavigation
                       }));

            var complexSortProperties = parentSortClass.GetTypeInfo()
                       .DeclaredProperties
                       .Where(p => p.GetCustomAttributes<NestedSortableAttribute>().Any());

            if (complexSortProperties.Any())
            {
                foreach (var parentProperty in complexSortProperties)
                {
                    var parentType = parentProperty.PropertyType;

                    if (string.IsNullOrWhiteSpace(parentsName))
                    {
                        parentsName = parentType.Name;
                    }
                    else
                    {
                        parentsName += $".{parentType.Name}";
                    }

                    return GetTermsFromModel(parentType, sortTerms, parentsName, true);
                }
            }

            return sortTerms;
        }

this happens because of the return statement inside the foreach loop. How to rewrite this? with this example I need to get a list of FirstName,LastName,BrandName,Name and Email. But I'm getting only the first four properties except Email.

Now that the above issue is resolved by removing the return statement as posted in my own answer below and also following @Dialecticus comments to use yield return. so I strike and updated the question.

Now I'm running into another issue. The parent class name is wrongly assigned if a class has multiple [NestedSortable] properties.

This method is called first time with User class like var declaredTerms = GetTermsFromModel(typeof(User), null);

Example,

After the first call, the parentsName parameter will be null and [Sortable] properties in User class don't have any effect.

Now for the [NestedSortable] Profile property in User class, the parentsName will be Profile and so the [Sortable] properties in Profile class will have Name as Profile.BrandName and so on.

Name property in final list to be as follows,

Expected Output:

FirstName, LastName, Profile.BrandName, Profile.Client.Name, Profile.ProfileContact.Email

But Actual Output:

FirstName, LastName, Profile.BrandName, Profile.Client.Name, Profile.Client.ProfileContact.Email

Please assist on how to fix this.

Thanks,

Abdul

fingers10
  • 6,675
  • 10
  • 49
  • 87
  • `sortTerms` variable should not be the input parameter of the method. It should in fact be the output of the method. Also, the method returns `IEnumerable` but there is no `yield return` anywhere. See this to find out how to `yield return` from recursive function: https://stackoverflow.com/q/2055927/395718 – Dialecticus Jul 10 '19 at 14:43
  • @Dialecticus what is yield and why this change needs to be done? Please assist – fingers10 Jul 10 '19 at 14:47
  • 1
    When learning new stuff is required It's easier for me to redirect you to google. Search for c# yield return. This is the way the `IEnumerable` works. – Dialecticus Jul 10 '19 at 14:50
  • And why `sortTerms` should not be input? – fingers10 Jul 10 '19 at 14:58
  • Because its nature is to be output. It is needed just to be returned to the caller. It is not needed internally. – Dialecticus Jul 10 '19 at 15:00
  • 1
    Instead of `sortTerms.AddRange(...)` there should be `foreach (var x in ...) yield return x;`. And calling `GetTermsFromModel` recursively should also be in the loop, `foreach (var x in GetTermsFromModel(...)) yield return x;` – Dialecticus Jul 10 '19 at 15:03
  • @Dialecticus Thanks for your assistance and guidance. Kudos to you. You earned my respect. – fingers10 Jul 11 '19 at 09:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/196318/discussion-between-abdul-rahman-and-dialecticus). – fingers10 Jul 11 '19 at 10:32

1 Answers1

0

after some debugging, I removed the return statement inside foreach loop and that fixed the first issue.

changed from,

return GetTermsFromModel(parentType, sortTerms, parentsName, true);

to,

GetTermsFromModel(parentType, sortTerms, parentsName, true);

Then as per @Dialecticus comments removed passing sortTerms as input parameter and removed the parameter inside the code and changed sortTerms.AddRange(...) to yield return.

changed from,

sortTerms.AddRange(parentSortClass.GetTypeInfo()
                       .DeclaredProperties
                       .Where(p => p.GetCustomAttributes<SortableAttribute>().Any())
                       .Select(p => new SortTerm
                       {
                           ParentName = parentSortClass.Name,
                           Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
                           EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
                           Default = p.GetCustomAttribute<SortableAttribute>().Default,
                           HasNavigation = hasNavigation
                       }));

to,

foreach (var p in properties)
            {
                yield return new SortTerm
                {
                    ParentName = parentSortClass.Name,
                    Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
                    EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
                    Default = p.GetCustomAttribute<SortableAttribute>().Default,
                    HasNavigation = hasNavigation
                };
            }

also for complex properties, changed from,

GetTermsFromModel(parentType, sortTerms, parentsName, true);

to,

var complexProperties = GetTermsFromModel(parentType, parentsName, true);

                    foreach (var complexProperty in complexProperties)
                    {
                        yield return complexProperty;
                    }

And for the final issue I'm facing with the name, adding the below code after the inner foreach loop fixed it,

parentsName = parentsName.Replace($".{parentType.Name}", string.Empty);

So here is the complete updated working code:

private static IEnumerable<SortTerm> GetTermsFromModel(
            Type parentSortClass,
            string parentsName = null,
            bool hasNavigation = false)
        {
            var properties = parentSortClass.GetTypeInfo()
                       .DeclaredProperties
                       .Where(p => p.GetCustomAttributes<SortableAttribute>().Any());

            foreach (var p in properties)
            {
                yield return new SortTerm
                {
                    ParentName = parentSortClass.Name,
                    Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
                    EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
                    Default = p.GetCustomAttribute<SortableAttribute>().Default,
                    HasNavigation = hasNavigation
                };
            }

            var complexSortProperties = parentSortClass.GetTypeInfo()
                       .DeclaredProperties
                       .Where(p => p.GetCustomAttributes<NestedSortableAttribute>().Any());

            if (complexSortProperties.Any())
            {
                foreach (var parentProperty in complexSortProperties)
                {
                    var parentType = parentProperty.PropertyType;

                    //if (string.IsNullOrWhiteSpace(parentsName))
                    //{
                    //    parentsName = parentType.Name;
                    //}
                    //else
                    //{
                    //    parentsName += $".{parentType.Name}";
                    //}

                    var complexProperties = GetTermsFromModel(parentType, string.IsNullOrWhiteSpace(parentsName) ? parentType.Name : $"{parentsName}.{parentType.Name}", true);

                    foreach (var complexProperty in complexProperties)
                    {
                        yield return complexProperty;
                    }

                    //parentsName = parentsName.Replace($".{parentType.Name}", string.Empty);
                }
            }
        }
fingers10
  • 6,675
  • 10
  • 49
  • 87
  • 1
    There is a potential bug where the last `Replace` would remove more than just the last `parentType.Name` (if there is more than one such token substring in the string). I would suggest that you don't manipulate `parentsName`, but pass the appended string directly to recurse call, `GetTermsFromModel(parentType, parentsName + $".{parentType.Name}", true)` – Dialecticus Jul 11 '19 at 09:55
  • good catch. but If i pass directly then for the first time when it is null then it will be `.User` and this will have unexpected output right? – fingers10 Jul 11 '19 at 10:24
  • 1
    Right, you can use conditional operator for that case, `string.IsNullOrWhiteSpace(parentsName) ? parentType.Name : $"{parentsName}.{parentType.Name}"` – Dialecticus Jul 11 '19 at 14:08
  • updated the answer as per your comments. Many thanks for your guidance. <3 – fingers10 Jul 11 '19 at 18:05