Shouldn't these two approaches effectively achieve the same thing?
Approach #1 aka 'conventional routing'
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapControllerRoute(name: "api", pattern: "api/[controller]");
endpoints.MapRazorPages();
});
Now, disable/comment the line above that calls MapControllerRoute, then do the following in the controller, adding the attibute based routing at the top.
approach #2, aka 'attribute based routing'
[ApiController]
[Route("api/Box")]
public class BoxController : BaseController
{
[HttpGet("{customer}")]
public IActionResult Get(string customer, string size, string color)
{
//string customer = "test";
string retval = "test"; //...do stuff here
return Ok(retval);
}
}
With approach#1, the following route resolved
localhost:5511/box/abc?size=val1&color=val2
but not
localhost:5511/api/box/abc?size=val1&color=val2
using attribute-based routing, the route I needed was resolved
localhost:5511/api/box/abc?size=val1&color=val2
They are the same, there are a few details you missed
Replace the square brackets [] with curly ones {} in the route pattern, as this way the framework evaluates route patterns:
endpoints.MapControllerRoute(name: "api", pattern: "api/{controller}")
Then remove [HttpGet("{customer}")] from above the action
Add to route pattern the action:
endpoints.MapControllerRoute(name: "api", pattern: "api/{controller}/{action}")
Voila, now the endpoint looks just right!
Related
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") })
I'm pretty new to .NET Core. I'm trying to set up a little MVC application. Where i implemented a controller with a defined Route.
[Route("api/ota")]
public class OTAController : ControllerBase
{
[HttpPost]
public async Task<ContentResult> EndPoint([FromBody] object otaHotelRatePlanNotifRQ)
{
Console.WriteLine("Something is posted");
...
for this controller i implemented a custom inputformatter and registered it in the Startup.cs works fine so far.
services.AddMvc(options => {
options.InputFormatters.Insert(0, new RawRequestBodyInputFormatter());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
But now this inputformatter is applied for any controller and specified route.
Is there any way to apply the formatter just for a specified controller/route.
add your inputformatter as the first formatter InputFormatters.Insert(0,new StringRawRequestBodyFormatter()) then in this formatter in CanRead method check if the parameter that is being bound has a custom attribute you specify alongside FromBody
public override Boolean CanRead(InputFormatterContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var contentType = context.HttpContext.Request.ContentType;
if (supportedMimes.Contains(contentType) &&
context.Metadata is DefaultModelMetadata mt &&
mt.Attributes.ParameterAttributes.Any(a=>a.GetType().Equals(typeof(RawJsonAttribute))))
return true;
else
return false;
}
Controller action:public IActionResult BeginExportToFile([RawJson,FromBody] string expSvcConfigJsonStr)
So in simple terms this formatter will only be used for the supported mimes and for parameters that have a custom attribute.
Hope it helps.
Yes this can be in the Startup.cs by adding a new route in the config method, you should have something like this by default you need to add a new one for the controller that you want:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Note: The order matters.
For a multi tenant application in ASP.NET MVC 5, I have created a custom IRouteConstraint to check if a subdomain exists in the base url, like client1.myapplication.com or client2.application.com.
public class TenantRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string appSetting = ConfigurationManager.AppSettings[AppSettings.IsMultiTenancyEnabled];
bool isMultiTenancyEnabled = false;
bool isParsedCorrectly = bool.TryParse(appSetting, out isMultiTenancyEnabled);
if (isMultiTenancyEnabled)
{
string subdomain = httpContext.GetSubdomain();
if (subdomain != null && !values.ContainsKey("subdomain"))
{
values.Add("subdomain", subdomain);
}
return string.IsNullOrEmpty(subdomain) ? false : true;
}
else
{
return true;
}
}
}
Here is the route config setup:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreWindowsLoginRoute();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "Dime.Scheduler.WebUI.Controllers" },
constraints: new { TenantAccess = new TenantRouteConstraint() }
);
}
}
The route constraint works very well but I need to understand this process better. I want to know what happens exactly when this method below returns FALSE. The end result is a HTTP Error 403.14 - Forbidden page, but is there some way I can intercept this to present my own custom page? I usually capture these errors in the Global Asax file but in this case, it never gets there.
Could this have something to do with the fact that there won't be any routes that match the request? Is there any way to redirect to a custom page if no matches are found?
After any route constraint that is associated with the route returns false, the route is considered a non-match and the .NET routing framework will check the next route registered in the collection (the matching is performed in order from the first route down to the last route that is registered in the collection).
The end result is a HTTP Error 403.14 - Forbidden page, but is there some way I can intercept this to present my own custom page?
You can get more precise control over routing by inheriting RouteBase (or inheriting Route). There is a pretty good example of domain-based routing here.
The key to doing this is to make sure you implement both methods. GetRouteData is where you analyze the request to determine if it matches and return a dictionary of route values if it does (and null if it doesn't). GetVirtualPath is where you get a list of route values, and if they match you should (typically) return the same URL that was input in the GetRouteData method that matched. GetVirtualPath is called whenever you use ActionLink or RouteLink within MVC, so it is usually important that an implementation be provided.
You can determine the page that the route will direct to by simply returning the correct set of route values in GetRouteData.
I haven't used the ASP.NET WebAPI much although I have used routes in straight MVC. Obviously I'm doing something wrong, perhaps someone can help?
I have one controller called UsersController and two methods on it, Register and Details, both take two string parameters and return a string, both marked as HttpGet.
Originally I started with two route mappings in WebApiConfig.cs:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/{action}/{userId}/{key}"
);
config.Routes.MapHttpRoute(
name: "Test2Api",
routeTemplate: "api/{controller}/{action}/{userId}/{class}"
);
With this set up only the first route is found for a URL such as:
http://<domain>/api/users/register/a123/b456/
If I call:
http://<domain>/api/users/details/a123/b456/
I get a 404. If I swap the two routes around then I can call the Details method but not the Register method, again getting a 404. The workaround I have in place is to be more specific with the routes:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/register/{userId}/{key}"
);
config.Routes.MapHttpRoute(
name: "Test2Api",
routeTemplate: "api/{controller}/details/{userId}/{class}/"
);
The UsersController looks like:
namespace HelloWebAPI.Controllers
{
using System;
using System.Web.Http;
using HelloWebAPI.Models;
public class UsersController : ApiController
{
[HttpGet]
public string Register(string userId, string key)
{
return userId + "-" + key;
}
[HttpGet]
public string Enrolments(string userId, string #class)
{
return userId + "-" + #class
}
}
}
So what I don't understand is why the {action} component of the route registered second is not being associated with the correct method.
Thanks
Joe
The routing in ASP.NET Web API works in three steps:
Matching the URI to a route template.
Selecting a controller.
Selecting an action.
The framework selects the first route in the route table that matches the URI, there is no "second guessing" in case when 2nd or 3rd step fails - just 404. In your case both URI are always matching the first route so the second is never used.
For further analysis let's assume that the first route is:
api/{controller}/{action}/{userId}/{key}
And you call it with following URI:
http://<domain>/api/users/enrolments/a123/b456/
In order to choose action framework is checking three things:
The HTTP method of the request.
The {action} placeholder in the route template, if present.
The parameters of the actions on the controller.
In this case the {action} part will be resolved correctly to enrolments, but framework will be looking for Enrolments method with userId and key parameters. Your method has a class parameter which is no match. This will result in 404.
To avoid the issue you have to make more specific routes (like you did) or unify parameters names.
You only need to define the one route:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/{action}/{userId}/{key}"
);
Then change your controller methods to the following:
[HttpGet]
public string Register(string userId, string key)
{
return userId + "-" + key;
}
[HttpGet]
public string Details(string userId, string key)
{
return userId + "-" + key
}
I would like to create a simple route which will allow me to have ONLY one item listed after the base URL (other than when it's a controller), and for that item to be passed into a controller as a parameter. for example:
www.mydomain.com/user1
www.mydomain.com/user2
www.mydomain.com/user3
www.mydomain.com/user3
where user1, user2 etc are usernames and are being passed dynamically, ie i don't want to have to create a controller for each username.
naturally i would want to make sure if something like this is possible that it wont cause conflicts with other genuine controller names, thus i guess the other controllers would have to have rules created specifically for them and listed above the wildcard route
i'm not sure how to do this as i guess the first item after the slash is usually a controller.
any ideas how to tackle this?
the examples i provided may seem ambiguous, when i put www.mydomain.com/user1 etc it represents that it could be anything (ie a username),for example, www.mydomain.com/jsmith, www.mydomain.com/djohnson, www.mydomain.com/sblake, www.mydomain.com/fheeley
the idea is that a users profile can be looked up simply by entering the domain name then a fwd slash and the username.
ASP.Net MVC routes are process from the top down, and as soon as a match is found it won't look any further for a match. So put your most specific routes first, and your wildcard route last. If none of the specific routes match, control will be passed to the wildcard route.
Use a route definition such as
routes.MapRoute("{username}", new { controller = "User", action = "View"});
in your global.asax.cs file but put it last in your list of route definitions.
MVC processes your route mappings from top to bottom, so put your most general route mappings at the top, and your more specific ones at the bottom.
After you do this you will need the corresponding controller/action so create a new Controller named "UsersController" with an action named "View", like as follows:
using System.Web.Mvc;
namespace Routes.Controllers
{
public class UsersController : Controller
{
//
// GET: /{username}
public ActionResult List(string username)
{
// Get User from database/repository and pass to view
var user = ...;
return View(user);
}
}
}
You have to do following thing.
Create one controller or Action for Handle above scenario. For example controller="Home" and Action="GetUser"
public ActionResult GetUser(string username){
// do your work
}
In Global.asax ( Top route)
// Look at point 3 for UserNameConstraint
route.MapRoute("UserRoute" , "{username}" , new { controller="Home" , action="GetUser" } , new { username = new UserNameConstraint() });
// Put your default route after above here
Now for Create Route constraint.
Hope this help you.
public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> userName = GetUserName from DB
userName.Contain(values[parameterName].ToString());
}
}
Create a route like as follows,
routes.MapRoute(
"DefaultWithUser", // Route name
"{user}/{controller}/{action}/{id}", // URL with parameters
new { user=UrlParameter.Optional, controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);