This is more a question than an issue at this point.
I am building an OData api project with the intention of using it as a facade to the microsoft graph api for several reasons, namely to address infrastructure concerns like authentication in one place as well as enforce some business logic tied to users in our system.
The new graph api sdk removed IEnumerable/IQueryable from its client 馃槶 One example I need to work with is the users graph api.
My hope is that I can leverage ODataQueryOptions to get the raw odata query parameters and then pass these along to the graph api client. I've whipped up a POC for this and generally, it works great.
Where it doesn't work great is with paging. The only $count I can seem to get my ODataController to return is the size of the IEnumerable result I got from graph api (and returned as the result). For example, if I'm querying a directory with 500 users, but getting the first page of 50 from graph api, the graph api paged result contains 50 users and has a count property of 500. However, I don't see any way to pass this count of 500 through my result. This means I can't accurately page through the full data set. Ideally, my ODataController would return a PageResult which would be a projection of the result I get back from the graph api, but OData throws an exception because PageResult does not implement IEnumerable.
I never used microsoft graph api before. From the information in the link, it seems that microsoft graph api support odata query options itself.
I'm curious about the reason why you use ODataController but not just ControllerBase. ControllerBase does't limit the return type.
Yes, @anranruye, MS graph api is an OData api. I want my api to adhere to standard odata api spec. However, I can't just return IEnumerable<T> as it seems is maybe the intended approach of the AspNetCore.OData implementation. My hope is to still leverage as much of this project as possible so I am not re-inventing wheels.
It looks like I can use ControllerBase and the IRouteBuilder.EnableDependencyInjection(); extension. Argument binding to ODataQueryOptions works but unfortunately this does not work with ApiExplorer to generate the $select, $filter params for the api definition (with swagger) which is a requirement for me. Swagger actually just locks up I suspect because it is making parameters for all properties of the ODataQueryOptions. I will see if I can work around this with a swagger document or operation filter.
Now that I can test this, it looks like PageResult<T> is not actually the response object I want because this just gets serialized as IEnumerable. I'll start looking through code for the actual root response object OData uses. If anyone knows what that is off the top of their head, I'd appreciate the help!
Thanks for the suggestion @anranruye
If you return a PageResult<T> (rather than PageResult), does that not give you what you need?
when I say using controllerBase instead of odataController, I mean abondening the whole aspnetcore.odata library. it seems that you don't really use odata features(maybe you use, but you haven't told us the details), you just want to return a response from another api provided by microsoft to your customer(without any modified) and you want to make your api look like an odata one. is this right? Do you use '[EnableQuery]' or 'ODataQueryOptions. ApplyTo'? If you use them, please tell me what is the reason.
if you don't use [EnableQuery] provided by odata library and just want to make your api look like an odata api, you only need to do the following:
@engenb , please tell me how things are going on.
yes, @anranruye , this is likely more along the lines of what I will have to do. I was hoping to be able to leverage at least some parts of this project, namely the ODataQueryOptions and whatever root response is being built so that I don't need to implement my own contracts that adhere to the OData spec.
To hopefully answer some of your questions:
By your statement, "you don't really use odata features" I'm assuming you're referring to this project and not the spec in general. I do intend for my API to provide OData features like $select, $expand, $filter, etc. and I intend to pass these along to the MS Graph API.
I was hoping to use this project to build my own EDM that represented a sub-set of the Graph API objects as well as some additional properties that are specific to my business domain. For example, a user's name should be fetched from the Graph API, but the organization(s) they belong to (organization is more or less what customer(s) they're associated with) needs to be fetched from my DB. We've considered B2C custom attributes to store data like this and decided that we really don't want to use our identity system to store app domain-specific metadata.
The thought was to pass along all $select $filter etc properties (like first/last name etc) to Graph API and then non-Graph API properties, such as "organization", would trigger a process to look up that info in the DB. The results from Graph API would be aggregated and returned as a model I define in my EDM. As such, I don't want to just return the raw results from Graph API. Instead, I want to return my own domain object that makes sense to my other services which basically means a sub-set of the Graph API user plus a few domain-specific bits.
As I describe this, I wonder if I shouldn't be considering GraphQL as that is more designed to aggregate disparate data sources like this, but I like OData with .Net because there are some very nice API client Linq providers out there so the development experience is pretty nice.
@engenb Did you try the solution suggested by @mikepizzo? I think it could easily solve your problem. Just to expound, here is how you could structure your action method
public class UsersController : ODataController
{
// ...
public IActionResult Get(ODataQueryOptions<User> options)
{
int skip = 0;
if (options.Skip != null)
{
skip = options.Skip.Value;
}
int top = 5; // Use the applicable default
if (options.Top != null)
{
top = options.Top.Value;
}
// ...
// Your logic for making the request to MS Graph with the values for $skip and $top
// You could also apply other query options in `options` object when making the request
// ...
// Quick and dirty logic for generating the next link
Uri nextLink = null;
if (skip + top < MSGRAPH_TOTAL_COUNT) \\ Total count from MS Graph
{
nextLink = new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}?$skip={skip + top}&$top={top}");
}
PageResult<User> pageResult = new PageResult<User>(
MSGRAPH_PAGERESULT, // Page result returned by MS Graph
nextLink,
MSGRAPH_TOTAL_COUNT);
return Ok(pageResult);
}
// ...
}
Kindly let me know if this solves your issue
@engenb I made some changes to the code of @gathogojr, which can work with Microsoft.AspNetCore.OData 7.5.1
public class UsersController : ODataController
{
// ...
public IActionResult Get(ODataQueryOptions<YourUserEntity> options)
{
// we don't use [EnableQuery] for this method, so we must do validation manually
var validationSettings = new ODataValidationSettings()
{
// conditions for validation
//AllowedQueryOptions = Select | OrderBy | Top | Skip | Count,
//AllowedOrderByProperties = { "firstName", "lastName" },
//AllowedArithmeticOperators = AllowedArithmeticOperators.None,
//AllowedFunctions = AllowedFunctions.None,
//AllowedLogicalOperators = AllowedLogicalOperators.None,
//MaxOrderByNodeCount = 2,
//MaxTop = 50,
};
try
{
options.Validate( validationSettings );
}
catch ( ODataException )
{
return BadRequest();
}
catch ( Exception ) // sometimes 'options.Validate' will throw exceptions which are not ODataException type for some kind of validation errors, i don't know whether this is fixed or not
{
return BadRequest();
}
PageResult<MicrosoftGraphUserEntity> rawData = GetUsersFromMicrosoftGraph(options); // implement this method yourself
List<YourUserEntity> users = AddAdditionalProperties(rawData.Items); // implement this method yourself
int skip = 0;
if (options.Skip != null)
{
skip = options.Skip.Value;
}
int top = 5; // Use the applicable default
if (options.Top != null)
{
top = options.Top.Value;
}
// Quick and dirty logic for generating the next link
Uri nextLink = null;
if (skip + top < rawData.Count)
{
nextLink = new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}?$skip={skip + top}&$top={top}");
}
PageResult<YourUserEntity> pageResult = new PageResult<YourUserEntity>(
users,
nextLink,
rawData.Count);
options.ApplyTo( Array.Empty<YourUserEntity>().AsQueryable() ); // Important! This changes the behavior of odata serializer. You will get all properties and no navigation property without this
return Ok(pageResult);
}
// ...
}
yes, @anranruye, thank you! options.ApplyTo(...); is what I was missing.
The only remaining issue is that using ODataQueryOptions this way seems to break ApiExplorer (and therefore, Swagger) but that is probably an issue for that team and not this one and I can move forward with Postman.
Thanks again!