How can one perform subdomain based URL routing in ASP.NET MVC5 using attribute based routing? I'm aware of this post but we're trying to move to a cleaner attribute based approach and I would like to move my route
from http://domain.com/Account/Logout/
to http://my.domain.com/Account/Logout/
Without subdomain routing, this standard code works:
[RoutePrefix("Account")]
public class AccountController : ApiController
{
[Route("Logout")]
public IHttpActionResult Logout()
{
// logic
}
}
To add subdomain based routing, I wrote a custom attribute and a custom constraint. Basically replaced the Route attributes so I can specify the subdomain but my custom SubdomainRoute attribute doesn't work. My attempt is below. I presume a better implementation would also customize the RoutePrefix attribute to specify subdomains ...
SubdomainRoute
public class SubdomainRouteAttribute : RouteFactoryAttribute
{
public SubdomainRouteAttribute(string template, string subdomain) : base(template)
{
Subdomain = subdomain;
}
public string Subdomain
{
get;
private set;
}
public override RouteValueDictionary Constraints
{
get
{
var constraints = new RouteValueDictionary();
constraints.Add("subdomain", new SubdomainRouteConstraint(Subdomain));
return constraints;
}
}
}
SubdomainRouteConstraint
public class SubdomainRouteConstraint : IRouteConstraint
{
private readonly string _subdomain;
public SubdomainRouteConstraint(string subdomain)
{
_subdomain = subdomain;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return httpContext.Request.Url != null && httpContext.Request.Url.Host.StartsWith(_subdomain);
}
}
Any ideas on how to make this work?
Related
Within the Asp.net Core 3.x Razor Pages web site, I need to set multiple predefined constant route names pointing to the single page and then access them on the particular cshtml.cs PageModel.
options.Conventions.AddPageRoute("/propertylist", "/properties/category1");
options.Conventions.AddPageRoute("/propertylist", "/properties/category2");
options.Conventions.AddPageRoute("/propertylist", "/properties/category3");
// I don't want any other categories to reach to the propertylist page so I didn't set it as '/properties/{categoryName}'
And then inside the PropertyList.cshtml.cs, I need to know what category was used as the route data
public async Task OnGet()
{
// Get the route data to find the category name
}
I have come up with this solution. First I've created a route constraint.
public class PropertyListRouteConstraint : IRouteConstraint
{
private string[] categories = new[] {
Constants.Routes.Categories.Category1,
Constants.Routes.Categories.Category2,
Constants.Routes.Categories.Category3
};
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
return categories.Contains(values[routeKey]);
}
}
Then in Startup.cs, I removed the previous route names and added this
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("allowedpropertycats", typeof(PropertyListRouteConstraint));
});
In PropertyList.cshtml
#page "/properties/{categoryname:allowedpropertycats}"
In PropertyList.cshtml.cs
public class PropertyListModel : PageModel
{
[BindProperty(SupportsGet = true)]
public string CategoryName { get; set; }
public async Task OnGet()
{
...
}
}
So CategoryName contains the value.
I have the custom AuthorizeAttribute where I need to use one of the business layer services to validate some data in the database before giving user a permission to view the resource. In order to be able to allocate this service within the my AuthorizeAttribute I decided to use service location "anti-pattern", this is the code:
internal class AuthorizeGetGroupByIdAttribute : AuthorizeAttribute
{
private readonly IUserGroupService _userGroupService;
public AuthorizeGetGroupByIdAttribute()
{
_userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
}
//In this method I'm validating whether the user is a member of a group.
//If they are not they won't get a permission to view the resource, which is decorated with this attribute.
protected override bool IsAuthorized(HttpActionContext actionContext)
{
Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
int groupId = int.Parse(parameters["groupId"]);
int currentUserId = HttpContext.Current.User.Identity.GetUserId();
return _userGroupService.IsUserInGroup(currentUserId, groupId);
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContex)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
base.HandleUnauthorizedRequest(actionContex);
}
else
{
actionContex.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
}
}
}
I have couple of other attributes like this in my application. Using service locator is probably not a good approach. After searching the web a little bit I found some people suggesting to use IAuthorizationFilter with dependency injection instead. But I don't know how to write this kind of IAuthorizationFilter. Can you help me writing IAuthorizationFilter that will do the same thing that the AuthorizeAttribute above?
So after struggling for a while I think I managed to resolve this issue. Here are the steps you have to do in order to that:
1) First you have to make GetGroupByIdAttribute passive, and by passive I mean an empty attribute without any logic within it (it will be used strictly for decoration purposes)
public class GetGroupByIdAttribute : Attribute
{
}
2) Then you have to mark a controller method, for which you want to add authorization, with this attribute.
[HttpPost]
[GetGroupById]
public IHttpActionResult GetGroupById(int groupId)
{
//Some code
}
3) In order to write your own IAuthorizationFilter you have to implement its method ExecuteAuthorizationFilterAsync. Here is the full class (I included comments to guide you through the code):
public class GetGroupByIdAuthorizationFilter : IAuthorizationFilter
{
public bool AllowMultiple { get; set; }
private readonly IUserGroupService _userGroupService;
//As you can see I'm using a constructor injection here
public GetGroupByIdAuthorizationFilter(IUserGroupService userGroupService)
{
_userGroupService = userGroupService;
}
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
//First I check whether the method is marked with the attribute, if it is then check whether the current user has a permission to use this method
if (actionContext.ActionDescriptor.GetCustomAttributes<GetGroupByIdAttribute>().SingleOrDefault() != null)
{
Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
int groupId = int.Parse(parameters["groupId"]);
int currentUserId = HttpContext.Current.User.Identity.GetUserId();
//If the user is not allowed to view view the resource, then return 403 status code forbidden
if (!_userGroupService.IsUserInGroup(currentUserId, groupId))
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden));
}
}
//If this line was reached it means the user is allowed to use this method, so just return continuation() which basically means continue processing
return continuation();
}
}
4) The last step is to register your filter in the WebApiConfig.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Here I am registering Dependency Resolver
config.DependencyResolver = ServiceLocator.Instance.DependencyResolver;
//Then I resolve the service I want to use (which should be fine because this is basically the start of the application)
var userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
//And finally I'm registering the IAuthorizationFilter I created
config.Filters.Add(new GetGroupByIdAuthorizationFilter(userGroupService));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Now, if needed, I can create additional IActionFilters that use IUserGroupService and then inject this service at the start of the application, from WebApiConfig class, into all filters.
Perhaps try it like shown here:
Add the following public method to your class.
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
// gets the dependecies from the serviceProvider
// and creates an instance of the filter
return new GetGroupByIdAuthorizationFilter(
(IUserGroupService )serviceProvider.GetService(typeof(IUserGroupService )));
}
Also Add interface IFilterMetadata to your class.
Now when your class is to be created the DI notices that there is a CreateInstance method and will use that rather then the constructor.
Alternatively you can get the interface directly from the DI in your method by calling
context.HttpContext.Features.Get<IUserGroupService>()
I've got an ASP.NET MVC website that needs to display user-provided URLs stored in the DB. The way they're displayed will be different depending on how that URL would be routed if that URL refers to the website itself.
For example, supposing the website is foo.com:
URL stored in DB: foo.com/pie/3/nutrition
Controller is "pie"
Action is "nutrition"
ID is 3
The way the link is formatted depends on all three of these.
How would I extract this information correctly? Can I query the URL routing device?
Note: "Use a regular expression" type of answers don't interest me -- the site, action, or controller names could change, the website may be accessible through multiple site names and ports, etc...
You may find the RouteInfo class illustrated in this blog post useful:
public class RouteInfo
{
public RouteData RouteData { get; private set; }
public RouteInfo(Uri uri, string applicationPath)
{
RouteData = RouteTable.Routes.GetRouteData(new InternalHttpContext(uri, applicationPath));
}
private class InternalHttpContext : HttpContextBase
{
private readonly HttpRequestBase request;
public InternalHttpContext(Uri uri, string applicationPath)
{
this.request = new InternalRequestContext(uri, applicationPath);
}
public override HttpRequestBase Request
{
get { return this.request; }
}
}
private class InternalRequestContext : HttpRequestBase
{
private readonly string appRelativePath;
private readonly string pathInfo;
public InternalRequestContext(Uri uri, string applicationPath)
{
this.pathInfo = uri.Query;
if (string.IsNullOrEmpty(applicationPath) || !uri.AbsolutePath.StartsWith(applicationPath, StringComparison.OrdinalIgnoreCase))
{
this.appRelativePath = uri.AbsolutePath.Substring(applicationPath.Length);
}
else
{
this.appRelativePath = uri.AbsolutePath;
}
}
public override string AppRelativeCurrentExecutionFilePath
{
get { return string.Concat("~", appRelativePath); }
}
public override string PathInfo
{
get { return this.pathInfo; }
}
}
}
You could use it like this:
public ActionResult Index()
{
Uri uri = new Uri("http://foo.com/pie/3/nutrition");
RouteInfo routeInfo = new RouteInfo(uri, this.HttpContext.Request.ApplicationPath);
RouteData routeData = routeInfo.RouteData;
string controller = routeData.GetRequiredString("controller");
string action = routeData.GetRequiredString("action");
string id = routeData.Values["id"] as string;
...
}
From the section about unit testing in this: scott guthrie blog post
you can do something like this:
MockHttpContext httpContxt = new MockHttpContext("foo.com/pie/3/nutrition");
RouteData routeData = new routes.GetRouteData(httpContext);
where routes is the RouteCollection you used to initialize your routes in your application. Then you can interrogate routeData["controller"] etc
the post is about an early version of MVC, so the class names may have changed since then.
I want to host my ASP.NET MVC website with both http and https bindings.
But only few paths should be available via http, where as all paths should be available via https.
e.g.
My application exposes following urls:
https://server/v1/setup
https://server/v1/exchange
https://server/v1/time
I want time url to be available via http as well
http://server/v1/time
I do not want to set any rules in IIS. Is there any way I can control urls available via http in code?
I also had loook at RequiresHttps attribute, but there is some redirection issue with it.
If http request is made for not allowed paths, response should be 404 (not found).
You could make an an actionfilter to check for https.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class HttpsOnlyAttribute : ActionFilterAttribute
{
/// <summary>
/// Called by the MVC framework before the action method executes.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.Request.IsSecureConnection)
{
throw new HttpException(404, "HTTP/1.1 404 Not Found");
}
}
}
Just place the attribute on top of the controllers you want to be https only
[HttpsOnly]
public class SecureController : Controller
{
// your actions here
}
You can even target just actions
public class SampleController : Controller
{
[HttpsOnly]
public ActionResult SecureAction()
{
return View();
}
}
The RequireHttpsAttribute can still be used in this case.
Decorating your Controllers of Actions with this will Redirect GET requests to the Secure version, and throw errors for all other methods.
If you extend from this method, you can override the handling to either always return a 404, or to use the default handling.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class RequireHttpsExtendedAttribute : RequireHttpsAttribute
{
public RequireHttpsExtendedAttribute(bool throwNotFound = false)
{
ThrowNotFound = throwNotFound;
}
private bool ThrowNotFound { get; set; }
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
if (ThrowNotFound)
throw new HttpException(404, "HTTP/1.1 404 Not Found");
base.HandleNonHttpsRequest(filterContext);
}
}
Thanks for the responses!! I came up with solution of using custom route constraints.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Test1_default",
"Test1/CurrentTime",
new { action = "CurrentTime", controller = "Default1" },
new { https = new HttpsConstraint() });
}
}
public class HttpsConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return httpContext.Request.IsSecureConnection;
}
}
I'm using asp.net mvc 4 and web api. My route is like this:
/api/{controller}/jqGrid/{action}/{id}
for example, if the route is :
/api/User/jqGrid/List
I hope it will route to the action name "jqGrid_List" of the User controller.
How can I achieve this?
hmm, I don't know if it's acceptable to answer my own question. I found out a solution.
First of all, I need to add a JqGridControllerConfiguration attribute to replace the default action selector applied to the controller with my one.
[JqGridControllerConfiguration]
public class UserController : ApiController
{
// GET: /api/User/jqGrid/List
[HttpGet]
public JqGridModel<User> jqGrid_List()
{
JqGridModel<User> result = new JqGridModel<User>();
result.rows = Get();
return result;
}
}
Here's the code of JqGridControllerConfiguration:
public class JqGridControllerConfiguration : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
controllerSettings.Services.Replace(typeof(IHttpActionSelector), new JqGridActionSelector());
}
}
in JqGridActionSelector, the "action" is modified if a "jqGrid/" exists in the request URL.
public class JqGridActionSelector : ApiControllerActionSelector
{
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
Uri url = controllerContext.Request.RequestUri;
if (url.Segments.Any(s => string.Compare(s, "jqGrid/", true) == 0))
{
controllerContext.RouteData.Values["action"] = "jqGrid_" + controllerContext.RouteData.Values["action"].ToString();
}
return base.SelectAction(controllerContext);
}
}
Not sure why you'd want to do this. But you can still create a "jqGrid_List" action in your User controller and set an ActionName for it, and it'll work.
UserController:
[HttpGet, ActionName("List")]
public string jqGrid_List()
{
return "WORKS";
}
Your Route:
routeTemplate: "api/{controller}/jqGrid/{action}/{id}"