JasperFx / marten

.NET Transactional Document DB and Event Store on PostgreSQL
https://martendb.io
MIT License
2.79k stars 441 forks source link

Calling MartenLinqQueryable.GetEnumerator() results in a NotSupportedException being thrown #3182

Closed iress-ljm closed 4 months ago

iress-ljm commented 4 months ago

Marten 7.9.0:

Attempting to perform a query using Marten Queryable, ASP.NET OData 8 and Newtonsoft for ASP .NET and seeing the following exception:

System.NotSupportedException: Marten does not know how to use result type System.Collections.IEnumerable
   at Marten.Internal.Storage.DocumentStorage`2.BuildHandler[TResult](IMartenSession session, ISqlFragment statement, ISqlFragment currentStatement)
   at Marten.Linq.Parsing.LinqQueryParser.buildHandlerForCurrentStatement[TResult](Statement top, SelectorStatement selector)
   at Marten.Linq.Parsing.LinqQueryParser.BuildHandler[TResult]()
   at Marten.Linq.MartenLinqQueryProvider.Execute[TResult](Expression expression)
   at Marten.Linq.MartenLinqQueryable`1.System.Collections.IEnumerable.GetEnumerator()
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Serialize(JsonWriter jsonWriter, Object value)
   at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
   at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
   at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultAsync>g__Logged|22_0(ResourceInvoker invoker, IActionResult result)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Prometheus.HttpMetrics.HttpRequestDurationMiddleware.Invoke(HttpContext context)
   at Prometheus.HttpMetrics.HttpRequestCountMiddleware.Invoke(HttpContext context)
   at Prometheus.HttpMetrics.HttpInProgressMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

It looks like Newtonsoft tries to iterate over the IQueryable implementation using the non-generic iterator. It calls MartenLinqQueryable.GetEnumerator() which results in the BuildHandler throwing an exception on this line because TResult is a non-generic IEnumerable without a type parameter matching TDocument.

Sample of the controller:

using Api.Data;
using Api.Model;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;

namespace Buyside.OrdersAndTrades.Api.Controllers;

[Authorize]
[Route("/rest/v0/orders")]
public sealed class OrderController(IOrderRepository orderRepository) : ODataController
{
    [HttpGet]
    [EnableQuery]
    public IQueryable<Order> GetAllOrders()
    {
        return orderRepository.GetAllOrders();
    }
}

Sample of the repository:

using Api.Model;
using Api.Queries;
using Marten;

namespace Buyside.OrdersAndTrades.Api.Data;

public class OrderRepository(IDocumentStore store) : IOrderRepository
{
    private readonly IDocumentStore _store = store;

    public IQueryable<Order> GetAllOrders()
    {
        using var session = CreateSession();

        return session.Query<Order>();
    }

    private IDocumentSession CreateSession() => _store.LightweightSession();
}

Sample of setup for OData and Newtonsoft:

    var modelBuilder = new ODataConventionModelBuilder();

    modelBuilder.EntitySet<Order>("Orders");
    modelBuilder.EntityType<Order>();

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<ValidationFilterAttribute>();
        options.Filters.Add<ErrorResultExceptionFilterAttribute>();
    })
    .AddNewtonsoftJson(opt =>
    {
        opt.SerializerSettings.Converters.Add(new StringEnumConverter());
    })
    .AddOData(options =>
    {
        options.EnableQueryFeatures().AddRouteComponents(modelBuilder.GetEdmModel());
    })
    .AddODataNewtonsoftJson();