-
Notifications
You must be signed in to change notification settings - Fork 709
OData API Explorer Should Support OData Query Options #400
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
Comments
This feature would be awesome since there'd be no need for operation processors anymore :) In the project I use we do not use the ODataQueryOptions attribute as a parameter on our controllers. Having a range attribute added to the $skip and $top options would be nice, you can get the maximum values for these from the enable query attribute with the MaxTop and MaxSkip properties. |
That's definitely one of the targeted scenarios. Personally, I've moved away from all the attribute stuff, in particular, That said, attributes, imperatively in code, or configured for the entire application at startup are all places where this information can be defined in part or in whole. My initial thinking is that each option would be broken out into it's own respective parameter. Most parameters are straight forward, but a few like $select and $expand are more of a challenge to document with anything useful beyond the fact they can be used. Providing descriptions and examples will be something that should be enabled. Since the OData libs no longer require it, there should be a configuration option as to whether the built-in parameters should use the |
Wow it is nice to see that our solution is very similar to yours @LuukN2. Well done. public class ODataQueryOptionsFilter: IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var queryAttribute = context.MethodInfo.GetCustomAttributes(true)
.Union(context.MethodInfo.DeclaringType.GetCustomAttributes(true))
.OfType<EnableQueryAttribute>().FirstOrDefault();
if (queryAttribute != null)
{
if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Select))
{
operation.Parameters.Add(new NonBodyParameter
{
Name = "$select",
In = "query",
Description = "Selects which properties to include in the response.",
Type = "string"
});
}
if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Expand))
{
operation.Parameters.Add(new NonBodyParameter
{
Name = "$expand",
In = "query",
Description = "Expands related entities inline.",
Type = "string"
});
}
// Additional OData query options are available for collections of entities only
if (context.MethodInfo.ReturnType.IsArray ||
typeof(IQueryable).IsAssignableFrom(context.MethodInfo.ReturnType) ||
typeof(IEnumerable).IsAssignableFrom(context.MethodInfo.ReturnType))
{
if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Filter))
{
operation.Parameters.Add(new NonBodyParameter
{
Name = "$filter",
In = "query",
Description = "Filters the results, based on a Boolean condition.",
Type = "string"
});
}
// Same for OrderBy, Top, Skip, Count
}
}
}
} Of course this is not a good solution at all, but it is currently working for the most of our cases. OData is complex and leads to many challanges in combination of an Allowed Query OptionsIn my opinion the most difficult challange is the extraction of
Navigational PropertiesNested navigational properties presents another challange. Check out the following OData request:
So you want the information of the The same applies to the provided information of navigational properties in general, because OData provides the information of navigational properties in the So to put all in a nutshell I just tried to name some challanges of what kind of information must be provide to a Swagger generator and as @commonsensesoftware said in #397 I also want to share my current implementation with you as a temporary suggestion and |
I've managed to start some work on this. The high-level thinking is to use an approach which is similar to the existing conventions applied to API versions. This will allow supporting settings applied by decorating code with attributes as well as scenarios where attributes are not or cannot be used. I don't have anything ready to push and review just yet, but this is what it's shaping up to look like: using static Microsoft.AspNet.OData.Query.AllowedLogicalOperators;
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
AddODataApiExplorer( options =>
{
options.QueryOptions.NoDollarPrefix = true;
options.QueryOptions
.Controller<ValuesController>()
.Action( c => c.Get() ).AllowTop( max: 100 ).Allow( Or | And )
.Action( c => c.Get(default) ).Allow( Select | Expand );
}) The current implementation behind the scenes is based on documenting the query options using a constructed ODataValidationSettings instance; however, I've tried carefully to not expose it or make it the required mechanism for which documenting query options is based off of. This feels like it makes sense since it's already the reciprocal of defining and validating query options. Configuring query options can be verbose so I tried to make it as succinct as possible. Here's the full list of current convention methods:
The
At this point, I don't really see a need to have controller-level conventions. Although OData allows this, I can't think of a single time where this ever made sense in practice. In other words, no two controller actions have the exact same set of query options or even overlapping query options. The only scenario I could think of whether that might be true is on controller with a bunch of unbounded functions. Arguably, if you're doing that, you're doing it all wrong. ;) Do you guys have any thoughts? Obviously attributes will still need to be supported, but supporting by these conventions adds quite a bit of complexity that I don't think has a high value proposition or may not even be used. The last part of the design that I'm struggling to resolve is how to provide flexibility in allowing developers to customize what the parameter description will be. Providing a default description is pretty straight forward, but customization may be needed because:
I thought about having some type of callback or other configuration method that would provide this hook, but thus far, I have not been able to come up with a way to do that without directly exposing ODataValidationSettings as part of the formal API. This information must be provided somehow in order to generate an appropriate message. For example, describing interface IODataQueryOptionDescriptionProvider
{
string Describe(AllowedQueryOptions option, DescriptionContext context)
} Where the Other alternatives include defining an interface to be used with an adapter over ODataValidationSettings, but I'm not sure how much value that really brings. I'm open to any suggestions you guys might have. |
OverviewThe preview of the feature will have two models: attribute-based and convention-based. The order of precedence will be:
The following query options are not supported:
These are infrequently used or do not have corresponding information that can be queried (that I can find). In addition, only the top-level structured type will be processed. While not impossible, processing the query options on related child structured types is not only complex, but could create potentially very verbose messages. Furthermore, child structured types cannot express independent query options as query parameters. Query options will get their description from an IODataQueryOptionDescriptionProvider, which will provide the description for a given query option and a context which contains information about the allowed properties, functions, operators, and so on. FeedbackThis is iteration one, so I'm definitely looking for some feedback on how this is shaping up. The DefaultODataQueryOptionDescriptionProvider, in particular, will determine what the default query option descriptions will be. Currently, the text is largely based on the text defined in the OData specification. To keep the descriptions from being verbose, especially Attribute ModelThis relies on Model Bound attributes and the EnableQueryAttribute. The EnableQueryAttribute indicates options that cannot otherwise be set such as [Select]
[Select( "effectiveDate", SelectType = SelectExpandType.Disabled )]
public class Order
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.Now;
public DateTime EffectiveDate { get; set; } = DateTime.Now;
public string Customer { get; set; }
public string Description { get; set; }
}
[ApiVersion( "3.0" )]
[ODataRoutePrefix( "Orders" )]
public class OrdersController : ODataController
{
[ODataRoute]
[Produces( "application/json" )]
[ProducesResponseType( typeof( ODataValue<IEnumerable<Order>> ), Status200OK )]
[EnableQuery( MaxTop = 100, AllowedQueryOptions = Select | Top | Skip | Count )]
public IQueryable<Order> Get()
{
var orders = new[]
{
new Order(){ Id = 1, Customer = "John Doe" },
new Order(){ Id = 2, Customer = "John Doe" },
new Order(){ Id = 3, Customer = "Jane Doe", EffectiveDate = DateTime.UtcNow.AddDays(7d) }
};
return orders.AsQueryable();
}
[ODataRoute( "({key})" )]
[Produces( "application/json" )]
[ProducesResponseType( typeof( Order ), Status200OK )]
[ProducesResponseType( Status404NotFound )]
[EnableQuery( AllowedQueryOptions = Select )]
public SingleResult<Order> Get( int key ) =>
SingleResult.Create( new[] { new Order(){ Id = key, Customer = "John Doe" }}.AsQueryable() );
}
Convention ModelThis model relies on Model Bound conventions and the new Query Option conventions. The Query Option configures options that cannot otherwise be set such as public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
public class PeopleController : ODataController
{
[Produces( "application/json" )]
[ProducesResponseType( typeof( ODataValue<IEnumerable<Person>> ), Status200OK )]
public IActionResult Get( ODataQueryOptions<Person> options )
{
var validationSettings = new ODataValidationSettings()
{
AllowedQueryOptions = Select | OrderBy | Top | Skip | Count,
AllowedOrderByProperties = { "firstName", "lastName" },
AllowedArithmeticOperators = AllowedArithmeticOperators.None,
AllowedFunctions = AllowedFunctions.None,
AllowedLogicalOperators = AllowedLogicalOperators.None,
MaxOrderByNodeCount = 2,
MaxTop = 100,
};
try
{
options.Validate( validationSettings );
}
catch ( ODataException )
{
return BadRequest();
}
var people = new[]
{
new Person()
{
Id = 1,
FirstName = "John",
LastName = "Doe",
Email = "john.doe@somewhere.com",
Phone = "555-987-1234",
},
new Person()
{
Id = 2,
FirstName = "Bob",
LastName = "Smith",
Email = "bob.smith@somewhere.com",
Phone = "555-654-4321",
},
new Person()
{
Id = 3,
FirstName = "Jane",
LastName = "Doe",
Email = "jane.doe@somewhere.com",
Phone = "555-789-3456",
}
};
return Ok( options.ApplyTo( people.AsQueryable() ) );
}
[Produces( "application/json" )]
[ProducesResponseType( typeof( Person ), Status200OK )]
[ProducesResponseType( Status404NotFound )]
public IActionResult Get( int key, ODataQueryOptions<Person> options )
{
var people = new[]
{
new Person()
{
Id = key,
FirstName = "John",
LastName = "Doe",
Email = "john.doe@somewhere.com",
Phone = "555-987-1234",
}
};
var person = options.ApplyTo( people.AsQueryable() ).SingleOrDefault();
if ( person == null )
{
return NotFound();
}
return Ok( person );
}
}
public class PersonModelConfiguration : IModelConfiguration
{
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion )
{
var person = builder.EntitySet<Person>( "People" ).EntityType;
person.HasKey( p => p.Id );
person.Select().OrderBy( "firstName", "lastName" );
if ( apiVersion < ApiVersions.V3 )
{
person.Ignore( p => p.Phone );
}
if ( apiVersion <= ApiVersions.V1 )
{
person.Ignore( p => p.Email );
}
if ( apiVersion > ApiVersions.V1 )
{
var function = person.Collection.Function( "NewHires" );
function.Parameter<DateTime>( "Since" );
function.ReturnsFromEntitySet<Person>( "People" );
}
if ( apiVersion > ApiVersions.V2 )
{
person.Action( "Promote" ).Parameter<string>( "title" );
}
}
}
public void ConfigureServices( IServiceCollection services )
{
services.AddODataApiExplorer(
options =>
{
// configure query options (which cannot otherwise be configured by OData conventions)
options.QueryOptions.Controller<V2.PeopleController>()
.Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
.Allow( Skip | Count ).AllowTop( 100 );
options.QueryOptions.Controller<V3.PeopleController>()
.Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
.Allow( Skip | Count ).AllowTop( 100 );
} );
}
|
As it seems there isn't any additional feedback, I'm imminently about to complete the associated PR which will close this issue out. This is the final feature for 3.0 and the bug count has burned down. I plan releasing 3.0 ASAP; possibly later today. Any additional changes will either come in a patch or 3.1. |
The API Explorer for OData does not currently support the Query Options, Query Settings, or Model Bound configurations. These are important pieces of API documentation that should be provided by the API Explorer so that they can be leveraged by other tools such as Swagger generators.
Features
The text was updated successfully, but these errors were encountered: