AspNetCoreOData
AspNetCoreOData copied to clipboard
Return HTTP Status Code 202 Accepted (ex. AcceptedODataResult<T>)
I would like to return an HTTP Status Code 202 Accepted as a result of POST, PUT, and DELETE requests.
For example, as a result of a POST request, instead of returning new CreateODataResult<MyEntity>(entity);, I want to return new AcceptedODataResult<MyEntity<(entity); so the only difference being an HTTP status code of 202 instead of 201.
This doesn't seem to be easily possible without exposing code marked as internal.
I'm currently using Microsoft.AspNetCore.OData 7.5.12.
Thank you.
Can you elaborate why the standard MVC accepted is not sufficient?
ODataCreatedResult and ODataUpdatedResult exist because they have special behavior to them that is exclusive to OData.
ODataCreatedResult and ODataUpdatedResult exist because they have special behavior to them that is exclusive to OData.
I want to retain the same special behavior exclusive to OData and return an HTTP status of 202.
I'm expecting that some logic would be missing if I return this.Accepted(entity);. Is that not the case?
Thank you.
@icnocop
I think it's easy to create a accept result 'AcceptedODataResult' by yourself to change the default behavior.
You can find a lot of samples at:
https://github.com/OData/WebApi/tree/master/src/Microsoft.AspNetCore.OData/Results
I tried to create a custom ODataResult in my own library as follows:
internal class AcceptedODataResult<T> : IActionResult
{
private readonly T _innerResult;
public AcceptedODataResult(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
this._innerResult = entity;
}
public virtual T Entity
{
get
{
return _innerResult;
}
}
public async virtual Task ExecuteResultAsync(ActionContext context)
{
HttpRequest request = context.HttpContext.Request;
HttpResponse response = context.HttpContext.Response;
IActionResult result = GetInnerActionResult(request);
Uri location = GenerateLocationHeader(request);
response.Headers["Location"] = location.AbsoluteUri;
await result.ExecuteResultAsync(context);
ResultHelpers.AddEntityId(response, () => GenerateEntityId(request));
ResultHelpers.AddServiceVersion(response, () => ODataUtils.ODataVersionToString(ResultHelpers.GetODataVersion(request)));
}
internal IActionResult GetInnerActionResult(HttpRequest request)
{
if (RequestPreferenceHelpers.RequestPrefersReturnNoContent(new WebApiRequestHeaders(request.Headers)))
{
return new StatusCodeResult((int)HttpStatusCode.NoContent);
}
else
{
ObjectResult objectResult = new ObjectResult(_innerResult)
{
StatusCode = StatusCodes.Status202Accepted // <-- changed to HTTP Status Code 202
};
return objectResult;
}
}
internal Uri GenerateEntityId(HttpRequest request)
{
return ResultHelpers.GenerateODataLink(request, _innerResult, isEntityId: true);
}
internal Uri GenerateLocationHeader(HttpRequest request)
{
return ResultHelpers.GenerateODataLink(request, _innerResult, isEntityId: false);
}
}
But this results in compiler errors because the code uses internal classes such as ResultHelpers, RequestPreferenceHelpers, and WebApiRequestHeaders.
With this approach, I'd also have to also copy the code in those classes and its dependencies as well, which doesn't seem like the best approach.
I also tried to inherit from CreatedODataResult<T> and modify the HTTP status code, but I get InvalidOperationException: StatusCode cannot be set because the response has already started:
internal class AcceptedODataResult<T> : CreatedODataResult<T>
{
public AcceptedODataResult(T entity)
: base(entity)
{
}
public override async Task ExecuteResultAsync(ActionContext context)
{
await base.ExecuteResultAsync(context);
context.HttpContext.Response.StatusCode = StatusCodes.Status202Accepted; // <-- changed to HTTP Status Code 202
}
}
Would a pull request with AcceptedODataResult<T> implemented be accepted and deployed in a subsequent 7.5.x release?
Thank you.
ODataCreatedResult and ODataUpdatedResult exist because they have special behavior to them that is exclusive to OData.
I want to retain the same special behavior exclusive to OData and return an HTTP status of 202.
I'm expecting that some logic would be missing if I return
this.Accepted(entity);. Is that not the case?
Again, what special behavior in particular do you want to retain @icnocop ? 202 Accepted is usually used for asynchronous logic, so you wouldn't be returning an entity payload on those methods. Headers controlling the payload wouldn't make sense here. Also, I think the location header should only be included on creation, which is also something you probably shouldn't do with 202.
Again, what special behavior in particular do you want to retain
I would like the logic which adds OData specific HTTP headers to be retained.
202 Accepted is usually used for asynchronous logic, so you wouldn't be returning an entity payload on those methods.
In my case, when a POST request is sent to the API endpoint, the entity could be stored with a status of "Pending" because the server needs to process it by sending the data to a third party service. The third party service may currently be offline, and so the data won't be processed until it comes back online for example.
I expected to return the entity payload as a result of the POST request because a unique identifier is created for it, and the current status could also be returned. The entity can still be updated in its "Pending" state from a subsequent PUT request for example.
Headers controlling the payload wouldn't make sense here.
I would like to retain the same behavior, and not have to re-write the same code, if the user prefers no-content as a result:
https://github.com/OData/WebApi/blob/0a23c07bfd74052a744e8607c9a0420d2790f3ef/src/Microsoft.AspNetCore.OData/Results/CreatedODataResult.cs#L74-L77
I would rather not have to maintain duplicate code, especially if/when the logic changes in "Microsoft.AspNetCore.OData" for example.
Also, I think the location header should only be included on creation, which is also something you probably shouldn't do with 202.
I expected to return the location header so it can be used by the caller to check the status of the entity. For example, if the third party service comes back online and the data has been processed its status of "Pending" will have changed.
Thank you.
Fair enough, thanks for clarifying @icnocop . In this case, I'd suggest just checking the source code for one of the OData results and copying it on your side: the code is actually fairly straightforward.
What you are attempting to do is not part of the OData spec as far as I know, so I don't think adding such result into the framework would make sense. Someone can correct me if I'm wrong here.
@icnocop Have you tried setting the statuscode before calling ExecuteAsync see this https://github.com/OData/WebApi/blob/0a23c07bfd74052a744e8607c9a0420d2790f3ef/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs#L59-#L67
What @icnocop is really asking for is support for §8.2.8 in the official OData specification, which doesn't appear to be supported at this level. More specifically, it's the inclusion of RFC 7240 in OData. The core OData APIs do support parsing the header, but it is unused.
The Prefer header allows a client to express a preference to the server, but the server ultimately decides whether it honors it. A server is supposed to respond with Preference-Applied so the client knows what, if any, preferences were honored.
A client that sends Prefer: respond-async expects the server to execute asynchronously and return 202. A client that sends Prefer: return=minimal expects 204 with no content, which helps the server from unnecessarily echoing back the content the client provided in PUT or POST. Client could send Prefer: respond-async; return=minimal. That might seem nonsensical, but remember that the server chooses which preferences are applied.
This has been in the spec for a many, many years. It's old and on the old Web API stack, but here's example usage of Prefer with OData: CircuitBreakerT.cs. Perhaps you can modernize it and make use of it in your scenario.
@icnocop if you're building a RESTful API, then getting an identifier from the body is wrong. When using HTTP, a resource's identifier is expressed in the Location header for a compliant REST API. The most common misunderstanding that I see service authors make is thinking that 123 in /order/123 is the ID; it's not. In accordance with the Uniform Interface, the URL, and hence the path, is the resource identifier. www.somewhere.com indicates where to find it and order/123 indicates how to identify it. I realize that might sound pedantic, but that's how Fielding intended it to work in HTTP. Even the / conveys zero hierarchy and just makes a path easier for humans to read. There's a reasonable chance you may need to extract the implementation-specific value from the URL in code (ex: 123 - the representation in REST), but this couldn't be any simpler:
// extract '123' from 'https://my.api.com/order/123'
var id = int.Parse( response.Headers.Location.Segments[^1].TrimEnd( '/' ) );
If you're not building a RESTful API, then so be it. This approach has the advantage of not returning the entire resource just for its identifier. It will also play nice with other concepts such as Prefer: return=minimal.
Apologies for the commentary, but OData has some pretty awesome capabilities in the protocol that are still yet to be fully realized in any of the implementation frameworks. Hopefully one day... 😉