Routing is not working for web api with realtions - asp.net-core

I am creating web api using asp.net core. The api end point is logically mapped to resource's relations based on guidelines here
So my API looks like
http://tax.mydomain.com/api/v1/clients/1/batches/12/start
Where Client is parent of Batch, 1 is clientid and 12 is batchid, and Start is POST action method.
Here is the corresponding controller
public class TaxController : Controller
{
[HttpPost]
[Route("clients/{clientid}/batches/{batchid}/start")]
public void Start([FromRoute]string clientId, [FromRoute]string batchId,
[FromBody]IEnumerable<string> urls)
{
// do something
}
}
since api/v1 is common to all controllers i configured that in startup's Configure method. Also i want Home as default controller.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMvc(routes =>
{
routes.MapRoute("default","api/v1/{controller=Home}/{action=Index}/{id?}");
});
}
However client is getting not found error for api http://tax.mydomain.com/api/v1/clients/1/batches/12/start

Any controller methods that do not have a route attribute use convention-based routing.
When you use [Route] attribute, you define attribute routing and so conventional routing is not used for that action/controller. Therefore, your controller is accessible by
http://tax.mydomain.com/clients/1/batches/12/start
As an option, you can use the fact, that attribute routes can be combined with inheritance. Set a Route attribute on the entire controller and this will work as route prefix (the same behavior as [RoutePrefix] attribute in WebApi):
[Route("api/v1")]
public class TaxController : Controller
{
}
More general example from routing documentation:
[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }
public class ProductsController : MyBaseController
{
[HttpGet] // Matches '/api/Products'
public IActionResult List() { ... }
[HttpPost("{id}")] // Matches '/api/Products/{id}'
public IActionResult Edit(int id) { ... }
}

There are two things wrong with your setup
You call http://tax.mydomain.com/clients/1/batches/12/start but you don't have specified the controller name within it. This route looks for a controller named ClientsController. So the correct url would have to be http://tax.mydomain.com/tax/clients/1/batches/12/start instead
You seem to be using default MVC/Viewbased route, but your url suggest you use WebAPI.
When you use WebAPI to create a Rest service, you don't have any actions. Instead, actions map to the Http Verbs (GET (Read), PUT (update/replace), POST (insert), DELETE).
So for REST Services your default route should look like this instead: api/v1/{controller=Home}/{id?}

Related

Attribute Routing in ASP.NET Core OData 8.0 not working

I have been attempting to follow this post to enable attribute routing in OData 8.0.10:
Attribute Routing in ASP.NET Core OData 8.0 RC
During development of v8 ODataRouteAttribute and ODataRoutePrefixAttribute have been removed and routing is supposed to follow regular ASP.NET Core attribute routing, however I cannot get this to work as described.
I register OData as follows:
// build edm:
model = builder.EntitySet<Stuff.PersonProfile>("personProfiles");
// startup.cs
odataOptions.Count().Filter().Expand().Select().OrderBy().SetMaxTop(3).AddRouteComponents("", model)
// person profiles controller:
[Route("personProfiles")]
public class PersonProfilesController : ODataController
{
[HttpGet("Person")]
IActionResult GetPerson(ODataQueryOptions<Stuff.PersonProfileService.Models.PersonProfile> options)
{
}
}
This creates the endpoint correctly and I can reach it:
APIStuff.Controllers.PersonProfilesController.GetPerson (Stuff.API)
GET personProfiles/Person
However no OData endpoint mapping is created. If I remove the attribute route on the GetPerson method, then it DOES. i.e.: I get OData returned in the payload of the personProfiles endpoint that it creates.
It appears this was possible in the 8.0 preview as described in the following:
Routing in ASP NET Core 8.0 Preview
Where clearly there are examples of using attribute routing on the controller and the method. e.g.:
[ODataRoutePrefix("Customers({id})")]
public class AnyControllerNameHereController : ODataController
{
[ODataRoute("Address")]
public IHttpActionResult GetAddress(int id)
{
//......
}
[ODataRoute("Address/City")]
public IHttpActionResult GetCity(int id)
{
//......
}
}
I can only assume this has been removed or I am missing a very big elephant in the room.
Since 8.0 RC, attribute routing is changed to use [Route] and [HttpGet], etc
From your description, ‘personProfiles’ is an entity set, and “Person” looks like a property defined by the type of “personProfiles”, right?
If that’s the case, based on OData spec, you should query a property from an entity (a single entity). It means you should specify the key/id.
You can put the key in [Route] or in [HttpGet].
// person profiles controller:
[Route("personProfiles")]
public class PersonProfilesController : ODataController
{
[HttpGet("{key}/Person")] // this will generater route as: ‘personProfiles/{key}/Person’. It’s key as segment.
IActionResult GetPerson(ODataQueryOptions<Stuff.PersonProfileService.Models.PersonProfile> options)
{
}
}
// person profiles controller:
[Route("personProfiles({key})")] // this will generater route as: ‘personProfiles({key})/Person’. It’s key in parenthesis.
public class PersonProfilesController : ODataController
{
[HttpGet("Person")]
IActionResult GetPerson(ODataQueryOptions<Stuff.PersonProfileService.Models.PersonProfile> options)
{
}
}
Thanks,
-Sam

Asp.Versioning.Http AmbiguousMatchException: The request matched multiple endpoints on controllers

I am trying to achieve header based versioning on my controllers with Asp.Versioning.Http package version 6.4.0
it is supposed to be super simple here however i get AmbiguousMatchException: The request matched multiple endpoints exception
Here is my Program class
and my controllers defined like that:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options => {
// options.ApiVersionReader = new HeaderApiVersionReader("api-version");
options.DefaultApiVersion = new ApiVersion(1.0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
}).EnableApiVersionBinding();
builder.Services.AddMvc();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
// app.UseSwagger();
//app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapHealthChecks("/health/live");
app.MapControllers();
app.Run();
namespace Things.Service.Controllers.V1
{
[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("[controller]")]
[Asp.Versioning.ApiVersion(1.0)]
public class ThingsController : ControllerBase
{
// controller logic
}
}
namespace Things.Service.Controllers.V2
{
[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("[controller]")]
[Asp.Versioning.ApiVersion(2.0)]`your text`
public class ThingsController : ControllerBase
{
// controller logic
}
}
I get this exception:
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:
Things.Service.Controllers.V2.ThingsController.GetAllAsync (Things.Service)
Things.Service.Controllers.V1.ThingsController.GetAllAsync (Things.Service)
This is happening because you are missing AddMvc. Don't let the name fool you, this adds MVC Core, not the full MVC stack. Starting in 6.0, the new setup pivots on IApiVersioningBuilder so that all of the setup is in one place and, hopefully, easier to follow. If you're coming from earlier versions (e.g. <= 5.x), this might be a surprise. This change was necessary because AddApiVersioning is now the foundation for Minimal APIs, which doesn't include MVC Core or controller support. AddMvc adds those features.
services
.AddApiVersioning() // Asp.Versioning.Http : Core services and Minimal APIs
.AddMvc() // Asp.Versioning.Mvc : MVC Core
.AddApiExplorer() // Asp.Versioning.Mvc.ApiExplorer : API Explorer
.AddOData() // Asp.Versioning.OData : OData support
.AddODataApiExplorer(); // Asp.Versioning.OData.ApiExplorer : OData API Explorer
Since you're using MVC Core and controllers, you do not need EnableApiVersionBinding. MVC Core has support for Model Binders. AddMvc will register all those services. If you want to receive the incoming ApiVersion in your controller action, you need only add a parameter of type ApiVersion with the name of your choice. For example:
namespace Things.Service.Controllers.V1
{
[ApiVersion(1.0)]
[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("[controller]")]
public class ThingsController : ControllerBase
{
[HttpGet]
public IActionResult Get(ApiVersion version) => Ok();
}
}
Minimal APIs do not have a way to support this type of model binding. EnableApiVersionBinding provides a way to make it work. It won't hurt anything if you've added it, but it's unnecessary.
Finally, it looks like you have included a version number in your namespaces. If this is indeed your setup, you might consider using the VersionByNamespaceConvention. This would negate the need to decorate controllers with [ApiVersion]. The API version would be derived from the namespace itself. For additional details, see the Version By Namespace Convention documentation.

How does Swagger achieve the same route but different query parameters?

I created an asp.net core web api project, using the .net5 version, and I have a route like this.
[Route("api/detail")]
public IEnumerable<User> Get()
{
//TODO
return users;
}
[Route("api/detail")]
public IEnumerable<User> Get(string name)
{
//TODO
return users;
}
Although my request method is the same and the request parameters are different, the 500 error will be reported in swagger. Is there any way to solve it? Any help is greatly appreciated.
There could be multiple reasons why you're getting a 500 error. When I pasted your code into a new controller the first is error I received was:
Ambiguous HTTP method for action... Actions require an explicit HttpMethod binding for Swagger
It's telling you that you need to decorate each action in the controller with an HttpMethod binding, like [HttpGet]. More on that in a second...
The next issue is that you're using [Route] to bind two different action methods to the exact same route with the same HttpMethod. That's not possible in an API controller.
Conflicting method/path combination... Actions require a unique
method/path combination for Swagger
My preferred method for routing is to use Attribute routing with Http verb attributes.
The first step would be to move the route attribute to the controller. I'm going to assume you've created a DetailsController:
[Route("api/[controller]")]
[ApiController]
public class DetailsController : ControllerBase { }
Now, update your actions. Remove the [Route] attribute, replace with the HttpGet attribute, and add the name parameter to your second endpoint. I also prefer to return an IActionResult:
[HttpGet]
public IActionResult Get()
{
//TODO
return Ok(users);
}
[HttpGet("{name}")]
public IActionResult Get(string name)
{
//TODO
return Ok(users);
}
Note that parameters are identified by using curly braces around the variable {name} in the Http method attribute. Both endpoints work and are accessible through swagger. I urge you to read the linked page above for a better understanding of the possible routing options (linked again).

Does ASP.NET WebAPI conventional routing works without specifying action and HTTP verb constraints?

In our classic ASP.NET WebAPI project, we could declare a route and the framework would select the correct action based on the HTTP verb in the request.
However in .NET Core WebAPI, I tried the following route configuration
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapControllerRoute(
name: "DefaultRoute",
pattern: "{controller}/{id?}"
);
});
My controller has one method
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public WeatherForecast Get()
{
//return weather forecast
}
}
When trying the following URL, I get 404 whereas in a similar classic ASP.NET WebAPI project it would automatically execute the Get method.
https://localhost/weatherforecast
Does that mean for conventional routing we need to add multiple routes with same pattern, with default action and HTTP method constraints for it to work properly?
This question is only about conventional routing, suggesting to switch to attribute routing is not an answer.
I found a question that tries to simulate this behavior in classic ASP.NET WebAPI in ASP.NET Core: Route action based on HTTP verb?
The example is in .NET Core 2 and MVC, but trying it in .NET Core 3 WebAPI works the same.
Seems the answer is No, in ASP.NET Core WebAPI, if the route doesn't have action in the route pattern and no HTTP method constraints, the framework won't automatically try to match with actions based on HTTP verb in the requests.
In order to achieve this, multiple routes with default actions and Verb constraints need to be added.
Routing is responsible for mapping request URL to an endpoint and it comes with two types Conventional and Attributes routing.
And from your question, you are expecting conventional routing with default rout which you can achieve it .NET CORE using below line of code.
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Search}/{action}/{id?}");
});
Note: But keep in mind that convetional routing will not work if you decorate your controller with [ApiController] attribute.
By default .NET CORE supports attribute routing so you have to prefix the route by placing [Route] attribute on a controller level. Please see below example
[Route("api/[controller]")]
[ApiController]
public class SearchController : ControllerBase
{
[HttpGet("{company}")]
public IActionResult Get(string company)
{
return Ok($"company: {company}");
}
[HttpGet("{country}/{program}")]
public IActionResult Get(string country, string program)
{
return Ok($"country: {country} program: {program}");
}
}
The above code will work as you expected (Attribute routing).
If you are decorating your controller by [ApiController] attribute then you have to use Attribute routing and any conventional routing defined in startup class will be overridden. Please see more details here.
Does that mean for conventional routing we need to add multiple routes with same pattern, with default action and HTTP method constraints for it to work properly?
Yes, in asp.net core web api, if you want to use conventional routing, you need to remove [ApiController] attribute and [Route] attribute firstly and use the following route with default action
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=WeatherForecast}/{action=Get}/{id?}");
});
Refer to Conventional Routing in ASP.NET Core API
Update:Using Url Rewriting
You could always write your own url rewrite rules to meet your requirements.Refer to below demo which deal with url like /weatherforecast:
Create Rewrite Rules:
public class RewriteRuleTest : IRule
{
public void ApplyRule(RewriteContext context)
{
var request = context.HttpContext.Request;
var path = request.Path.Value;// path= "/weatherforecast" for example
if(path !=null)
{
context.HttpContext.Request.Path = path + "/" + request.Method;
// "/weatherforecast/post"
}
}
}
Startup.cs
app.UseRewriter(new RewriteOptions().Add(new RewriteRuleTest()));
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapControllerRoute(
name: "GetRoute",
pattern: "{controller=WeatherForecast}/{action=Get}/{id?}"
);
});

How create a middleware with api endpoints in .NET Core

I have created the web application with the web api. The application contains some Controllers for example TodoController:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : Controller
{
private readonly TodoContext _context;
public TodoController(TodoContext context)
{
_context = context;
}
[HttpGet]
public IEnumerable<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}
}
}
If I create the GET request - /api/todo - I get the list of Todos from database.
I have a list of controllers and api endpoints like above.
I would like distribute this api to another application ideally like middleware - my idea is register in Startup.cs like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddTodoApi();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseTodoApi();
}
This will be awesome use case for my api but I don't know how this controllers api endpoints rewrite like middleware and return same JSON data same approache like using classic Controllers.
How can I write the middleware in .NET Core for creating API endpoints?
Instead of the separate middleware, you may configure the MVC middleware to discovery controllers from another assembly:
// using System.Reflection;
public void ConfigureServices(IServiceCollection services)
{
...
services
.AddMvc()
.AddApplicationPart(typeof(TodoController).GetTypeInfo().Assembly);
Controllers are part of MVC middleware, they are not a separate part of request pipeline (but this is what middlewares are). When you register the custom middleware, it by default invokes on each request and you have HttpContext context as an input parameter to work with/edit
Request/Response data. But ASP.NET Core provides Map* extensions that are used as a convention for branching the pipeline.
Map branches the request pipeline based on matches of the given request path. If the request path starts with the given path, the branch is executed.
Example:
private static void HandleMapTodo(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("/api/todo was handled");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/api/todo", HandleMapTodo);
}
Note, that as middleware knows nothing about MVC middleware, you have only access to "raw" request and do not have features like model binding or MVC action filters.
Because it looks like the perfect microservices approach (similar than what my team is doing right now) I'd create a client assembly that can consume your API, the one that contains your TodoController, if you define a contract, and interface, for that API you can register it in your other assembly as it was a midleware and also you could mock that behaviour in your unit tests.
So, as I said, you could inject your client in ConfigureServices method, you can create:
public static IServiceCollection AddTodoRestClient(this IServiceCollection services)
{
services.AddSingleton<ITodoRestClient, TodoRestClient>();
return services;
}
Also consider that you will need to provide the enpoint so, it might looks like:
public static IServiceCollection AddConfiguredTodoClient(this IServiceCollection services, string todoEndpoint)
{
AddTodoClient(services);
ITodoRestClient todoRestClient = services.BuildServiceProvider().GetService<ITodoRestClient>();
// Imagine you have a configure method...
todoRestClient.Configure(services, todoEndpoint);
return services;
}
You can create those methods in a TodoRestClientInjector class and use them in Configure method on your startup.
I hope it helps
--- MORE DETAILS TO ANSWER COMMENTS ---
For me TodoClient is a Rest client library that implements calls to the ToDo API, (I've edited previous code to be TodoRestClient) methos like, i.e., CreateTodoItem(TodoDto todoItem) which implementation would call to the TodoController.Post([FromBody] item) or GetTodos() which wuold call TodoController.Get() and so on and so forth....
Regarding the enpoints... This approach implies to have (at least) two different applications (.NET Core apps), on the one hand the ASP NET Core app that has your TodoController and on the other hand a console application or another ASP NET Core API on which startup class you'll do the inyection adn the Rest client (the Todo Rest client) configuration ...
In a microservices approach using docker, in a dev environment, you'll use docker-compose-yml, but in a traditional approach you'll use concrete ports to define the endpoints...
So, imagine that you have in the second service a controller that need to use TodoController, to achieve so I'll use the above aproach and the "SecondController" would look like:
public class SecondController : Controller
{
private readonly SecondContext _context;
private readonly TodoRestClient _todoRestClient;
public TodoController(SecondContext context, ITodoRestClient todoRestClient)
{
_context = context;
_todoRestClient= todoRestClient;
}
// Whatever logic in this second controller... but the usage would be like:
_todoRestClient.GetTodos()
}
Just few final hints: it's key to minimize calls between services because it increases latency, and more and more if this happens on cascade. Also consider Docker usage, looks challenging but it is quite easy to start and, indeed, is thought to be used in scenarios that the one you presented and solutions like mine.
Again, I hope it helps.
Juan