I'm developing an application and I'm securing it using Fluent Security.
I have 2 different areas within the web application, namely an Admin area and a Members area.
I implemented my Policies Violation Handlers following the guidelines in the fluent security website.
Everything works fine except that when an exception is thrown I need to redirect to an error page. These are different depending on which area you are. For instance, the ForbiddenAccess error page from the Admin area is different than the one from the Members Area.
My Implementation of IPolicyViolationHandler is the following.
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
return
new RedirectToRouteResult(
new RouteValueDictionary(new { action = "AnonymousError", controller = "Error", area = "Admin" }));
}
}
I need to get where the error occurs so that when I redirect my area is either "
Admin" or "Members" and I can't figure out how to get that information from the exception. If I can get that information, redirecting to the appropriate page is trivial.
Any help, suggestions, or if not possible, a workaround is appreciated.
Thanks,
It is currently not possible to extract that information from FluentSecurity but it should be really easy to do anyway.
Below are two extensionmethods that should help you get the current area name.
public static string GetAreaName(this RouteData routeData)
{
object value;
if (routeData.DataTokens.TryGetValue("area", out value))
{
return (value as string);
}
return GetAreaName(routeData.Route);
}
public static string GetAreaName(this RouteBase route)
{
var areRoute = route as IRouteWithArea;
if (areRoute != null)
{
return areRoute.Area;
}
var standardRoute = route as Route;
if ((standardRoute != null) && (standardRoute.DataTokens != null))
{
return (standardRoute.DataTokens["area"] as string) ?? string.Empty;
}
return string.Empty;
}
An here's how you would use it:
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(HttpContext.Current));
var areaName = routeData.GetAreaName();
return
new RedirectToRouteResult(
new RouteValueDictionary(new { action = "AnonymousError", controller = "Error", area = areaName }));
}
}
Feel free to add a feature request for it.
Related
I have created a Login system that provides the user with an API-key on successful login. The API key has an access level attached to it.
I am sending the API key in the header with every Ajax request from my front end, however it is no longer sending a correct response.
This is one of my controllers:
[AllowCrossSiteJson]
public class WebOrdersController : Controller
{
protected IWebOrdersService _webOrdersService = new WebOrdersService();
// GET: WebOrders
public ActionResult Index()
{
return View();
}
[WebOrdersAPIFilter]
public String GetTotals()
{
return _webOrdersService.GetAllTotals();
}
}
And this is my Filter class:
public class WebOrdersAPIFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
IAccountsService _accountsService = new AccountsService();
filterContext.RequestContext.HttpContext.Response.AddHeader("Authority", "*");
var rawUrl = HttpContext.Current.Request.RawUrl;
var routeArray = rawUrl.Split('/');
var route = routeArray[0].ToLower();
if (String.IsNullOrEmpty(route))
{
route = routeArray[1].ToLower();
}
var headers = filterContext.HttpContext.Request.Headers;
// Ensure that all of your properties are present in the current Request
if (!String.IsNullOrEmpty(headers["Authority"]))
{
var apiKey = headers["Authority"];
//GET API LEVEL
int accessLevel = _accountsService.CheckAPIKey(apiKey);
if(route == "weborders" && (accessLevel != 1 || accessLevel != 3 || accessLevel != 6))
{
filterContext.Result = new RedirectResult("http://localhost/error/unauthorised");
}
}
else
{
filterContext.Result = new RedirectResult("http://localhost/error/notloggedin");
}
base.OnActionExecuting(filterContext);
}
}
If the user is not logged in, the API Key is not sent in the header, so if it does not exist it is safe to say they are logged out. If they are logged in, The key is sent. I am then getting the access level of they key and comparing it to the route.
I have debugged it and found that the route and accessLevel variables are working correctly however even then it does not return and response and if the IF() statement fails, it is returning the html for the page /error/unauthorised or /error/notloggedin and not actually redirecting.
How can I achieve the functionality i require, to ensure that the user requesting the data is authorised?
I have a MVC 5 Web API which returns a custom response in case of unexpected exceptions or if the controller or action were not found. Essentially, I've done exactly as shown there: http://weblogs.asp.net/imranbaloch/handling-http-404-error-in-asp-net-web-api Everything's working like a charm.
The problem is: I'd like to submit the error code from SelectController() and SelectAction() to my ErrorController. This way I would not have duplicate code and all the logic would be in the controller.
Unfortunately, I do not find any possible way to submit the error code to my controller. All the examples are redirecting to a specific error action (e.g. ErrorController.NotFound404) I'd like to redirect to ErrorController.Main and do all the magic there.
Another issue with the custom ApiControllerActionSelector is that the Request property is null in the ErrorController. This problem does not exist with the custom DefaultHttpControllerSelector.
Any ideas?
Best regards,
Carsten
Fortunately, I was able to find the solution myself. Let me show you how I got it up and running.
The custom controller and action selector are forwarding the requested language and the current HTTP response code:
public class CustomDefaultHttpControllerSelector: DefaultHttpControllerSelector
{
public CustomDefaultHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
{
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor descriptor = null;
try
{
descriptor = base.SelectController(request);
}
catch (HttpResponseException e)
{
var routeValues = request.GetRouteData().Values;
routeValues.Clear();
routeValues["controller"] = "Error";
routeValues["action"] = "Main";
routeValues["code"] = e.Response.StatusCode;
routeValues["language"] = request.Headers?.AcceptLanguage?.FirstOrDefault()?.Value ?? "en";
descriptor = base.SelectController(request);
}
return descriptor;
}
}
public class CustomControllerActionSelector: ApiControllerActionSelector
{
public CustomControllerActionSelector()
{
}
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
HttpActionDescriptor descriptor = null;
try
{
descriptor = base.SelectAction(controllerContext);
}
catch (HttpResponseException e)
{
var routeData = controllerContext.RouteData;
routeData.Values.Clear();
routeData.Values["action"] = "Main";
routeData.Values["code"] = e.Response.StatusCode;
routeData.Values["language"] = controllerContext.Request?.Headers?.AcceptLanguage?.FirstOrDefault()?.Value ?? "en";
IHttpController httpController = new ErrorController();
controllerContext.Controller = httpController;
controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "Error", httpController.GetType());
descriptor = base.SelectAction(controllerContext);
}
return descriptor;
}
}
Two important changes:
1.1. The list of route values needs to be cleared. Otherwise it tries to find an action in the ErrorController which maps to this list of values.
1.2. The code and language were added.
The ErrorController itself:
[RoutePrefix("error")]
public class ErrorController: BaseController
{
[HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH")]
[Route("{code}/{language}")]
public HttpResponseMessage Main(string code, string language)
{
HttpStatusCode parsedCode;
var responseMessage = new HttpResponseMessage();
if (!Enum.TryParse(code, true, out parsedCode))
{
parsedCode = HttpStatusCode.InternalServerError;
}
responseMessage.StatusCode = parsedCode;
...
}
}
I've removed the route mapping routes.MapHttpRoute(...). No matter what I've entered in the browser, it never called Handle404.
HTTP status 400 (bad request) was not covered, yet. This could be easily achieved by using the ValidationModelAttribute as described on http://www.asp.net/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api (section "Handling Validation Errors").
Maybe this will help someone...
I am trying to add various Web API plugins to my MVC website. Copying and pasting DLLs of API files allow me to call appropriate service methods using the default route - hostname/api/{controller}/{id}, but it throws an error, when I have controllers with the same name but in different locations (DLLs, namespaces). The error message is something like this (which is normal):
Multiple types were found that match the controller named 'Names'. This can happen if the route that services this request ('api/{controller}/{id}') found multiple controllers defined with the same name but differing namespaces, which is not supported. The request for 'Names' has found the following matching controllers: ApiExt.NamesController ApiExt1.NamesController
I have the same "Names" controller in different DLLs (namespaces) ApiExt, ApiExt1.
I already found a similar topic to select controller depending of API version - http://shazwazza.com/post/multiple-webapi-controllers-with-the-same-name-but-different-namespaces/, but this is not quite what I need. I need to select the controller (namespace) depending on route value, something like this:
hostname/api/{namespace}/{controller}/{id}
I believe this is absolutely possible, but I'm not familiar with overriding MVC controller selector (implementing IHttpControllerSelector).
Any suggestions?
Thank you.
You can definitely achieve this. You need to write your own Controller Selector by implementing IHttpControllerSelector. Please refer to this link for detailed, step-by-step explanation.
The blog post describing a solution, https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/, doesnt contain the complete code and has a bad link to what used to provide it.
Here's a blob providing the class, original from Umbraco
https://github.com/WebApiContrib/WebAPIContrib/blob/master/src/WebApiContrib/Selectors/NamespaceHttpControllerSelector.cs,
Full listing in case it's removed:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
namespace WebApiContrib.Selectors
{
//originally created for Umbraco https://github.com/umbraco/Umbraco-CMS/blob/7.2.0/src/Umbraco.Web/WebApi/NamespaceHttpControllerSelector.cs
//adapted from there, does not recreate HttpControllerDescriptors, instead caches them
public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
{
private const string ControllerKey = "controller";
private readonly HttpConfiguration _configuration;
private readonly Lazy<HashSet<NamespacedHttpControllerMetadata>> _duplicateControllerTypes;
public NamespaceHttpControllerSelector(HttpConfiguration configuration)
: base(configuration)
{
_configuration = configuration;
_duplicateControllerTypes = new Lazy<HashSet<NamespacedHttpControllerMetadata>>(InitializeNamespacedHttpControllerMetadata);
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var routeData = request.GetRouteData();
if (routeData == null || routeData.Route == null || routeData.Route.DataTokens["Namespaces"] == null)
return base.SelectController(request);
// Look up controller in route data
object controllerName;
routeData.Values.TryGetValue(ControllerKey, out controllerName);
var controllerNameAsString = controllerName as string;
if (controllerNameAsString == null)
return base.SelectController(request);
//get the currently cached default controllers - this will not contain duplicate controllers found so if
// this controller is found in the underlying cache we don't need to do anything
var map = base.GetControllerMapping();
if (map.ContainsKey(controllerNameAsString))
return base.SelectController(request);
//the cache does not contain this controller because it's most likely a duplicate,
// so we need to sort this out ourselves and we can only do that if the namespace token
// is formatted correctly.
var namespaces = routeData.Route.DataTokens["Namespaces"] as IEnumerable<string>;
if (namespaces == null)
return base.SelectController(request);
//see if this is in our cache
var found = _duplicateControllerTypes.Value.FirstOrDefault(x => string.Equals(x.ControllerName, controllerNameAsString, StringComparison.OrdinalIgnoreCase) && namespaces.Contains(x.ControllerNamespace));
if (found == null)
return base.SelectController(request);
return found.Descriptor;
}
private HashSet<NamespacedHttpControllerMetadata> InitializeNamespacedHttpControllerMetadata()
{
var assembliesResolver = _configuration.Services.GetAssembliesResolver();
var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
var groupedByName = controllerTypes.GroupBy(
t => t.Name.Substring(0, t.Name.Length - ControllerSuffix.Length),
StringComparer.OrdinalIgnoreCase).Where(x => x.Count() > 1);
var duplicateControllers = groupedByName.ToDictionary(
g => g.Key,
g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase);
var result = new HashSet<NamespacedHttpControllerMetadata>();
foreach (var controllerTypeGroup in duplicateControllers)
{
foreach (var controllerType in controllerTypeGroup.Value.SelectMany(controllerTypesGrouping => controllerTypesGrouping))
{
result.Add(new NamespacedHttpControllerMetadata(controllerTypeGroup.Key, controllerType.Namespace,
new HttpControllerDescriptor(_configuration, controllerTypeGroup.Key, controllerType)));
}
}
return result;
}
private class NamespacedHttpControllerMetadata
{
private readonly string _controllerName;
private readonly string _controllerNamespace;
private readonly HttpControllerDescriptor _descriptor;
public NamespacedHttpControllerMetadata(string controllerName, string controllerNamespace, HttpControllerDescriptor descriptor)
{
_controllerName = controllerName;
_controllerNamespace = controllerNamespace;
_descriptor = descriptor;
}
public string ControllerName
{
get { return _controllerName; }
}
public string ControllerNamespace
{
get { return _controllerNamespace; }
}
public HttpControllerDescriptor Descriptor
{
get { return _descriptor; }
}
}
}
}
Then just add the Namespaces token to the route
route.DataTokens["Namespaces"] = new string[] {"Foo.Controllers"};
It's also more production ready with the caching.
I've configured my ASP.NET MVC5 application to use AttributeRouting for WebApi:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
}
}
I have an ApiController as follows:
[RoutePrefix("api/v1/subjects")]
public class SubjectsController : ApiController
{
[Route("search")]
[HttpPost]
public SearchResultsViewModel Search(SearchCriteriaViewModel criteria)
{
//...
}
}
I would like to generate a URL to my WebApi controller action without having to specify an explicit route name.
According to this page on CodePlex, all MVC routes have a distinct name, even if it is not specified.
In the absence of a specified route name, Web API will generate a
default route name. If there is only one attribute route for the
action name on a particular controller, the route name will take the
form "ControllerName.ActionName". If there are multiple attributes
with the same action name on that controller, a suffix gets added to
differentiate between the routes: "Customer.Get1", "Customer.Get2".
On ASP.NET, it doesn't say exactly what is the default naming convention, but it does indicate that every route has a name.
In Web API, every route has a name. Route names are useful for
generating links, so that you can include a link in an HTTP response.
Based on these resources, and an answer by StackOverflow user Karhgath, I was led to believe that the following would produce a URL to my WebApi route:
#(Url.RouteUrl("Subjects.Search"))
However, this produces an error:
A route named 'Subjects.Search' could not be found in the route
collection.
I've tried a few other variants based on other answers I found on StackOverflow, none with success.
#(Url.Action("Search", "Subjects", new { httproute = "" }))
#(Url.HttpRouteUrl("Search.Subjects", new {}))
In fact, even providing a Route name in the attribute only seems to work with:
#(Url.HttpRouteUrl("Search.Subjects", new {}))
Where "Search.Subjects" is specified as the route name in the Route attribute.
I don't want to be forced to specify a unique name for my routes.
How can I generate a URL to my WebApi controller action without having to explicitly specify a route name in the Route attribute?
Is it possible that the default route naming scheme has changed or is documented incorrectly at CodePlex?
Does anyone have some insight on the proper way to retrieve a URL for a route that has been setup with AttributeRouting?
Using a work around to find the route via inspection of Web Api's IApiExplorer along with strongly typed expressions I was able to generate a WebApi2 URL without specifying a Name on the Route attribute with attribute routing.
I've created a helper extension which allows me to have strongly typed expressions with UrlHelper in MVC razor. This works very well for resolving URIs for my MVC Controllers from with in views.
Home
<li>#(Html.ActionLink<AccountController>("Sign in", c => c.Signin(null)))</li>
<li>#(Html.ActionLink<AccountController>("Create an account", c => c.Signup(), htmlAttributes: null))</li>
#using (Html.BeginForm<ToolsController>(c => c.Track(null), FormMethod.Get, htmlAttributes: new { #class = "navbar-form", role = "search" })) {...}
I now have a view where I am trying to use knockout to post some data to my web api and need to be able to do something like this
var targetUrl = '#(Url.HttpRouteUrl<TestsApiController>(c => c.TestAction(null)))';
so that I don't have to hard code my urls (Magic strings)
My current implementation of my extension method for getting the web API url is defined in the following class.
public static class GenericUrlActionHelper {
/// <summary>
/// Generates a fully qualified URL to an action method
/// </summary>
public static string Action<TController>(this UrlHelper urlHelper, Expression<Action<TController>> action)
where TController : Controller {
RouteValueDictionary rvd = InternalExpressionHelper.GetRouteValues(action);
return urlHelper.Action(null, null, rvd);
}
public const string HttpAttributeRouteWebApiKey = "__RouteName";
public static string HttpRouteUrl<TController>(this UrlHelper urlHelper, Expression<Action<TController>> expression)
where TController : System.Web.Http.Controllers.IHttpController {
var routeValues = expression.GetRouteValues();
var httpRouteKey = System.Web.Http.Routing.HttpRoute.HttpRouteKey;
if (!routeValues.ContainsKey(httpRouteKey)) {
routeValues.Add(httpRouteKey, true);
}
var url = string.Empty;
if (routeValues.ContainsKey(HttpAttributeRouteWebApiKey)) {
var routeName = routeValues[HttpAttributeRouteWebApiKey] as string;
routeValues.Remove(HttpAttributeRouteWebApiKey);
routeValues.Remove("controller");
routeValues.Remove("action");
url = urlHelper.HttpRouteUrl(routeName, routeValues);
} else {
var path = resolvePath<TController>(routeValues, expression);
var root = getRootPath(urlHelper);
url = root + path;
}
return url;
}
private static string resolvePath<TController>(RouteValueDictionary routeValues, Expression<Action<TController>> expression) where TController : Http.Controllers.IHttpController {
var controllerName = routeValues["controller"] as string;
var actionName = routeValues["action"] as string;
routeValues.Remove("controller");
routeValues.Remove("action");
var method = expression.AsMethodCallExpression().Method;
var configuration = System.Web.Http.GlobalConfiguration.Configuration;
var apiDescription = configuration.Services.GetApiExplorer().ApiDescriptions
.FirstOrDefault(c =>
c.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(TController)
&& c.ActionDescriptor.ControllerDescriptor.ControllerType.GetMethod(actionName) == method
&& c.ActionDescriptor.ActionName == actionName
);
var route = apiDescription.Route;
var routeData = new HttpRouteData(route, new HttpRouteValueDictionary(routeValues));
var request = new System.Net.Http.HttpRequestMessage();
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = configuration;
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpRouteDataKey] = routeData;
var virtualPathData = route.GetVirtualPath(request, routeValues);
var path = virtualPathData.VirtualPath;
return path;
}
private static string getRootPath(UrlHelper urlHelper) {
var request = urlHelper.RequestContext.HttpContext.Request;
var scheme = request.Url.Scheme;
var server = request.Headers["Host"] ?? string.Format("{0}:{1}", request.Url.Host, request.Url.Port);
var host = string.Format("{0}://{1}", scheme, server);
var root = host + ToAbsolute("~");
return root;
}
static string ToAbsolute(string virtualPath) {
return VirtualPathUtility.ToAbsolute(virtualPath);
}
}
InternalExpressionHelper.GetRouteValues inspects the expression and generates a RouteValueDictionary that will be used to generate the url.
static class InternalExpressionHelper {
/// <summary>
/// Extract route values from strongly typed expression
/// </summary>
public static RouteValueDictionary GetRouteValues<TController>(
this Expression<Action<TController>> expression,
RouteValueDictionary routeValues = null) {
if (expression == null) {
throw new ArgumentNullException("expression");
}
routeValues = routeValues ?? new RouteValueDictionary();
var controllerType = ensureController<TController>();
routeValues["controller"] = ensureControllerName(controllerType); ;
var methodCallExpression = AsMethodCallExpression<TController>(expression);
routeValues["action"] = methodCallExpression.Method.Name;
//Add parameter values from expression to dictionary
var parameters = buildParameterValuesFromExpression(methodCallExpression);
if (parameters != null) {
foreach (KeyValuePair<string, object> parameter in parameters) {
routeValues.Add(parameter.Key, parameter.Value);
}
}
//Try to extract route attribute name if present on an api controller.
if (typeof(System.Web.Http.Controllers.IHttpController).IsAssignableFrom(controllerType)) {
var routeAttribute = methodCallExpression.Method.GetCustomAttribute<System.Web.Http.RouteAttribute>(false);
if (routeAttribute != null && routeAttribute.Name != null) {
routeValues[GenericUrlActionHelper.HttpAttributeRouteWebApiKey] = routeAttribute.Name;
}
}
return routeValues;
}
private static string ensureControllerName(Type controllerType) {
var controllerName = controllerType.Name;
if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) {
throw new ArgumentException("Action target must end in controller", "action");
}
controllerName = controllerName.Remove(controllerName.Length - 10, 10);
if (controllerName.Length == 0) {
throw new ArgumentException("Action cannot route to controller", "action");
}
return controllerName;
}
internal static MethodCallExpression AsMethodCallExpression<TController>(this Expression<Action<TController>> expression) {
var methodCallExpression = expression.Body as MethodCallExpression;
if (methodCallExpression == null)
throw new InvalidOperationException("Expression must be a method call.");
if (methodCallExpression.Object != expression.Parameters[0])
throw new InvalidOperationException("Method call must target lambda argument.");
return methodCallExpression;
}
private static Type ensureController<TController>() {
var controllerType = typeof(TController);
bool isController = controllerType != null
&& controllerType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
&& !controllerType.IsAbstract
&& (
typeof(IController).IsAssignableFrom(controllerType)
|| typeof(System.Web.Http.Controllers.IHttpController).IsAssignableFrom(controllerType)
);
if (!isController) {
throw new InvalidOperationException("Action target is an invalid controller.");
}
return controllerType;
}
private static RouteValueDictionary buildParameterValuesFromExpression(MethodCallExpression methodCallExpression) {
RouteValueDictionary result = new RouteValueDictionary();
ParameterInfo[] parameters = methodCallExpression.Method.GetParameters();
if (parameters.Length > 0) {
for (int i = 0; i < parameters.Length; i++) {
object value;
var expressionArgument = methodCallExpression.Arguments[i];
if (expressionArgument.NodeType == ExpressionType.Constant) {
// If argument is a constant expression, just get the value
value = (expressionArgument as ConstantExpression).Value;
} else {
try {
// Otherwise, convert the argument subexpression to type object,
// make a lambda out of it, compile it, and invoke it to get the value
var convertExpression = Expression.Convert(expressionArgument, typeof(object));
value = Expression.Lambda<Func<object>>(convertExpression).Compile().Invoke();
} catch {
// ?????
value = String.Empty;
}
}
result.Add(parameters[i].Name, value);
}
}
return result;
}
}
The trick was to get the route to the action and use that to generate the URL.
private static string resolvePath<TController>(RouteValueDictionary routeValues, Expression<Action<TController>> expression) where TController : Http.Controllers.IHttpController {
var controllerName = routeValues["controller"] as string;
var actionName = routeValues["action"] as string;
routeValues.Remove("controller");
routeValues.Remove("action");
var method = expression.AsMethodCallExpression().Method;
var configuration = System.Web.Http.GlobalConfiguration.Configuration;
var apiDescription = configuration.Services.GetApiExplorer().ApiDescriptions
.FirstOrDefault(c =>
c.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(TController)
&& c.ActionDescriptor.ControllerDescriptor.ControllerType.GetMethod(actionName) == method
&& c.ActionDescriptor.ActionName == actionName
);
var route = apiDescription.Route;
var routeData = new HttpRouteData(route, new HttpRouteValueDictionary(routeValues));
var request = new System.Net.Http.HttpRequestMessage();
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = configuration;
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpRouteDataKey] = routeData;
var virtualPathData = route.GetVirtualPath(request, routeValues);
var path = virtualPathData.VirtualPath;
return path;
}
So now if for example I have the following api controller
[RoutePrefix("api/tests")]
[AllowAnonymous]
public class TestsApiController : WebApiControllerBase {
[HttpGet]
[Route("{lat:double:range(-90,90)}/{lng:double:range(-180,180)}")]
public object Get(double lat, double lng) {
return new { lat = lat, lng = lng };
}
}
Works for the most part so far when I test it
#section Scripts {
<script type="text/javascript">
var url = '#(Url.HttpRouteUrl<TestsApiController>(c => c.Get(1,2)))';
alert(url);
</script>
}
I get /api/tests/1/2, which is what I wanted and what I believe would satisfy your requirements.
Note that it will also default back to the UrlHelper for actions with route attributes that have the Name.
According to this page on CodePlex, all MVC routes have a distinct name, even if it is not specified.
Docs on codeplex is for WebApi 2.0 beta and looks like things have changed since that.
I have debugded attribute routes and it looks like WebApi create single route for all actions without specified RouteName with the name MS_attributerouteWebApi.
You can find it in _routeCollection._namedMap field:
GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap
This collection is also populated with named routes for which route name was specified explicitly via attribute.
When you generate URL with Url.Route("RouteName", null); it searches for route names in _routeCollection field:
VirtualPathData virtualPath1 =
this._routeCollection.GetVirtualPath(requestContext, name, values1);
And it will find only routes specified with route attributes there. Or with config.Routes.MapHttpRoute of course.
I don't want to be forced to specify a unique name for my routes.
Unfortunately, there is no way to generate URL for WebApi action without specifying route name explicitly.
In fact, even providing a Route name in the attribute only seems to work with Url.HttpRouteUrl
Yes, and that is because API routes and MVC routes use different collections to store routes and have different internal implementation.
Very first thing is, if you want to access a route then definitely you need a unique identifier for that just like any other variable we use in normal c# programming.
Hence if defining a unique name for each route is a headache for you, but still I think you will have to with it because the benefit its providing is much better.
Benefit: Think of a scenario where you want to change your route to a new value but it will require you to change that value across the applciation wherever you have used it.
In this scenario, it will be helpful.
Following is the code sample to generate link from route name.
public class BooksController : ApiController
{
[Route("api/books/{id}", Name="GetBookById")]
public BookDto GetBook(int id)
{
// Implementation not shown...
}
[Route("api/books")]
public HttpResponseMessage Post(Book book)
{
// Validate and add book to database (not shown)
var response = Request.CreateResponse(HttpStatusCode.Created);
// Generate a link to the new book and set the Location header in the response.
string uri = **Url.Link("GetBookById", new { id = book.BookId });**
response.Headers.Location = new Uri(uri);
return response;
}
}
Please read this link
And yes you are gonna need to define this routing name in order to access them with the ease you want to access. The convention based link generation you want is currently not available.
One more thing I would like to add here is, if this is really very concerning issue for you then we can write out own helper methods which will take two parameters {ControllerName} and {ActionName} and will return the route value using some logic.
Let us know if you really think that its worthy to do that.
How do you get the current area name in the view or controller?
Is there anything like ViewContext.RouteData.Values["controller"] for areas?
From MVC2 onwards you can use ViewContext.RouteData.DataTokens["area"]
HttpContext.Current.Request.RequestContext.RouteData.DataTokens["area"]
You can get it from the controller using:
ControllerContext.RouteData.DataTokens["area"]
In ASP.NET Core 1.0 the value is found in
ViewContext.RouteData.Values["area"];
I just wrote a blog entry about this, you can visit that for more details, but my answer was to create an Extension Method, shown below.
The key kicker was that you pull the MVC Area from the .DataTokens and the controller/action from the .Values of the RouteData.
public static MvcHtmlString TopMenuLink(this HtmlHelper htmlHelper, string linkText, string controller, string action, string area, string anchorTitle)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var url = urlHelper.Action(action, controller, new { #area = area });
var anchor = new TagBuilder("a");
anchor.InnerHtml = HttpUtility.HtmlEncode(linkText);
anchor.MergeAttribute("href", url);
anchor.Attributes.Add("title", anchorTitle);
var listItem = new TagBuilder("li");
listItem.InnerHtml = anchor.ToString(TagRenderMode.Normal);
if (CheckForActiveItem(htmlHelper, controller, action, area))
listItem.GenerateId("menu_active");
return MvcHtmlString.Create(listItem.ToString(TagRenderMode.Normal));
}
private static bool CheckForActiveItem(HtmlHelper htmlHelper, string controller, string action, string area)
{
if (!CheckIfTokenMatches(htmlHelper, area, "area"))
return false;
if (!CheckIfValueMatches(htmlHelper, controller, "controller"))
return false;
return CheckIfValueMatches(htmlHelper, action, "action");
}
private static bool CheckIfValueMatches(HtmlHelper htmlHelper, string item, string dataToken)
{
var routeData = (string)htmlHelper.ViewContext.RouteData.Values[dataToken];
if (routeData == null) return string.IsNullOrEmpty(item);
return routeData == item;
}
private static bool CheckIfTokenMatches(HtmlHelper htmlHelper, string item, string dataToken)
{
var routeData = (string)htmlHelper.ViewContext.RouteData.DataTokens[dataToken];
if (dataToken == "action" && item == "Index" && string.IsNullOrEmpty(routeData))
return true;
if (dataToken == "controller" && item == "Home" && string.IsNullOrEmpty(routeData))
return true;
if (routeData == null) return string.IsNullOrEmpty(item);
return routeData == item;
}
Then you can implement it as below :
<ul id="menu">
#Html.TopMenuLink("Dashboard", "Home", "Index", "", "Click here for the dashboard.")
#Html.TopMenuLink("Courses", "Home", "Index", "Courses", "List of our Courses.")
</ul>
I created an extension method for RouteData that returns the current area name.
public static string GetAreaName(this RouteData routeData)
{
object area;
if (routeData.DataTokens.TryGetValue("area", out area))
{
return area as string;
}
return null;
}
Since RouteData is available on both ControllerContext and ViewContext it can be accessed in your controller and views.
It is also very easy to test:
[TestFixture]
public class RouteDataExtensionsTests
{
[Test]
public void GetAreaName_should_return_area_name()
{
var routeData = new RouteData();
routeData.DataTokens.Add("area", "Admin");
routeData.GetAreaName().ShouldEqual("Admin");
}
[Test]
public void GetAreaName_should_return_null_when_not_set()
{
var routeData = new RouteData();
routeData.GetAreaName().ShouldBeNull();
}
}
There is no need to check if RouteData.DataTokens is null since this always initialized internally.
Get area name in View (.NET Core 2.2):
ViewContext?.ActionDescriptor?.RouteValues["area"]
MVC Futures has an AreaHelpers.GetAreaName() method. However, use caution if you're using this method. Using the current area to make runtime decisions about your application could lead to difficult-to-debug or insecure code.
I know this is old, but also, when in a filter like ActionFilter, the context does not easily provide you with the area information.
It can be found in the following code:
var routeData = filterContext.RequestContext.RouteData;
if (routeData.DataTokens["area"] != null)
area = routeData.DataTokens["area"].ToString();
So the filterContext is being passed in on the override and the correct RouteData is found under the RequestContext. There is a RoutData at the Base level, but the DataTokens DO NOT have the area in it's dictionary.
To get area name in the view, in ASP.NET Core MVC 2.1:
#Context.GetRouteData().Values["area"]
I dont know why but accepted answer is not working. It returns null with e.g ( maybe about mvc, i use .net core )
http://localhost:5000/Admin/CustomerGroup
I always debug the variable and fetch data from in it.
Try this. It works for me
var area = ViewContext.RouteData.Values["area"]
Detailed logical example
Layout = ViewContext.RouteData.Values["area"] == null ? "_LayoutUser" : "_LayoutAdmin";
Asp .Net Core 3.1
Scenario: I wanted to retrieve the current area name in a ViewCompnent Invoke method.
public IViewComponentResult Invoke()
{
string areaName = this.RouteData.Values["area"];
//Your code here...
return View(items);
}
I know this is a very very old post but we can use the Values Property exactly the same way as the DataTokens
Url.RequestContext.RouteData.Values["action"] worked for me.
In MVC 5, this seems to be needed now. When in a Controller, pass this.ControllerContext.RouteData to this routine:
/// <summary>
/// Get the name of the Area from the RouteData
/// </summary>
/// <param name="routeData"></param>
/// <returns></returns>
private static string GetArea(RouteData routeData)
{
var area = routeData.DataTokens["area"]?.ToString();
if (area != null)
{
// this used to work
return area;
}
// newer approach
var matchedList = routeData.Values["MS_DirectRouteMatches"] as List<RouteData>;
if (matchedList != null)
{
foreach (var matchedRouteData in matchedList)
{
if (matchedRouteData.DataTokens.TryGetValue("area", out var rawArea))
{
return rawArea.ToString();
}
}
}
return "";
}