AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

TargetInvocationException when returning a null enumerable

Open msolorioms opened this issue 2 years ago • 1 comments

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

msolorioms avatar Sep 06 '22 19:09 msolorioms

You should not have null collection properties. I'd strongly suggest initializing them to empty lists instead.

Disclaimer: I'm not saying this is not a bug necessarily...

julealgon avatar Sep 06 '22 21:09 julealgon

I am seeing the same behavior when attempting to $expand property in a derived type within a the broader collection (i.e. /myBaseTypes?$expand=my.derived.type.fullname/myNavigationProperty)

mlafleur avatar Nov 01 '22 22:11 mlafleur