Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support OData Functions/Actions #28

Closed
2 tasks
techniq opened this issue Jan 5, 2019 · 25 comments
Closed
2 tasks

Support OData Functions/Actions #28

techniq opened this issue Jan 5, 2019 · 25 comments

Comments

@techniq
Copy link
Contributor

techniq commented Jan 5, 2019

Currently with OData/WebApi I am able to register an OData function in the EdmModel...

builder.EntitySet<User>("Users");
var userWithRoles = builder
    .EntityType<User>()
    .Collection
    .Function("WithRoles")
    .ReturnsFromEntitySet<User>("Users");
userWithRoles.CollectionParameter<string>("roleCodes");

..and then expose it in the Controller

[HttpGet]
public IActionResult WithRoles(ODataQueryOptions<User> queryOptions, [FromODataUri] IEnumerable<string> roleCodes)
{
    var userIdsWithRoleCodes = from ur in DbContext.UserRoles
                                where roleCodes.Contains(ur.Role.Code)
                                select ur.UserId;

    var query = from u in GetQuery()
                where userIdsWithRoleCodes.Contains(u.Id)
                select u;

    var result = queryOptions.ApplyTo(query);

    return Ok(result);
}

I can then call it using

http://localhost:5000/odata/security/Users/WithRoles(roleCodes=['Foo','Bar','Baz'])?$select=Id,FirstName,MiddleName,LastName&$top=10&$count=true&$orderby=LastName, FirstName

where you pass the function parameters via (roleCodes=['Foo','Bar','Baz']) as well as process the OData query string $select, $top, etc.

I attempted something similar using pure ASP.NET Core routing and ODataToEntity...

[HttpGet("WithRoles(roleCodes=[[{roleCodes}]])")]
public IActionResult WithRoles(string roleCodes = "")
{
    // TODO: Not properly parsing string (ex. `"['Foo','Bar','Baz']"` to IList<string> (or IEnumerable<string>, string[], etc).  Preferable to take in `IEnumerable<string> roleCodes` via param binding
    // var roleCodesAsArray = new StringValues(roleCodes.Split(',')).ToList();
    var roleCodesAsArray = new[] { "ProductivityViewer", "ProductivityReporter", "ProductivityAdministrator" };

    var userIdsWithRoleCodes = from ur in DbContext.UserRoles
                                where roleCodesAsArray.Contains(ur.Role.Code)
                                select ur.UserId;

    var query = from u in GetQuery()
                where userIdsWithRoleCodes.Contains(u.Id)
                select u;

    var parser = new OeAspQueryParser(HttpContext);
    var result = parser.ExecuteReader<User>(query);
    return parser.OData(result);
}

...but am running into the following issues

  • I'm struggling to take in the roleCodes param as an array (ex. WithRoles(roleCodes=['Foo','Bar','Baz']))
  • If I hard code the params to temporarily work around the issue, I run into an Exception from parser.ExecuteReader<User>(query) call
       An unhandled exception has occurred while executing the request.
Microsoft.OData.ODataException: The operation import overloads matching 'WithRoles' are invalid. This is most likely an error in the IEdmModel. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.OData.Metadata.EdmLibraryExtensions.RemoveActionImports(IEnumerable`1 source, IList`1& actionImportItems)
   at Microsoft.OData.UriParser.FunctionOverloadResolver.ResolveOperationImportFromList(String identifier, IList`1 parameterNames, IEdmModel model, IEdmOperationImport& matchingOperationImport, ODataUriResolver resolver)
   --- End of inner exception stack trace ---
   at Microsoft.OData.UriParser.FunctionOverloadResolver.ResolveOperationImportFromList(String identifier, IList`1 parameterNames, IEdmModel model, IEdmOperationImport& matchingOperationImport, ODataUriResolver resolver)
   at Microsoft.OData.UriParser.ODataPathParser.TryBindingParametersAndMatchingOperationImport(String identifier, String parenthesisExpression, ODataUriParserConfiguration configuration, ICollection`1& boundParameters, IEdmOperationImport& matchingFunctionImport)
   at Microsoft.OData.UriParser.ODataPathParser.TryCreateSegmentForOperationImport(String identifier, String parenthesisExpression)
   at Microsoft.OData.UriParser.ODataPathParser.CreateFirstSegment(String segmentText)
   at Microsoft.OData.UriParser.ODataPathParser.ParsePath(ICollection`1 segments)
   at Microsoft.OData.UriParser.ODataPathFactory.BindPath(ICollection`1 segments, ODataUriParserConfiguration configuration)
   at Microsoft.OData.UriParser.ODataUriParser.Initialize()
   at Microsoft.OData.UriParser.ODataUriParser.ParsePath()
   at OdataToEntity.OeParser.ParseUri(IEdmModel model, Uri serviceRoot, Uri uri) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity/OeParser.cs:line 171
   at OdataToEntity.AspNetCore.OeAspQueryParser.GetAsyncEnumerator(IQueryable source, Boolean navigationNextLink, Nullable`1 maxPageSize) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 90
   at OdataToEntity.AspNetCore.OeAspQueryParser.ExecuteReader[T](IQueryable source, Boolean navigationNextLink, Nullable`1 maxPageSize) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 65
   at Finance.Web.Controllers.Security.UsersController.WithRoles(String roleCodes) in /Users/techniq/Documents/Development/sbcs-chh/app-finance/Finance.Web/Controllers/Security/UsersController.cs:line 133
   at lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[]parameters)
   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope&scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)

I think to support this use case, the following needs to be supported:

[HttpGet("WithRoles(roleCodes={roleCodes})")]
public IActionResult WithRoles([OeArrayParameter]IEnumerable<string> roleCodes)
  • See this article and related source for potential approach
    • Note: this implementation does not wrap individual strings in quotes, put them in an array, or wrap all of this in params (for example they use /products?sizes=s,m,l and not /products(sizes=['s','m','l'])
  • Resolve issue when calling parser.ExecuteReader<User>(query)
    • Maybe required to register function in EdmModel like in OData/WebApi (although this isn't the case with stored procedures in ODataToEntity, so I dunno).
@techniq
Copy link
Contributor Author

techniq commented Jan 7, 2019

Update on the 2 checkboxes above

Passing function parameters

Reading the OData spec regarding Complex and Collection Literals:

Complex literals and collection literals in URLs are represented as JSON objects and arrays according to the arrayOrObject rule in [OData-ABNF]. Such literals MUST NOT appear in the path portion of the URL but can be passed to bound functions and function imports in path segments by using parameter aliases.

Since these values can always be treated as JSON (either a JSON array or JSON string, etc), I was able to handle the list of strings mentioned above using:

[HttpGet("WithRoles(roleCodes={roleCodes})")]
public IActionResult WithRoles(string roleCodes = "")
{
    var roleCodesAsArray = JArray.Parse(roleCodes).ToObject<IEnumerable<string>>();
    // ...
}

when passed as a JSON-escaped string (ex. SomeFunc(someProp='test')) I can handle it using:

[HttpGet("SomeFunc(someProp={someProp})")]
public virtual IActionResult SomeFunc(string someProp)
{
    var parsedValue = JValue.Parse(someProp).Value<string>();
    // ...
}

There is supposedly a performance difference between .Value() and .ToObject() but I haven't verified myself (or seen a multi-second response).

I haven't tested if this will handle an collection parameters with a complex type (which is then aliased). See this odata-query test for an example, but basically passing [{foo: 1, bar: 2}] as a parameter. I guess something like this might work (rather ugly though).

[HttpGet("SomeFunc(someProp=@someProp)?@someProp={someProp}")]
public virtual IActionResult SomeFunc(string someProp)
{
    var parsedValue = JArray.Parse(roleCodes).ToObject<IEnumerable<string>>();
    // ...
}

Not sure how it would work when also passing OData query via the query string as well. For example:

http://localhost:5000/SomeFunc(someProp=@someProp)?@someProp=[{foo: 1, bar: 2}]&$filter=Name eq 'Test'

I also came across AspNetCoreJTokenModelBinder which appears to allow you to bind to a JToken (still need to call .ToObject<...>() on a JArray or Value<...>() on a JValue but might be useful instead of taking in a string and parsing directly. The implementation seemed to be doing a good bit more though


Resolve issue when calling parser.ExecuteReader(query)

This is still a big issue for me and not sure I can find a way to work around it. Hoping you have a good idea :).

One thought was to register the function in the EdmModel we could expose:

[ODataFunction("WithRoles(roleCodes={roleCodes})")]
public IActionResult WithRoles(string roleCodes = "")
{
    // ...
}

or

[ODataAction("WithRoles(roleCodes={roleCodes})")]
public IActionResult WithRoles(string roleCodes = "")
{
    // ...
}

depending on the use case (Actions can have side effects (typically HttpPost) and Functions can not (typically HttpGet), but might need to also also attribute accordingly. See spec.

Anyways, just being able to take an OData query string and apply it anymore is really my use case (including applying to OData query to a custom Queryable, see WithRoles example in the original comment above.

@techniq techniq changed the title Support OData Functions (with Collection Parameters) Support OData Functions Jan 7, 2019
@techniq techniq changed the title Support OData Functions Support OData Functions/Actions Jan 7, 2019
@techniq
Copy link
Contributor Author

techniq commented Jan 8, 2019

After more research, it looks like only ActionImport and FunctionImport bound at the EntityContainer level are currently supported.

To support Action and Function bounded on an Entity (ex. /odata/Foo(1)/DoSomething()) or Entity Collection (ex. /odata/Foo/DoSomething()) here is a rough list of things needed.

  • Add an attribute (maybe [OeOpeartion] or explicit [OeFunction] / [OeAction]) which can be used to scan an assembly and register the function.
    • Similar to OeOperationAdapter.GetOperations() and OeOperationAdapter.GetOperationsCore()
    • Maybe if OeOperationAdapter.GetOperations() was virtual we could create a subclass OeAspNetCoreOperationAdapter similar to OeEfCoreOperationAdapter but it could override GetOperations() to find the registrations. Currently OeEfCoreDataAdapter or takes a single OperatorAdapter but maybe it could take multiple.
  • Would need to inspect the ActionResult<T> or ODataResult<T> to determine the return type (and possibly read the [Produces(typeof(Department))] attribute. Here's a descent description
  • Support using OeAspQueryParser.ExecuteReader to apply OData query string ($filter, etc) to the result.
  • Any special handling with the parameters to parse them as JValue / JArray would be nice (but can me manually handled right now. OData/WebApi handles this for you after registering using the fluent API).

Here is a rough example of how I think it might look:

[OeFunction]
[HttpGet("Default.HasOperatingStandard(roleCode={roleCode})")]
public virtual ODataResult<Department> HasOperatingStandard(string roleCode)
{
    var parsedValue = JValue.Parse(roleCode).Value<string>();

    var departmentIds = IdentityContext.GetEntityIdsForRole(RoleEntityType.Department, parsedValue);
    var departmentsWithOperatingStandards = DbContext.Set<OperatingStandard>()
                                                        .Where(op => op.EntityTypeId == EntityTypeConstants.Department && departmentIds.Contains(op.EntityId))
                                                        .Select(d => d.EntityId);
    var query = DbContext.Set<Department>().Where(d => departmentsWithOperatingStandards.Contains(d.Id));

    var parser = new OeAspQueryParser(HttpContext);
    var result = parser.ExecuteReader<User>(query);
    return parser.OData(result);
}

Specs for Operations (Actions/Functions)

Here is also a good good description of the 3 types of functions in OData - https://stackoverflow.com/a/29023704/191902. Plan is to add support for option 1. We currently support option 3 by adding functions on the DbContext. Not sure about supporting the unbounded option 2 (I've not used these myself yet).

@voronov-maxim
Copy link
Owner

@techniq
Test

sql function

provider specific implementation because add sql server data adapter

@techniq
Copy link
Contributor Author

techniq commented Jan 8, 2019

@voronov-maxim hmm, this mostly looks like support for passing an array to a Table Value Function as a context-level function.

My request is to expose a function bounded to an entity/collection. Pulled from StackOverflow link above...

There are three types of functions in OData:

  1. Functions that are bound to something (e.g. an entity). Example would be
    GET http://host/service/Products(1)/Namespace.GetCategories()
    such function is defined in the metadata using the element and with its isBound attribute set to true.
  2. Unbound functions. They are usually used in queries. E.g.
    GET http://host/service/Products?$filter(Name eq Namespace.GetTheLongestProductName())
    such function is defined in the metadata using the element with its isBound attribute set to false
  3. Function imports. They are the functions that can be invoked at the service root. E.g.
    GET http://host/service/GetMostExpensiveProduct()
    Their concept is a little bit similar as the concept of static functions in program languages, and they are defined in metadata using the element.

Similar distinguishing applies to and as well.

We currently support #3 (The table-valued function, stored procedure, etc) by adding a function to the DbContext and then exposing them via a ContextController.

My hope is to support #1, where you can expose a non-DB function on an entity controller. For example, if the OrdersController had a /api/Orders/WithItems(itemIds=[1234,9876])

[HttpGet("WithItems(itemIds={itemIds}"]
public async Task<ODataResult<Model.Order>> WithItems(IEnumerable<int> itemIds)
{
    var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
    Model.OrderContext orderContext = parser.GetDbContext<Model.OrderContext>();
    IAsyncEnumerable<Model.Order> orders = parser.ExecuteReader<Model.Order>(
        orderContext.Orders.Where(o => o.Items.Any(i => itemIds.Contains(i.Id))
    );
    List<Model.Order> orderList = await orders.OrderBy(o => o.Id).ToList();
    return parser.OData(orderList);
}

This is a contrived example (and might not even compile). Instead of a simple Where statement like this example uses (which could be handled using $filter), you might be joining to another Db.Set<> or Db.Query<> entity/table (see my HasOperatingStandard above)).

Does this explain it any better?

And thank for you for for this project, I've been very impressed with how it's designed and written (it's exactly what I've been looking for).

@voronov-maxim
Copy link
Owner

voronov-maxim commented Jan 9, 2019

DbContext function

public static LambdaExpression WithItems(IEnumerable<Order> orders, IEnumerable<int> itemIds)
{
    Expression<Func<IEnumerable<Order>, IEnumerable<Order>>> e = z => z.Where(o => o.Items.Any(i => itemIds.Contains(i.Id)));
    return e;
}

@techniq
Copy link
Contributor Author

techniq commented Jan 9, 2019

@voronov-maxim Are you saying that should work now... or suggesting how it might work?

@voronov-maxim
Copy link
Owner

this is suggesting how it might work.
This is a controller independent implementation. Many angular user use library for fast prototype server side without mvc

@techniq
Copy link
Contributor Author

techniq commented Jan 9, 2019

@voronov-maxim Hmm, something like that might work (I guess you would know from the IEnumerable<Order> orders parameter to register the <Function ...> on the Orders Entity Set?

There is also a single function bound to a single entity (went a key is passed) as well as a collection.

Single entity function

called via /odata/Departments(1234)/Users(roleCode=['foo','bar'])

// in DepartmentsController

[HttpGet("({key})/Users(roleCodes={roleCodes})")]
public virtual ODataResult<User> Users(int key, IEnumerable<string> roleCodes)
{
    var query =
        from cu in DbContext.ContainedUsers
        join u in DbContext.Users on cu.UserId equals u.Id
        join g in DbContext.Groups on cu.GroupId equals g.Id
        join r in DbContext.Roles on g.Id equals r.GroupId

        join ua in DbContext.UserAssociations on u.Id equals ua.UserId into uaGroup
        from ua in uaGroup.DefaultIfEmpty()

        join e in DbContext.Employees on ua.EmployeeId equals e.Id into eGroup
        from e in eGroup.DefaultIfEmpty()

        join p in DbContext.Positions on e.PositionId equals p.Id into pGroup
        from p in pGroup.DefaultIfEmpty()

        where r.DepartmentId == key
        select new { User = u, Role = r, Position = p };

    if (roleCodes != null && roleCodes.Any())
    {
        query = from q in query
                where roleCodes.Contains(q.Role.Code)
                select q;
    }

    var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
    IAsyncEnumerable<User> users = parser.ExecuteReader<User>(query);
    return parser.OData(users);
}

Produced CSDL

<Function Name="Users" IsBound="true">
  <Parameter Name="bindingParameter" Type="Finance.Data.Organizations.Taxonomy.Department"/>
  <Parameter Name="roleCodes" Type="Collection(Edm.String)"/>
  <ReturnType Type="Collection(Finance.Web.Controllers.Organizations.DepartmentUserDto)"/>
</Function>

Entity collection function

called via /odata/Users/WithRoles(roleCode=['foo','bar'])

// in UsersController

[HttpGet("WithRoles(roleCodes={roleCodes}")]
public IActionResult WithRoles(IEnumerable<string> roleCodes)
{
    var userIdsWithRoleCodes = from ur in DbContext.UserRoles
                                where roleCodes.Contains(ur.Role.Code)
                                select ur.UserId;

    var query = from u in DbContext.Users
                where userIdsWithRoleCodes.Contains(u.Id)
                select u;

    var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
    IAsyncEnumerable<User> users = parser.ExecuteReader<User>(query);
    return parser.OData(users);
}

Produced CSDL

<Function Name="WithRoles" IsBound="true">
  <Parameter Name="bindingParameter" Type="Collection(Finance.Data.Security.Users.User)"/>
  <Parameter Name="roleCodes" Type="Collection(Edm.String)"/>
  <ReturnType Type="Finance.Data.Security.Users.User"/>
</Function>

@techniq
Copy link
Contributor Author

techniq commented Jan 9, 2019

One feature (semi-related to this) is it would be nice if you could get an expression tree from the parser, and then apply it later. Similar to how the tests are structured...

RequestUri = "Orders?$apply=filter(Status eq OdataToEntity.Test.Model.OrderStatus'Unknown')/groupby((Name), aggregate(Id with countdistinct as cnt))",
Expression = t => t.Where(o => o.Status == OrderStatus.Unknown).GroupBy(o => o.Name).Select(g => new { Name = g.Key, cnt = g.Count() }),

You could pass in a the OData query string and get the expression back, and then apply the expression later (I guess via an IQueryableProvider). Maybe something like

var parser = new OeParse(baseUri, edmModel);
var expression = parser.GetExpression<Order>("$apply=filter(Status eq OdataToEntity.Test.Model.OrderStatus'Unknown')/groupby((Name), aggregate(Id with countdistinct as cnt))");
// t => t.Where(o => o.Status == OrderStatus.Unknown).GroupBy(o => o.Name).Select(g => new { Name = g.Key, cnt = g.Count() }), 
var results = DbContext.Orders.AsQueryable().Provider.Execute(expression)

A lot of times I just want the OData query string parsed as an expression tree, and then let me worry about the execution (or further refinement of the query).

@voronov-maxim
Copy link
Owner

Get expression tree will work for simplest cases, query "$apply=filter(Status eq OdataToEntity.Test.Model.OrderStatus'Unknown')/groupby((Name), aggregate(Id with countdistinct as cnt))" return expression type Tuple<IGrouping, Tuple>

@techniq
Copy link
Contributor Author

techniq commented Jan 10, 2019

Understood. Usually if the query is complex I create is manually using EFCore (or make a DB View) but like to leverage Odata for simple filters, expands, pagonation, maybe a group by/roll-up, etc.

@voronov-maxim
Copy link
Owner

expand query instead anonymous type new { o.Order, Customer = c } use Tuple<Order, Customer>

@techniq
Copy link
Contributor Author

techniq commented Jan 10, 2019

@voronov-maxim 👍 . Shouldn't be an issue. When we handle inline (?$count=true this would be a special case as well since it would issue 2 queries/expressions.

Might be useful to be able to get them individually as well.

var url = "...";
parser.GetFilterExpression<Order>(url);
parser.GetExpandExpression<Order>(url);
parser.GetApplyExpression<Order>(url);

but a single

parser.GetExpression<Order>(url);

would still be useful.

@voronov-maxim
Copy link
Owner

Implement this feature after bound function.

expression builder

@voronov-maxim
Copy link
Owner

@techniq
Copy link
Contributor Author

techniq commented Jan 20, 2019

@voronov-maxim thanks! Here is some feedback from my observations / needs for my project:

Support for OData query parameters

See this PR but also tried using new bounded function:

http://localhost:5000/api/orders/OdataToEntity.Test.Model.BoundFunctionCollection(customerNames=['Natasha'])?$select=Price

which threw this exception

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/api/orders/OdataToEntity.Test.Model.BoundFunctionCollection(customerNames=['Natasha'])?$select=Price
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
      Route matched with {action = "BoundFunctionCollection", controller = "Orders"}. Executing action OdataToEntity.Test.AspMvcServer.Controllers.OrdersController.BoundFunctionCollection (OdataToEntity.Test.AspMvcServer)
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
      Executing action method OdataToEntity.Test.AspMvcServer.Controllers.OrdersController.BoundFunctionCollection (OdataToEntity.Test.AspMvcServer) with arguments (['Natasha']) - Validation state: Valid
foo
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
      Executed action OdataToEntity.Test.AspMvcServer.Controllers.OrdersController.BoundFunctionCollection (OdataToEntity.Test.AspMvcServer) in26.9151ms
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HLJUS23BBB89", Request id "0HLJUS23BBB89:00000001": An unhandled exception was thrown by the application.
System.ArgumentNullException: Value cannot be null.
Parameter name: member
   at System.Linq.Expressions.Expression.MakeMemberAccess(Expression expression, MemberInfo member)
   at OdataToEntity.Parsers.Translators.OeSelectTranslator.CreateSelectExpression(Expression source, OeJoinBuilder joinBuilder) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity/Parsers/Translators/OeSelectTranslator.cs:line 291
   at OdataToEntity.Parsers.Translators.OeSelectTranslator.Build(Expression source, OeQueryContext queryContext) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity/Parsers/Translators/OeSelectTranslator.cs:line 103
   at OdataToEntity.Parsers.OeExpressionBuilder.ApplySelect(Expression source, OeQueryContext queryContext) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity/Parsers/OeExpressionBuilder.cs:line 126
   at OdataToEntity.Parsers.OeQueryContext.CreateExpression(OeConstantToVariableVisitor constantToVariableVisitor) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity/Parsers/OeQueryContext.cs:line 149
   at OdataToEntity.EfCore.OeEfCoreDataAdapter`1.GetFromCache[TResult](OeQueryContext queryContext, T dbContext, OeQueryCache queryCache, MethodCallExpression& countExpression) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.EfCore/OeEfCoreDataAdapter.cs:line 352
   at OdataToEntity.EfCore.OeEfCoreDataAdapter`1.ExecuteEnumerator(Object dataContext, OeQueryContext queryContext, CancellationToken cancellationToken) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.EfCore/OeEfCoreDataAdapter.cs:line 309
   at OdataToEntity.AspNetCore.OeAspQueryParser.ExecuteGet(IEdmModel refModel, ODataUri odataUri, OeRequestHeaders headers, CancellationToken cancellationToken, IQueryable source) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 53
   at OdataToEntity.AspNetCore.OeAspQueryParser.GetAsyncEnumerator(IQueryable source, Boolean navigationNextLink, Nullable`1 maxPageSize) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 108
   at OdataToEntity.AspNetCore.OeAspQueryParser.ExecuteReader[T](IQueryable source, Boolean navigationNextLink, Nullable`1 maxPageSize) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 68
   at OdataToEntity.Test.AspMvcServer.Controllers.OrdersController.BoundFunctionCollection(String customerNames) in /Users/techniq/Documents/Development/open-source/OdataToEntity/test/OdataToEntity.Test.Asp/OdataToEntity.Test.AspMvcServer/Controllers/OrdersController.cs:line 26
   at lambda_method(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

More control over endpoint

I understand the need to allow non-MVC setup, but exposing the fully qualified method name as the function is a bit of an issue

any way we could support "custom" endpoints to allow for example:

http://localhost:5000/api/orders/BoundFunctionCollection(customerNames=['Natasha'])

I guess one solution might be to somehow re-write the OData Path before the parser/function resolver goes to look up the method.

Customize query before execution

In your Bound function example, it looks like you are issuing multiple DB requests (while and foreach loops) (I've had difficult enabling EF sql logs to confirm though). I assume you could still modify the orderContext to make other adjustments to the query before execution

For example, this is one of my current OData functions I need to port

[HttpGet]
public IActionResult WithRoles(ODataQueryOptions<User> queryOptions, [FromODataUri] IEnumerable<string> roleCodes)
{
    var userIdsWithRoleCodes = from ur in DbContext.UserRoles
                                where roleCodes.Contains(ur.Role.Code)
                                select ur.UserId;

    var query = from u in GetQuery()
                where userIdsWithRoleCodes.Contains(u.Id)
                select u;

    var result = queryOptions.ApplyTo(query);

    return Ok(result);
}

As always, thanks for all your hard work on this project.

@voronov-maxim
Copy link
Owner

@techniq
Test
Controller action

BoundFunctionCollection(customerNames=['Natasha'])?$select=Price

It will not work, result bound function entities NOT expression tree, $select can only be applied to the expression tree

@techniq
Copy link
Contributor Author

techniq commented Jan 22, 2019

It will not work, result bound function entities NOT expression tree, $select can only be applied to the expression tree

Could something like this be supported?

("WithItems(itemIds={itemIds})")]
public async Task<ODataResult<Model.OrderItem>> WithItems(String itemIds)
{
    List<int> ids = JArray.Parse(itemIds).Select(j => j.Value<int>()).ToList();

    var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
    Model.OrderContext orderContext = parser.GetDbContext<Model.OrderContext>();
    var query = orderContext.OrderItems.Where(i => ids.Contains(i.Id));
    var orderItems = await parser.ExecuteReader<Model.OrderItem>(query).ToList();
    return parser.OData(orderItems);
}

(currently throws this stack)

      Connection id "0HLK02MQ09HG3", Request id "0HLK02MQ09HG3:00000001": An unhandled exception was thrown by the application.
Microsoft.OData.ODataException: Bad Request - Error in query syntax.
   at Microsoft.OData.UriParser.ODataUriResolver.ResolveKeys(IEdmEntityType type, IList`1 positionalValues, Func`3 convertFunc)
   at Microsoft.OData.UriParser.SegmentArgumentParser.TryConvertValues(IEdmEntityType targetEntityType, IEnumerable`1& keyPairs, ODataUriResolver resolver)
   at Microsoft.OData.UriParser.SegmentKeyHandler.CreateKeySegment(ODataPathSegment segment, KeySegment previousKeySegment, SegmentArgumentParser key, ODataUriResolver resolver)
   at Microsoft.OData.UriParser.SegmentKeyHandler.TryHandleSegmentAsKey(String segmentText, ODataPathSegment previous, KeySegment previousKeySegment, ODataUrlKeyDelimiter odataUrlKeyDelimiter, ODataUriResolver resolver, KeySegment& keySegment, Boolean enableUriTemplateParsing)
   at Microsoft.OData.UriParser.ODataPathParser.TryHandleAsKeySegment(String segmentText)
   at Microsoft.OData.UriParser.ODataPathParser.CreateNextSegment(String text)
   at Microsoft.OData.UriParser.ODataPathParser.ParsePath(ICollection`1 segments)
   at Microsoft.OData.UriParser.ODataPathFactory.BindPath(ICollection`1 segments, ODataUriParserConfiguration configuration)
   at Microsoft.OData.UriParser.ODataUriParser.Initialize()
   at Microsoft.OData.UriParser.ODataUriParser.ParseUri()
   at OdataToEntity.OeParser.ParseUri(IEdmModel model, Uri serviceRoot, Uri uri) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity/OeParser.cs:line 171
   at OdataToEntity.AspNetCore.OeAspQueryParser.GetAsyncEnumerator(IQueryable source, Boolean navigationNextLink, Nullable`1 maxPageSize) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 96
   at OdataToEntity.AspNetCore.OeAspQueryParser.ExecuteReader[T](IQueryable source, Boolean navigationNextLink, Nullable`1 maxPageSize) in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 68
   at OdataToEntity.Test.AspMvcServer.Controllers.OrdersController.WithItems(String itemIds) in /Users/techniq/Documents/Development/open-source/OdataToEntity/test/OdataToEntity.Test.Asp/OdataToEntity.Test.AspMvcServer/Controllers/OrdersController.cs:line 83
   at lambda_method(Closure , Object )
   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.TaskOfActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HLK02MQ09HG3", Request id "0HLK02MQ09HG3:00000001": An unhandled exception was thrown by the application.
System.NullReferenceException: Object reference not set to an instance of an object.
   at OdataToEntity.AspNetCore.OeAspQueryParser.Dispose() in /Users/techniq/Documents/Development/open-source/OdataToEntity/source/OdataToEntity.AspNetCore/OeAspQueryParser.cs:line 37
   at Microsoft.AspNetCore.Http.HttpResponse.<>c.<.cctor>b__30_1(Object disposable)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.FireOnCompletedAwaited(Stack`1 onCompleted)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 710.5368ms 500

@voronov-maxim
Copy link
Owner

Function without namespace not valid odata query. Dont use OeAspQueryPaser, see my sample.

@techniq
Copy link
Contributor Author

techniq commented Jan 22, 2019

@voronov-maxim Currently WebApi/OData exposes all functions and actions using the Default namespace (by default)

http://odata.github.io/WebApi/#04-21-Set-namespaces-for-operations

You used to be able to remove the namespace completely, but this may have changed.

http://odata.github.io/WebApi/#12-02-WebApiV7newDefaultSettings


The WithRoles functions mentioned above that I'm trying to port to OdataToEntity is callable using:

/odata/users/Default.WithRoles(roleCodes=['Foo','Bar'])?$select=Something&$expand=Something

The queryOptions.ApplyTo(query); applies the OData query to the pre-built query.

@voronov-maxim
Copy link
Owner

hi @techniq
create branch "bound_function" for bound function
new design api for bound function
bound function
test

support $select, $top, $skip, $expand

@techniq
Copy link
Contributor Author

techniq commented Jan 23, 2019

@voronov-maxim thanks! I'll try to take a look at it tonight or tomorrow. Looking at the commit changes, it looks good. It looks like.

It looks like I need to register a bound function in the DbContext using Db.OeBoundFunction attribute, and then in the Controller I assume I'll just have an endpoint registered with than name in the route?

So instead of (as it shows currently...)

        [HttpGet("OdataToEntity.Test.Model.BoundFunctionCollection(customerNames={customerNames})")]
        public ODataResult<Model.OrderItem> BoundFunctionCollection(String customerNames)
        {
            var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
            IAsyncEnumerable<Model.OrderItem> orderItems = parser.ExecuteReader<Model.OrderItem>();
            return parser.OData(orderItems);
        }

        [HttpGet("{id}/OdataToEntity.Test.Model.BoundFunctionSingle(customerNames={customerNames})")]
        public ODataResult<Model.OrderItem> BoundFunctionSingle(int id, String customerNames)
        {
            var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
            IAsyncEnumerable<Model.OrderItem> orderItems = parser.ExecuteReader<Model.OrderItem>();
            return parser.OData(orderItems);
        }

it would be...

        [HttpGet("BoundFunctionCollection(customerNames={customerNames})")]
        public ODataResult<Model.OrderItem> BoundFunctionCollection(String customerNames)
        {
            var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
            IAsyncEnumerable<Model.OrderItem> orderItems = parser.ExecuteReader<Model.OrderItem>();
            return parser.OData(orderItems);
        }

        [HttpGet("{id}/BoundFunctionSingle(customerNames={customerNames})")]
        public ODataResult<Model.OrderItem> BoundFunctionSingle(int id, String customerNames)
        {
            var parser = new OeAspQueryParser(_httpContextAccessor.HttpContext);
            IAsyncEnumerable<Model.OrderItem> orderItems = parser.ExecuteReader<Model.OrderItem>();
            return parser.OData(orderItems);
        }

@voronov-maxim
Copy link
Owner

add support bound function 9fd4767

@voronov-maxim
Copy link
Owner

@techniq
add support bound function without namespace
test
controller

@techniq
Copy link
Contributor Author

techniq commented Feb 2, 2019

@voronov-maxim thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants