OData / OData/AspNetCoreOData

TargetInvocationException when returning a null enumerable

Open
#686 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
C#
Stars
505
Forks
186
PR merge metrics
No merged PRs in 30d

Description

Assemblies affected
ASP.NET Core OData 8.0.11

Describe the bug
If we return a model which has an enumerable property for example an IEnumerable with null value a TargetInvokationException is thrown, I see that this happens under specific conditions, if any of the conditions is not met, the issue doesn't happen

  • The endpoint has a PageSize defined
  • We are using query parameters to select the IEnumerable property which value is null

Reproduce steps

namespace Issue
{
    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.OData;
    using Microsoft.AspNetCore.OData.Query;
    using Microsoft.AspNetCore.OData.Routing.Controllers;
    using Microsoft.OData.Edm;
    using Microsoft.OData.ModelBuilder;
    public class WeatherForecast
    {
        [Key]
        public Guid Key { get; set; }
        public IEnumerable<string> Additional => null;
    }

    public class WeatherForecastController : ODataController
    {
        [EnableQuery(PageSize = 4)]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Key = Guid.NewGuid(),
            });
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            IEdmModel GetEdmModel()
            {
                ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
                var entity = builder.EntitySet<WeatherForecast>("WeatherForecast");
                return builder.GetEdmModel();
            }

            var builder = WebApplication.CreateBuilder(args);
            // Add services to the container.
            builder.Services.AddControllers().AddOData(opt =>
            {
                opt.AddRouteComponents("odata", GetEdmModel()).Select().Expand().OrderBy().SetMaxTop(10).Count().Filter().SkipToken();
            });

            var app = builder.Build();
            app.UseDeveloperExceptionPage();
            app.UseRouting();
            app.UseEndpoints(endpoints => endpoints.MapControllers());

            app.Run();
        }
    }
}

Request/Response
Query http://localhost:54321/odata/WeatherForecast and we can see the Additional property is serialized as an empty array which I guess is the correct behavior

{
    "@odata.context": "http://localhost:54321/odata/$metadata#WeatherForecast",
    "value": [
        {
            "Key": "10fcb5d4-7558-4527-a88d-59ebd7ed18be",
            "Additional": []
        },
        {
            "Key": "55542ada-9a3a-4e28-a338-7b2908360654",
            "Additional": []
        },
        {
            "Key": "a57bb67c-ce54-4b1e-b51e-b929068e9371",
            "Additional": []
        },
        {
            "Key": "b035b3f9-244b-4251-9d4a-104ce0c26b68",
            "Additional": []
        }
    ],
    "@odata.nextLink": "http://localhost:54321/odata/WeatherForecast?$skiptoken=Key-b035b3f9-244b-4251-9d4a-104ce0c26b68"
}

But if we make a query to http://localhost:54321/odata/WeatherForecast?$select=Key,Additional a System.Reflection.TargetInvocationException is thrown

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.ArgumentNullException: Value cannot be null. (Parameter 'source')
   at System.Linq.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument)
   at lambda_method15(Closure , WeatherForecast )
   at System.Linq.Enumerable.SelectIPartitionIterator`2.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at Microsoft.AspNetCore.OData.Query.Container.TruncatedCollection`1..ctor(IQueryable`1 source, Int32 pageSize, Boolean parameterize)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.LimitResults[T](IQueryable`1 queryable, Int32 limit, Boolean parameterize, Boolean& resultsLimited)
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Span`1& arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.LimitResults(IQueryable queryable, Int32 limit, Boolean parameterize, Boolean& resultsLimited)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyPaging(IQueryable result, ODataQuerySettings querySettings)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuted(ActionExecutedContext actionExecutedContext, Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuted(ActionExecutedContext actionExecutedContext)
   at Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- 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 Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

If we have the attribute like [EnableQuery] instead of [EnableQuery(PageSize = 4)] the issue doesn't happen

Expected behavior
Should be consistent and not throw the exception in that specific case

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the reproduction with ASP.NET Core OData 8.0.11 and the $select=Key,Additional query. Read ODataQueryOptions.LimitResults, TruncatedCollection, and EnableQueryAttribute around the paging stack shown in the exception. Done means the paged endpoint handles a null enumerable consistently with the unpaged endpoint without throwing TargetInvocationException.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.