Set custom route using OData 8 - .net-core

Recently I updated to OData 8.0.10. I added this in my Startup.cs file:
services.AddRouting();
services.AddControllers().AddOData(opt =>
opt.AddRouteComponents("odata", GetEdmModel()).Filter().Select().OrderBy().Count());
where
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Project>("Project");
return builder.GetEdmModel();
}
I have this small controller
[Route("api/[controller]")]
[ApiController]
public class ProjectController : ControllerBase
{
[HttpGet]
[EnableQuery(PageSize = 20)]
public IQueryable<Project> GetAsync()
{
var projects = _projectRepository.GetAll();
return projects;
}
[HttpGet("{id}", Name = "GetProjectById")]
public async Task<ActionResult> GetAsyncById(long id)
{
var project = await _projectService.GetProjectByIDAsync(id);
return Ok(project);
}
[HttpPatch("{id}", Name = "PatchProjectById")]
public async Task<ActionResult> PatchProject(long id, [FromBody] ProjectPatchDetails projectPatch)
{
var project = await _projectRepository.GetAsync(id);
var updated = await _projectService.UpdateProjectAsync(id, project, projectPatch);
return Ok(updated);
}
}
that has three endpoints, one of them is annotated by [EnableQuery] and the rest aren't. When I access api/project?$count=true&$skip=0&$orderby=CreateDate%20desc, I get a paged info (20 records) but I don't get the #odata.context and #odata.count. If I access /odata/project?$count=true&$skip=0&$orderby=CreateDate%20desc, with odata/ prefix, it gives me #odata.context and #odata.count. I tried changing AddRouteComponents to AddRouteComponents("api", GetEdmModel()) but in this case I get the following error:
"The request matched multiple endpoints. Matches: MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api) MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api)"
I have multiple questions in this case:
Is there a way to reroute odata to api, make /odata prefix as /api and make it work?
Should I make another controller that will store all OData tagged actions and on this way maybe workaround this as a solution, if possible?

#anthino
Is there a way to reroute odata to api, make /odata prefix as /api and make it work?
if you add 'opt.AddRouteComponents("api", GetEdmModel())', remember to remove
[Route("api/[controller]")] and other attribute routings
Should I make another controller that will store all OData tagged actions and on this way maybe workaround this as a solution, if possible?
Basically, it's better to create two controllers, one for odata, the other for others. In your scenario, you mixed them together. You should be careful about this. You can use 'app.UseODataRouteDebug()' middleware to help you debug.

I think your ProjectController should be inheriting from ODataController not ControllerBase. With the ODataController, you should get the context url and the #odata.count

Related

OData doesn't return context and count after upgrading to .NET 6

I used .NET 3.1 until I decided to upgrade my app to .NET 6. I did it successfully but some of my modules broke, one of them is OData. I used OData and I got #odata.context, #odata.count and value returned back in 3.1. Now in .NET 6, I get only the value which means that #odata.context and #odata.count aren't present in the return object. In order to make it run, I added this line in my code
services.AddControllers().AddOData(options => options.AddRouteComponents("v1", GetEdmModel()).Select().Filter().OrderBy()); in my Startup.cs where
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Project>("Project");
return builder.GetEdmModel();
}
My endpoint is as
[HttpGet]
[EnableQuery(PageSize = 20)]
public async Task<ActionResult> GetAsync()
{
var projects = await _projectRepository.GetAllAsync();
return Ok(projects);
}
Do you know what can I change and how should I change it in order to get the context and count from OData in .NET 6? I use the usual OData library with 8.0.0 version
EDIT to add more info about #Tiny Wang's answer:
This seems to be working and thank you very much for it! However, I stumbled upon another problem. I tried your example and the working version was on https://localhost:44327/odata/project$count=true&$skip=0&$orderby=CreateDate%20desc. My api prefix is api/[ControllerName] and I changed
options => options.EnableQueryFeatures().AddRouteComponents("odata", GetEdmModel()).Select().Filter().OrderBy()
to
options => options.EnableQueryFeatures().AddRouteComponents("api", GetEdmModel()).Select().Filter().OrderBy()
but when I access the endpoint, I get the following error:
"The request matched multiple endpoints. Matches: MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api) MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api)"
even tho GetAsync() is defined only once. Do you know how can I fix this and what causes it?
The change was in the Microsoft.AspNetCore.OData package, not .NET 6. You can use that package in .NET 5 if you want.
First of all, you always had to specify $count=true. That's an expensive operation. In a paging query it meant you had to execute two queries, one to receive a single page of data and another to count all possible results.
To do that you have to enable counting, just like any other OData operation, in the AddOData() call that adds the OData middleware.
Finally, for this to have any real effect, the controller action should return an IQueryable that will be used to construct the final LINQ and by extension SQL query. Otherwise your code will load everything in memory. You'd load a 100K row table in memory only to return 10 rows
OData failed to gain traction because it allowed clients to execute inefficient and unoptimized queries. Service developers had no idea what to optimize because client developers were free to execute anything. In later versions of the protocol, all query capabilites are off by default and have to be explicitly enabled. Server developers now can restrict the maximum size, whether expensive sort operations are allowed etc. They can prevent client code from filtering or sorting by unindexed fields for example.
In my own application I add the OData middleware with :
var edmModel=ODataModels.GetEdmModel();
services.AddControllersWithViews()
.AddOData(opt => opt.Select()
.OrderBy()
.Filter()
.Count()
.Expand()
.SetMaxTop(250)
.AddRouteComponents("odata", edmModel)
);
This enables Count and sets the maximum result size to a fairly large 250 - I have some grids users tend to scroll through.
To use the OData endpoints, a Controller that inherits from ODataController is needed. Query methods should return an IQueryable. If that IQueryable comes from a DbContet, the OData query will be used to construct a LINQ query and by extension the final SQL query. This will ensure that only the necessary data will be loaded.
[EnableQuery]
public IQueryable<Customers> Get()
{
return _db.Customers.AsNoTracking();
}
An OData controller that works on top of EF Core could look like this :
public class CustomersController:ODataController
{
private readonly ILogger<CustomersController> _logger;
private readonly SalesContext _db;
public CustomersController(SalesContext db, ILogger<CustomersController> logger)
{
_logger = logger;
_db = db;
}
[EnableQuery]
public IQueryable<Customers> Get()
{
return _db.Customers.AsNoTracking();
}
[EnableQuery]
[HttpGet]
public IActionResult Get(long key)
{
var cust = _db.Customers
.AsNoTracking()
.FirstOrDefault(t => t.ID == key);
if (cust == null)
{
return NotFound($"Not found: Customer ID = {key}");
}
return Ok(cust);
}
...
The Get(key) action is necessary to allow retrieving items by ID in OData eg using customers(123).
==================================
Per my test(created a .net 6 web api project and install Microsoft.AspNetCore.OData 8.0.10), I need to add ?$count=true behind my url then it can appear in my response.
My program.cs
builder.Services.AddControllers().AddOData(options => options.EnableQueryFeatures().AddRouteComponents("odata", GetEdmModel()).Select().Filter().OrderBy());
IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<WeatherForecast>("Hello");
return builder.GetEdmModel();
}
My test controller:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;
namespace WebApi.Controllers
{
[Route("odata/[Controller]")]
public class HelloController : ODataController
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[EnableQuery]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Id = index,
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}

How to set default versioning in ASP.NET Core 6 Web API for my scenario?

Just realised that my understanding about ASP.NET Core 6 Web API versioning is wrong.
This is my controller:
[ApiVersion("1.0")]
[ApiController]
[Authorize]
public class FundController
{
[MapToApiVersion("1.0")]
[Route("/Fund/v{version:apiVersion}/delta")]
public async Task<List<PortfolioHolding<Holding>>> Delta([FromQuery] Request dataModel)
{
}
}
What I want is to support route /Fund/v1.0/delta and /Fund/delta, when versioning not provided by the consumer (e.g. calling /Fund/delta), the default version will be hit.
So I configured the versioning like this. However, when I call /Fund/delta, I get a http 404 error.
But /Fund/v1.0/delta will hit the correct controller.
What am I doing wrong?
services.AddApiVersioning(option =>
{
option.DefaultApiVersion = new ApiVersion(1, 0);
option.AssumeDefaultVersionWhenUnspecified = true;
option.ReportApiVersions = true;
});
Usually, it's pretty easy to do this that way. The disadvantage of this approach is that you need to manually change the "default" version of API with this attribute
The problem is that you have not specified the routes in the controller.
You should add the default route as well as the formatted version route. Then you should ensure that your endpoints have the version specified in the MapToApiVersion attribute.
Here is a code sample of what your controller should look like:
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("[controller]")]
[Route("[controller]/v{version:apiVersion}")]
public class FundController : ControllerBase
{
[MapToApiVersion("1.0")]
[Route("delta")]
[HttpGet]
public async Task<List<PortfolioHolding<Holding>>> DeltaV1([FromQuery] Request dataModel)
{
}
[MapToApiVersion("2.0")]
[Route("delta")]
[HttpGet]
public async Task<List<PortfolioHolding<Holding>>> DeltaV2([FromQuery]
Request dataModel)
{
}
}

OData + Swagger. URL generation

In my Net 6 Web-API project I'm using OData and Swagger (it was added automatically when project was created).
It works out of the box, but there is an issue with some URLs generated by Swagger.
Here is my OData controller:
public class UsersController : ODataController
{
// skipped for brevity
[EnableQuery]
public IActionResult Get()
{
return Ok(_dbContextRepo.Select<DbModel.User>());
}
[EnableQuery]
public SingleResult<User> Get([FromODataUri] int key)
{
return SingleResult.Create(_dbContextRepo.Select<User>().Where(u => u.Id == key));
}
}
SwaggerUI output:
When I try to run query that gets entity by Id from Swagger it fails because of wrong url.
By some reason Swagger generates query parameter and URL like on picture above. For OData URL has to be like this (path parameter, https://swagger.io/docs/specification/describing-parameters/):
https://localhost:7250/api/Users/1
In swagger.json parameter described as
Spent all day trying to figure this out. Will appreciate any help.
Found solution myself. I'm using OData 8 and it seems there is no need to mark method parameter as [FromODataUri].
[EnableQuery]
public SingleResult<User> Get(int key)
{
//...
}
Whithout it Swagger generates correct links.

Url.Action returning incorrect URL for webapi action with Route attrubute

I have a problem with the behaviour of Url.Action();
I have a webapi where all controllers require explicit route prefix attribute and all actions require a route attribute.
I register my routes in the WebApiConfig.cs
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap =
{
["apiVersion"] = typeof( ApiVersionRouteConstraint )
}
};
config.MapHttpAttributeRoutes(constraintResolver);
I have currently commented out the line below, but (because) it did not change the incorrect behaviour:
//config.Routes.MapHttpRoute(name: "DefaultApi",
//routeTemplate: "api/v{version:apiVersion}/{controller}/{action}/{id}", defaults: new {id = RouteParameter.Optional});
My controllers look as follows:
[RoutePrefix("api/v{version:apiVersion}/programs")]
public class ProgramsController : ApiController
{
[HttpGet, Route("{telemetryKey}/versions/latest")]
public async Task<LatestVersionResponse> GetLatestVersionInfo(Guid telemetryKey)
{
// serious business logic
}
}
I expect that '#Url.Action("GetLatestVersionInfo", "Programs", new { telemetryKey = Guid.Parse("43808405-afca-4abb-a92a-519489d62290") })'
should return /api/v1/programs/43808405-afca-4abb-a92a-519489d62290/versions/latest
however, I get /Programs/GetLatestVersionInfo?telemetryKey=43808405-afca-4abb-a92a-519489d62290 instead. So, my routeprefix and route attributes are ignored.
Swagger correctly discovers my routes and I can validate that requests to the expected routes work OK - it's only the Url.Action() that is confused.
What can be wrong...?
Well, it seems there were a few things wrong.
Wrong helper:
I should be using the Url.HttpRouteUrl for generating API links from a razor view (Url.Link is for generating link from within API controllers)
Conflict with aspnet-api-versioning library
For some reason (perhaps a bug?) the prefix that I have on the controller (apiVersion variable) breaks the URL helper mechanism.
For now, I have ditched the aspnet-api-versioning library, but created an issue on their github repo, in case its a bug.
Since I really hate the idea of creating and maintaing magic strings, so I took the following approach - each controller has a public static class which contains const values for the route names:
[RoutePrefix("api/v1/developers")]
public class DevelopersController : ApiController
{
[HttpGet, Route("{developerId}/programs", Name = Routes.GetPrograms)]
public async Task<IEnumerable<Program>> GetPrograms(Guid developerId){}
public static class Routes
{
public const string GetPrograms = nameof(DevelopersController) +"."+ nameof(DevelopersController.GetPrograms);
}
}
Now that can be used from a razor controller in a simple and relatively safe manner:
#Url.HttpRouteUrl(DevelopersController.Routes.GetPrograms, new { developerId = /* uniquest of guids */})
A bit better than magic strings. I've also added a bunch of unit tests for controllers where I validate that each route is unique and proper and that the routes class only contains routes for the action it contains.
Try the following:
Name your route:
[HttpGet, Route("{telemetryKey}/versions/latest", Name="LatestVersionInfoRoute")]
public async Task<LatestVersionResponse> GetLatestVersionInfo(Guid telemetryKey)
{
// serious business logic
}
Use Url.Link method:
#Url.Link("LatestVersionInfoRoute", new { telemetryKey = Guid.Parse("43808405-afca-4abb-a92a-519489d62290") })

WebAPI service design

I'm pretty comfortable with how Asp.NET MVC controllers worked when designing services.
However the new WebAPI controllers. how am I supposed to design my services here?
Lets say we have 3 different ways to list e.g. Users.
Get 10 latest , Get all, Get inactive or whatever.
none of these might need parameters. so how would you solve this in WebAPI
IEnumerable<User> Get10Latest()
IEnumerable<User> GetAll()
IEnumerable<User> GetInactive()
That won't work since they have the same param signature.
So what is the correct way to design this here?
You can support multiple methods in one controller for a single HTTP method by using the action parameter.
E.g.
public class UsersController : ApiController
{
[ActionName("All")]
public HttpResponseMessage GetAll()
{
return new HttpResponseMessage();
}
[ActionName("MostIQ")]
public HttpResponseMessage GetMostIQ()
{
return new HttpResponseMessage();
}
[ActionName("TenLatest")]
public HttpResponseMessage GetTenLatest()
{
return new HttpResponseMessage();
}
}
Unfortunately, I have not found a way to get a single controller to handle both with and without the action at the same time.
e.g.
public class UsersController : ApiController
{
[ActionName("")] // Removing this attribute doesn't help
public HttpResponseMessage Get()
{
return new HttpResponseMessage();
}
[ActionName("All")]
public HttpResponseMessage GetAll()
{
return new HttpResponseMessage();
}
[ActionName("MostIQ")]
public HttpResponseMessage GetMostIQ()
{
return new HttpResponseMessage();
}
[ActionName("TenLatest")]
public HttpResponseMessage GetTenLatest()
{
return new HttpResponseMessage();
}
}
Being able to use a single controller for a collection resource and all of its subsets would be nice.
Someone will probably be along and wrap me on the knuckles for this, but you need to configure your routing to handle the Gets. This is how I got it working with the above operations:
config.Routes.MapHttpRoute(
name: "CustomApi",
routeTemplate: "api/{controller}/{action}",
defaults: new { id = RouteParameter.Optional }
);
So now your requests are mapped to the correct controller -> action via the route template. Note that the new route needs to be registered first in WebApiConfig.cs. If you keep the old, default one.
EDIT
Having re-read the question I realize I wasn't quite answering the design question. I would think that one way to go about it, from a REST perspective, would be to use a separate resource to expose the proper collections (Get10Latest for example) since I assume that there is a business reason for exposing that exact subset of data through the service. In that case you'd expose that resource though a single Get in its own Controller (if that is the desired behaviour).
Well why not have urls like this:
GET /users
GET /users/latest
GET /users/inactive
Using routing you could route them to
public classs UserController : ApiController
{
public IEnumerable<User> Get(string mode)
{
// mode is in routing restricted to be either empty, latest, or inactive
}
}
Otherwise use multiple controllers. The use of action names in Web API is kind of a wrong way to about it.

Resources