OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
457 stars 158 forks source link

Annotations are not selectable #857

Open tomfrenzel opened 1 year ago

tomfrenzel commented 1 year ago

Assemblies affected ASP.NET Core OData 8.0.12

Describe the bug When specifying an instance annotation inside the select of a GET request, the user receives an error that this is not supported. The OData specification on the other hand states that it should be possible.

Reproduce steps This behavior can be reproduced simply by specifying any annotation inside the select of a request.

Data Model

namespace ODataRoutingSample.Models
{
    public class Customer
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public Color FavoriteColor { get; set; }

        public int Amount { get; set; }

        public virtual Address HomeAddress { get; set; }

        public virtual IList<Address> FavoriteAddresses { get; set; }
    }

    public class VipCustomer : Customer
    {
        public IList<string> Emails { get; set; }
    }
}

Request/Response Request: GET http://localhost:64771/v1/Customers?select=@ns.annotation Response:

{
    "error": {
        "code": "",
        "message": "The query specified in the URI is not valid. The last segment 'AnnotationSegment' of the select or expand query option is not supported.",
        "details": [],
        "innererror": {
            "message": "The last segment 'AnnotationSegment' of the select or expand query option is not supported.",
            "type": "Microsoft.OData.ODataException",
            "stacktrace": "   at Microsoft.AspNetCore.OData.Query.SelectExpandPathExtensions.GetFirstNonTypeCastSegment(ODataPath path, Func`2 middleSegmentPredicte, Func`2 lastSegmentPredicte, IList`1& remainingSegments) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\SelectExpandPathExtensions.cs:line 89\r\n   at Microsoft.AspNetCore.OData.Query.SelectExpandPathExtensions.GetFirstNonTypeCastSegment(ODataSelectPath selectPath, IList`1& remainingSegments) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\SelectExpandPathExtensions.cs:line 36\r\n   at Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.ProcessSelectedItem(PathSelectItem pathSelectItem, IEdmNavigationSource navigationSource, IDictionary`2 currentLevelPropertiesInclude) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\Expressions\\SelectExpandBinder.cs:line 651\r\n   at Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.GetSelectExpandProperties(IEdmModel model, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource, SelectExpandClause selectExpandClause, IDictionary`2& propertiesToInclude, IDictionary`2& propertiesToExpand, ISet`1& autoSelectedProperties) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\Expressions\\SelectExpandBinder.cs:line 512\r\n   at Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.ProjectElement(QueryBinderContext context, Expression source, SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\Expressions\\SelectExpandBinder.cs:line 400\r\n   at Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.BindSelectExpand(SelectExpandClause selectExpandClause, QueryBinderContext context) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\Expressions\\SelectExpandBinder.cs:line 84\r\n   at Microsoft.AspNetCore.OData.Query.Expressions.BinderExtensions.ApplyBind(ISelectExpandBinder binder, IQueryable source, SelectExpandClause selectExpandClause, QueryBinderContext context) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\Expressions\\BinderExtensions.cs:line 275\r\n   at Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption.ApplyTo(IQueryable queryable, ODataQuerySettings settings) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\Query\\SelectExpandQueryOption.cs:line 214\r\n   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplySelectExpand[T](T entity, ODataQuerySettings querySettings) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\ODataQueryOptions.cs:line 1086\r\n   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\ODataQueryOptions.cs:line 441\r\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\EnableQueryAttribute.cs:line 510\r\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\EnableQueryAttribute.cs:line 473\r\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuted(ActionExecutedContext actionExecutedContext, Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request) in C:\\Users\\admin\\Downloads\\AspNetCoreOData-main\\AspNetCoreOData-main\\src\\Microsoft.AspNetCore.OData\\Query\\EnableQueryAttribute.cs:line 308"
        }
    }
}

Expected behavior If the annotation exists, it should be included in the response

Additional context Although this sample project does not make use of annotations it shows that it is generally not possible to receive annotations in a selected list. I first noted the Bug when annotations of entities inside a list would not show up in the response body if only specific properties got requested by specifying them inside the select. After reading through the OData specification, I tried to also select the annotation but this gave me the error above.

habbes commented 1 year ago

This seems like a feature gap.

In the meantime, you can try a workaround using the Prefer: include-annotations header to specify which annotations should be returned: http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_Preferenceincludeannotationsodatainc

For example, Prefer: include-annotations=* to include all annotations.

Does that work for you?

tomfrenzel commented 1 year ago

Hi @habbes!

Unfortunately, the header does not help. I tried extending the Product model with the following property and expected to get the annotation returned when the header is present.

        [NotMapped]
        public ODataInstanceAnnotation Annotation => new("ns.annotations", new ODataCollectionValue()
        {
            Items = new List<ODataUntypedValue>()
            {
                new()
                {
                    RawValue = @"{""Name"": ""Test""}",
                    TypeAnnotation = new ODataTypeAnnotation(typeof(Customer).FullName)
                }
            },
            TypeName = $"Collection({typeof(ODataUntypedValue).FullName})"
        });

That was not the case. To get annotations working in general, I used the following custom code:

        public override ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
        {
            var resource = base.CreateResource(selectExpandNode, resourceContext);
            if (resourceContext != null &&
                resourceContext.ResourceInstance != null &&
                annotationsRequested(resourceContext)
            )
            {
                setAnnotations(resourceContext.ResourceInstance, resource);
            }
            return resource;
        }

        private bool annotationsRequested(ResourceContext resourceContext)
        {
            return resourceContext.SerializerContext.Request.Headers.TryGetValue("Prefer", out var headerValues) &&
                headerValues.Any(s => s.Contains("odata.include-annotations"));
        }

        private void setAnnotations(object resourceInstance, ODataResource resource)
        {
            foreach (var annotationProp in getAnnotationProperties(resourceInstance))
            {
                var annotationValue = annotationProp.GetValue(resourceInstance) as ODataInstanceAnnotation;
                if (annotationValue != null)
                {
                    if (resource.InstanceAnnotations == null)
                    {
                        resource.InstanceAnnotations = new List<ODataInstanceAnnotation>();
                    }
                    resource.InstanceAnnotations.Add(annotationValue);
                }
            }
        }

Is there currently even a way to make use of annotations out of the box? Because I couldn't get it working.