Skip to content

When using OData $apply in a query, the response does not comply with the OData protocol. I have created a demo application to demonstrate a solution to this issue.

Notifications You must be signed in to change notification settings

peterkovecses/ODataApplyDemo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

I've experienced a bug when using OData $apply. The point is that the response will be in the wrong format, not compliant with the OData protocol.

Some sample requests:

https://localhost:7109/odata/WeatherForecast?$apply=groupby((TemperatureC))

https://localhost:7109/odata/WeatherForecast?$apply=groupby((TemperatureC),aggregate($count%20as%20Count))

The responses:

image image

As you can see, the @odata.context and value properties are missing, instead the value of the value property is returned directly.

I saw that the issue had already been reported, but I needed a quick solution. I found that if the return value of the controller method is IEnumerable or IQueriable instead of IActionResult or ActionResult, the response is in the correct format, but that was not an option for me, I had to find another solution. As a first step, I derived it from the EnableQueryAttribute class, and in the case of $apply, I added a marker key-value pair to the response header:

'''

public class CustomEnableQuery : EnableQueryAttribute
{
    private HttpContext? _httpContext;

    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        if (queryOptions.Apply is not null)
        {
            _httpContext!.Response.Headers.TryAdd(HeaderKeys.ODataApplyPatch, "1");
        }
        
        return queryOptions.ApplyTo(queryable);
    }

    public override void ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions)
    {
        _httpContext = request.HttpContext;
        base.ValidateQuery(request, queryOptions);
    }
}

'''

Next, I created a wrapper class:

'''

public class OdataResponseWrapper
{
    [JsonPropertyName("@odata.context")]
    public required string Context { get; set; }

    [JsonPropertyName("value")]
    public required object[] Value { get; set; }
}

'''

I monitor the marker header in a middleware, and if it appears, I wrap the value in the response body so that it complies with the OData protocol.

'''

public class ODataApplyPatchMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var response = context.Response;
        var originalResponseBodyStream = response.Body;
        using var updatedBodyStream = new MemoryStream();
        response.Body = updatedBodyStream;

        await next(context);

        if (context.Response.Headers.TryGetValue(HeaderKeys.ODataApplyPatch, out _))
        {
            context.Response.Headers.Remove(HeaderKeys.ODataApplyPatch);
            await UpdateResponseBodyAsync(response, updatedBodyStream , context.Request.FullUrl());
        }

        await FinalizeResponseBody(updatedBodyStream, originalResponseBodyStream, response);
    }

    private static async Task UpdateResponseBodyAsync(HttpResponse response, Stream updatedBodyStream, string requestUrl)
    {
        var stream = response.Body;
        updatedBodyStream.Seek(0, SeekOrigin.Begin);
        var responseBody = await new StreamReader(updatedBodyStream).ReadToEndAsync();
        updatedBodyStream.Seek(0, SeekOrigin.Begin);
        var jsonContent = GenerateODataResponseContent(requestUrl, responseBody);
        stream.SetLength(0);
        using var writer = new StreamWriter(stream, leaveOpen: true);
        await writer.WriteAsync(jsonContent);
        await writer.FlushAsync();
        response.ContentLength = stream.Length;
    }

    private static string GenerateODataResponseContent(string requestUrl, string responseBody)
    {
        var value = JsonSerializer.Deserialize<object[]>(responseBody);
        var odataResponse = new OdataResponseWrapper
        {
            Context = requestUrl,
            Value = value!
        };
        var jsonContent = JsonSerializer.Serialize(odataResponse);
        return jsonContent;
    }

    private static async Task FinalizeResponseBody(Stream updatedBodyStream, Stream originalResponseBodyStream,
        HttpResponse response)
    {
        updatedBodyStream.Seek(0, SeekOrigin.Begin);
        await updatedBodyStream.CopyToAsync(originalResponseBodyStream);
        response.Body = originalResponseBodyStream;
    }
}

'''

In addition, I only had to register the middleware in the Program.cs: '''

app.UseMiddleware<ODataApplyPatchMiddleware>();

'''

And the results:

image image

About

When using OData $apply in a query, the response does not comply with the OData protocol. I have created a demo application to demonstrate a solution to this issue.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages