Route action based on HTTP verb? - asp.net-core-mvc

I'm trying to get ASP.NET Core 2 MVC to route the action based on the HTTP verb via the following code in Startup.cs:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "post",
template: "api/{controller}/{id?}",
defaults: new { action = "Post" },
constraints: new RouteValueDictionary(new { httpMethod = new HttpMethodRouteConstraint("POST") })
);
routes.MapRoute(
name: "delete",
template: "api/{controller}/{id?}",
defaults: new { action = "Delete" },
constraints: new RouteValueDictionary(new { httpMethod = new HttpMethodRouteConstraint("DELETE") })
);
routes.MapRoute(
name: "default",
template: "api/{controller}/{action=Get}/{id?}");
});
I.e.,
If the client calls GET http://example.com/api/foo, that runs the Get() method on my FooController : Controller class.
If they call GET http://example.com/api/foo/123, that runs the Get(int id) method on my FooController : Controller class.
If they call POST http://example.com/api/foo, that runs the Post([FromBody] T postedItem) method on my FooController<T> : Controller class.
If they call POST http://example.com/api/foo/123, that runs the Post(int id, [FromBody] T postedItem) method on my FooController<T> : Controller class.
If they call DELETE http://example.com/api/foo/123, that runs the Delete(int id) method on my FooController : Controller
When I run the project, it doesn't seem to run any of my controllers. I have some Razor pages that respond but all of the controller-based routes just return 404. Not even the default route seems to work.
I've been using https://github.com/ardalis/AspNetCoreRouteDebugger to try and help me narrow the issue down but I'm still not finding the problem. It shows the methods on the controllers as available actions but doesn't list any of the names, templates or constraints added via MapRoute. I'd be glad to know of any other helpful tools as well.
FWIW, I'm trying to use the same verb constraints as here:
https://github.com/aspnet/Routing/blob/2.0.1/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs#L252-L268

So I don't recall exactly what the problem turned out to be but the meta-solution is that you can debug routing problems by increasing the log level from "Information" to "Debug". E.g., via appsettings.json:
{
"Logging": {
"Debug": {
"LogLevel": {
"Default": "Debug"
}
},
"Console": {
"LogLevel": {
"Default": "Debug"
}
}
}
}
...then you'll get messages like this in e.g., the Application Output pane of Visual Studio:
[40m[37mdbug[39m[22m[49m: Microsoft.AspNetCore.Routing.RouteConstraintMatcher[1]
Route value '(null)' with key 'httpMethod' did not match the constraint 'Microsoft.AspNetCore.Routing.Constraints.HttpMethodRouteConstraint'.
Microsoft.AspNetCore.Routing.RouteConstraintMatcher:Debug: Route value '(null)' with key 'httpMethod' did not match the constraint 'Microsoft.AspNetCore.Routing.Constraints.HttpMethodRouteConstraint'.
[40m[37mdbug[39m[22m[49m: Microsoft.AspNetCore.Routing.RouteBase[1]
Request successfully matched the route with name 'get' and template 'api/{controller}/{id?}'.
Microsoft.AspNetCore.Routing.RouteBase:Debug: Request successfully matched the route with name 'get' and template 'api/{controller}/{id?}'.
[40m[37mdbug[39m[22m[49m: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action Contoso.Media.ServiceHost.Controllers.MediaController.Get (Contoso.Media.ServiceHost)
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Debug: Executing action Contoso.Media.ServiceHost.Controllers.MediaController.Get (Contoso.Media.ServiceHost)

Related

MVC6 routing to single-page application without losing 404

I'm writing a single-page application with angular2 and MVC5. I'm new to both, though, and I'm having trouble with the routing.
I'd like to match URLs as:
/ -> go to my index page, which bootstraps angular
/api/{controller}/{id?} -> REST API
/{*anythingelse} -> if a file exists there, return it as static content; otherwise if angular can route it, have angular route it; otherwise return 404.
The second point's easy enough, and I can get the client-side routing working if I'm willing to give up 404 returns, but I can't seem to reconcile it all.
It seems like this ought to work:
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "api",
template: "api/{controller}/{id?}");
routes.MapRoute(
name: "spa",
template: "{*anythingelse}",
defaults: new { controller = "Home", action = "Index" });
});
and:
#RouteConfig([
{ path: "/", name: 'Splash', component: SplashView },
{ path: '/accounts/login', name: 'Login', component: LoginView },
{ path: '/accounts/register', name: 'Registration', component: RegistrationView },
{ path: '/home/...', name: 'Home', component: HomeView },
])
But that just serves Index.cshtml for every request that isn't a static file.
I feel like this must already be a solved problem, but I haven't been able to find anything online about it. How does one do this properly?
I'm using "HTML5"-style paths rather than hash-style.
So there are two ways to go about doing it. If you are using the HashLocationStrategy I would strongly encourage you to do this on your server side implementation as I have found it much easier to deal with.
Otherwise you could make your own RouterOutlet component that handled the exceptions. I am not 100% clear on how you could get it to work with your RouterConfig as I have not delved that deep into the routing aspect, but I bet you could see if there exists a route then go there otherwise 404 error. Here is my code that deals with seeing if a user is logged in with Json Web tokens.
import {Directive, Attribute, ElementRef, DynamicComponentLoader} from 'angular2/core';
import {Router, RouterOutlet, ComponentInstruction} from 'angular2/router';
#Directive({
selector: 'router-outlet'
})
export class LoggedInRouterOutlet extends RouterOutlet {
publicRoutes: any;
private parentRouter: Router;
constructor(_elementRef: ElementRef, _loader: DynamicComponentLoader,
_parentRouter: Router, #Attribute('name') nameAttr: string) {
super(_elementRef, _loader, _parentRouter, nameAttr);
this.parentRouter = _parentRouter;
}
activate(instruction: ComponentInstruction) {
if (!localStorage.getItem('jwt') || !tokenNotExpired('jwt')) {//Public Routes does not work with Hash Location Strategy, need to come up with something else.
// todo: redirect to Login, may be there is a better way?
if(localStorage.getItem('jwt')){
localStorage.removeItem('jwt');
}
this.parentRouter.navigate(['Login']);
}
return super.activate(instruction);
}
}
As you can see I handle my checking for the Token, and if they don't have a token they can only go to my login page. Then in your app.component or your bootstrapped component just use this as your router-outlet instead of the original.
Sorry I can't be more helpful but I hope this gets you pointed in the right direction!
I think you're looking for a regex route constraint:
routes.MapRoute("app", "{*anything}",
new { controller = "Home", action = "Index" },
new {anything = new RegexRouteConstraint("^(?!api\\/).+") });
This will prevent your catch all route from mapping to any request that begins with "api/"

Additional parameters for grid

I am using the jquery-bootgrid to render a couple of grinds. It works brilliant.
I want to send some additional parameters for the grid to my MVC controller.
How can i pass those parameters ?
I have tried :
$("#results-grid").bootgrid({
ajaxSettings: {
url: testResultsListUrl,
data: { testSubject: '2', another : '3' }
}
});
But it does not seem to work. If i put all the properties for the ajax object inside the ajaxSettings, the un set ulr error is thrown.
Can you please help ?
I managed to do it, i saw a discussion on git for this project. What I had to do to be able to send additional params to my controller was :
$("#results-grid").bootgrid({
ajax: true,
url: testResultsListUrl,
requestHandler: function (request) {
if (testSubject != "") {
request.testSubject = testSubject;
}
if (medicalDevice != "") {
request.medicalDevice = medicalDevice;
}
return request;
}
The requestHandler is the object that is sent with all the parameters, for the grid. You can add all your parameters inside of it.

WebAPI : 403 Forbidden after publish website

Alright, I'm having a tough time locating the problem since it works locally but after doing a publish the results are simply:
Error Code: 403 Forbidden. The server denied the specified Uniform Resource Locator (URL). Contact the server administrator. (12202)
The code:
[RoutePrefix("api/v1/project")]
public class ProjectController : BaseApiController
{
[HttpGet]
public HttpResponseMessage GetProjects()
{
HttpResponseMessage resp = new HttpResponseMessage(HttpStatusCode.OK);
if(User.Identity.IsAuthenticated)
{
var model = new ModelFactory().CreateProjects();
resp = Request.CreateResponse(HttpStatusCode.OK, model);
}
return resp;
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// all actions under /project routes require authentication
config.Routes.MapHttpRoute(
name: "ProjectApi",
routeTemplate: "api/v1/{controller}/{action}/{apikey}",
defaults: new { apikey = RouteParameter.Optional },
constraints: new { controller = "project" },
handler: new BasicAuthHandler(config));
// all routes requires an api key
config.MessageHandlers.Add(new ApiKeyHandler());
config.MapHttpAttributeRoutes();
}
}
I've tried several "solutions" from the net yet none of them seems to fix this. I've added the:
// Stop IIS/Asp.Net breaking our routes
RouteTable.Routes.RouteExistingFiles = true;
from: http://www.grumpydev.com/2013/09/17/403-14-error-when-trying-to-access-a-webapi-route/
And also made sure that:
<modules runAllManagedModulesForAllRequests="true">
Having the code above, using the following link gives a successful connection where it checks (in the correct order) the APIkey (ApiKeyHandler), checks if the user needs to log in(BasicAuthHandler) and then goes to method in the controller ({controller}/{action}).
// THIS WORKS!
http://localhost:51077/api/v1/project/getprojects?apikey=123456
then we do a publish and tries the same thing
// This is haunted with number 403
http://website.com/api/v1/project/getprojects?apikey=123456
gives the Error Code: 403 Forbidden.
I am clueless. I've even tried changing the whole publish folder's security settings for "NETWORK SERVICE" to full access.. no change.
Let me know if you need any more intel.
Called the web server machine fellas and they had a firewall blocking incoming webapi calls with authenticating. It now works as it should :)

WEB API. Multiple actions were found that match the request

Need help.
I have 2 controllers:
// POST (Single SMS)
[ActionName("AddSMS")]
public HttpResponseMessage Post(MySMS singleSMS)
{
try
{
SMS_Repository.Add(singleSMS);
return Request.CreateResponse<MySMS>(HttpStatusCode.Created, singleSMS);
}
catch (Exception)
{
return Request.CreateErrorResponse(HttpStatusCode.ExpectationFailed, "Error");
}
}
// POST (Collection of SMSes)
[ActionName("AddSMSCollection")]
public HttpResponseMessage Post(List<MySMS> smses)
{
try
{
SMS_Repository.Add(smses);
return Request.CreateResponse<List<MySMS>>(HttpStatusCode.Created, smses);
}
catch (Exception)
{
return Request.CreateErrorResponse(HttpStatusCode.ExpectationFailed, "errorus");
}
}
and Route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional, action = RouteParameter.Optional }
Now. If I send request like this:
localhost:25856/api/sms/AddSMSCollection
it works
Is it possible to tune route so that I can use localhost:25856/api/sms and didn't get Multiple actions were found that match the request error??
sorry for my bad english..
You could define the action that should be executed in this case:
defaults: new { id = RouteParameter.Optional, action = "AddSMS" }
But with only the following url localhost:25856/api/sms and not including the action name, I hope you realize that the routing engine has no way of disambiguate which action to execute. The routing engine could use the HTTP verb but in your case both actions are POST.

Why is my message handler running even when it is not defined in WebAPI?

I have the following set-up in my config:
routes.MapHttpRoute("NoAuthRequiredApi", "api/auth/", new { id = RouteParameter.Optional } );
routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional }, null, new WebApiAuthenticationHandler());
If I post anything to the url on api/auth the message handler still runs and checks for an Auth-Token header. Is there any reason why this is happening? Is there something I should change in the configuration of the WebApi routes? I obviously don't want any auth token on the header when making requests to the auth controller because at that point I'm trying to retrieve the token for use on other controllers.
Your topmost route is never being matched as there is no indication of which controller is required. Add the controller name in as a default. (And remove the ID optional if this is not required).
So:
routes.MapHttpRoute(
name: "NoAuthRequiredApi",
routeTemplate: "api/auth/",
defaults: new { Controller = "Auth" }
);

Resources