OData / AspNetCoreOData

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

Expansion failure, new since .Net 5 upgrade #248

Open TehWardy opened 3 years ago

TehWardy commented 3 years ago

So I have a custom Action on one of my Odata controllers where I post a set of data of type T and I get back a set of results of type Result

Essentially what we do is something like this ...

HTTP POST  odata/T/UpdateAll?$expand=Item($expand=References,Buckets,Companies,Lines)
Body: 
{
    "value": [  array of T ]
}

Pushed to this action method ...

[HttpPut]
[EnableQuery()]
public virtual async Task<IActionResult> UpdateAll([FromBody] ODataCollection<T> items, ODataQueryOptions<Result<T>> queryOptions)
      => ModelState.IsValid ? Ok(await Service.UpdateAllAsync(items.Value)) : BadRequest(ModelState);

The controller hands of the business logic work to a dependency and is then given back a IEnumerable<Result> to return to the caller.

The call above does this work then wants back "for each result" it's Item property then expand in to the items child "References,Buckets,Lines" properties. This used to work fine under .Net Core but since our .Net 5 upgrade the expand is returning the items but not the second level expand.

Is there something I am missing here / some key difference in the way the Odata 8.0.1 package or .Net 5 implementation works that prevents this?

callummarshall9 commented 3 years ago

Hiya... When the method for the controller is changed to this...

        [HttpPost]
        [EnableQuery()]
        public virtual async Task<IActionResult> UpdateAll([FromBody] ODataCollection<T> items, ODataQueryOptions<Result<T>> queryOptions)
        {
            var updated = await Service.UpdateAllAsync(items.Value);
            var queryableSet = queryOptions.ApplyTo(updated.AsQueryable());
            return ModelState.IsValid ? Ok(queryableSet) : BadRequest(ModelState);
        } 

The following stack trace is obtained...

Object of type 'System.Linq.EnumerableQuery`1[Microsoft.AspNetCore.OData.Query.Wrapper.SelectAllAndExpand`1[Core.Objects.Result`1[Core.B2B.Objects.Entities.Invoice]]]' cannot be converted to type 'System.Linq.IQueryable`1[Core.Objects.Result`1[Core.B2B.Objects.Entities.Invoice]]'.
   at System.RuntimeType.TryChangeType(Object value, Binder binder, CultureInfo culture, Boolean needsSpecialCast)
   at System.Reflection.MethodBase.CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.Bind(IQueryable queryable, SelectExpandQueryOption selectExpandQuery)
   at Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.Bind(IQueryable queryable, ODataQuerySettings settings, SelectExpandQueryOption selectExpandQuery)
   at Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption.ApplyTo(IQueryable queryable, ODataQuerySettings settings)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplySelectExpand[T](T entity, ODataQuerySettings querySettings)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
   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>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_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.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)

Looks like the query options are correct as per the query, this was our attempt at manually applying the query options to acquire the desired result set. Is there a better suggestion?

callummarshall9 commented 3 years ago

Some more information about the setup in the model builder for OData

Builder.EntityType<Result<T>>();
Builder.EntityType<T>().Collection.Action("UpdateAll").ReturnsCollectionFromEntitySet<Result<T>>(setName + "UpdateResults");
HeinA commented 3 years ago

this seems simular to my issue: https://github.com/OData/AspNetCoreOData/issues/246 [EnableQuery] tries to process the result even though you return a BadRequest(ModelState)

TehWardy commented 3 years ago

The problem for us is when the request succeeds it doesn't expand properly. I think your problem @HeinA is when it fails that you don't want it to apply the ODataQueryOptions ... correct me if i'm wrong though?

HeinA commented 3 years ago

@TehWardy. Yes you are correct :)

TehWardy commented 3 years ago

From wat we can see Expands are either broken in 8.0.1 or there's some magic to configuring it beyond this in our app startup ...

services.AddControllers()
    //.AddNewtonsoftJson(opts => opts.SerializerSettings.UpdateFrom(ObjectExtensions.ODataJsonSettings))
    .AddOData(opt =>
    {
        opt.EnableAttributeRouting = true;
        opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(1000);
        builders.ForEach(b => opt.AddRouteComponents(b.GetType().Name.Replace("ModelBuilder", ""), b.Build().EDMModel));
        //opt.Conventions.Add(new MyConvention())
    });

I think there may be something in the way we are using return types though for our custom batch operations ...

protected virtual EntitySetConfiguration<T> AddSet<T, TKey>(bool enableBatchingToo = false, string setName = null) where T : class
{
    //  This has an Item property of type T, expanding on that seems to fail for any of the below custom actions
    Builder.EntityType<Result<T>>();    
    setName ??= typeof(T).Name;
    var setConfig = Builder.EntitySet<T>(setName);

    // register base OData controller defined functions
    Builder.EntityType<T>().Collection.Function("GetMetadata").Returns<MetadataContainer>();

    // these functions don't allow expanding like this ... 
    //     ?$expand=Item($expand=Anything)
    Builder.EntityType<T>().Collection.Action("AddAll").ReturnsCollectionFromEntitySet<Result<T>>(setName + "Results");
    Builder.EntityType<T>().Collection.Action("UpdateAll").ReturnsCollectionFromEntitySet<Result<T>>(setName + "UpdateResults");
    Builder.EntityType<T>().Collection.Action("DeleteAll").ReturnsCollection<Result<TKey>>();
    Builder.EntityType<T>().Collection.Action("AddOrUpdateAll").ReturnsCollectionFromEntitySet<Result<T>>(setName + "AddOrUpdateResults");

    var removedProps = typeof(T).GetProperties()
        .Where(p => p.CustomAttributes.Any(a => a.AttributeType == typeof(ApiIgnore)))
        .ToArray();

    var typeInfo = Builder.StructuralTypes.First(t => t.ClrType == typeof(T));
    removedProps.ForEach(p => typeInfo.RemoveProperty(p));

    return setConfig;
}
TehWardy commented 3 years ago

Ok expansions on get by id calls don't appear to work either. here's my base definition in my "generic root controller" that's inherited in to all my controllers ...

public virtual IActionResult Get([FromODataUri] TKey key)
{
    var result = Service.GetAll(false).Where(typeof(T).IdEquals<T>(key).Compile());
    return (result != null) ? Ok(SingleResult.Create(result)) : NotFound();
}

The service call returns IEnumerable which is an abstract reference to an EF IDbSet with an appropriate security filter applied. Any expand in the OData query is seemingly ignored / not processed correctly (doesn't appear in the repsonse).

TehWardy commented 3 years ago

I thought i'd put together the full details i'm seeing ...

From $metadata

<EntityType Name="App">
    <Key>
        <PropertyRef Name="Id"/>
    </Key>
    <Property Name="Id" Type="Edm.Int32" Nullable="false"/>
    <Property Name="DefaultCultureId" Type="Edm.String" Nullable="false"/>
    <Property Name="Name" Type="Edm.String" Nullable="false"/>
    <Property Name="Domain" Type="Edm.String" Nullable="false"/>
    <Property Name="DefaultTheme" Type="Edm.String" Nullable="false"/>
    <Property Name="ConfigJson" Type="Edm.String"/>
    <NavigationProperty Name="Cultures" Type="Collection(Core.Objects.Entities.CMS.AppCulture)"/>
    <NavigationProperty Name="Pages" Type="Collection(Core.Objects.Entities.CMS.Page)"/>
    <NavigationProperty Name="Components" Type="Collection(Core.Objects.Entities.CMS.Component)"/>
    <NavigationProperty Name="Roles" Type="Collection(Core.Objects.Entities.Role)"/>
    <NavigationProperty Name="Templates" Type="Collection(Core.Objects.Entities.CMS.Template)"/>
    <NavigationProperty Name="Resources" Type="Collection(Core.Objects.Entities.CMS.Resource)"/>
    <NavigationProperty Name="Tasks" Type="Collection(Core.Objects.Entities.Tasks.ScheduledTask)"/>
    <NavigationProperty Name="Calendars" Type="Collection(Core.Objects.Entities.Planning.Calendar)"/>
    <NavigationProperty Name="Folders" Type="Collection(Core.Objects.Entities.DMS.Folder)"/>
    <NavigationProperty Name="Layouts" Type="Collection(Core.Objects.Entities.CMS.Layout)"/>
    <NavigationProperty Name="Flows" Type="Collection(Core.Objects.Entities.Workflow.FlowDefinition)"/>
    <NavigationProperty Name="MailServers" Type="Collection(Core.Objects.Entities.Mail.MailServer)"/>
    <NavigationProperty Name="MailQueue" Type="Collection(Core.Objects.Entities.Mail.QueuedEmail)"/>
    <NavigationProperty Name="SentMail" Type="Collection(Core.Objects.Entities.Mail.SentEmail)"/>
</EntityType>
<EntityType Name="Result_1OfApp">
    <Key>
        <PropertyRef Name="Id"/>
    </Key>
    <Property Name="Id" Type="Edm.String" Nullable="false"/>
    <Property Name="Success" Type="Edm.Boolean" Nullable="false"/>
    <Property Name="Message" Type="Edm.String"/>
    <NavigationProperty Name="Item" Type="Core.Objects.Entities.CMS.App"/>
</EntityType>

<Action Name="AddAll" IsBound="true">
    <Parameter Name="bindingParameter" Type="Collection(Core.Objects.Entities.CMS.App)"/>
    <ReturnType Type="Collection(Core.Objects.Result_1OfApp)"/>
</Action>

From $odata

Api.Controllers.Core.AppController.AddAll (Api) POST    Core/App/Default.AddAll
Api.Controllers.Core.AppController.AddAll (Api) POST    Core/App/AddAll

From swagger/v1/swagger.json

  "/Core/App": {
    "/Core/App/AddAll": {
      "post": {
        "tags": [
          "App"
        ],
        "operationId": "Core/App/AddAll",
        "parameters": [
          {
            "name": "queryOption",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/AppResultODataQueryOptions"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json;odata.metadata=minimal;odata.streaming=true": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=minimal;odata.streaming=false": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=minimal": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=full;odata.streaming=true": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=full;odata.streaming=false": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=full": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=none;odata.streaming=true": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=none;odata.streaming=false": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.metadata=none": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.streaming=true": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json;odata.streaming=false": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/xml": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "text/plain": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AppODataCollection"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          }
        }
      }
    }
  }

From the model configuration

Builder.EntityType<Result<App>>();
var setConfig = Builder.EntitySet<App>(setName);
Builder.EntityType<App>().Collection.Action("AddAll").ReturnsCollectionFromEntitySet<Result<App>>(setName + "Results");

And finally the action definition

[HttpPost]
[EnableQuery(
    AllowedArithmeticOperators = AllowedArithmeticOperators.All,
    AllowedFunctions = AllowedFunctions.AllFunctions,
    AllowedLogicalOperators = AllowedLogicalOperators.All,
    AllowedQueryOptions = AllowedQueryOptions.All,
    MaxAnyAllExpressionDepth = 3,
    MaxExpansionDepth = 3
)]
public virtual async Task<IActionResult> AddAll([FromBody] ODataCollection<App> items, ODataQueryOptions<Result<T>> queryOption)
    => ModelState.IsValid ? Ok(await Service.AddAllAsync(items.Value)) : BadRequest(ModelState);

I then wrote this test against that method

var client = await new HttpClient() { BaseAddress = new Uri(TestParameters.Client) }.Authenticate(TestParameters.AdminCredentials);

var testData = new[] {
    new App { DefaultCultureId = "", Name = "TestApp1", Domain = "Domain1", DefaultTheme = "Default" },
    new App { DefaultCultureId = "", Name = "TestApp2", Domain = "Domain2", DefaultTheme = "Default" }
};

// notice the OData query here $expand=Item($expand=Roles)
var results = await client.AddAllAsync("Core/App/AddAll?$expand=Item($expand=Roles)", testData);
var createdApps = results.Select(r => r.Item).ToArray();

foreach (var app in createdApps) await client.DeleteAsync($"Core/App({app.Id})");

foreach (var app in createdApps)
{
    Assert.IsNotNull(app.Roles);
    Assert.AreEqual(app.Roles.Count, 3);
}

In my service layer there's quite a bit of complexity but long story short it loops over each app and does the add. Each app is then returned with ...

 return Db.GetAll<App>(false)
         .Include(a => a.Roles)
         .First(a => a.Id == result.Id);

... and then wrapped up in a result like this ... Result { Item = theNewApp }

Prior to that EF which provides my database model is being given the app + a set of 3 roles for each app. And I have confirmed (breakpointing ect) that the result looks like this when given to the controller actions Ok() method ...

new [] 
{
    new Result<App> {
        Success = "True",
        Message = "Created Successfully",
        Item = new App { 
            Id = 1, 
            DefaultCultureId = "", 
            Name = "TestApp1", 
            Domain = "Domain1", 
            DefaultTheme = "Default", 
            Roles = new [] 
                        {
                new Role
                {
                    Id = Guid.NewGuid(),
                    Name = "Administrators",
                },
                new Role
                {
                    Id = Guid.NewGuid(),
                    Name = "Users",
                },
                new Role
                {
                    Id = Guid.NewGuid(),
                    Name = "Guests",
                }
            }
         }
    },
    new Result<App> {
        Success = "True",
        Message = "Created Successfully",
        Item = new App { 
            Id = 2, 
            DefaultCultureId = "", 
            Name = "TestApp2", 
            Domain = "Domain2", 
            DefaultTheme = "Default", 
            Roles = new [] 
                        {
                new Role
                {
                    Id = Guid.NewGuid(),
                    Name = "Administrators",
                },
                new Role
                {
                    Id = Guid.NewGuid(),
                    Name = "Users",
                },
                new Role
                {
                    Id = Guid.NewGuid(),
                    Name = "Guests",
                }
            }
         }
    }
};

Is there any reason why the Roles should not be returned here?

TehWardy commented 3 years ago

So we found this ...

private static IEdmNavigationSource GetNavigationSource(IEdmModel model, IEdmType elementType, ODataPath odataPath)
{
    Contract.Assert(model != null);
    Contract.Assert(elementType != null);
    IEdmNavigationSource navigationSource = (odataPath != null) ? odataPath.GetNavigationSource() : null;
    if (navigationSource != null)
    {
        return navigationSource;
    }
    IEdmEntityContainer entityContainer = model.EntityContainer;
    if (entityContainer == null)
    {
        return null;
    }
    List<IEdmEntitySet> matchedNavigationSources =
        entityContainer.EntitySets().Where(e => e.EntityType() == elementType).ToList();
    return (matchedNavigationSources.Count != 1) ? null : matchedNavigationSources[0];
}

This little gem is in Query/ODataQueryContext.cs in the MicrosoftAspNetCoreOData project It loks like if you do something like this ...

Builder.EntityType<T>().Collection.Action("Custom1").ReturnsCollectionFromEntitySet<Result<T>>(setName + "Results");
Builder.EntityType<T>().Collection.Action("Custom2").ReturnsCollectionFromEntitySet<Result<T>>(setName + "Results");

If the string passed in as the last parameter is different on each line then expands don't work properly and this is caused by the method above returning null as more than one navigation source is found after the first level of expand.

Technically I suppose this is an issue in the model construction that the framework doesn't like but the framework is swallowing the problem here so we could probably do with some better validation feedback on our model configurations.

TehWardy commented 3 years ago

For the reason above about v8 seeming to be "more strict" with the model I don't think v7 has this issue but I admit I have not tested it so it might not be limited to v8.

leoerlandsson commented 2 years ago

Any progress here?

We're experiencing the same problem. However, we only have one Action registered for the entity.

If we don't register .ReturnsCollectionFromEntitySet for the Action, then expand does not work for the Action.

If we do register .ReturnsCollectionFromEntitySet for the Action it works there, but then Expand breaks for lower levels for other Get methods (?).

I'll get back with more details.

TehWardy commented 2 years ago

As I posted above it seems that in many cases the issue is consistency in how the model is built. Without seeing more of the model you're building it's hard to say if this is the problem in your case.

ahmadfarrokh commented 5 months ago

hi I use Odata 8.2.5 and EF 8.0.4

I have this code ` public class Zone : TavModelAudit {

   public string ZoneNo{ get; set; }

   public string ZoneName { get; set; }

   public string ZoneNote { get; set; }
}

public class Area : TavModelAudit {

    public int ZoneId{ get; set; }

    [ForeignKey(nameof(ZoneId))]
    public virtual Zone Zone { set; get; }

    public string AreaName { get; set; }

    public string AreaNo { get; set; }

    public int? AreaGeoObjID { get; set; }

}

var ZoneEntityName = nameof(Zone).ToLower() + "s"; var ZoneIdEntity = builder.EntitySet(ZoneEntityName);

var AreaEntityName = nameof(Area).ToLower() + "s"; var AreaEntity = builder.EntitySet(AreaEntityName);

    [EnableQuery]
    [HttpGet]
    public IActionResult Get(ODataQueryOptions<TEntity> options)
    {
            var Items = _unitOfWork.Set<TEntity>();
            return Ok(Items);
     }

` when I call this request http://localhost:5166/odata/areas everything is ok but when i call http://localhost:5166/odata/areas?$expand=zone I get this error

System.ArgumentException: Object of type 'Microsoft.AspNetCore.OData.Query.Wrapper.SelectAllAndExpand1[ICTBlockDomainCore.Model.Area]' cannot be converted to type 'ICTBlockDomainCore.Model.Area'. at System.RuntimeType.CheckValue(Object& value, Binder binder, CultureInfo culture, BindingFlags invokeAttr) at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Delegate.DynamicInvokeImpl(Object[] args) at Microsoft.AspNetCore.OData.Query.Expressions.BinderExtensions.ApplyBind(ISelectExpandBinder binder, Object source, SelectExpandClause selectExpandClause, QueryBinderContext context) at Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption.ApplyTo(Object entity, ODataQuerySettings settings) at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplySelectExpand[T](T entity, ODataQuerySettings querySettings) at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(Object entity, ODataQuerySettings querySettings) at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ApplyQuery(Object entity, ODataQueryOptions queryOptions) 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.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.OutputCaching.OutputCacheMiddleware.<>c__DisplayClass11_0.<<InvokeAwaited>g__ExecuteResponseAsync|0>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.OutputCaching.WorkDispatcher2.ScheduleAsync(TKey key, Func2 valueFactory) at Microsoft.AspNetCore.OutputCaching.OutputCacheMiddleware.InvokeAwaited(HttpContext httpContext, IReadOnlyList1 policies) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

please help me

andygjp commented 5 months ago

@ahmadfarrokh maybe you are missing a controller action, does this help:

    [EnableQuery]
    [HttpGet]
    public IActionResult Get(int key, ODataQueryOptions<TEntity> options)
    {
            var Item = _unitOfWork.Set<TEntity>().Find(key);
            return Ok(Item);
     }
julealgon commented 5 months ago

[EnableQuery] [HttpGet] public IActionResult Get(ODataQueryOptions options) { var Items = _unitOfWork.Set(); return Ok(Items); }

@ahmadfarrokh couple of comments on this code:

  1. You should avoid using both [EnableQuery] and passing ODataQueryOptions<T> at the same time. Use one approach or the other
  2. You are not using options at all here. Did you just forget to show the entire code? Otherwise, just remove that parameter.

Now I don't know exactly what other problems you have but I would recommend changing your logic to this:

[EnableQuery]
[HttpGet]
public ActionResult<IQueryable<TEntity>> Get()
{
    return _unitOfWork.Set<TEntity>();
}

The strong typing provided by ActionResult<T> works better in some cases and gives you more compile-time information as well (which is a benefit in and of itself).

ahmadfarrokh commented 5 months ago

[EnableQuery] [HttpGet] public IActionResult Get(ODataQueryOptions options) { var Items = _unitOfWork.Set(); return Ok(Items); }

@ahmadfarrokh couple of comments on this code:

  1. You should avoid using both [EnableQuery] and passing ODataQueryOptions<T> at the same time. Use one approach or the other
  2. You are not using options at all here. Did you just forget to show the entire code? Otherwise, just remove that parameter.

Now I don't know exactly what other problems you have but I would recommend changing your logic to this:

[EnableQuery]
[HttpGet]
public ActionResult<IQueryable<TEntity>> Get()
{
    return _unitOfWork.Set<TEntity>();
}

The strong typing provided by ActionResult<T> works better in some cases and gives you more compile-time information as well (which is a benefit in and of itself).

Thanks.

I changed code such as bellow but I get same error

[EnableQuery]
[HttpGet]
public ActionResult<IQueryable<TEntity>> Get()
{
    return _unitOfWork.Set<TEntity>();
}
julealgon commented 5 months ago

Perhaps you could share a bit more of your code @ahmadfarrokh ? Ideally a sample repro?

I see you are using some generics there for the entity, maybe there is something going on on your implementation specifically that is causing the issue.

ahmadfarrokh commented 5 months ago

hi I use Odata 8.2.5 and EF 8.0.4

I have this code ` public class Zone : TavModelAudit {

   public string ZoneNo{ get; set; }

   public string ZoneName { get; set; }

   public string ZoneNote { get; set; }
}

public class Area : TavModelAudit {

    public int ZoneId{ get; set; }

    [ForeignKey(nameof(ZoneId))]
    public virtual Zone Zone { set; get; }

    public string AreaName { get; set; }

    public string AreaNo { get; set; }

    public int? AreaGeoObjID { get; set; }

}

var ZoneEntityName = nameof(Zone).ToLower() + "s"; var ZoneIdEntity = builder.EntitySet(ZoneEntityName);

var AreaEntityName = nameof(Area).ToLower() + "s"; var AreaEntity = builder.EntitySet(AreaEntityName);

    [EnableQuery]
    [HttpGet]
    public IActionResult Get(ODataQueryOptions<TEntity> options)
    {
            var Items = _unitOfWork.Set<TEntity>();
            return Ok(Items);
     }

` when I call this request http://localhost:5166/odata/areas everything is ok but when i call http://localhost:5166/odata/areas?$expand=zone I get this error

System.ArgumentException: Object of type 'Microsoft.AspNetCore.OData.Query.Wrapper.SelectAllAndExpand1[ICTBlockDomainCore.Model.Area]' cannot be converted to type 'ICTBlockDomainCore.Model.Area'. at System.RuntimeType.CheckValue(Object& value, Binder binder, CultureInfo culture, BindingFlags invokeAttr) at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Delegate.DynamicInvokeImpl(Object[] args) at Microsoft.AspNetCore.OData.Query.Expressions.BinderExtensions.ApplyBind(ISelectExpandBinder binder, Object source, SelectExpandClause selectExpandClause, QueryBinderContext context) at Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption.ApplyTo(Object entity, ODataQuerySettings settings) at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplySelectExpand[T](T entity, ODataQuerySettings querySettings) at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(Object entity, ODataQuerySettings querySettings) at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ApplyQuery(Object entity, ODataQueryOptions queryOptions) 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.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.OutputCaching.OutputCacheMiddleware.<>c__DisplayClass11_0.<<InvokeAwaited>g__ExecuteResponseAsync|0>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.OutputCaching.WorkDispatcher2.ScheduleAsync(TKey key, Func2 valueFactory) at Microsoft.AspNetCore.OutputCaching.OutputCacheMiddleware.InvokeAwaited(HttpContext httpContext, IReadOnlyList1 policies) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

please help me

I found reason that this error occure and I correct it In Odata V4 if you register enablequery globbaly such this

Services .AddMvc() .AddControllers() .AddOData(options => options .AddRouteComponents("odata", OdataBuilder.GetEdmModel()) .EnableQueryFeatures(null) ).AddNewtonsoftJson(); you must dont use EnableQuery attribute before method

[HttpGet]
public IActionResult Get()
{
        var Items = _unitOfWork.Set<TEntity>();
        return Ok(Items);
 }

i remove EnableQuery in Base controller in expand work fine