In .Net MVC, you define routes into a RouteCollection. The URL helper methods make it easy to turn a controller + action + optional params into a URL.
When .Net MVC processes a request from a client browser, it clearly maps this URL to the right controller + action, to execute the appropriate command.
However, I can't see a way to programatically access this routing on the fly, such that I can turn a fully qualified URL (or a list of 10k+ URLs) into it's route components.
Does anyone know how you'd turn, for example, the following string input:
"http://stackoverflow.com/questions/2342325/c-sharp-net-mvc-turning-url-into-controller-action-pair"
into the following output:
{
controller: "questions",
action: "view",
id: 2342325,
seoText: "c-sharp-net-mvc-turning-url-into-controller-action-pair"
}
Given this mapping is clearly being done by .Net, is it exposed anywhere?
Why would anyone want to do this?
Imagine you have a list of URLs you know have been accessed, mostly dynamic in nature, for example stackoverflow.com/questions/2342325/c-sharp-net-mvc-turning-url-into-controller-action-pair, and you want to work out which actual endpoints / actions / controllers are being hit programatically (without much care about the actual data being passed).
You could hand code mappings, such that you know /questions/{id}/{text} -> controller: questions, action: question, but that's not future-proof, nor is it fun, and relies on text manipulation / processing.
Given a Route Dictionary and a list of URLs, with a function as described above, you could look at which controllers are most hit, or which actions, etc.
You should take a look at creating your own MvcRouteHandler. This is the point in the MVC stack where the Route Engine has already parsed the URL to find which Controller and Action to call, and then it goes through this method to get the actual C# class and method to invoke. No authorization or even HTTP Verb has been applied yet, so you will see every call that is made to your application.
public class CustomRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext context)
{
var controller = context.RouteData.Values["controller"];
var action = context.RouteData.Values["action"];
// Do whatever logging you want with this data, maybe grab the other params too.
return base.GetHttpHandler(context);
}
}
This can easily be registered where you set up your Routing.
routes.MapRoute("Home", "{controller}/{action}", new
{
controller = "Home",
action = "Index"
})
.RouteHandler = new CustomRouteHandler();
Looks like the only way to do this is by creating a dummy HTTP Context, similar to how you would unit test routes. It's a shame MVC doesn't provide better access to this, given it's being run on every request, rather than wrapping it up inside the context object.
Anyway, here is a working solution which can be modified to suit your needs:
public class UrlToRouteMapper
{
public static RouteValueDictionary GetRouteDataFromURL(string absoluteURL)
{
var testUrl = "~" + new Uri(absoluteURL).AbsolutePath;
var context = new StubHttpContextForRouting(requestUrl: testUrl);
var routes = new System.Web.Routing.RouteCollection();
MvcApplication.RegisterRoutes(routes);
System.Web.Routing.RouteData routeData = routes.GetRouteData(context);
return routeData.Values;
}
public static string GetEndpointStringFromURL(string absoluteURL)
{
var routeData = GetRouteDataFromURL(absoluteURL);
return routeData["controller"] + "/" + routeData["action"];
}
}
public class StubHttpContextForRouting : HttpContextBase {
StubHttpRequestForRouting _request;
StubHttpResponseForRouting _response;
public StubHttpContextForRouting(string appPath = "/", string requestUrl = "~/") {
_request = new StubHttpRequestForRouting(appPath, requestUrl);
_response = new StubHttpResponseForRouting();
}
public override HttpRequestBase Request {
get { return _request; }
}
public override HttpResponseBase Response {
get { return _response; }
}
}
public class StubHttpRequestForRouting : HttpRequestBase {
string _appPath;
string _requestUrl;
public StubHttpRequestForRouting(string appPath, string requestUrl) {
_appPath = appPath;
_requestUrl = requestUrl;
}
public override string ApplicationPath {
get { return _appPath; }
}
public override string AppRelativeCurrentExecutionFilePath {
get { return _requestUrl; }
}
public override string PathInfo {
get { return ""; }
}
}
public class StubHttpResponseForRouting : HttpResponseBase {
public override string ApplyAppPathModifier(string virtualPath) {
return virtualPath;
}
}
Related
I would like to create URL address based on some specific conditions. For now I have simple code in some controller's action:
string url ="";
if(some conditions based on data fetched from DB)
{
url = Url.Action("action","controller");
}
else{
url = some other url;
}
The problem is that this kind of logic will be used in a few other places. Is it posible to move it to some other class and still use the MVC Url.Action helper? Or there is another simple way to solve this problem?
You can create a custom action filter for this.the same can be used for all actions in a controller or for only particular actions in a controller.
Inside the filter
public class GenerateUrlAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
GenerateUrl(filterContext);
}
private void GenerateUrl(ActionExecutingContext filterContext)
{
//your logic
}
}
Add this attribute above the action methods or for an entire controller.
[GenerateUrl]
This attribute will be called before the action is executed.
You can write own extension to make it available globally.
public static string NewAction(this IUrlHelper helper, string action, string controller)
{
if (some conditions)
{
//Manipulate however you like
return helper.Action(action, controller, values: null, protocol: null, host: null, fragment: null);
}
else
{
//This is default action
return helper.Action(action, controller, values: null, protocol: null, host: null, fragment: null);
}
}
Usage
Url.NewAction("action","controller");
Don't forget to create extension in static class.
You can redirect in an action filter instead of in your action.https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/understanding-action-filters-cs
An action filter is an attribute that you can apply to a controller action -- or an entire controller -- that modifies the way in which the action is executed.
public class RedirectUrlAction
{
private int value;
public string GetActionUrl(string action, string controller, string DefaultRediretUrl, Func<int,Boolean> Condition, UrlHelper urlHrlper)
{
string url = "";
if (Condition(value) == true)
{
urlHrlper.Action(action, controller);
}
else
{
url = DefaultRediretUrl;
}
return url;
}
}
calling the function
RedirectUrlAction act = new RedirectUrlAction();
act.GetActionUrl("value", "get", "http//:www.google.com", (x) => x % 2 == 0, Url);
Disclaimer: First of all, I want to mention that I looked all over the internet for an answer, read all the documentation, read all the question I could possibly find here, but no luck so far.
So, here's my case. I am building an API using ASP.NET Core 2.2 and I'm using HATEOAS (HAL specification and Halcyon library). I should provide links along with the resource itself. This what drove me towards HATEOAS in the first place. Some of the links are templated, since it might be a PUT method and id is to be specified by frontend.
The issue is, that my controllers can have very different routes (using attribute-based routing) and hardcoding links is a bad thing,cause if the route changes I need to remember to change the link where it's used as well. For this reason I decided to generate link based on Controller type and Action name. LinkGenerator is what I found, but it seems it returns null, if I don't specify all the parameters for the route. Here's a code example:
[Route("api/metadata")]
[ApiController]
public class MetadataController : ControllerBase
{
private readonly IMetadataProvider _metadataProvider;
private readonly LinkGenerator _linkGenerator;
public MetadataController(
IMetadataProvider metadataProvider,
LinkGenerator linkProvider)
{
_metadataProvider = metadataProvider;
_linkGenerator = linkProvider;
}
[HttpGet]
public IActionResult GetMetadata()
{
var metadata = _metadataProvider.GetMetadata();
// here url will be 'null', because last parameter is null
// and route requires parameter 'name' to be specified instead of 'null'
// EXPECTED: "api/metadata/{name}"
// ACTUAL: null
string url = _linkGenerator.GetPathByAction(
nameof(MetadataController.GetByName),
nameof(MetadataController).Replace(nameof(Controller), string.Empty),
null);
var response = new HALResponse(metadata)
.AddSelfLink(HttpContext.Request)
.AddLinks(new Link(name, url));
return Ok(response);
}
[HttpGet("{name}")]
public IActionResult GetByName(string name)
{
var metadata = _metadataProvider.GetMetadataForEntity(name);
return Ok(metadata);
}
}
How can I generate a link, so that it's not hardcoded and it is templated?
After a couple of hours of debugging the ASP.NET source code, I think I found a way to do this.
It seems, that LinkGenerator is intended to build a complete and valid url, so all the parameters are required. What I was looking for was actually a route pattern.
While debugging, I found a IEndpointAddressScheme<RouteValuesAddress> service injected into LinkGnerator. It is used to actually find the route patern. After that, LinkGenerator tries to fill all the parameters.
Here's the above code fixed and working:
[ApiController]
public class MetadataController : ControllerBase
{
private readonly IMetadataProvider _metadataProvider;
private readonly IEndpointAddressScheme<RouteValuesAddress> _endpointAddress;
public MetadataController(
IMetadataProvider metadataProvider,
IEndpointAddressScheme<RouteValuesAddress> endpointAddress)
{
_metadataProvider = metadataProvider;
_endpointAddress = endpointAddress;
}
[HttpGet]
public IActionResult GetMetadata()
{
var metadata = _metadataProvider.GetMetadata();
// EXPECTED: "api/metadata/{name}"
// ACTUAL: "api/metadata/{name}"
string actionName = nameof(MetadataController.GetById);
string controllerName = nameof(MetadataController).Replace(nameof(Controller), string.Empty);
var url = _endpointAddress.FindEndpoints(CreateAddress(actionName, controllerName))
.OfType<RouteEndpoint>()
.Select(x => x.RoutePattern)
.FirstOrDefault();;
var response = new HALResponse(metadata)
.AddSelfLink(HttpContext.Request)
.AddLinks(new Link(name, url));
return Ok(response);
}
[HttpGet("{name}")]
public IActionResult GetByName(string name)
{
var metadata = _metadataProvider.GetMetadataForEntity(name);
return Ok(metadata);
}
private static RouteValuesAddress CreateAddress(string action, string controller)
{
var explicitValues = new RouteValueDictionary(null);
var ambientValues = GetAmbientValues(httpContext);
explicitValues ["action"] = action;
explicitValues ["controller"] = controller;
return new RouteValuesAddress()
{
AmbientValues = ambientValues,
ExplicitValues = explicitValues
};
}
}
I have a controller in my web api. Let's call it TimeController.
I have a GET action and a PUT action. They look like this:
public class TimeController : ApiController
{
[HttpGet]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK, DateTime.UtcNow);
}
[HttpPut]
public HttpResponseMessage Put(int id)
{
_service.Update(id);
return Request.CreateResponse(HttpStatusCode.OK);
}
}
I also have a route config as follows:
routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional });
so I can access it in a restful manner.
Now I also want to version the GET action using a custom Route attribute. I'm using code very similar to what Richard Tasker talks about in this blog post.
(the difference being that I use a regular expression to get the version from the accept header. Everything else is pretty much the same)
So my controller now looks like this:
public class TimeController : ApiController
{
private IService _service;
public TimeController(IService service)
{
_service = service;
}
[HttpGet, RouteVersion("Time", 1)]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.Ok, DateTime.UtcNow);
}
[HttpGet, RouteVersion("Time", 2)]
public HttpResponseMessage GetV2()
{
return Request.CreateResponse(HttpStatusCode.Ok, DateTime.UtcNow.AddDays(1));
}
[HttpPut]
public HttpResponseMessage Put(int id)
{
_service.Update(id);
return Request.CreateResponse(HttpStatusCode.OK);
}
}
However, now when I try to access the PUT endpoint I'm getting a 404 response from the server. If I step through the code in debug mode, I can see that the RouteVersion attribute is being fired, even though I haven't decorated the action with it.
If I add the attribute to the PUT action with a version of 1, or I add the built in Route attribute like this: Route("Time") then it works.
So my question is: why is the attribute firing even though I haven't decorated the action with it?
Edit: Here is the code for the attribute:
public class RouteVersion : RouteFactoryAttribute
{
private readonly int _allowedVersion;
public RouteVersion(string template, int allowedVersion) : base(template)
{
_allowedVersion = allowedVersion;
}
public override IDictionary<string, object> Constraints
{
get
{
return new HttpRouteValueDictionary
{
{"version", new VersionConstraint(_allowedVersion)}
};
}
}
}
public class VersionConstraint : IHttpRouteConstraint
{
private const int DefaultVersion = 1;
private readonly int _allowedVersion;
public VersionConstraint(int allowedVersion)
{
_allowedVersion = allowedVersion;
}
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
if (routeDirection != HttpRouteDirection.UriResolution)
{
return true;
}
int version = GetVersionFromHeader(request) ?? DefaultVersion;
return (version == _allowedVersion);
}
private int? GetVersionFromHeader(HttpRequestMessage request)
{
System.Net.Http.Headers.HttpHeaderValueCollection<System.Net.Http.Headers.MediaTypeWithQualityHeaderValue> acceptHeader = request.Headers.Accept;
var regularExpression = new Regex(#"application\/vnd\.\.v([0-9]+)",
RegexOptions.IgnoreCase);
foreach (var mime in acceptHeader)
{
Match match = regularExpression.Match(mime.MediaType);
if (match.Success)
{
return Convert.ToInt32(match.Groups[1].Value);
}
}
return null;
}
}
Edit2: I think there is some confusion so I've updated the Put action to match the route config
So my question is: why is the attribute firing even though I haven't decorated the action with it?
It is clear from both the way your question is phrased "when I try to access the PUT endpoint" and the fact that it matches the GET action (and then subsequently runs its constraint) that you have not issued a PUT request to the server. Most browsers are not capable of issuing a PUT request, you need a piece of code or script to do that.
Example
using (var client = new System.Net.WebClient())
{
// The byte array is the data you are posting to the server
client.UploadData(#"http://example.com/time/123", "PUT", new byte[0]);
}
Reference: How to make a HTTP PUT request?
I think its because of your action signature in combination with the default route
In your default route you specify the Id attribute as optional, however in your action you use the parameter days, in this case the framework can't resolve it. you either have to add it as a query string parameter eg:
?days={days}
Or change the signature to accept id as input.
Since it can't resove the action with days in the url it will return a 404
Personally i don't use the default routes and always use Attribute routing to prevent this kinda behavior
So my question is: why is the attribute firing even though I haven't decorated the action with it?
Any controller methods that do not have a route attribute use convention-based routing. That way, you can combine both types of routing in the same project.
Please see this link :
attribute-routing-in-web-api-2
Also as method is not decorated with route attribute, When the Web API framework receives an HTTP request, it tries to match the URI against one of the route templates in the routing table. If no route matches, the client receives a 404 error. That is why you are getting 404
Please see this one as well : Routing in ASP.NET Web API
I'm using a custom filter to validate the content type, like:
public override void OnActionExecuting(HttpActionContext httpActionContext)
{
List<String> errors = new List<String>();
// a
if (httpActionContext.Request.Content.Headers.ContentType.MediaType == "application/json")
{
}
else
{
errors.Add("Invalid content type.");
}
// more checks
}
The above code is working fine, but the validation should check the request http verb, because it should validate the content type only for put or post. I don't want to remove the custom filter from httpget actions because I have more checks inside it, and I don't want to split the filter in two parts, meaning I have to check the http verb inside the filter, but I can't find how.
Any tips?
You can get the method type (post or put) from this:
public override void OnActionExecuting(HttpActionContext actionContext)
{
string methodType = actionContext.Request.Method.Method;
if (methodType.ToUpper().Equals("POST")
|| methodType.ToUpper().Equals("PUT"))
{
// Your errors
}
}
If you need to get the HTTP Method of the request being validated by the filter, you can inspect the Method property of the request:
var method = actionContext.Request.Method;
I would recommend however that you break the filter apart, as you are quickly headed towards a big ball of mud scenario.
You really should be using the standard HTTPVerb attributes above your controller methods:
[HttpGet]
[HttpPut]
[HttpPost]
[HttpDelete]
[HttpPatch]
MVC Controllers for multiple:
[AcceptVerbs(HttpVerbs.Get, HttpVerbs.Post)]
WebAPI Controlelrs for multiple
[AcceptVerbsAttribute("GET", "POST")]
In the constructor of the action filter, you can pass in options/named parameters that will set the settings for the OnActionExecuting logic. Based on those settings you can switch up your logic.
public class MyActionFilterAttribute : ActionFilterAttribute
{
private HttpVerbs mOnVerbs;
public MyActionFilterAttribute(HttpVerbs onVerbs)
{
mOnVerbs = onVerbs;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var currentVerb = filterContext.HttpContext.Request.HttpMethod;
if (mOnVerbs.HasFlag(HttpVerbs.Post)) { }
else if (mOnVerbs.HasFlag(HttpVerbs.Get)) { }
base.OnActionExecuting(filterContext);
}
}
[MyActionFilter(HttpVerbs.Get | HttpVerbs.Post)]
public ActionResult Index()
{
}
I'm working on an ASP.NET MVC solution that has a number of different menus. The menu to display depends on the role of the currently logged in user.
In MVC 3 I had some custom code to support this scenario, by having a single controller method that would return the right menu. It would do this by deferring the request to the appropriate controller and action depending on the current user.
This code appears to be broken in MVC 4 and I'm looking for help to fix it.
First, I added a TransferResult helper class to perform the redirection:
public class TransferResult : RedirectResult
{
#region Transfer to URL
public TransferResult( string url ) : base( url )
{
}
#endregion
#region Transfer using RouteValues
public TransferResult( object routeValues ) : base( GetRouteUrl( routeValues ) )
{
}
private static string GetRouteUrl( object routeValues )
{
var url = new UrlHelper( new RequestContext( new HttpContextWrapper( HttpContext.Current ), new RouteData() ), RouteTable.Routes );
return url.RouteUrl( routeValues );
}
#endregion
#region Transfer using ActionResult (T4MVC only)
public TransferResult( ActionResult result ) : base( GetRouteUrl( result.GetT4MVCResult() ) )
{
}
private static string GetRouteUrl( IT4MVCActionResult result )
{
var url = new UrlHelper( new RequestContext( new HttpContextWrapper( HttpContext.Current ), new RouteData() ), RouteTable.Routes );
return url.RouteUrl( result.RouteValueDictionary );
}
#endregion
public override void ExecuteResult( ControllerContext context )
{
HttpContext httpContext = HttpContext.Current;
httpContext.RewritePath( Url, false );
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest( HttpContext.Current );
}
}
Second, I modified T4MVC to emit a few controller helper methods, resulting in every controller having this method:
protected TransferResult Transfer( ActionResult result )
{
return new TransferResult( result );
}
This allowed me to have a shared controller action to return a menu, without having to clutter the views with any conditional logic:
public virtual ActionResult Menu()
{
if( Principal.IsInRole( Roles.Administrator ) )
return Transfer( MVC.Admin.Actions.Menu() );
return View( MVC.Home.Views.Partials.Menu );
}
However, the code in ExecuteResult in the TransferResult class does not seem to work with the current preview release of MVC 4. It gives me the following error (pointing to the "httpHandler.ProcessRequest" line):
'HttpContext.SetSessionStateBehavior' can only be invoked before
'HttpApplication.AcquireRequestState' event is raised.
Any idea how to fix this?
PS: I realize that I could achieve the same using a simple HtmlHelper extension, which is what I'm currently using as a workaround. However, I have many other scenarios where this method has allowed me to mix and reuse actions, and I would hate to give up this flexibility when moving to MVC 4.
Sometimes I think "MVC" should be called "RCMV" for "Router Controller Model View" since that is really the order that things happen. Also, since it is just "MVC", people always tend to forget about routing. The great thing about MVC is that routing configurable and extensible. I believe what you are trying to do could be solved with a custom route handler.
I haven't tested this, but you should be able to do something like this:
routes.Add(
new Route(
"{controller}/{action}/{id}",
new RouteValueDictionary(new { controller = "Home", action = "Menu" }),
new MyRouteHandler(Roles.Administrator, new { controller = "Admin" })));
Then your route handler would look like this:
public class MyRouteHandler : IRouteHandler
{
public string Role { get; set; }
public object RouteValues { get; set; }
public MyRouteHandler(string role, object routeValues)
{
Role = role;
RouteValues = routeValues;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MyHttpHandler(Role, RouteValues);
}
}
And finally handle the re-routing in your HttpHandler:
public class MyHttpHandler : IHttpHandler
{
public string Role { get; set; }
public object RouteValues { get; set; }
public MyHttpHandler(string role, object routeValues)
{
Role = role;
RouteValues = routeValues;
}
public void ProcessRequest(HttpContext httpContext)
{
if (httpContext.User.IsInRole(Role))
{
RouteValueDictionary routeValues = new RouteValueDictionary(RouteValues);
// put logic here to create path similar to what you were doing
// before but you will need to replace any keys in your route
// with the values from the dictionary created above.
httpContext.RewritePath(path);
}
IHttpHandler handler = new MvcHttpHandler();
handler.ProcessRequest(httpContext);
}
}
That may not be 100% correct, but it should get you in the right direction in a way that shouldn't run into anything deprecated in MVC4.
I think a TransferResult should be included in the framework without each developer wrestling with having to reimplement it for different versions when it becomes broken.
(as in this thread and for example the following thread too: Implementing TransferResult in MVC 3 RC - does not work ).
If you agree with me, I would just like to encourage you to vote for "Server.Transfer" to become included in the MVC framework itself:
http://aspnetwebstack.codeplex.com/workitem/798