Webapi: Expanding entities with dynamic properties throws a linq/entity error.

Created on 13 Sep 2016  路  17Comments  路  Source: OData/WebApi

_Short summary (3-5 sentences) describing the issue._
An error is generate when using Expand on entities that have dynamic properties.

Assemblies affected

_Which assemblies and versions are known to be affected e.g. OData .Net lib 6.15-beta._
Microsoft.Odata.Core v6.15.0
EntityFramework v6.1.3

Reproduce steps

Using AirVinyl Example from pluralSight tutorial "Building a Consistent RESTful API with OData V4 in ASP.NET" I'm able to produce the error by requesting:
http://localhost:5810/odata/People(1)?expand=VinylRecords($expand=DynamicVinylRecordProperties)

I've added a sample solution to github here:
https://github.com/jaywebguy/AirVinylDynamicPropertyExample

I'm able to reproduce this with another project using dynamic properties (OpenTypes) on all Entities. Any request using expand will throw a similiar error. It's as if OData is generating a Linq expressing using the Properties property within a Where clause.

Expected result

I would expect to get back a People record containing all the VinyRecords including the dynamic properties for those records.

Actual result

An error is thrown. With the following error message
"The specified type member 'Properties' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported."

Additional details

_Optional, details of the root cause if known. Delete this section if you have no additional details to add._
Looks like a similar issue was reported and closed, though it was never resolved
https://github.com/OData/WebApi/issues/462
https://github.com/OData/odata.net/issues/354

P2

All 17 comments

I am running into the same problem with Open Types as well. Debugging into OData's code shows that the select clause seems to be fine until they apply the expand where OData code throws out the original select clause and the resulting select clause no longer has the dynamic properties in it. HintonBR comments in #462 that he saw the same thing. This is quite a huge limitation of using Open Types.

Any chance I can get some help on this? I am able to get around it by substantiating my data (using toList()) before handing it off to OData. However this is very performance intensive.

Anyone else see this or found a better work around?

Also have the same issue - can't implement open types on my model because of it

Agreed. We had to drop Open Types because of this shortcoming. We do hope they solve this issue so we can one day use this functionality.

Really wish they would address this issue.

Not too bothered about the client-side issue since I'm using Simple.OData.Client and explicitly creating my DTOs, but without the main support I can't use open types.

What I've done as a workaround is this...

    public class Party
    {
        public Party()
        {
            Properties = new Dictionary<string, object>();
        }

        public int Id { get; set; }

        public string Name { get; set; }

        public IDictionary<string, object> Properties { get; set; }

        public string PropertiesJson
        {
            get
            {
                return JsonConvert.SerializeObject(Properties);
            }
            set
            {
                Properties = string.IsNullOrEmpty(value)
                            ? new Dictionary<string, object>()
                            : JsonConvert.DeserializeObject<Dictionary<string, object>>(value);
            }
        }
    }

Then the EF mapping looks like this..

    public class PartyConfiguration : EntityTypeConfiguration<Party>
    {
        public PartyConfiguration()
        {
            Ignore(x => x.Properties);
            this.StringProperty("PropertiesJson").HasColumnName("Properties");
        }
    }

And the OData like this

       case "Party":
              builder.EntityType<Party>().Ignore(x => x.Properties);

              builder.EntitySet<Party>(name);
              break;

This gets me a lot of the benefits of an open type whilst I'm waiting - you can also be more fancy and expose PropertiesJson as Properties on the OData model etc.

Sharing my thoughts about the solution

I've faced the same problem and been thinking about it. What @VikingsFan in issue #462 said about

all of the properties of open segment always added to expand clause is the right behavior I think.

is totally right. I've used something similar to what @phatcher is proposing in here. @VikingsFan is right because what if the consumer wants to select something from my dynamic properties?! How can we approach this select statement using EF? We simply can not! But if we use the ObjectQueryProvider like @jaywebguy have used (by using ToList()) everything works exactly as expected.

I think the problem does not lie within how SelectExpandBinder works. The problem is in model configurations. We should be able to specify if consumer can perform select on a dynamic property or not, just like a normal property.

entityType.Property(e => e.SomeProperty).Select(SelectExpandType.Disabled);

We will need some class like _DynamicPropertyConfiguration_ that we can use to configure our dynamic property to not be selectable or expandable like below.

entityType.HasDynamicProperties(e => e.SomeDynamicProperty).Select(SelectExpandType.Disabled);
entityType.HasDynamicProperties(e => e.SomeDynamicProperty).Expand(SelectExpandType.Disabled);

My workaround

First I must say that I've disabled lazy loading on my DbContext and used eager loading whenever I need additional data to be loaded.

I have changed my Get methods on my controllers to something like below.

[EnableQuery]
public IHttpActionResult Get(ODataQueryOptions<SomeEntity> options)
{
    if (options.SelectExpand != null) // This is only required on open types.
    {
        IQueryable<SomeEntity> query = _context.SomeEntities;
        var translator = new ExpandedNavigationSelectedItemTranslator();
        foreach (var includedNavigationProperty in translator.TranslateSelectExpandClause(options.SelectExpand.SelectExpandClause))
        {
            query = query.Include(includedNavigationProperty);
        }
        return Ok(query.ToList());
    }

    return Ok(_context.SomeEntities);
}

[EnableQuery]
public IHttpActionResult Get([FromODataUri] Guid key, ODataQueryOptions<SomeEntity> options)
{
    if (options.SelectExpand != null) // This is only required on open types.
    {
        IQueryable<SomeEntity> query = _context.SomeEntities.Where(se => se.EntityId == key);
        var translator = new ExpandedNavigationSelectedItemTranslator();
        foreach (var includedNavigationProperty in translator.TranslateSelectExpandClause(options.SelectExpand.SelectExpandClause))
        {
            query = query.Include(includedNavigationProperty);
        }
        return Ok(query.SingleOrDefault());
    }

    return Ok(SingleResult.Create(_context.SomeEntities.Where(se => se.EntityId == key)));
}

And ExpandedNavigationSelectedItemTranslator class is as following. I'm not sure about the implementation or even if I've used the right class(es) or not. So I'd be happy if anyone wants to correct me or even suggest something better.

public class ExpandedNavigationSelectedItemTranslator : SelectItemTranslator<IEnumerable<string>>
{
    public override IEnumerable<string> Translate(ExpandedNavigationSelectItem item)
    {
        var expandedNavigationProperies = new List<string>();
        expandedNavigationProperies.Add(item.PathToNavigationProperty.Cast<NavigationPropertySegment>().Single().NavigationProperty.Name);
        if (item.SelectAndExpand.SelectedItems.Any())
        {
            expandedNavigationProperies.AddRange(TranslateSelectExpandClause(item.SelectAndExpand).Select(p => item.PathToNavigationProperty.Cast<NavigationPropertySegment>().Single().NavigationProperty.Name + "." + p));
        }
        return expandedNavigationProperies;
    }

    public IEnumerable<string> TranslateSelectExpandClause(SelectExpandClause selectExpandClause)
    {
        return selectExpandClause.SelectedItems.OfType<ExpandedNavigationSelectItem>().SelectMany(Translate);
    }
}

@halipourian

I have tried this thing but getting following error ...

Self referencing loop detected for property 'DeclaringType' with type 'Microsoft.Odata.Edm.Library.EdmEntityType'. Path 'Context.Model.SchemaElements[0].DeclaredKey[0]'

would you please let me know if any workaround is there ?

@mkjoshi-86 I'm afraid I can not help you with just a message of an exception. Though it seems like a serialization exception... But why are you even trying to serialize an EdmEntityType?!!

Please provide more info about the exception like when did it happen?! What is the Source of it? And what does the StackTrace says on it? Did it happen because you used my workaround? What version of the EdmLib are you referencing in your project?

@halipourian Yes, after ading ODataQueryOption in Get method, I am getting circular reference error.

Model Structure
class ParentModel{

public int ParentId {get;set;}

public IDictionary <string,object> DynamicProperties {get;set;}

public ICollection <ChildModel> ChildModel {get;set;}
}

class ChildModel{
public int ChildId{get;set;}

[ForeignKey("ParentId")]
public int ParentId{get;set;}

public virtual ParentModel parent {get;set;}
}

case 1: If I removed Dynamic property from ParentModel then everything works fine.

case 2: After adding Dynamic property expand doesn't work

case 3: To work with DynamicProperty + expand I have used the way suggested by you but it's giving circular reference error while calling Get action.

[EnableQuery]
public IHttpActionResult Get(ODataQueryOption<ParentModel> options)
{
// getting circular reference error
}

If i removed ODataQueryOption from Get method input parameter then everything works fine except expand query.

@mkjoshi-86 I've created a sample service with your proposed model and it works as expected. I'm sorry to say that this is not the place to resolve your problem and I can not help you any further in here. You probably have some configuration wrong somewhere else. You can contact me on my email, or even better, I strongly recommend you to ask your questions on stackoverflow where hundreds of professional developers are there to help you... But bear in mind to provide enough info so others will able to repro your issue.

Hi everyone. Yesterday I bumped to this problem again and this time I had to deal with it seriously. Fortunately I've found quite an elegant solution which is neat and easy to use. As I said before I've used something similar to what @phatcher is proposing above to store dynamic properties in the database.

Suggested solution

The solution relies on the EnableQueryAttribute mechanism, so you just have to declare the attribute on your action methods or add it globally through configuration. Basically it does two things:

  1. Try to find expanded navigation properties and include them manually in query. I'm using Include() method which is coming from Entity Framework. So it is targeted for EF.
  2. Try to find open properties in select or filter query options, and if it finds any, it will perform part of query on data source and the rest of it in memory and after loading from data source. So it is trying its best to perform queries on data source to be efficient.

BTW there is a property name AllowOpenProperties which allow open properties to be used in select and filter clauses. I'm really sorry that I'm putting this huge piece of code in here but I have no other choice because the solution is tight coupled with EF (By using Include() method).

public class ExtendedEnableQueryAttribute : EnableQueryAttribute
{
    public ExtendedEnableQueryAttribute()
    {
        AllowOpenProperties = true;
    }

    public bool AllowOpenProperties { get; set; }

    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        var excludedQueryOptions = AllowedQueryOptions.None;

        var expandedNavigationProperties = GetExpandedNavigationProperties(queryOptions).ToList();
        foreach (var expandedNavigationProperty in expandedNavigationProperties)
        {
            // Manually including expanded navigation properties in results
            queryable = queryable.Include(expandedNavigationProperty);
        }
        if (expandedNavigationProperties.Any())
        {
            // We are excluding expand operator because we are already included them in results
            // by using Include(). We should also exclude select query option to prevent issues 
            // when expand and select are being applied together.
            excludedQueryOptions |= AllowedQueryOptions.Expand | AllowedQueryOptions.Select;
        }
        if (HasOpenPropertySelectedItem(queryOptions))
        {
            // When an open property is detected in a select we should perform projection in memory.
            excludedQueryOptions |= AllowedQueryOptions.Select;
        }
        if (HasOpenPropertyFilteredItem(queryOptions))
        {
            // When an open property is being used in filter clause we preform filtering in memory.
            // We shall perform the skip, top and count options in memory and after filtering, otherwise
            // we'll get incorrect results.
            excludedQueryOptions |= AllowedQueryOptions.Filter | AllowedQueryOptions.Skip | AllowedQueryOptions.Top | AllowedQueryOptions.Count;
        }

        // If one of the above situations happens, be will perform part of query in the data source
        // and the rest in memory.
        if (excludedQueryOptions != AllowedQueryOptions.None)
        {
            var querySettings = new ODataQuerySettings
            {
                EnableConstantParameterization = EnableConstantParameterization,
                HandleNullPropagation = HandleNullPropagation,
                PageSize = !excludedQueryOptions.HasFlag(AllowedQueryOptions.Filter) && PageSize > 0 ? (int?) PageSize : null, // Only apply paging when we apply the filtering in data source
                EnsureStableOrdering = EnsureStableOrdering
            };

            var excludedQuery = queryOptions.ApplyTo(queryable, querySettings, excludedQueryOptions); // Queryable for data source
            var items = (IList) Activator.CreateInstance(typeof (List<>).MakeGenericType(queryable.ElementType));
            foreach (var item in excludedQuery)
            {
                items.Add(item);
            }
            queryable = items.AsQueryable();

            querySettings.PageSize = PageSize > 0 ? (int?) PageSize : null; // Use the correct page size in final 
            return queryOptions.ApplyTo(queryable, querySettings, AllowedQueryOptions.All ^ excludedQueryOptions); // Queryable for memory
        }
        return base.ApplyQuery(queryable, queryOptions);
    }

    public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
    {
        if (!AllowOpenProperties)
        {
            if (HasOpenPropertySelectedItem(queryOptions))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, "The query parameter '$select' is not supported on open properties."));
            }
            if (HasOpenPropertyFilteredItem(queryOptions))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, "The query parameter '$filter' is not supported on open properties."));
            }
        }
        base.ValidateQuery(request, queryOptions);
    }

    private static IEnumerable<string> GetExpandedNavigationProperties(ODataQueryOptions queryOptions)
    {
        if (queryOptions.SelectExpand == null)
        {
            return new List<string>().AsReadOnly();
        }

        var expandedNavigationSelectedItemTranslator = new ExpandedNavigationSelectedItemTranslator();
        return expandedNavigationSelectedItemTranslator.Translate(queryOptions.SelectExpand.SelectExpandClause).ToList().AsReadOnly();
    }

    private static bool HasOpenPropertySelectedItem(ODataQueryOptions queryOptions)
    {
        if (queryOptions.SelectExpand == null)
        {
            return false;
        }

        var openPropertySelectedItemTranslator = new OpenPropertySelectedItemTranslator();
        return openPropertySelectedItemTranslator.Translate(queryOptions.SelectExpand.SelectExpandClause);
    }

    private static bool HasOpenPropertyFilteredItem(ODataQueryOptions queryOptions)
    {
        if (queryOptions.Filter == null)
        {
            return false;
        }

        var openPropertyVisitor = new OpenPropertyVisitor();
        return openPropertyVisitor.HasOpenPropertyAccessNode(queryOptions.Filter.FilterClause.Expression);
    }

    private class ExpandedNavigationSelectedItemTranslator : SelectItemTranslator<IEnumerable<string>>
    {
        public IEnumerable<string> Translate(SelectExpandClause selectExpandClause)
        {
            return selectExpandClause.SelectedItems.SelectMany(t => t.TranslateWith(this));
        }

        public override IEnumerable<string> Translate(ExpandedNavigationSelectItem item)
        {
            var expandedNavigationProperties = new List<string>
            {
                item.PathToNavigationProperty.Cast<NavigationPropertySegment>().Single().NavigationProperty.Name
            };

            var translation = Translate(item.SelectAndExpand);
            if (translation != null)
            {
                expandedNavigationProperties.AddRange(translation.Select(p => item.PathToNavigationProperty.Cast<NavigationPropertySegment>().Single().NavigationProperty.Name + "." + p));
            }
            return expandedNavigationProperties;
        }

        public override IEnumerable<string> Translate(ExpandedReferenceSelectItem item)
        {
            return new string[] {};
        }

        public override IEnumerable<string> Translate(NamespaceQualifiedWildcardSelectItem item)
        {
            return new string[] {};
        }

        public override IEnumerable<string> Translate(PathSelectItem item)
        {
            return new string[] {};
        }

        public override IEnumerable<string> Translate(WildcardSelectItem item)
        {
            return new string[] {};
        }
    }

    private class OpenPropertySelectedItemTranslator : SelectItemTranslator<bool>
    {
        public bool Translate(SelectExpandClause selectExpandClause)
        {
            return selectExpandClause.SelectedItems.Any(t => t.TranslateWith(this));
        }

        public override bool Translate(ExpandedNavigationSelectItem item)
        {
            return false;
        }

        public override bool Translate(ExpandedReferenceSelectItem item)
        {
            return false;
        }

        public override bool Translate(NamespaceQualifiedWildcardSelectItem item)
        {
            return false;
        }

        public override bool Translate(PathSelectItem item)
        {
            return item.SelectedPath.OfType<OpenPropertySegment>().Any();
        }

        public override bool Translate(WildcardSelectItem item)
        {
            return false;
        }
    }

    private class OpenPropertyVisitor : QueryNodeVisitor<bool>
    {
        public bool HasOpenPropertyAccessNode(QueryNode node)
        {
            if (node == null)
            {
                return false;
            }
            return node.Accept(this);
        }

        public override bool Visit(SingleValueOpenPropertyAccessNode node)
        {
            return true;
        }

        public override bool Visit(AllNode node)
        {
            return HasOpenPropertyAccessNode(node.Source) || HasOpenPropertyAccessNode(node.Body);
        }

        public override bool Visit(AnyNode node)
        {
            return HasOpenPropertyAccessNode(node.Source) || HasOpenPropertyAccessNode(node.Body);
        }

        public override bool Visit(BinaryOperatorNode node)
        {
            return HasOpenPropertyAccessNode(node.Left) || HasOpenPropertyAccessNode(node.Right);
        }

        public override bool Visit(CollectionFunctionCallNode node)
        {
            return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
        }

        public override bool Visit(CollectionNavigationNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(CollectionOpenPropertyAccessNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(CollectionPropertyAccessNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(CollectionPropertyCastNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(ConstantNode node)
        {
            return false;
        }

        public override bool Visit(ConvertNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(CountNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(EntityCollectionCastNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(EntityCollectionFunctionCallNode node)
        {
            return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
        }

        public override bool Visit(EntityRangeVariableReferenceNode node)
        {
            return HasOpenPropertyAccessNode(node.RangeVariable.EntityCollectionNode);
        }

        public override bool Visit(NamedFunctionParameterNode node)
        {
            return HasOpenPropertyAccessNode(node.Value);
        }

        public override bool Visit(NonentityRangeVariableReferenceNode node)
        {
            return HasOpenPropertyAccessNode(node.RangeVariable.CollectionNode);
        }

        public override bool Visit(ParameterAliasNode node)
        {
            return false;
        }

        public override bool Visit(SearchTermNode node)
        {
            return false;
        }

        public override bool Visit(SingleEntityCastNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(SingleEntityFunctionCallNode node)
        {
            return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
        }

        public override bool Visit(SingleNavigationNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(SingleValueCastNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(SingleValueFunctionCallNode node)
        {
            return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
        }

        public override bool Visit(SingleValuePropertyAccessNode node)
        {
            return HasOpenPropertyAccessNode(node.Source);
        }

        public override bool Visit(UnaryOperatorNode node)
        {
            return HasOpenPropertyAccessNode(node.Operand);
        }
    }
}

Hi @halipourian can you post what namespace you are using for your solution because I have some issue with Microsoft.OData.UriParser and Microsoft.Data.OData.Query.SemanticAst

this is the updated version for ExtendedEnableQueryAttribute Microsoft.Odata.Core v7.0

    public class ExtendedEnableQueryAttribute : EnableQueryAttribute
    {
        public ExtendedEnableQueryAttribute()
        {
            AllowOpenProperties = true;
        }

        public bool AllowOpenProperties { get; set; }

        public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
        {
            var excludedQueryOptions = AllowedQueryOptions.None;

            var expandedNavigationProperties = GetExpandedNavigationProperties(queryOptions).ToList();
            foreach (var expandedNavigationProperty in expandedNavigationProperties)
            {
                // Manually including expanded navigation properties in results
                queryable = queryable.Include(expandedNavigationProperty);
            }
            if (expandedNavigationProperties.Any())
            {
                // We are excluding expand operator because we are already included them in results
                // by using Include(). We should also exclude select query option to prevent issues
                // when expand and select are being applied together.
                excludedQueryOptions |= AllowedQueryOptions.Expand | AllowedQueryOptions.Select;
            }
            if (HasOpenPropertySelectedItem(queryOptions))
            {
                // When an open property is detected in a select we should perform projection in memory.
                excludedQueryOptions |= AllowedQueryOptions.Select;
            }
            if (HasOpenPropertyFilteredItem(queryOptions))
            {
                // When an open property is being used in filter clause we preform filtering in memory.
                // We shall perform the skip, top and count options in memory and after filtering, otherwise
                // we'll get incorrect results.
                excludedQueryOptions |= AllowedQueryOptions.Filter | AllowedQueryOptions.Skip | AllowedQueryOptions.Top | AllowedQueryOptions.Count;
            }

            // If one of the above situations happens, be will perform part of query in the data source
            // and the rest in memory.
            if (excludedQueryOptions != AllowedQueryOptions.None)
            {
                var querySettings = new ODataQuerySettings
                {
                    EnableConstantParameterization = EnableConstantParameterization,
                    HandleNullPropagation = HandleNullPropagation,
                    PageSize = !excludedQueryOptions.HasFlag(AllowedQueryOptions.Filter) && PageSize > 0 ? (int?)PageSize : null, // Only apply paging when we apply the filtering in data source
                    EnsureStableOrdering = EnsureStableOrdering
                };

                var excludedQuery = queryOptions.ApplyTo(queryable, querySettings, excludedQueryOptions); // Queryable for data source
                var items = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(queryable.ElementType));
                foreach (var item in excludedQuery)
                {
                    items.Add(item);
                }
                queryable = items.AsQueryable();

                querySettings.PageSize = PageSize > 0 ? (int?)PageSize : null; // Use the correct page size in final
                return queryOptions.ApplyTo(queryable, querySettings, AllowedQueryOptions.All ^ excludedQueryOptions); // Queryable for memory
            }
            return base.ApplyQuery(queryable, queryOptions);
        }

        public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
        {
            if (!AllowOpenProperties)
            {
                if (HasOpenPropertySelectedItem(queryOptions))
                {
                    throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, "The query parameter '$select' is not supported on open properties."));
                }
                if (HasOpenPropertyFilteredItem(queryOptions))
                {
                    throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, "The query parameter '$filter' is not supported on open properties."));
                }
            }
            base.ValidateQuery(request, queryOptions);
        }

        private static IEnumerable<string> GetExpandedNavigationProperties(ODataQueryOptions queryOptions)
        {
            if (queryOptions.SelectExpand == null)
            {
                return new List<string>().AsReadOnly();
            }

            var expandedNavigationSelectedItemTranslator = new ExpandedNavigationSelectedItemTranslator();
            return expandedNavigationSelectedItemTranslator.Translate(queryOptions.SelectExpand.SelectExpandClause).ToList().AsReadOnly();
        }

        private static bool HasOpenPropertySelectedItem(ODataQueryOptions queryOptions)
        {
            if (queryOptions.SelectExpand == null)
            {
                return false;
            }

            var openPropertySelectedItemTranslator = new OpenPropertySelectedItemTranslator();
            return openPropertySelectedItemTranslator.Translate(queryOptions.SelectExpand.SelectExpandClause);
        }

        private static bool HasOpenPropertyFilteredItem(ODataQueryOptions queryOptions)
        {
            if (queryOptions.Filter == null)
            {
                return false;
            }

            var openPropertyVisitor = new OpenPropertyVisitor();
            return openPropertyVisitor.HasOpenPropertyAccessNode(queryOptions.Filter.FilterClause.Expression);
        }

        private class ExpandedNavigationSelectedItemTranslator : SelectItemTranslator<IEnumerable<string>>
        {
            public IEnumerable<string> Translate(Microsoft.OData.UriParser.SelectExpandClause selectExpandClause)
            {
                return selectExpandClause.SelectedItems.SelectMany(t => t.TranslateWith(this));
            }

            public override IEnumerable<string> Translate(Microsoft.OData.UriParser.ExpandedNavigationSelectItem item)
            {
                var expandedNavigationProperties = new List<string>
            {
                item.PathToNavigationProperty.Cast<Microsoft.OData.UriParser.NavigationPropertySegment>().Single().NavigationProperty.Name
            };

                var translation = Translate(item.SelectAndExpand);
                if (translation != null)
                {
                    expandedNavigationProperties.AddRange(translation.Select(p => item.PathToNavigationProperty.Cast<Microsoft.Data.OData.Query.SemanticAst.NavigationPropertySegment>().Single().NavigationProperty.Name + "." + p));
                }
                return expandedNavigationProperties;
            }

            public override IEnumerable<string> Translate(ExpandedReferenceSelectItem item)
            {
                return new string[] { };
            }

            public override IEnumerable<string> Translate(NamespaceQualifiedWildcardSelectItem item)
            {
                return new string[] { };
            }

            public override IEnumerable<string> Translate(Microsoft.OData.UriParser.PathSelectItem item)
            {
                return new string[] { };
            }

            public override IEnumerable<string> Translate(Microsoft.OData.UriParser.WildcardSelectItem item)
            {
                return new string[] { };
            }
        }

        private class OpenPropertySelectedItemTranslator :SelectItemTranslator<bool>
        {
            public bool Translate(Microsoft.OData.UriParser.SelectExpandClause selectExpandClause)
            {
                return selectExpandClause.SelectedItems.Any(t => t.TranslateWith(this));
            }

            public override bool Translate(Microsoft.OData.UriParser.ExpandedNavigationSelectItem item)
            {
                return false;
            }

            public override bool Translate(ExpandedReferenceSelectItem item)
            {
                return false;
            }

            public override bool Translate(NamespaceQualifiedWildcardSelectItem item)
            {
                return false;
            }

            public override bool Translate(Microsoft.OData.UriParser.PathSelectItem item)
            {
                return item.SelectedPath.OfType<OpenPropertySegment>().Any();
            }

            public override bool Translate(Microsoft.OData.UriParser.WildcardSelectItem item)
            {
                return false;
            }
        }

        private class OpenPropertyVisitor : Microsoft.OData.UriParser.QueryNodeVisitor<bool>
        {
            public bool HasOpenPropertyAccessNode(Microsoft.OData.UriParser.QueryNode node)
            {
                if (node == null)
                {
                    return false;
                }
                return node.Accept(this);
            }

            public override bool Visit(Microsoft.OData.UriParser.SingleValueOpenPropertyAccessNode node)
            {
                return true;
            }

            public override bool Visit(Microsoft.OData.UriParser.AllNode node)
            {
                return HasOpenPropertyAccessNode(node.Source) || HasOpenPropertyAccessNode(node.Body);
            }

            public override bool Visit(Microsoft.OData.UriParser.AnyNode node)
            {
                return HasOpenPropertyAccessNode(node.Source) || HasOpenPropertyAccessNode(node.Body);
            }

            public override bool Visit(Microsoft.OData.UriParser.BinaryOperatorNode node)
            {
                return HasOpenPropertyAccessNode(node.Left) || HasOpenPropertyAccessNode(node.Right);
            }

            public override bool Visit(Microsoft.OData.UriParser.CollectionFunctionCallNode node)
            {
                return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
            }

            public override bool Visit(Microsoft.OData.UriParser.CollectionNavigationNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(CollectionOpenPropertyAccessNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(Microsoft.OData.UriParser.CollectionPropertyAccessNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(Microsoft.OData.UriParser.ConstantNode node)
            {
                return false;
            }

            public override bool Visit(Microsoft.OData.UriParser.ConvertNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(CountNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(Microsoft.OData.UriParser.NamedFunctionParameterNode node)
            {
                return HasOpenPropertyAccessNode(node.Value);
            }

            public override bool Visit(ParameterAliasNode node)
            {
                return false;
            }

            public override bool Visit(SearchTermNode node)
            {
                return false;
            }



            public override bool Visit(SingleValueCastNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(Microsoft.OData.UriParser.SingleValueFunctionCallNode node)
            {
                return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
            }

            public override bool Visit(Microsoft.OData.UriParser.SingleValuePropertyAccessNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(Microsoft.OData.UriParser.UnaryOperatorNode node)
            {
                return HasOpenPropertyAccessNode(node.Operand);
            }


            public override bool Visit(NonResourceRangeVariableReferenceNode node)
            {
                return HasOpenPropertyAccessNode(node.RangeVariable.CollectionNode);
            }

            public override bool Visit(CollectionResourceCastNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(SingleResourceCastNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

            public override bool Visit(SingleResourceFunctionCallNode node)
            {
                return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
            }

            public override bool Visit(CollectionResourceFunctionCallNode node)
            {
                return HasOpenPropertyAccessNode(node.Source) || node.Parameters.Any(HasOpenPropertyAccessNode);
            }

            public override bool Visit(ResourceRangeVariableReferenceNode node)
            {
                return HasOpenPropertyAccessNode(node.RangeVariable.CollectionResourceNode);
            }
            public override bool Visit(Microsoft.OData.UriParser.SingleNavigationNode node)
            {
                return HasOpenPropertyAccessNode(node.Source);
            }

        }
    }

Hi @a-elnajjar, sorry for the late answer... I'm kinda too busy these days that I forgot to answer your question. My code is based on ODL 6.15.0.

Thanks for sharing your updated version.

This issue seems to still be present. The custom attributes made by @halipourian and @a-elnajjar work, but it doesn't seem right having to maintain such a solution yourself. Is this something that will be added?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

abkmr picture abkmr  路  3Comments

davidmorissette picture davidmorissette  路  3Comments

NetTecture picture NetTecture  路  4Comments

VikingsFan picture VikingsFan  路  5Comments

LianwMS picture LianwMS  路  5Comments