AspNetCoreOData
AspNetCoreOData copied to clipboard
Property restrictions not applied when not using OData model builder
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.
@gathogojr In the second and third 'GetEdmModel', try to add the annotations manually when finished the model construction?