OData / AspNetCoreOData

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

Property restrictions not applied when not using OData model builder #1076

Open gathogojr opened 1 year ago

gathogojr commented 1 year ago

Assemblies affected ASP.NET Core OData 8.2.3

Describe the bug Consider the following data model and OData controller:

namespace NS.Models
{
    public class Employee
    {
        public int Id { get; set; }
        public decimal Salary { get; set; }
    }
}

public class EmployeesController : ODataController
{
    [EnableQuery]
    public ActionResult<IEnumerable<Employee>> Get()
    {
        return Ok(new List<Employee>
        {
            new Employee { Id = 1, Salary = 1700 },
            new Employee { Id = 2, Salary = 1300 }
        });
    }
}

Consider further the following 3 methods with each initializing a matching Edm model as well as adding a sort restriction on the Salary property:

IEdmModel GetEdmModel01()
{
    var modelBuilder = new ODataConventionModelBuilder();
    var entityTypeConfiguration = modelBuilder.EntitySet<Employee>("Employees").EntityType;
    entityTypeConfiguration.Property(d => d.Salary).NotSortable = true;

    return modelBuilder.GetEdmModel();
}

IEdmModel GetEdmModel02()
{
    var model = new EdmModel();

    var employeeEntityType = model.AddEntityType("NS.Models", "Employee");
    employeeEntityType.AddKeys(employeeEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32));
    employeeEntityType.AddStructuralProperty("Salary", EdmPrimitiveTypeKind.Decimal);

    var entityContainer = model.AddEntityContainer("Default", "Container");

    var employeesEntitySet = entityContainer.AddEntitySet("Employees", employeeEntityType);

    var notSortableVocabularyAnnotation = new EdmVocabularyAnnotation(
        employeesEntitySet,
        new EdmTerm(
            "Org.OData.Capabilities.V1",
            "SortRestrictions",
            new EdmEntityTypeReference(employeeEntityType, isNullable: false)),
        new EdmRecordExpression(
            new EdmPropertyConstructor("Sortable", new EdmBooleanConstant(true)),
            new EdmPropertyConstructor("AscendingOnlyProperties", new EdmCollectionExpression()),
            new EdmPropertyConstructor("DescendingOnlyProperties", new EdmCollectionExpression()),
            new EdmPropertyConstructor("NonSortableProperties", new EdmCollectionExpression(
                new EdmPropertyPathExpression("Salary")))));
    model.SetVocabularyAnnotation(notSortableVocabularyAnnotation);

    return model;
}

IEdmModel GetEdmModel03()
{
    var csdl = @"<?xml version=""1.0"" encoding=""utf-8""?>
<edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
    <edmx:DataServices>
        <Schema Namespace=""NS.Models"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
            <EntityType Name=""Employee"">
                <Key>
                    <PropertyRef Name=""Id"" />
                </Key>
                <Property Name=""Salary"" Type=""Edm.Decimal"" Nullable=""false"" Scale=""Variable"" />
                <Property Name=""Id"" Type=""Edm.Int32"" Nullable=""false"" />
                <Property Name=""Name"" Type=""Edm.String"" />
            </EntityType>
        </Schema>
        <Schema Namespace=""Default"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
            <EntityContainer Name=""Container"">
                <EntitySet Name=""Employees"" EntityType=""NS.Models.Employee"">
                    <Annotation Term=""Org.OData.Capabilities.V1.SortRestrictions"">
                        <Record>
                            <PropertyValue Property=""Sortable"" Bool=""true"" />
                            <PropertyValue Property=""AscendingOnlyProperties"">
                                <Collection />
                            </PropertyValue>
                            <PropertyValue Property=""DescendingOnlyProperties"">
                                <Collection />
                            </PropertyValue>
                            <PropertyValue Property=""NonSortableProperties"">
                                <Collection>
                                    <PropertyPath>Salary</PropertyPath>
                                </Collection>
                            </PropertyValue>
                        </Record>
                    </Annotation>
                </EntitySet>
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>";

    IEdmModel model;
    using (var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(csdl)))
    using (var reader = XmlReader.Create(memoryStream))
    {
        if (!CsdlReader.TryParse(reader, out model, out IEnumerable<EdmError> errors))
        {
            throw new Exception(string.Join("\r\n", errors.Select(d => d.ErrorMessage)));
        }

        return model;
    }
}

If you configure an OData service with the Edm model based off of GetEdmModel01 method, the sort restriction is enforced such that sorting by Salary property is not allowed, but if you do the same using the Edm model based off of either GetEdmModel02 or GetEdmModel03 methods, sorting is not restricted.

using System.Text;
using System.Xml;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Csdl;
using Microsoft.OData.Edm.Validation;
using Microsoft.OData.Edm.Vocabularies;
using Microsoft.OData.ModelBuilder;
using NS.Models;

var builder = WebApplication.CreateBuilder(args);

var model = GetEdmModel01(); // GetEdmModel02() or GetEdmModel03()
builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        model));

var app = builder.Build();

app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());

app.Run();

Request/Response Scenario 1: GetEdmModel01()

GET http://localhost:5242/Employees?$orderby=Salary

Result:

{
    "error": {
        "code": "",
        "message": "The query specified in the URI is not valid. The property 'Salary' cannot be used in the $orderby query option.",
        "details": [],
        "innererror": {
            "message": "The property 'Salary' cannot be used in the $orderby query option.",
            "type": "Microsoft.OData.ODataException",
            "stacktrace": "   at Microsoft.AspNetCore.OData.Query.Validator.OrderByModelLimitationsValidator.TryValidate(OrderByClause orderByClause, Boolean explicitPropertiesDefined) in C:\\Users\\jogathog\\Projects\\AspNetCoreOData\\src\\Microsoft.AspNetCore.OData\\Query\\Validator\\OrderByModelLimitationsValidator.cs:line 50\r\n   at Microsoft.AspNetCore.OData.Query.Validator.OrderByQueryValidator.Validate(OrderByQueryOption orderByOption, ODataValidationSettings validationSettings) in C:\\Users\\jogathog\\Projects\\AspNetCoreOData\\src\\Microsoft.AspNetCore.OData\\Query\\Validator\\OrderByQueryValidator.cs:line 57\r\n   at Microsoft.AspNetCore.OData.Query.OrderByQueryOption.Validate(ODataValidationSettings validationSettings) in C:\\Users\\jogathog\\Projects\\AspNetCoreOData\\src\\Microsoft.AspNetCore.OData\\Query\\Query\\OrderByQueryOption.cs:line 231\r\n   at Microsoft.AspNetCore.OData.Query.Validator.ODataQueryValidator.Validate(ODataQueryOptions options, ODataValidationSettings validationSettings) in C:\\Users\\jogathog\\Projects\\AspNetCoreOData\\src\\Microsoft.AspNetCore.OData\\Query\\Validator\\ODataQueryValidator.cs:line 62\r\n   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.Validate(ODataValidationSettings validationSettings) in C:\\Users\\jogathog\\Projects\\AspNetCoreOData\\src\\Microsoft.AspNetCore.OData\\Query\\ODataQueryOptions.cs:line 651\r\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions) in C:\\Users\\jogathog\\Projects\\AspNetCoreOData\\src\\Microsoft.AspNetCore.OData\\Query\\EnableQueryAttribute.cs:line 743\r\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuting(ActionExecutingContext actionExecutingContext) in C:\\Users\\jogathog\\Projects\\AspNetCoreOData\\src\\Microsoft.AspNetCore.OData\\Query\\EnableQueryAttribute.cs:line 79"
        }
    }
}

Scenario 2: GetEdmModel02() or GetEdmModel03()

GET http://localhost:5242/Employees?$orderby=Salary

Result:

{
    "@odata.context": "http://localhost:5242/$metadata#Employees",
    "value": [
        {
            "Id": 2,
            "Salary": 1300
        },
        {
            "Id": 1,
            "Salary": 1700
        }
    ]
}

Expected behavior Independent of the way the Edm model is built, the property restrictions should be applied.

Additional context A QueryablePropertyRestriction annotation value is added to the model's annotation manager in the working scenario but not in the non-working scenario. That restriction is relied upon when checking whether a restriction is enabled or not.

xuzhg commented 1 year ago

@gathogojr In the second and third 'GetEdmModel', try to add the annotations manually when finished the model construction?