OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
447 stars 158 forks source link

$apply=aggregate returns only JSON payload #1144

Open audacity76 opened 7 months ago

audacity76 commented 7 months ago

This old issue still is present in the latest AspNetCoreOData Version 8.2.3: https://github.com/OData/WebApi/issues/1712

habbes commented 7 months ago

@audacity76 did you also observe this issue when using unbound functions?

audacity76 commented 7 months ago

@habbes In my case it was a bound function. It happened when using the aggregate function on an endpoint

habbes commented 7 months ago

We've confirmed that the current output does not match what's expected and that the response should be a standard OData response. We'll investigate and work on a fix.

sturla78 commented 2 months ago

I can confirm that is still present with AspNetCoreOData Version 8.2.5, using $apply=aggregate but also $apply=groupby

Xriuk commented 3 weeks ago

+1, from a quick look at the code I see that there is no serializer being selected because here (I guess) https://github.com/OData/AspNetCoreOData/blob/599dc1bdca9edc2cf88935ed54fe46ad5478da5b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs#L200 the type IEnumerable<GroupByWrapper> cannot get a corresponding IEdmType, thus no serializer is being selected.

Xriuk commented 3 weeks ago

Quick fix:

public class CustomODataSerializerProvider : ODataSerializerProvider {
    private readonly IServiceProvider _serviceProvider;

    public CustomODataSerializerProvider(IServiceProvider serviceProvider) :
        base(serviceProvider) {

        _serviceProvider = serviceProvider;
    }

    public override IODataSerializer GetODataPayloadSerializer(Type type, HttpRequest request) {
        // Handle GroupByWrapper
        var ienumerable = type.GetGenericBaseType(typeof(IEnumerable<>))?.GetGenericArguments()[0];
        while(ienumerable != null) {
            // Here you could also instantiate it directly with new GroupByODataResourceSetSerializer(...)
            if(ienumerable.FullName == "Microsoft.AspNetCore.OData.Query.Wrapper.GroupByWrapper")
                return _serviceProvider.GetRequiredService<GroupByODataResourceSetSerializer>();

            if (ienumerable.BaseType != null && ienumerable.BaseType != typeof(object))
                ienumerable = ienumerable.BaseType;
            else
                ienumerable = null;
        }

        return base.GetODataPayloadSerializer(type, request);
    }
}

public class GroupByODataResourceSetSerializer : ODataResourceSetSerializer {
    public GroupByODataResourceSetSerializer(IODataSerializerProvider serializerProvider) :
        base(serializerProvider) {}

    public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) {
        IEdmEntitySetBase entitySet = (writeContext.NavigationSource as IEdmEntitySetBase)!;

        // The resource type should be the original collection before GroupBy so we retrieve it from the path
        var pathType = writeContext.Path.LastSegment.EdmType.AsElementType();
        IEdmTypeReference resourceSetType = new EdmCollectionTypeReference(new EdmCollectionType(pathType is IEdmEntityType ? new EdmEntityTypeReference((IEdmEntityType)pathType, false) : new EdmComplexTypeReference((IEdmComplexType)pathType, false)));
        var resourceType = resourceSetType.AsCollection().ElementType().AsStructured();

        ODataWriter writer = await messageWriter.CreateODataResourceSetWriterAsync(entitySet, resourceType.StructuredDefinition())
            .ConfigureAwait(false);
        await WriteObjectInlineAsync(graph, resourceSetType, writer, writeContext)
            .ConfigureAwait(false);
    }
}

And then while configuring:

services.AddMvc(...)
.AddOData((options, s) => {
        ...
        options.AddRouteComponents("odata", s.GetRequiredService<IEdmModel>(), odataServiceCollection => {
            ...
            odataServiceCollection.AddSingleton<IODataSerializerProvider, CustomODataSerializerProvider>();
            odataServiceCollection.AddSingleton<GroupByODataResourceSetSerializer>();
        });
    })

This also returns the correct @odata.context.