Closed techniq closed 5 years ago
Update on the 2 checkboxes above
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
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.
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.
[OeOpeartion]
or explicit [OeFunction]
/ [OeAction]
) which can be used to scan an assembly and register the function.
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.ActionResult<T>
or ODataResult<T>
to determine the return type (and possibly read the [Produces(typeof(Department))]
attribute. Here's a descent descriptionOeAspQueryParser.ExecuteReader
to apply OData query string ($filter
, etc) to the result.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);
}
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 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:
- 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. - 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 - 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 theelement. 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).
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;
}
@voronov-maxim Are you saying that should work now... or suggesting how it might work?
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
@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.
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>
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>
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...
https://github.com/voronov-maxim/OdataToEntity/blob/b55608ab0588b5bd1011d16005973c383cd7c088/test/OdataToEntity.Test/Common/SelectTest.cs#L45-L46
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).
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
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.
expand query instead anonymous type new { o.Order, Customer = c } use Tuple<Order, Customer>
@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.
Implement this feature after bound function.
@techniq Test Controller action Bound function
@voronov-maxim thanks! Here is some feedback from my observations / needs for my project:
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)
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.
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.
@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
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
Function without namespace not valid odata query. Dont use OeAspQueryPaser, see my sample.
@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.
hi @techniq create branch "bound_function" for bound function new design api for bound function bound function test
support $select, $top, $skip, $expand
@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);
}
add support bound function 9fd4767d752909cb98cde0e43fbb01e72c2223ae
@techniq add support bound function without namespace test controller
@voronov-maxim thanks!
Currently with OData/WebApi I am able to register an OData function in the EdmModel...
..and then expose it in the Controller
I can then call it using
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...
...but am running into the following issues
roleCodes
param as an array (ex.WithRoles(roleCodes=['Foo','Bar','Baz'])
)parser.ExecuteReader<User>(query)
callI think to support this use case, the following needs to be supported:
odata-query
tests.FromODataUri
):/products?sizes=s,m,l
and not/products(sizes=['s','m','l'])
parser.ExecuteReader<User>(query)
EdmModel
like in OData/WebApi (although this isn't the case with stored procedures in ODataToEntity, so I dunno).