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.
Related
I've been trying to use Namespace routing to build some APIs dynamically without the need to worry about hardcoding the routes. However, I did find an example from MSDN to use namespaces and folder structure as your API structure. Here's the sample that I have to use Namespace routing:
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
private readonly string _baseNamespace;
public NamespaceRoutingConvention(string baseNamespace)
{
_baseNamespace = baseNamespace;
}
public void Apply(ControllerModel controller)
{
var hasRouteAttributes = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
return;
}
var namespc = controller.ControllerType.Namespace;
if (namespc == null) return;
var templateParts = new StringBuilder();
templateParts.Append(namespc, _baseNamespace.Length + 1, namespc.Length - _baseNamespace.Length - 1);
templateParts.Replace('.', '/');
templateParts.Append("/[controller]/[action]/{environment}/{version}");
var template = templateParts.ToString();
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template
};
}
}
}
And here's the controller:
namespace Backend.Controllers.Api.Project.Core
{
public class UserController : ApiBaseController
{
public UserController()
{
}
[HttpPost]
public IActionResult Login(LoginInput loginInput) // <-- loginInput properties return null
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
}
}
in Startup.cs
namespace Backend
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Let's use namespaces as the routing default way for our APIs
services.AddControllers(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention(typeof(Startup).Namespace + ".Controllers"));
});
}
}
}
Everything works ok except that when I trigger a POST api call to Login action the LoginInput doesn't get populated the values I'm sending through Postman i.e. {"username": "value", "password": "sample"} and it always returns null value. I'm not sure what am I doing wrong with the NamespaceRoutingConvention. Bear in mind if I remove it and hard-code the route in the controller like:
[ApiController]
[Route("api/project/core/[controller]/[action]/proda/v1")]
It works as expected. Any ideas?
Try to use this instead:
[HttpPost]
public IActionResult Login([FromBody]LoginInput loginInput)
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
I think that by setting AttributeRouteModel, you're preventing the middleware invoked by having ApiControllerAttribute in the Controller to do its job, and so the defaults of treating object parameters as body is not applied.
This is a guess though, I haven't been able to find the corresponding code in the source code.
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
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.
I have ported an existing web api project - which has been run using IIS - to OWIN (selfhosted). I´m using dependency injection (unity) and have implemented a service which needs some information from the current request´s header (i.e. var x = HttpContext.Current.Request.Headers["xxx"]).
Since HttpContext is not available in OWIN (which makes sense) - how can I get the current request? Please keep in mind that I do need this information inside an injected service (not inside a controller and an OWIN middleware module - owincontext).
Your controller should inherit from ApiController, which has a Request property that will be populated for each request. So from within your controller action just use Request.Headers[...].
Create a sample class like below
public class HeaderParser
{
IDictionary<string, object> _requestContext;
IDictionary<string, string[]> _headers;
public HeaderParser(IDictionary<string, object> requestContext)
{
_requestContext = requestContext;
_headers = requestContext["owin.RequestHeaders"] as IDictionary<string, string[]>;
}
public string GetEmployeeNoFromHeader()
{
if (_headers != null && _headers.ContainsKey("X-EmployeeNo") && _headers["X-EmployeeNo"] != null && _headers["X-EmployeeNo"].Length > 0)
{
return _headers["X-EmployeeNo"][0];
}
else
{
var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
response.Content = new StringContent("EMPLOYEE NO NOT AVAILABLE IN REQUEST");
throw new HttpResponseException(response);
}
}
}
In the controller something like below should work
var owincontext = request.GetOwinContext().Environment;
var headerParser= new HeaderParser(owincontext);
headerParser.GetEmployeeNoFromHeader()
What we have done is we have implemented interface IHttpControllerActivator.Create like below, so it runs for all controller class, The controller is generated by dependency injection windsor castle
public IHttpController Create(
HttpRequestMessage request,
HttpControllerDescriptor controllerDescriptor,
Type controllerType)
{
var owincontext = request.GetOwinContext().Environment;
var headerParser= new HeaderParser(owincontext);
var logger = _dpendencyManager.Resolve(typeof(IPOSLogger)) as IPOSLogger;
var executionContext = new ExecutionContext(logger, owincontext,headerParser.GetEmployeeNoFromHeader());
var controller =
(IHttpController)_dpendencyManager.Resolve(controllerType, new { context = executionContext });
//var controller =
// (IHttpController)_dpendencyManager.Resolve(controllerType);
request.RegisterForDispose(
new Release(
() => _dpendencyManager.Release(controller)));
return controller;
}
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.