OData / WebApi

OData Web API: A server library built upon ODataLib and WebApi
https://docs.microsoft.com/odata
Other
855 stars 475 forks source link

Using MapODataServiceRoute to register OData route prevent query operations #1903

Open norcino opened 5 years ago

norcino commented 5 years ago

Setting up OData route using MapODataServiceRoute causes the controller to block all operations such, selection, filtering and ordering. I tried to use SetDefaultQuerySettings enabling everything, or whitelisting everything in the EnableQuery attribute but with no success. The only way I could solve my problem was to use app.UseOData to register odata route.

Assemblies affected

Microsoft.AspNetCore.App 2.2.0 Microsoft.AspNetCore.OData 7.2.1

Reproduce steps

Create an AspNet core 2.2 application, and register the OdataRoute using:

Startup.cs

[...]
var builder = new ODataConventionModelBuilder(app.ApplicationServices);
var assessments = builder.EntitySet<Assessment>(nameof(Assessment) + "s");

// Method 1 attempt to enable filtering and ordering
assessments.EntityType.Filter(nameof(Assessment.IsDeleted));
assessments.EntityType.Filter(nameof(Assessment.AssessmentId));
assessments.EntityType.OrderBy(nameof(Assessment.IsDeleted));
assessments.EntityType.OrderBy(nameof(Assessment.AssessmentId));

var model = builder.GetEdmModel();

// Method 2 to enable filtering ordering and so on
var query = new DefaultQuerySettings
{
    EnableSelect = true,
    EnableCount = true,
    EnableFilter = true,
    EnableExpand = true,
    EnableOrderBy = true,
    EnableSkipToken = true,
    MaxTop = 100
};

// Enable authentication
app.UseAuthentication() 
    .UseMvc(routeBuilder =>
    {
        // Completion of Method 2
        routeBuilder.SetDefaultQuerySettings(query);
        // Model comees with Method 1 changes
        routeBuilder.MapODataServiceRoute("odata", "{tenant}/odata", model);
        routeBuilder.EnableDependencyInjection();
    });
[...]

AssessmentsController.cs

[Authorize]
[Route("{tenant}/odata/[controller]")]
public class AssessmentsController : ODataController
{
    [...]

    // Method 3 to enable query features
    [EnableQuery(AllowedFunctions = Microsoft.AspNet.OData.Query.AllowedFunctions.All,
        AllowedArithmeticOperators =Microsoft.AspNet.OData.Query.AllowedArithmeticOperators.All,
        AllowedQueryOptions = Microsoft.AspNet.OData.Query.AllowedQueryOptions.All,
        AllowedLogicalOperators = Microsoft.AspNet.OData.Query.AllowedLogicalOperators.All)]
    public IQueryable<Assessment> Get()
    {
        var list = new List<Assessment> {
            new Assessment { AssessmentId = 1, IsDeleted = false },
            new Assessment { AssessmentId = 2, IsDeleted = true }
        };

       return list.AsQueryable<Assessment>();
    }
    [...]

Expected result

I would expect to be able to filter, select, order and so on.

Actual result

Selection, filtering and ordering requests are not accepted:

"Message": "The query specified in the URI is not valid. The property 'AssessmentId' cannot be used in the $select query option.",
"ExceptionMessage": "The property 'AssessmentId' cannot be used in the $select query option.",
"ExceptionType": "Microsoft.OData.ODataException",
"StackTrace": "   at Microsoft.AspNet.OData.Query.Validators.SelectExpandQueryValidator.ValidateSelectItem(SelectItem selectItem, IEdmProperty pathProperty, IEdmStructuredType pathStructuredType, IEdmModel edmModel)\r\n   at Microsoft.AspNet.OData.Query.Validators.SelectExpandQueryValidator.ValidateRestrictions(Nullable`1 remainDepth, Int32 currentDepth, SelectExpandClause selectExpandClause, IEdmNavigationProperty navigationProperty, ODataValidationSettings validationSettings)\r\n   at Microsoft.AspNet.OData.Query.Validators.SelectExpandQueryValidator.Validate(SelectExpandQueryOption selectExpandQueryOption, ODataValidationSettings validationSettings)\r\n   at Microsoft.AspNet.OData.Query.SelectExpandQueryOption.Validate(ODataValidationSettings validationSettings)\r\n   at Microsoft.AspNet.OData.Query.Validators.ODataQueryValidator.Validate(ODataQueryOptions options, ODataValidationSettings validationSettings)\r\n   at Microsoft.AspNet.OData.Query.ODataQueryOptions.Validate(ODataValidationSettings validationSettings)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.CreateAndValidateQueryOptions(HttpRequest request, ODataQueryContext queryContext)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.<>c__DisplayClass1_0.<OnActionExecuted>b__1(ODataQueryContext queryContext)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, Func`2 modelFunction, IWebApiRequestMessage request, Func`2 createQueryOptionFunction)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.OnActionExecuted(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, IWebApiRequestMessage request, Func`2 modelFunction, Func`2 createQueryOptionFunction, Action`1 createResponseAction, Action`3 createErrorAction)"

Additional detail

The order by is accepted when I use in the EnableQueryAttribute AllowedOrderByProperties = "PropertyName". Then to fix the issue I had to change the way I register the OData route, as shown below. app.UseOData("odata", "{tenant}/odata", model);

Maybe I am doing something wrong, but if find it very misleading, so it is possible that this is a bug.

UPDATE 1 Using routeBuilder.Select().Filter(); in UseMvc also allows me to enable filtering selection and so on. But if I enable in this way I cannot restrict filtering or allow filtering in detail using the model.

So to clarify:

var assessments = builder.EntitySet<Assessment>(nameof(Assessment) + "s");
assessments.EntityType.Filter(QueryOptionSetting.Disabled, nameof(Assessment.AssessmentId));

used in combination with:

routeBuilder.Select().Filter();

Allows me to perform filtering on the property "AssessmentId" even if I try to disable it from the model, so it seems that either is ignored (as I think) or the setup done on the route builder has higher priority then the settings in the model.

norcino commented 5 years ago

I had to remove the routing tag from the controller. [Route("{tenant}/odata/[controller]")] Turns out that this messes completely OData, and in fact having the rout specified in the controller, I get exception when I add another get method, for example to Get all and Get(int key) by Id.

In this case the error is:

An unhandled exception occurred while processing the request. AmbiguousMatchException: The request matched multiple endpoints. Matches:

WebApplication1.Controllers.ValuesController.Get (WebApplication1) WebApplication1.Controllers.ValuesController.Get (WebApplication1) Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(CandidateSet candidates)

I am not closing this issue, because I tried to change the base controller from ODataController to ControllerBase, and with (or without) the ApiControllerAttribute I still get the same behaviour. I though OData was supposed to be easily retrofit to an existing controller, so I think the RouteAttribute should not clash in this way with the OData routing convention.

An additional issue I am trying to solve, is that if I remove the RouteAttribute, the IHttpContextAccessor does not have anymore the RouteData in the request context, which is something I used so far to take the tenant name from the routed url for the Authorization.