From 6912f1c0b85a278cafad40c1c4bf5ae65a7e315c Mon Sep 17 00:00:00 2001 From: Trevor Pilley Date: Sun, 17 May 2020 12:07:25 +0100 Subject: [PATCH] For #15 Refactoring --- .../QueryableExtensionsTests.cs} | 66 +++--- .../Linq/ODataQueryOptionsExtensions.cs | 215 ------------------ Net.Http.OData/Query/Linq/OrderByBinder.cs | 80 +++++++ Net.Http.OData/Query/Linq/SkipBinder.cs | 38 ++++ Net.Http.OData/Query/Linq/TopBinder.cs | 37 +++ Net.Http.OData/Query/QueryableExtensions.cs | 122 ++++++++++ 6 files changed, 310 insertions(+), 248 deletions(-) rename Net.Http.OData.Tests/{Linq/ODataQueryOptionsExtensionsTests.cs => Query/QueryableExtensionsTests.cs} (90%) delete mode 100644 Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs create mode 100644 Net.Http.OData/Query/Linq/OrderByBinder.cs create mode 100644 Net.Http.OData/Query/Linq/SkipBinder.cs create mode 100644 Net.Http.OData/Query/Linq/TopBinder.cs create mode 100644 Net.Http.OData/Query/QueryableExtensions.cs diff --git a/Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs b/Net.Http.OData.Tests/Query/QueryableExtensionsTests.cs similarity index 90% rename from Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs rename to Net.Http.OData.Tests/Query/QueryableExtensionsTests.cs index e65ff70..21b5a28 100644 --- a/Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs +++ b/Net.Http.OData.Tests/Query/QueryableExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Net.Http.OData.Tests.Linq { - public class ODataQueryOptionsExtensionsTests + public class QueryableExtensionsTests { private readonly IList _categories; private readonly IList _customers; @@ -19,7 +19,7 @@ public class ODataQueryOptionsExtensionsTests private readonly IList _managers; private readonly IList _products; - public ODataQueryOptionsExtensionsTests() + public QueryableExtensionsTests() { _categories = new[] { @@ -61,6 +61,23 @@ public ODataQueryOptionsExtensionsTests() }; } + [Fact] + public void Apply_Throws_ArgumentNullException_For_Null_Queryable() + { + TestHelper.EnsureEDM(); + + var queryOptions = new ODataQueryOptions( + "?$count=true", + EntityDataModel.Current.EntitySets["Customers"], + Mock.Of()); + + Assert.Throws(() => QueryableExtensions.Apply(null, queryOptions)); + } + + [Fact] + public void Apply_Throws_ArgumentNullException_For_Null_QueryOptions() + => Assert.Throws(() => QueryableExtensions.Apply(_categories.AsQueryable(), null)); + [Fact] public void ApplyTo_OrderBy_NotDeclared() { @@ -71,7 +88,7 @@ public void ApplyTo_OrderBy_NotDeclared() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -89,7 +106,7 @@ public void ApplyTo_OrderBy_Properties() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -109,7 +126,7 @@ public void ApplyTo_OrderBy_Properties_IncludingPropertyPath() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -129,7 +146,7 @@ public void ApplyTo_OrderBy_SingleProperty_Ascending() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); Assert.Equal(_products.Min(x => x.Rating), ((dynamic)results[0]).Rating); @@ -146,7 +163,7 @@ public void ApplyTo_OrderBy_SingleProperty_Descending() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); Assert.Equal(_products.Max(x => x.Rating), ((dynamic)results[0]).Rating); @@ -163,7 +180,7 @@ public void ApplyTo_Select_Expand() EntityDataModel.Current.EntitySets["Customers"], Mock.Of()); - IList results = queryOptions.ApplyTo(_customers.AsQueryable()).ToList(); + IList results = _customers.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_customers.Count, results.Count); @@ -205,7 +222,7 @@ public void ApplyTo_Select_NotDeclared() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -233,7 +250,7 @@ public void ApplyTo_Select_Properties() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -256,7 +273,7 @@ public void ApplyTo_Select_Properties_IncludingPropertyPath() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -283,7 +300,7 @@ public void ApplyTo_Select_SingleProperty() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -304,7 +321,7 @@ public void ApplyTo_Select_Star() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(_products.Count, results.Count); @@ -332,7 +349,7 @@ public void ApplyTo_Skip() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); IEnumerable skippedProducts = _products.Skip(4); @@ -341,23 +358,6 @@ public void ApplyTo_Skip() Assert.Equal(skippedProducts.Last().ProductId, ((dynamic)results[results.Count - 1]).ProductId); } - [Fact] - public void ApplyTo_Throws_ArgumentNullException_For_Null_Queryable() - { - TestHelper.EnsureEDM(); - - var queryOptions = new ODataQueryOptions( - "?$count=true", - EntityDataModel.Current.EntitySets["Customers"], - Mock.Of()); - - Assert.Throws(() => ODataQueryOptionsExtensions.ApplyTo(queryOptions, null)); - } - - [Fact] - public void ApplyTo_Throws_ArgumentNullException_For_Null_QueryOptions() - => Assert.Throws(() => ODataQueryOptionsExtensions.ApplyTo(null, _categories.AsQueryable())); - [Fact] public void ApplyTo_Throws_InvalidOperationException_For_Incorrect_QueryType() { @@ -368,7 +368,7 @@ public void ApplyTo_Throws_InvalidOperationException_For_Incorrect_QueryType() EntityDataModel.Current.EntitySets["Customers"], Mock.Of()); - Assert.Throws(() => queryOptions.ApplyTo(_categories.AsQueryable())); + Assert.Throws(() => _categories.AsQueryable().Apply(queryOptions)); } [Fact] @@ -381,7 +381,7 @@ public void ApplyTo_Top() EntityDataModel.Current.EntitySets["Products"], Mock.Of()); - IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + IList results = _products.AsQueryable().Apply(queryOptions).ToList(); Assert.Equal(4, results.Count); Assert.Equal(_products[0].ProductId, ((dynamic)results[0]).ProductId); diff --git a/Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs b/Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs deleted file mode 100644 index d0b5154..0000000 --- a/Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs +++ /dev/null @@ -1,215 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright Project Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// -// ----------------------------------------------------------------------- -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.Linq; -using System.Linq.Expressions; -using Net.Http.OData.Model; -using Net.Http.OData.Query; -using Net.Http.OData.Query.Expressions; - -namespace Net.Http.OData.Linq -{ - /// - /// Adds support for IQueryable to the ODataQueryOptions. - /// - public static class ODataQueryOptionsExtensions - { - /// - /// Applies the query expressed by the to the specified . - /// - /// The to apply. - /// The to apply the query options to. - /// The result of the OData query. - public static IEnumerable ApplyTo(this ODataQueryOptions queryOptions, IQueryable queryable) - { - if (queryOptions is null) - { - throw new ArgumentNullException(nameof(queryOptions)); - } - - if (queryable is null) - { - throw new ArgumentNullException(nameof(queryable)); - } - - if (queryable.ElementType != queryOptions.EntitySet.EdmType.ClrType) - { - throw new InvalidOperationException(); - } - - return Apply(queryOptions, queryable); - } - - private static IEnumerable Apply(ODataQueryOptions queryOptions, IQueryable queryable) - { - foreach (object entity in queryable.ApplyOrder(queryOptions).ApplySkip(queryOptions).ApplyTop(queryOptions)) - { - yield return ApplySelect(entity, queryOptions); - } - } - - private static IQueryable ApplyOrder(this IQueryable queryable, ODataQueryOptions queryOptions) - { - if (queryOptions.OrderBy == null) - { - return queryable; - } - - IQueryable q = queryable; - - for (int i = 0; i < queryOptions.OrderBy.Properties.Count; i++) - { - OrderByProperty orderByProperty = queryOptions.OrderBy.Properties[i]; - PropertyPath path = orderByProperty.PropertyPath; - Type entityType = queryOptions.EntitySet.EdmType.ClrType; - Type propertyType = path.Property.ClrProperty.PropertyType; - - // the 'entity' in the lambda expression (entity => entity.Property) - ParameterExpression entityParameterExpression = Expression.Parameter(entityType, "entity"); - - // the 'property' in the lambda expression (entity => entity.Property) - MemberExpression propertyMemberExpression = Expression.Property(entityParameterExpression, path.Property.Name); - - while (path.Next != null) - { - path = path.Next; - propertyMemberExpression = Expression.Property(propertyMemberExpression, path.Property.Name); - propertyType = path.Property.ClrProperty.PropertyType; - } - - // Represents the lambda in the method argument "(entity => entity.Property)" - LambdaExpression lambdaExpression = Expression.Lambda( - typeof(Func<,>).MakeGenericType(entityType, propertyType), - propertyMemberExpression, - new ParameterExpression[] { entityParameterExpression }); - - // Represents the method call itself "OrderBy(entity => entity.Property)" - MethodCallExpression orderByCallExpression = Expression.Call( - typeof(Queryable), - OrderByMethodName(orderByProperty.Direction, i), - new Type[] { entityType, propertyType }, - q.Expression, - lambdaExpression); - - q = q.Provider.CreateQuery(orderByCallExpression); - } - - return q; - } - - private static ExpandoObject ApplySelect(object entity, ODataQueryOptions queryOptions) - { - IEnumerable propertyPaths = queryOptions.Select?.PropertyPaths ?? queryOptions.EntitySet.EdmType.Properties.Where(p => !p.IsNavigable).Select(PropertyPath.For); - - if (queryOptions.Expand != null) - { - propertyPaths = propertyPaths.Concat(queryOptions.Expand.PropertyPaths); - } - - var expandoObject = new ExpandoObject(); - - foreach (PropertyPath propertyPath in propertyPaths) - { - PropertyPath path = propertyPath; - var dictionary = (IDictionary)expandoObject; - object obj = entity; - - while (path.Next != null) - { - if (!dictionary.ContainsKey(path.Property.Name)) - { - dictionary[path.Property.Name] = new ExpandoObject(); - } - - dictionary = (IDictionary)dictionary[path.Property.Name]; - obj = path.Property.ClrProperty.GetValue(obj); - path = path.Next; - } - - if (path.Property.IsNavigable) - { - dictionary[path.Property.Name] = new ExpandoObject(); - dictionary = (IDictionary)dictionary[path.Property.Name]; - obj = path.Property.ClrProperty.GetValue(obj); - - var edmComplexType = path.Property.PropertyType as EdmComplexType; - - while (edmComplexType != null) - { - foreach (EdmProperty edmProperty in edmComplexType.Properties) - { - if (!edmProperty.IsNavigable) - { - dictionary[edmProperty.Name] = edmProperty.ClrProperty.GetValue(obj); - } - } - - edmComplexType = edmComplexType.BaseType as EdmComplexType; - } - } - else - { - dictionary[path.Property.Name] = path.Property.ClrProperty.GetValue(obj); - } - } - - return expandoObject; - } - - private static IQueryable ApplySkip(this IQueryable queryable, ODataQueryOptions queryOptions) - { - if (queryOptions.Skip.HasValue) - { - MethodCallExpression skipCallExpression = Expression.Call( - typeof(Queryable), - "Skip", - new Type[] { queryOptions.EntitySet.EdmType.ClrType }, - queryable.Expression, - Expression.Constant(queryOptions.Skip.Value)); - - return queryable.Provider.CreateQuery(skipCallExpression); - } - - return queryable; - } - - private static IQueryable ApplyTop(this IQueryable queryable, ODataQueryOptions queryOptions) - { - if (queryOptions.Top.HasValue) - { - MethodCallExpression skipCallExpression = Expression.Call( - typeof(Queryable), - "Take", - new Type[] { queryOptions.EntitySet.EdmType.ClrType }, - queryable.Expression, - Expression.Constant(queryOptions.Top.Value)); - - return queryable.Provider.CreateQuery(skipCallExpression); - } - - return queryable; - } - - private static string OrderByMethodName(OrderByDirection direction, int index) - { - if (direction == OrderByDirection.Ascending) - { - return index == 0 ? "OrderBy" : "ThenBy"; - } - - return index == 0 ? "OrderByDescending" : "ThenByDescending"; - } - } -} diff --git a/Net.Http.OData/Query/Linq/OrderByBinder.cs b/Net.Http.OData/Query/Linq/OrderByBinder.cs new file mode 100644 index 0000000..ad111da --- /dev/null +++ b/Net.Http.OData/Query/Linq/OrderByBinder.cs @@ -0,0 +1,80 @@ +// +// Copyright Project Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// +// ----------------------------------------------------------------------- +using System; +using System.Linq; +using System.Linq.Expressions; +using Net.Http.OData.Query.Expressions; + +namespace Net.Http.OData.Query.Linq +{ + internal static class OrderByBinder + { + internal static IQueryable ApplyOrder(this IQueryable queryable, ODataQueryOptions queryOptions) + { + if (queryOptions.OrderBy == null) + { + return queryable; + } + + IQueryable q = queryable; + + for (int i = 0; i < queryOptions.OrderBy.Properties.Count; i++) + { + OrderByProperty orderByProperty = queryOptions.OrderBy.Properties[i]; + PropertyPath path = orderByProperty.PropertyPath; + Type entityType = queryOptions.EntitySet.EdmType.ClrType; + Type propertyType = path.Property.ClrProperty.PropertyType; + + // the 'entity' in the lambda expression (entity => entity.Property) + ParameterExpression entityParameterExpression = Expression.Parameter(entityType, "entity"); + + // the 'property' in the lambda expression (entity => entity.Property) + MemberExpression propertyMemberExpression = Expression.Property(entityParameterExpression, path.Property.Name); + + while (path.Next != null) + { + path = path.Next; + propertyMemberExpression = Expression.Property(propertyMemberExpression, path.Property.Name); + propertyType = path.Property.ClrProperty.PropertyType; + } + + // Represents the lambda in the method argument "(entity => entity.Property)" + LambdaExpression lambdaExpression = Expression.Lambda( + typeof(Func<,>).MakeGenericType(entityType, propertyType), + propertyMemberExpression, + new ParameterExpression[] { entityParameterExpression }); + + // Represents the method call itself "OrderBy(entity => entity.Property)" + MethodCallExpression orderByCallExpression = Expression.Call( + typeof(Queryable), + OrderByMethodName(orderByProperty.Direction, i), + new Type[] { entityType, propertyType }, + q.Expression, + lambdaExpression); + + q = q.Provider.CreateQuery(orderByCallExpression); + } + + return q; + } + + private static string OrderByMethodName(OrderByDirection direction, int index) + { + if (direction == OrderByDirection.Ascending) + { + return index == 0 ? "OrderBy" : "ThenBy"; + } + + return index == 0 ? "OrderByDescending" : "ThenByDescending"; + } + } +} diff --git a/Net.Http.OData/Query/Linq/SkipBinder.cs b/Net.Http.OData/Query/Linq/SkipBinder.cs new file mode 100644 index 0000000..3f46f4d --- /dev/null +++ b/Net.Http.OData/Query/Linq/SkipBinder.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright Project Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// +// ----------------------------------------------------------------------- +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace Net.Http.OData.Query.Linq +{ + internal static class SkipBinder + { + internal static IQueryable ApplySkip(this IQueryable queryable, ODataQueryOptions queryOptions) + { + if (queryOptions.Skip.HasValue) + { + MethodCallExpression skipCallExpression = Expression.Call( + typeof(Queryable), + "Skip", + new Type[] { queryOptions.EntitySet.EdmType.ClrType }, + queryable.Expression, + Expression.Constant(queryOptions.Skip.Value)); + + return queryable.Provider.CreateQuery(skipCallExpression); + } + + return queryable; + } + } +} diff --git a/Net.Http.OData/Query/Linq/TopBinder.cs b/Net.Http.OData/Query/Linq/TopBinder.cs new file mode 100644 index 0000000..dab632f --- /dev/null +++ b/Net.Http.OData/Query/Linq/TopBinder.cs @@ -0,0 +1,37 @@ +// +// Copyright Project Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// +// ----------------------------------------------------------------------- +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace Net.Http.OData.Query.Linq +{ + internal static class TopBinder + { + internal static IQueryable ApplyTop(this IQueryable queryable, ODataQueryOptions queryOptions) + { + if (queryOptions.Top.HasValue) + { + MethodCallExpression skipCallExpression = Expression.Call( + typeof(Queryable), + "Take", + new Type[] { queryOptions.EntitySet.EdmType.ClrType }, + queryable.Expression, + Expression.Constant(queryOptions.Top.Value)); + + return queryable.Provider.CreateQuery(skipCallExpression); + } + + return queryable; + } + } +} diff --git a/Net.Http.OData/Query/QueryableExtensions.cs b/Net.Http.OData/Query/QueryableExtensions.cs new file mode 100644 index 0000000..6058519 --- /dev/null +++ b/Net.Http.OData/Query/QueryableExtensions.cs @@ -0,0 +1,122 @@ +// ----------------------------------------------------------------------- +// +// Copyright Project Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// +// ----------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using Net.Http.OData.Model; +using Net.Http.OData.Query; +using Net.Http.OData.Query.Expressions; +using Net.Http.OData.Query.Linq; + +namespace Net.Http.OData.Linq +{ + /// + /// Adds support query support for ODataQueryOptions to an IQueryable. + /// + public static class QueryableExtensions + { + /// + /// Applies the query expressed by the to the specified . + /// + /// The to apply the query options to. + /// The to apply. + /// The result of the OData query. + public static IEnumerable Apply(this IQueryable queryable, ODataQueryOptions queryOptions) + { + if (queryOptions is null) + { + throw new ArgumentNullException(nameof(queryOptions)); + } + + if (queryable is null) + { + throw new ArgumentNullException(nameof(queryable)); + } + + if (queryable.ElementType != queryOptions.EntitySet.EdmType.ClrType) + { + throw new InvalidOperationException(); + } + + return ApplyImpl(queryable, queryOptions); + } + + private static IEnumerable ApplyImpl(IQueryable queryable, ODataQueryOptions queryOptions) + { + foreach (object entity in queryable.ApplyOrder(queryOptions).ApplySkip(queryOptions).ApplyTop(queryOptions)) + { + yield return ApplySelect(entity, queryOptions); + } + } + + private static ExpandoObject ApplySelect(object entity, ODataQueryOptions queryOptions) + { + IEnumerable propertyPaths = queryOptions.Select?.PropertyPaths ?? queryOptions.EntitySet.EdmType.Properties.Where(p => !p.IsNavigable).Select(PropertyPath.For); + + if (queryOptions.Expand != null) + { + propertyPaths = propertyPaths.Concat(queryOptions.Expand.PropertyPaths); + } + + var expandoObject = new ExpandoObject(); + + foreach (PropertyPath propertyPath in propertyPaths) + { + PropertyPath path = propertyPath; + var dictionary = (IDictionary)expandoObject; + object obj = entity; + + while (path.Next != null) + { + if (!dictionary.ContainsKey(path.Property.Name)) + { + dictionary[path.Property.Name] = new ExpandoObject(); + } + + dictionary = (IDictionary)dictionary[path.Property.Name]; + obj = path.Property.ClrProperty.GetValue(obj); + path = path.Next; + } + + if (path.Property.IsNavigable) + { + dictionary[path.Property.Name] = new ExpandoObject(); + dictionary = (IDictionary)dictionary[path.Property.Name]; + obj = path.Property.ClrProperty.GetValue(obj); + + var edmComplexType = path.Property.PropertyType as EdmComplexType; + + while (edmComplexType != null) + { + foreach (EdmProperty edmProperty in edmComplexType.Properties) + { + if (!edmProperty.IsNavigable) + { + dictionary[edmProperty.Name] = edmProperty.ClrProperty.GetValue(obj); + } + } + + edmComplexType = edmComplexType.BaseType as EdmComplexType; + } + } + else + { + dictionary[path.Property.Name] = path.Property.ClrProperty.GetValue(obj); + } + } + + return expandoObject; + } + } +}