AspNetCoreOData
AspNetCoreOData copied to clipboard
Must call ActionConfiguration.ReturnsFromEntitySet or Request.GetETag() returns null
Assemblies affected
Microsoft.AspNetCore.OData v9.2.1
Describe the bug
Request.GetETag() returns null when used in an action that does not call ActionConfiguration.ReturnsFromEntitySet().
entitySet.EntityType.Action(nameof(CustomersController.ThisWorks)).ReturnsFromEntitySet(entitySet);
entitySet.EntityType.Action(nameof(CustomersController.ThisFails));
While I can include the call to ActionConfiguration.ReturnsFromEntitySet() to get around the issue, I don't want to because the action definition is wrong - I don't want to return an entity.
Reproduce steps
See the repo: https://github.com/andygjp/ActionMissingPath
EDM (CSDL) Model
<?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="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Customer">
<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false"/>
<Property Name="Version" Type="Edm.Int32" Nullable="false"/>
<Property Name="Name" Type="Edm.String" Nullable="false"/>
</EntityType>
<Action Name="ThisWorks" IsBound="true">
<Parameter Name="bindingParameter" Type="Default.Customer"/>
<ReturnType Type="Default.Customer" Nullable="false"/>
</Action>
<Action Name="ThisFails" IsBound="true">
<Parameter Name="bindingParameter" Type="Default.Customer"/>
</Action>
<EntityContainer Name="Container">
<EntitySet Name="Customers" EntityType="Default.Customer">
<Annotation Term="Org.OData.Core.V1.OptimisticConcurrency">
<Collection>
<PropertyPath>Version</PropertyPath>
</Collection>
</Annotation>
</EntitySet>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Request/Response
You can find sample requests here: https://github.com/andygjp/ActionMissingPath/blob/main/ActionMissingPath/ActionMissingPath.http
Expected behavior
I should be able to define an action, without calling ReturnsFromEntitySet, and Request.GetETag() returns the etag, not null.
From OData spec, it seems it only generates the ETag for entity. So, if you try to get ETag for non-entity, you get 'null' expected.
If you call 'action', the final result is the action return, not the 'binding' resource of that action. So, if the action returns nothing, there's no way to calculate the ETag.
Would you please let us know more details about your scenario?
In my scenario, I have an entitySet of Customer (/api/Customers). A Customer can have many Orders. A Customer might request to be removed from the database. I want to remove the Customer, and maintain their Orders, but remove the Customer (and other identifiable information) from it. I plan to do that using an action: /api/Customers/123/AnonymiseOrders. The request should look like this:
POST /api/Customers/123/AnonymiseOrders
If-Match: {{etag}}
Content-Type: application/json
I don't want to return the Customer. I had planned to return 204, after anonymising the orders, and 404, if the Customer does not exist.
As I read this: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-odata/c3569037-0557-4769-8f75-a91ffcd7b05b, I think I should be able to retrieve the etag from the If-Match header:
Additionally, this header MAY be present on POST requests to invoke an action (section 2.2.1.3) bound to an entity. This allows clients to prevent an action from having inadvertent side effects based on the wrong version of a resource.
And the example, here: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_InvokinganAction, shows the action called with the If-Match header:
POST http://host/service/Customers('ALFKI')/SampleEntities.CreateOrder If-Match: W/"MjAxOS0wMy0yMVQxMzowNVo="
{ "items": [ { "product": 4001, "quantity": 2 }, { "product": 7062, "quantity": 1 } ], "discountCode": "BLACKFRIDAY" }