I have an ASP.NET Core 3.1 app. This app has an "Admin" area. I've put this area in it's own library to use in other apps. In my other app, I can currently access the "Admin" area by visiting https://{domain}/_admin. I want to reuse/share this library with other apps in my company. Now, other devs in my company have built other apps. So, I expect the _admin path may cause conflicts. For that reason, I want to make that part of the route configurable. Currently, I have the following
appsettings.json
{
"Miscellaneous": {
"AdminRoot":"_admin"
}
}
Startup.cs
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseEndpoints(endpoints =>
{
var adminRoot = Configuration["Miscellaneous:AdminRoot"];
adminRoot = String.IsNullOrEmpty(adminRoot) ? "_admin" : adminRoot;
endpoints.MapAreaControllerRoute(
name: "Admin",
areaName: "Admin",
pattern: (adminRoot + "/{controller=Home}/{action=Index}/{id?}")
);
}
...
}
...
AdminController.cs
[Area("Admin")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[Route("~/api/user-permissions")]
public IActionResult LoadUserPermissions()
{
var permissions = GetPermissions();
return Json(permissions);
}
}
This approach uses [convention-based routing][1]. At the same time, my Admin tool also has it's own REST API. For that reason, I want to mix convention-based routing with [attribute routing][2]. Basically, I want to have routes like /_admin/api/user-permissions. As this route demonstrates, the _admin creates a challenge if I add a Route attribute in my API since it's a configured value.
I thought if I used the ~ it would be relative to the area. However, I get a 404 when I try this approach. Is there a way to dynamically set parts of a route? For example, I would love to be able to either:
Use an approach relative to the area or
Use an attribute like [Route("{Configuration["Miscellaneous:AdminRoot"]}/api/user-permissions")]
I'm not finding a way to accomplish what I'm trying to do. Is there a way to do this? Please note, my Admin area is much more complex. I've tried to isolate this question to my actual problem. In addition, I'm looking to have a configurable approach for the sake of learning. For that reason, I'm not interested in solutions like changing "_admin" to a random value like a GUID or something. Thank you for your help!
I have been reviewing this for several hours. Essentially, since the one parameter is pulled from configuration, the routing requires convention based routing. However, some names, like those URL-friendly names that include dashes are invalid method names in C#. The way around this is use the following:
Rely on convention-based routing
Use attributes other than Route in the controllers. On actions, use the ActionName attribute.
Related
I have a custom IRouter implementation and I can't figure out how to register it in a .Net 7 MVC application.
What I am trying to accomplish is this: Incoming requests have the form of https://example.com/{id} and when such a request comes in I need to hit the database to retrieve the controller and action for that {id}, do some checks on it and if everything looks right pass the request on to the default router along with the entire RequestContext. The reason behind that is that such an url is valid only for a given time and a subset of visiting users. Also the underlying action and controller must not be guessable by looking at the url.
What I came up with is a cutom IRouter implementation (which also allows me to create those Urls) but I can't seem to figure out how to register on application startup.
Is using a custom IRouter still the right approach in .Net 7? How do I register one? Or am I totally on the wrong track?
One option is to switch back from endpoint routing:
builder.Services.AddMvc(options => options.EnableEndpointRouting = false);
app.UseMvc(routeBuilder => routeBuilder.Routes.Add(new CustomRouter(routeBuilder.DefaultHandler)));
UPD
Alternative approach is to use MapDynamicControllerRoute. Something along this lines:
builder.Services.AddScoped<MyDynamic>();
// ...
app.MapDynamicControllerRoute<MyDynamic>("/{*internalid}");
public class MyDynamic: DynamicRouteValueTransformer
{
public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
if (values.TryGetValue("internalid", out var value) && value is string s && s == "777") // simulate some search for id
{
values["controller"] = "Home";
values["action"] = "test";
}
return new ValueTask<RouteValueDictionary>(values);
}
}
Note that since you are still using the default routing pipeline this looks like security through obscurity which in general is not a good approach and probably you should better implement appropriate security restrictions (you should not rely on the "impossibility" to guess the actual routes).
I have a working API with a bunch of controllers, with a single database specified in config file.
Now I want to make the the API multi database and make the target database a part of the url.
I use attributes on controllers now and default routing.
Startup.cs:
app.UseMVC();
FolderController.cs:
[Route("api/[controller]")]
[ApiController]
public class FoldersController : ControllerBase { ...
and action on controller:
[HttpGet("{Parent:Guid}", Name = "Get")]
public IActionResult Get(Guid Parent) {...
So what that gives me is the standard overall template that looks like this:
https://api.example.com/api/{controller}/{action}
What I'd want is to make the database a part of the url, the intuitive place being in front of the controller. I can also skip the second api bit as I'm not running anything else on that base address.
https://api.example.com/{database}/{controller}/{action}
I've been able to extract the database name by changing the controller attribute to:
[Route("{database}/[controller]")]
But then I'd have to insert code in every action method to check for route etc, with the risk of not implementing it consitently (beside the extra typing).
Ideally I'd like to add this to the default route in startup.cs, and add a service to the middleware that would check the privileges for the authenticated user on the requested database and continue as appropriate. That way I'd have my security in one place and no way to forget it in a controller.
I havent been able to figure out how to mix that with the attributes, they seem to conflict with each other.
Can this be done? Does anyone have some pointers for me get out of this?
By understand I know we can do it. You need to implement IHttpHandler.
You can refer to the following example https://www.c-sharpcorner.com/article/dynamic-and-friendly-url-using-mvc/
I have the following controller which I wanted to use as an Web API Controller for ajax posts to retrieve data from my user table.
namespace MyProjectName.Controllers.API
{
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly myContext _context;
public UsersController(myContext context)
{
_context = context;
}
[HttpGet]
public List<string> GetInstitutionNamesById(int id)
{
// returns desired list
}
}
}
Now I'd expect the routing of this Function to be like this: /api/users/getinstitutionnamesbyid but apparently it seems to be just /api/users which I find really confusing (what if I add additional HttpGet Functions?).
Can anyone explain me what I am doing wrong? Am I using Web Api Controllers not the Intended way? Is my routing wrong?
Thanks in Advance.
[Route("api/[controller]")]
With this template, you're explicitly stating that you only care about the name of the controller. In your example, GetInstitutionNamesById is the name of the action, which isn't being considered by the template.
There are a few options for achieving what you're asking for here:
Change your [Route] template to include the action name:
[Route("api/[controller]/[action]")]
This option applies to all actions within your controller.
Change the HttpGet constraint attribute to specify the action implicitly:
[HttpGet("[action]")]
This option ensures that the name of your action method will always be used as the route
segment.
Change the HttpGet constraint attribute to specify the action explicitly:
[HttpGet("GetInstitutionNamesById")]
This option allows you to use a route segment that differs from the name of the action method itself.
In terms of whether you're using routing in the correct way here - that's somewhat opinion-based. Generally, you'll see that APIs are attempting to be RESTful, using route templates that match resources, etc. With this approach, you might have something more like the following:
/api/Users/{userId}/InstitutionNames
In this case, you might have a separate InstitutionNames controller or you might bundle it up into the Users controller. There really are many ways to do this, but I won't go into any more on that here as it's a little off-topic and opinion-based.
You just need to name it this way
[HttpGet("[action]/{id}")]
public List<string> GetInstitutionNamesById(int id)
{
// returns desired list
}
and from ajax call /api/users/GetInstitutionNamesById/1
To stop my application from getting cluttered I started working with areas. But now I always have to call:
http://localhost:49358/Document/Document/
instead of:
http://localhost:49358/Document/
How can I change my route to access the Controllers by name of the Area?
(no HomeController)
I have the following folder structure inside of my Project:
The code for my route to the Areas looks like this:
routes.MapRoute(name: "areaRoute",template: "{area:exists}/{controller=Home}/{action=Index}");
And I placed the [Area("Document")] Tag in my DocumentController.
Edit:
As suggested by Shyju and Jamie Taylor I went with the HomeControllers. (Thank you both for the quick answers and explanations)
My Structure now looks like this and the routing is working like expected:
For me it's still a bit dissapointing to have so many HomeControllers and Index Files. Navigating code isn't that easy anymore:
Edit2:
After getting too annoyed with all those Homecontrollers, I went with the solution suggested by Jamie Taylor and rearanged everything in a Features folder. It needs a bit more configuration but is much cleaner in my opinion.
It is also further explained in this Microsoft Article (Just skip the Area stuff):
https://msdn.microsoft.com/en-us/magazine/mt763233.aspx
My structure now looks like this and routing works like a charm and the controller names are still meaningful:
I'm not sure that's what areas are for. Perhaps you should rethink your architecture.
In my ASP.NET MVC Core application template, I've leveraged Feature folders, which works a little like areas.
To do this, I've added the following to the ConfigureServices method:
serviceCollection.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new FeatureLocationExpander());
});
Where FeatureLocationExpander is:
public class FeatureLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
// Don't need anything here, but required by the interface
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// The old locations are /Views/{1}/{0}.cshtml and /Views/Shared/{0}.cshtml
// where {1} is the controller and {0} is the name of the View
// Replace /Views with /Features
return new string[]
{
"/Api/{1}/{0}.cshtml",
"/Features/{1}/{0}.cshtml",
"/Features/Shared/{0}.cshtml"
};
}
}
Replacing the contents of the new string[] which is returned by ExpandViewLocations for your areas will mean that you won't have to add the routing attributes.
BUT, this does not fix your issue as that is not what areas are for.
Unless you added a razor page (named Index.cshtml) under the Documents area which acts as an index page for the Documents area. This Index.cshtml could provide all of the functionality of the Index.cshtml in your /Documents/Documents/Index.cshtml with the added bonus of having a code-behind like file (remember ASP.NET webforms?) which acts like your controller.
The default route registration for areas use HomeController as the default value for the controller in the url. If you want DocumentController to be the default, update it in the StatrtUp class.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Document}/{action=Index}/{id?}"
);
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Keep in mind that that registration code is for all the existing areas (because we have {area:exists} in the url template), not just Document area. That means, Any time a request comes like yourApp\someAreaName, the framework will send the request to the index action of DocumentController in someAreaName.
You already organized your code related to documents to the Document area. Now why you need your controller name to be document ? I feel, it is repetitive.
I would personally rename the DocumentController inside the Document area to HomeController and use the default route registration code for the area registration ( which uses HomeController as the default controller value in the url template). This way, It will work for your future areas and your code looks more clean. IMHO, A HomeController in any area make sense where a DocumentController in any area will be confusing.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
);
});
TL;DR
I need a way to programtically choose which RoutePrefix is chosen when generating URLs based on the properties of a user in my MVC app
Not TL;DR
I have an MVC 4 app (with the AttributeRouting NuGet package)
Due to the requirements of the hosting environment I have to have two routes for a lot of my actions so that the hosting environment can have different permissions for access.
I am solving this by decorating my controller with with [RoutePrefix("full")] [RoutePrefix("lite)]. which allows each action method to be accessed via /full/{action} and /lite/{action}.
This works perfectly.
[RoutePrefix("full")]
[RoutePrefix("lite")]
public class ResultsController : BaseController
{
// Can be accessed via /full/results/your-results and /lite/results/your-results and
[Route("results/your-results")]
public async Task<ActionResult> All()
{
}
}
However, each user should only use either full or lite in their urls, which is determined by some properties of that user.
Obviously when I use RedirectToAction() or #Html.ActionLink() it will just choose the first available route and won't keep the "correct" prefix.
I figured I can override the RedirectToAction() method as well as add my own version of #Html.ActionLink() methods.
This will work, but it will involve some nasty code for me to generate the URLs because all I get is a string representing the action and controllers, but not the reflected types. Also there might be route attributes such as in my example, so I am going to have to replicated a lot of MVCs built in code.
Is there a better solution to the problem I am trying to solve?
How about something like:
[RoutePrefix("{version:regex(^full|lite$)}")]
Then, when you create your links:
#Url.RouteUrl("SomeRoute", new { version = "full" })
Or
#Url.RouteUrl("SomeRoute", new { version = "lite" })
You could even do the following to just keep whatever was already set:
#Url.RouteUrl("SomeRoute", new { version = Request["version"] })
I ended up finding a solution to this
I just overrided the default routes to include this. ASP.Net automatically keeps the usertype value and puts it back in when it regenerates the routes
const string userTypeRegex = "^(full|lite)$";
routes.Add("Default", new Route("{usertype}/{controller}/{action}/{id}",
new { controller = "Sessions", action = "Login", id = UrlParameter.Optional }, new { usertype = userTypeRegex }));
I found that this didn't work with the Route or RoutePrefix attributes, and so I had to remove all of them. Forcing me to add specific routes in these cases
routes.Add("Profile-Simple", new Route("{usertype}/profile/simple",
new { controller = "ProfileSimple", action = "Index" }, new { usertype = userTypeRegex }));
I thought that a half-dozen hard coded routes in my RouteConfig file was a better solution than having to manually add values to each place I generated a URL (as in Chris's solution).