AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

$apply=aggregate returns only JSON payload

Open audacity76 opened this issue 1 year ago • 6 comments

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

audacity76 avatar Dec 19 '23 09:12 audacity76

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

habbes avatar Dec 19 '23 12:12 habbes

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

audacity76 avatar Dec 19 '23 12:12 audacity76

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.

habbes avatar Dec 20 '23 07:12 habbes

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

sturla78 avatar May 21 '24 17:05 sturla78

+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 avatar Jul 04 '24 09:07 Xriuk

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.

Xriuk avatar Jul 04 '24 12:07 Xriuk