ASP.NET RouteValueDictionary contains Entry with Key "RouteModels" - c#

While implementing a WebApplication we came across an dubious entry in the RouteValueDictionary with the key "RouteModels".
Even Google doesn't give me results for that entry.
Can someone explain where it comes from?
Does it come from the custom routes?
Or are this the values bound by modelbinding?
The declaration of the ActionMethode at the controller is:
[Route("User/{user}")]
public ActionResult UserForums(User user, int page = 1)
The RouteValueDictionary of the Request contains the following entries:

Does it come from the custom routes?
Unclear. However, route values are completely dependent on how a custom route class is designed.
public class SampleRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var path = httpContext.Request.Path.Substring(1);
if (path.Equals("the/virtual/path"))
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["user"] = "TheUser";
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "Custom";
routeData.Values["RouteModels"] = new List<RouteModel> { new RouteModel { Name = "Foo" }, new RouteModel { Name = "Bar" } };
routeData.DataTokens["MetaData"] = "SomeMetadata";
return routeData;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
string user = values["user"] as string;
string controller = values["controller"] as string;
string action = values["action"] as string;
IEnumerable<RouteModel> routeModels = values["RouteModels"] as IEnumerable<RouteModel>;
if ("TheUser".Equals(user) && "Home".Equals(controller) && "Custom".Equals(action))
{
// Use the route models to either complete the match or to do something else.
return new VirtualPathData(this, "the/virtual/path");
}
return null;
}
private class RouteModel
{
public string Name { get; set; }
}
}
My guess is that this is either some specialized route or filter that is storing extra data in the route collection. But why the author chose to store it as route values rather than DataTokens (for metadata) is a mystery.

Related

Redirecting responsibility from short to full URL in another controller

As a homework I have to do a simple URL shortener, where I can add full link to list, which is processed by Hashids.net library, and I get short version of an URL.
I've got something like this now, but I got stuck on redirecting it back to full link.
I would like to add a new controller, which will take the responsibility of redirecting short URL to full URL. After clicking short URL it should go to localhost:xxxx/ShortenedUrl and then redirect to full link. Any tips how can I create this?
I was trying to do it by #Html.ActionLink(#item.ShortenedLink, "Index", "Redirect") and return Redirect(fullLink) in Redirect controller but it didn't work as I expect.
And one more question about routes, how can I achieve that after clicking short URL it will give me localhost:XXXX/ShortenedURL (i.e. localhost:XXXX/FSIAOFJO2#). Now I've got
#Html.DisplayFor(model => item.ShortenedLink)
and
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Link}/{action=Index}");
});
but it gives me localhost:XXXX/Link/ShortenedURL, so I would like to omit this Link in URL.
View (part with Short URL):
<td>#Html.ActionLink(item.ShortenedLink,"GoToFull","Redirect", new { target = "_blank" }))</td>
Link controller:
public class LinkController : Controller
{
private ILinksRepository _repository;
public LinkController(ILinksRepository linksRepository)
{
_repository = linksRepository;
}
[HttpGet]
public IActionResult Index()
{
var links = _repository.GetLinks();
return View(links);
}
[HttpPost]
public IActionResult Create(Link link)
{
_repository.AddLink(link);
return Redirect("Index");
}
[HttpGet]
public IActionResult Delete(Link link)
{
_repository.DeleteLink(link);
return Redirect("Index");
}
}
Redirect controller which I am trying to do:
private ILinksRepository _repository;
public RedirectController(ILinksRepository linksRepository)
{
_repository = linksRepository;
}
public IActionResult GoToFull()
{
var links = _repository.GetLinks();
return Redirect(links[0].FullLink);
}
Is there a better way to get access to links list in Redirect Controller?
This is my suggestion, trigger the link via AJAX, here is working example:
This is the HTML element binded through model:
#Html.ActionLink(Model.ShortenedLink, "", "", null,
new { onclick = "fncTrigger('" + "http://www.google.com" + "');" })
This is the javascript ajax code:
function fncTrigger(id) {
$.ajax({
url: '#Url.Action("TestDirect", "Home")',
type: "GET",
data: { id: id },
success: function (e) {
},
error: function (err) {
alert(err);
},
});
}
Then on your controller to receive the ajax click:
public ActionResult TestDirect(string id)
{
return JavaScript("window.location = '" + id + "'");
}
Basically what I am doing here is that, after I click the link, it will call the TestDirect action, then redirect it to using the passed url parameter. You can do the conversion inside this action.
To create dynamic data-driven URLs, you need to create a custom IRouter. Here is how it can be done:
CachedRoute<TPrimaryKey>
This is a reusable generic class that maps a set of dynamically provided URLs to a single action method. You can inject an ICachedRouteDataProvider<TPrimaryKey> to provide the data (a URL to primary key mapping).
The data is cached to prevent multiple simultaneous requests from overloading the database (routes run on every request). The default cache time is for 15 minutes, but you can adjust as necessary for your requirements.
If you want it to act "immediate", you could build a more advanced cache that is updated just after a successful database update of one of the records. That is, the same action method would update both the database and the cache.
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class CachedRoute<TPrimaryKey> : IRouter
{
private readonly string _controller;
private readonly string _action;
private readonly ICachedRouteDataProvider<TPrimaryKey> _dataProvider;
private readonly IMemoryCache _cache;
private readonly IRouter _target;
private readonly string _cacheKey;
private object _lock = new object();
public CachedRoute(
string controller,
string action,
ICachedRouteDataProvider<TPrimaryKey> dataProvider,
IMemoryCache cache,
IRouter target)
{
if (string.IsNullOrWhiteSpace(controller))
throw new ArgumentNullException("controller");
if (string.IsNullOrWhiteSpace(action))
throw new ArgumentNullException("action");
if (dataProvider == null)
throw new ArgumentNullException("dataProvider");
if (cache == null)
throw new ArgumentNullException("cache");
if (target == null)
throw new ArgumentNullException("target");
_controller = controller;
_action = action;
_dataProvider = dataProvider;
_cache = cache;
_target = target;
// Set Defaults
CacheTimeoutInSeconds = 900;
_cacheKey = "__" + this.GetType().Name + "_GetPageList_" + _controller + "_" + _action;
}
public int CacheTimeoutInSeconds { get; set; }
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page id that matches.
TPrimaryKey id;
//If this returns false, that means the URI did not match
if (!GetPageList().TryGetValue(requestPath, out id))
{
return;
}
//Invoke MVC controller/action
var routeData = context.RouteData;
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
routeData.Values["controller"] = _controller;
routeData.Values["action"] = _action;
// This will be the primary key of the database row.
// It might be an integer or a GUID.
routeData.Values["id"] = id;
await _target.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
VirtualPathData result = null;
string virtualPath;
if (TryFindMatch(GetPageList(), context.Values, out virtualPath))
{
result = new VirtualPathData(this, virtualPath);
}
return result;
}
private bool TryFindMatch(IDictionary<string, TPrimaryKey> pages, IDictionary<string, object> values, out string virtualPath)
{
virtualPath = string.Empty;
TPrimaryKey id;
object idObj;
object controller;
object action;
if (!values.TryGetValue("id", out idObj))
{
return false;
}
id = SafeConvert<TPrimaryKey>(idObj);
values.TryGetValue("controller", out controller);
values.TryGetValue("action", out action);
// The logic here should be the inverse of the logic in
// RouteAsync(). So, we match the same controller, action, and id.
// If we had additional route values there, we would take them all
// into consideration during this step.
if (action.Equals(_action) && controller.Equals(_controller))
{
// The 'OrDefault' case returns the default value of the type you're
// iterating over. For value types, it will be a new instance of that type.
// Since KeyValuePair<TKey, TValue> is a value type (i.e. a struct),
// the 'OrDefault' case will not result in a null-reference exception.
// Since TKey here is string, the .Key of that new instance will be null.
virtualPath = pages.FirstOrDefault(x => x.Value.Equals(id)).Key;
if (!string.IsNullOrEmpty(virtualPath))
{
return true;
}
}
return false;
}
private IDictionary<string, TPrimaryKey> GetPageList()
{
IDictionary<string, TPrimaryKey> pages;
if (!_cache.TryGetValue(_cacheKey, out pages))
{
// Only allow one thread to poplate the data
lock (_lock)
{
if (!_cache.TryGetValue(_cacheKey, out pages))
{
pages = _dataProvider.GetPageToIdMap();
_cache.Set(_cacheKey, pages,
new MemoryCacheEntryOptions()
{
Priority = CacheItemPriority.NeverRemove,
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(this.CacheTimeoutInSeconds)
});
}
}
}
return pages;
}
private static T SafeConvert<T>(object obj)
{
if (typeof(T).Equals(typeof(Guid)))
{
if (obj.GetType() == typeof(string))
{
return (T)(object)new Guid(obj.ToString());
}
return (T)(object)Guid.Empty;
}
return (T)Convert.ChangeType(obj, typeof(T));
}
}
LinkCachedRouteDataProvider
Here we have a simple service that retrieves the data from the database and loads it into a Dictionary. The most complicated part is the scope that needs to be setup in order to use DbContext from within the service.
public interface ICachedRouteDataProvider<TPrimaryKey>
{
IDictionary<string, TPrimaryKey> GetPageToIdMap();
}
public class LinkCachedRouteDataProvider : ICachedRouteDataProvider<int>
{
private readonly IServiceProvider serviceProvider;
public LinkCachedRouteDataProvider(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider
?? throw new ArgumentNullException(nameof(serviceProvider));
}
public IDictionary<string, int> GetPageToIdMap()
{
using (var scope = serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetService<ApplicationDbContext>();
return (from link in dbContext.Links
select new KeyValuePair<string, int>(
link.ShortenedLink.Trim('/'),
link.Id)
).ToDictionary(pair => pair.Key, pair => pair.Value);
}
}
}
RedirectController
Our redirect controller accepts the primary key as an id parameter and then looks up the database record to get the URL to redirect to.
public class RedirectController
{
private readonly ApplicationDbContext dbContext;
public RedirectController(ApplicationDbContext dbContext)
{
this.dbContext = dbContext
?? throw new ArgumentNullException(nameof(dbContext));
}
public IActionResult GoToFull(int id)
{
var link = dbContext.Links.FirstOrDefault(x => x.Id == id);
return new RedirectResult(link.FullLink);
}
}
In a production scenario, you would probably want to make this a permanent redirect return new RedirectResult(link.FullLink, true), but those are automatically cached by browsers which makes testing difficult.
Startup.cs
We setup the DbContext, the memory cache, and the LinkCachedRouteDataProvider in our DI container for use later.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
services.AddMemoryCache();
services.AddSingleton<LinkCachedRouteDataProvider>();
}
And then we setup our routing using the CachedRoute<TPrimaryKey>, providing all dependencies.
app.UseMvc(routes =>
{
routes.Routes.Add(new CachedRoute<int>(
controller: "Redirect",
action: "GoToFull",
dataProvider: app.ApplicationServices.GetService<LinkCachedRouteDataProvider>(),
cache: app.ApplicationServices.GetService<IMemoryCache>(),
target: routes.DefaultHandler)
// Set to 60 seconds of caching to make DB updates refresh quicker
{ CacheTimeoutInSeconds = 60 });
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
To build these short URLs on the user interface, you can use tag helpers (or HTML helpers) the same way you would with any other route:
<a asp-area="" asp-controller="Redirect" asp-action="GoToFull" asp-route-id="1">
#Url.Action("GoToFull", "Redirect", new { id = 1 })
</a>
Which is generated as:
/M81J1w0A
You can of course use a model to pass the id parameter into your view when it is generated.
<a asp-area="" asp-controller="Redirect" asp-action="GoToFull" asp-route-id="#Model.Id">
#Url.Action("GoToFull", "Redirect", new { id = Model.Id })
</a>
I have made a Demo on GitHub. If you enter the short URLs into the browser, they will be redirected to the long URLs.
M81J1w0A -> https://maps.google.com/
r33NW8K -> https://stackoverflow.com/
I didn't create any of the views to update the URLs in the database, but that type of thing is covered in several tutorials such as Get started with ASP.NET Core MVC and Entity Framework Core using Visual Studio, and it doesn't look like you are having issues with that part.
References:
Get started with ASP.NET Core MVC and Entity Framework Core using Visual Studio
Change route collection of MVC6 after startup
MVC Routing template to represent infinite self-referential hierarchical category structure
Imlementing a Custom IRouter in ASP.NET 5 (vNext) MVC 6

Area not being picked up by View in custom RouteBase

I have a custom RouteBase, MyRoute which I want to work for an area "MyArea" which contains code like:
public override GetRouteData(HttpContextBase httpContext)
{
var result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("area", "MyArea");
result.Values.Add("controller", "MyController");
result.Values.Add("action", "Index");
}
I register this in the MyAreaAreaRegistration.cs file:
public override string AreaName { get { return "MyArea"; } }
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.Add(new MyRoute());
// other routes
context.MapRoute(/* ... */);
}
and when making requests, it successfully calls the Index action on MyController:
public ActionResult Index()
{
return this.View();
}
However, MVC doesn't search in the correct folders for the view:
The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/MyController/Index.aspx
~/Views/MyController/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/MyController/Index.cshtml
~/Views/MyController/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml
when the view is located in
~/Areas/MyArea/Views/MyController/Index.cshtml
How can I make MVC search in the correct area?
If you look at the source for AreaRegistrationContext.MapRoute you can see that it treats the area differently to the other route variables:
public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)
{
if (namespaces == null && this.Namespaces != null)
{
namespaces = this.Namespaces.ToArray<string>();
}
Route route = this.Routes.MapRoute(name, url, defaults, constraints, namespaces);
route.DataTokens["area"] = this.AreaName; // *** HERE! ***
bool flag = namespaces == null || namespaces.Length == 0;
route.DataTokens["UseNamespaceFallback"] = flag;
return route;
}
where this.AreaName is populated from the AreaRegistration.
So a quick fix for the problem is to replace the call:
result.Values.Add("area", "MyArea");
with
result.DataTokens["area"] = "MyArea";

How can I generate a WebApi2 URL without specifying a Name on the Route attribute with AttributeRouting?

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 to make sure controller and action exists before doing redirect, asp.net mvc3

In one of my controller+action pair, I am getting the values of another controller and action as strings from somewhere and I want to redirect my current action. Before making a redirect I want to make sure that controller+action exists in my app, if not then redirect to 404. I am looking for a way to do this.
public ActionResult MyTestAction()
{
string controller = getFromSomewhere();
string action = getFromSomewhereToo();
/*
At this point use reflection and make sure action and controller exists
else redirect to error 404
*/
return RedirectToRoute(new { action = action, controller = controller });
}
All I have done is this, but it doesn't work.
var cont = Assembly.GetExecutingAssembly().GetType(controller);
if (cont != null && cont.GetMethod(action) != null)
{
// controller and action pair is valid
}
else
{
// controller and action pair is invalid
}
You can implement IRouteConstraint and use it in your route table.
The implementation of this route constraint can than use reflection to check if controller/action exists. If it doesn't exist the route will be skipped. As a last route in your route table, you can set one that catches all and map it to action that renders 404 view.
Here's some code snippet to help you started:
public class MyRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var action = values["action"] as string;
var controller = values["controller"] as string;
var controllerFullName = string.Format("MvcApplication1.Controllers.{0}Controller", controller);
var cont = Assembly.GetExecutingAssembly().GetType(controllerFullName);
return cont != null && cont.GetMethod(action) != null;
}
}
Note that you need to use fully-qualified name of the controller.
RouteConfig.cs
routes.MapRoute(
"Home", // Route name
"{controller}/{action}", // URL with parameters
new { controller = "Home", action = "Index" }, // Parameter defaults
new { action = new MyRouteConstraint() } //Route constraints
);
routes.MapRoute(
"PageNotFound", // Route name
"{*catchall}", // URL with parameters
new { controller = "Home", action = "PageNotFound" } // Parameter defaults
);
If you can't obtain the fully-qualified name of the controller to pass in to GetType() you'll need to use GetTypes() and then do a string comparison over the results.
Type[] types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes();
Type type = types.Where( t => t.Name == controller ).SingleOrDefault();
if( type != null && type.GetMethod( action ) != null )
We solved this by adding this line to our WebApiConfig.cs file
config.Services.Replace(typeof(IHttpControllerSelector), new AcceptHeaderControllerSelector(config));
The core method that I have used is as follows This method was within the AcceptHeaderControllerSelector class that extended the IHttpControllerSelector interface.
The reason I have done it like this is that we have to version our API and this was a way to create a new controller e.g. V2 with just the methods that we were versioning and just drop back to V1 if a V2 didn't exists
private HttpControllerDescriptor TryGetControllerWithMatchingMethod(string version, string controllerName, string actionName)
{
var versionNumber = Convert.ToInt32(version.Substring(1, version.Length - 1));
while(versionNumber >= 1) {
var controllerFullName = string.Format("Namespace.Controller.V{0}.{1}Controller, Namespace.Controller.V{0}", versionNumber, controllerName);
Type type = Type.GetType(controllerFullName, false, true);
var matchFound = type != null && type.GetMethod(actionName) != null;
if (matchFound)
{
var key = string.Format(CultureInfo.InvariantCulture, "V{0}{1}", versionNumber, controllerName);
HttpControllerDescriptor controllerDescriptor;
if (_controllers.TryGetValue(key, out controllerDescriptor))
{
return controllerDescriptor;
}
}
versionNumber--;
}
return null;
}
The full file can be seen below:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Web.Http.Routing;
namespace WebApi
{
public class AcceptHeaderControllerSelector : IHttpControllerSelector
{
private const string ControllerKey = "controller";
private const string ActionKey = "action";
private const string VersionHeaderValueNotFoundExceptionMessage = "Version not found in headers";
private const string VersionFoundInUrlAndHeaderErrorMessage = "Version can not be in Header and Url";
private const string CouldNotFindEndPoint = "Could not find endpoint {0} for api version number {1}";
private readonly HttpConfiguration _configuration;
private readonly Dictionary<string, HttpControllerDescriptor> _controllers;
public AcceptHeaderControllerSelector(HttpConfiguration config)
{
_configuration = config;
_controllers = InitializeControllerDictionary();
}
private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
{
var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
var assembliesResolver = _configuration.Services.GetAssembliesResolver();
// This would seem to look at all references in the web api project and find any controller, so I had to add Controller.V2 reference in order for it to find them
var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
foreach (var t in controllerTypes)
{
var segments = t.Namespace.Split(Type.Delimiter);
// For the dictionary key, strip "Controller" from the end of the type name.
// This matches the behavior of DefaultHttpControllerSelector.
var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
var key = string.Format(CultureInfo.InvariantCulture, "{0}{1}", segments[segments.Length - 1], controllerName);
dictionary[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
}
return dictionary;
}
public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
var controllerName = GetRouteVariable<string>(routeData, ControllerKey);
var actionName = GetRouteVariable<string>(routeData, ActionKey);
if (controllerName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
var version = GetVersion(request);
HttpControllerDescriptor controllerDescriptor;
if (_controllers.TryGetValue(controllerName, out controllerDescriptor))
{
if (!string.IsNullOrWhiteSpace(version))
{
throw new HttpResponseException(request.CreateResponse(HttpStatusCode.Forbidden, VersionFoundInUrlAndHeaderErrorMessage));
}
return controllerDescriptor;
}
controllerDescriptor = TryGetControllerWithMatchingMethod(version, controllerName, actionName);
if (controllerDescriptor != null)
{
return controllerDescriptor;
}
var message = string.Format(CouldNotFindEndPoint, controllerName, version);
throw new HttpResponseException(request.CreateResponse(HttpStatusCode.NotFound, message));
}
private HttpControllerDescriptor TryGetControllerWithMatchingMethod(string version, string controllerName, string actionName)
{
var versionNumber = Convert.ToInt32(version.Substring(1, version.Length - 1));
while(versionNumber >= 1) {
var controllerFullName = string.Format("Namespace.Controller.V{0}.{1}Controller, Namespace.Controller.V{0}", versionNumber, controllerName);
Type type = Type.GetType(controllerFullName, false, true);
var matchFound = type != null && type.GetMethod(actionName) != null;
if (matchFound)
{
var key = string.Format(CultureInfo.InvariantCulture, "V{0}{1}", versionNumber, controllerName);
HttpControllerDescriptor controllerDescriptor;
if (_controllers.TryGetValue(key, out controllerDescriptor))
{
return controllerDescriptor;
}
}
versionNumber--;
}
return null;
}
public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
{
return _controllers;
}
private string GetVersion(HttpRequestMessage request)
{
IEnumerable<string> values;
string apiVersion = null;
if (request.Headers.TryGetValues(Common.Classes.Constants.ApiVersion, out values))
{
apiVersion = values.FirstOrDefault();
}
return apiVersion;
}
private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
{
object result = null;
if (routeData.Values.TryGetValue(name, out result))
{
return (T)result;
}
return default(T);
}
}
}
Reflection is a costly operation.
You should really be unit testing these methods to ensure they are redirecting to the appropriate action and controller.
E.g. (NUnit)
[Test]
public void MyTestAction_Redirects_To_MyOtherAction()
{
var controller = new MyController();
var result = (RedirectToRouteResult)controller.MyTestAction();
Assert.That(result.RouteValues["action"], Is.EqualTo("MyOtherAction");
Assert.That(result.RouteValues["controller"], Is.EqualTo("MyOtherController");
}

Get Current Area Name in View or Controller

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 "";
}

Categories

Resources