Where is it correct/incorrect to apply the EnableQueryAttribute as of Jan 2015?
The document linked below:
http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-endpoint
Says:
The [EnableQuery] attribute enables clients to modify the query, by using query options such as $filter, $sort, and $page. For more information, see Supporting OData Query Options.
The following linked document:
http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options
Says:
The EnableQuerySupport method enables query options globally for any controller action that returns an IQueryable type.
But this document for OData 4 on WebApi 2.2 has put it on actions returning IHttpActionResult:
http://blogs.msdn.com/b/webdev/archive/2014/03/13/getting-started-with-asp-net-web-api-2-2-for-odata-v4-0.aspx
[ODataRoutePrefix("Teams")]
public class TeamsEntitySetController : ODataController
{
private readonly LeageContext _leage = new LeageContext();
[EnableQuery]
[ODataRoute]
public IHttpActionResult GetFeed()
{
return Ok(_leage.Teams);
}
[ODataRoute("({id})")]
[EnableQuery]
public IHttpActionResult GetEntity(int id)
{
return Ok(SingleResult.Create<Team>(_leage.Teams.Where(t => t.Id == id)));
}
}
I'm going crazy trying to find up-to-date, accurate and consistent documentation on OData v4 / WebApi 2.2.
Which is correct today?
Use global configuration (instance of an HttpConfiguration object) and call
config.Filters.Add(new EnableQueryAttribute()
{
PageSize = 2
// .. other settings
});
this works
Related
I am coming from a heavy Java/Spring background and trying to transition some knowledge over to ASP.NET Core 6.
In Spring, on a RestController, I am able to route the request based on the presence of a query parameter.
So a HttpRequest with the uri: /students?firstName=Kevin can be routed to a different controller method than a HttpRequest with the uri: /students.
In ASP.NET Core 6, I am unable to determine if the equivalent is possible after working through some examples and reading the documentation for Web API.
Here is what I am trying to achieve, is this possible using two methods and routing configuration that will discern which controller method to invoke based on the query parameter?
[ApiController]
[Route("Students")]
public class StudentHomeProfileController : ControllerBase
{
[HttpGet] //Route here when no parameters provided
public async Task<ActionResult<IEnumerable<Student>>> GetStudentAsync()
{
/* Code omitted */
}
[HttpGet] //Route here when firstName query param provided
public async Task<ActionResult<IEnumerable<Student>>> SearchStudentAsync([FromQuery] string firstName)
{
/* Code omitted */
}
}
While filtering by query parameters does not come with ASP.NET Core out of the box, it's not too hard to supply this functionality on your own.
When it comes to extensibility, ASP.NET has some superpowers, one of them is IActionConstraint, which
Supports conditional logic to determine whether or not an associated action is valid to be selected for the given request. (Source)
Creating an annotation to filter for query parameters is as easy as
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class QueryParameterConstraintAttribute : Attribute, IActionConstraint
{
private readonly string _parameterName;
public QueryParameterConstraintAttribute(string parameterName)
{
this._parameterName = parameterName;
}
public bool Accept(ActionConstraintContext context)
{
return context.RouteContext.HttpContext.Request.Query.Keys.Contains(this._parameterName);
}
public int Order { get; }
}
All that's left is annotating your controller method with that constraint
[HttpGet] //Route here when firstName query param provided
[QueryParameterConstraint("firstName")]
public async Task<ActionResult<IEnumerable<Student>>> SearchStudentAsync([FromQuery] string firstName)
{
/* Code omitted */
}
In a quick test I was able to confirm that it seems to work as intended, even if you add multiple of those attributes for different query parameters (if all conditions match, the route is called).
(Please note, this was tested with .NET Core 2.1. Anyway, it shuold be pretty much the same with .NET 6)
I think you are looking for something like this, you need to specify the parameter in the "HttpGet" attribute
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-6.0#attribute-routing-with-http-verb-attributes
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}
[HttpGet("{id}")] // GET /api/test2/xyz
public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int/{id:int}")] // GET /api/test2/int/3
public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int2/{id}")] // GET /api/test2/int2/3
public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
You are trying to differentiate API calls using query params. this is not the way to do this. if you want to separate the calls you should probably use path params instead.
Read more about Routing in ASP.NET Core - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-6.0
CRM 2016 provides an odata endpoint such as:
https://mycrmorg.com/api/data/v8.1/
And it allows you to apply odata filters on it such as:
https://mycrmorg.com/api/data/v8.1/accounts(8308AD1C-1B1A-E711-941B-00155DC0D345)
If I have a controller such as :
class AccountsController
{
public IHttpActionResult Get(ODataQueryOptions options)
{
var endPoint = #"https://mycrmorg.com/api/data/v8.1/";
//how do we apply the odata query options here??
}
}
how do we apply the odata query options against this endpoint??
I understand that you simply want to get all the ODATA query options from your ODataQueryOptions object. You can get it from RequestUri:
class AccountsController
{
public IHttpActionResult Get(ODataQueryOptions options)
{
var stringOptions = options.Request.RequestUri.PathAndQuery;
var endPoint = #"https://mycrmorg.com/api/data/v8.1";
var endPointAndQuery = endPoint + stringOptions;
//call Odata
}
}
stringOptions will contain for example something like this: "/accounts?$filter=accountnumber eq '1234'" which you can simply append to your endpoint and call other odata api.
I have a basic WebApi OData 4 controller for TEAM model which has a POST action for adding a new TEAM to the database.
public async Task<IHttpActionResult> Post(USER_TEAMS userTeam)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
context.USER_TEAMS.Add(userTeam);
await context.SaveChangesAsync();
return Created(userTeam);
}
I want to have another endpoint in that controller for bulk insertion of teams which takes a list of team objects and adds them to the database. What is the best approach in WebApi OData 4 to do that?
The OData spec includes a batching concept, but for this problem an OData action is a cleaner solution. The action will be bound to the Teams entity set and will accept a collection of Team entities in the request payload. (I've changed USER_TEAMS to Team in the code below.)
Given the following simple definition of Team entity type:
public class Team
{
public int Id { get; set; }
public string Name { get; set; }
}
First, define the action method in the same controller as your Post method.
[HttpPost]
public IHttpActionResult BulkAdd(ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
var newTeams = (IEnumerable<Team>)parameters["NewTeams"];
// Your data access layer logic goes here.
return this.StatusCode(HttpStatusCode.NoContent);
}
Then declare the OData action in your Web API configuration code.
var builder = new ODataConventionModelBuilder();
builder.Namespace = "TeamService";
builder.EntitySet<Team>("Teams");
builder.EntityType<Team>().Collection
.Action("BulkAdd")
.CollectionParameter<Team>("NewTeams");
Note in the above:
EntityTypeConfiguration<T>.Collection is necessary to bind the action to the Team entity set (vs. a single Team entity)
ActionConfiguration.CollectionParameter<T> is necessary to specify that the parameter is a collection (vs. a scalar)
On the client, invoke the action as follows.
POST http://domain/Teams/TeamService.BulkAdd
Content-Type: application/json
{"NewTeams": [{"Name": "Demons"}, {"Name": "Angels"}]}
I'm using OData Attribute Routing for an OData endpoint. Here is an example of what I have:
[ODataRoutePrefix("Profile")]
public class ProfileODataController : ODataController
{
[ODataRoute]
[EnableQuery]
public IHttpActionResult Get()
{
var repo = new Repositories.ProfileRepository();
return Ok(repo.GetProfiles());
}
[ODataRoute("({key})")]
[EnableQuery]
public IHttpActionResult Get([FromODataUri] string key)
{
var repo = new Repositories.ProfileRepository();
var result = repo.GetProfiles().SingleOrDefault(x => x.Id== key);
if (result == null) return NotFound();
return Ok(result);
}
}
Here is my set up:
config.MapODataServiceRoute("odata", "odata", ModelGenerator.GetEdmModel());
Here is my EdmModel Generation:
public static IEdmModel GenerateEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Profile>("Profile").EntityType.HasKey(x => x.Id);
return builder.GetEdmModel();
}
The urls /odata/Profile and /odata/Profile('someid') both work, but when I try to access the $metadata endpoint (/odata/$metadata#Profile), I get the following error:
{"Message":"No HTTP resource was found that matches the request URI 'http://****/odata/$metadata'.","MessageDetail":"No type was found that matches the controller named 'Metadata'."}
Do I need to create a controller/action for serving the metadata? If so, how is that action implemented?
Turns out it had to do with my replacement of the IAssembliesResolver.
I had implemented a custom version to provide only the component assemblies that I had implemented controllers in. However, as the error said, it couldn't find a controller named MetadataController. Turns out, OData implements one: System.Web.OData.MetadataController, which provides for the $metadata keyword.
Since I had implemented my own IAssembliesResolver, the System.Web.OData assembly wasn't being included, and $metadata failed. Once I discovered this, and updated my assembly resolver to explicitly include the OData assembly, it now works as it should.
I have spent a about 2 days setting up my Odata Web API project that was before a simple Asp.net mvc4 project.
But still I am not successful in operating even CRUD Operations.
It says this:
<m:message xml:lang="en-US">
No HTTP resource was found that matches the request URI 'http://localhost:53208/odata/Product'.
</m:message>
<m:innererror>
<m:message>
No action was found on the controller 'Product' that matches the request.
</m:message>
<m:type/>
<m:stacktrace/>
</m:innererror>
It means its reaching the Controller but not finding actions or there is some problem in my routing settings.
I have following in my WebApiConfig:
config.Routes.MapODataRoute("OData","odata",GetEdmModel());
My getEdmModel method:
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Product");
builder.EntitySet<OfferedProduct>("OfferedProduct");
IEdmModel model = builder.GetEdmModel();
return model;
}
My controller is like this:
public class ProductController : EntitySetController<Product,int>
{
private OfferAssistantDbContext db = new OfferAssistantDbContext();
List<Product> Products = new OfferAssistantDbContext().Products.ToList();
// GET api/Product
[Queryable(PageSize = 10)]
public override IQueryable<Product> Get()
{
return Products.AsQueryable();
}
// GET api/Product/5
public Product GetProduct([FromODataUri] int id)
{
return Products[id];
}
/// and so on... but for this time lets work only on GET operation
Now when I write this in my browser:
http://localhost:53208/odata/Product
it says the error I showed above..
Please guide me where is the problem?
I believe that your controller needs to inherit from ODataController:
public class ProductController : ODataController