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
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.
Assemblies affected ASP.NET Core OData 8.2.3
Describe the bug Consider the following data model and OData controller:
Consider further the following 3 methods with each initializing a matching Edm model as well as adding a sort restriction on the
Salary
property:If you configure an OData service with the Edm model based off of
GetEdmModel01
method, the sort restriction is enforced such that sorting bySalary
property is not allowed, but if you do the same using the Edm model based off of eitherGetEdmModel02
orGetEdmModel03
methods, sorting is not restricted.Request/Response Scenario 1:
GetEdmModel01()
Result:
Scenario 2:
GetEdmModel02()
orGetEdmModel03()
Result:
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.