Add arbitrary route prefix to all attribute routes in webapi 2.2 - c#

This was kind of asked at Web Api 2 global route prefix for route attributes?.
I'm using attribute routing and class level route prefixes already. However, from a configuration of some sort (could be code) I would like to add another prefix to all the attribute routes. I do not want to create custom route attributes to use throughout my code base, just the built in ones.
Is this possible?
Simply put, I would like to take my routes
/a/1/b/2
and
/x/3/y/2/z/1
and turn them in to (although it doesn't necessarily need to be a /api prefix)
/api/1/b/2
and
/api/x/3/y/2/z/1

Option 1
You could create an abstract base controller class that all other controllers inherit from and apply the RoutePrefix attribute to that. For example:
[RoutePrefix("/api")
public abstract class BaseController : ApiController
{
}
And then my normal controllers would look like this:
public class ValuesController : BaseController
{
[Route("/get/value")]
public string GetValue()
{
return "hello";
}
}
Option 2
A secondary option is to use a reverse proxy that will transparently route all incoming requests to the correct URL. You could set the proxy up with a rewrite rule such as "any request that matches /api/*, redirect to internalserver/*". You can use ARR for IIS to do this and it is free. I have used it in the past and it works very well for situations like this.

You could also read the Routes of the default HttpConfiguration and just create a new HttpConfiguration with the only difference that you apply a prefix to the routeTemplate. At the end you use this HttpConfiguration then.
Theoretically you could also create a new WebApi Startup class and your old one provides it's HttpConfiguration as a property in case you want to change routes in a seperate web project.
Something like:
HttpConfiguration oldCofiguration = OtherWebService.Startup.Config;
HttpConfiguration newCofiguration = new HttpConfiguration();
foreach(var oldRoute in oldCofiguration.Routes){
newCofigurationRoutes.MapHttpRoute(
"YourRouteName",
"yourPrefix" + oldRoute .routeTemplate ,
new
{
controller = oldRoute.Controller
},
null,
null
);
}
You need to adapt the code to your needs. (Sorry the code is untested, as I have no access to IDE just now)

Related

How can I handle route prefixes?

I am building an ASP.NET MVC Core 2.2 library, and I am looking for a way to allow any prefix in the route configuration.
I want to add an attribute to a controller, that allows all the following URLs to be passed to the same controller:
/some/prefix/MyControllerName/MyAction/ => /MyControllerName/MyAction/
/yet/another/prefix/MyControllerName/MyAction/ => /MyControllerName/MyAction/
/MyControllerName/MyAction/ => (obviously) /MyControllerName/MyAction/
I tried a wildcard syntax as such, with no success:
[Route("*/[controller]/[action]")]
public abstract class MyBaseController : Controller { }
In other words, I would like to have the beginning of the path (before the actual controller name) totally ignored.

How can I disable some APIs of my ASP.NET application

Let's say that I have a ASP.NET application that have some APIs.
For example,
{HostName}/api/a/*
{HostName}/api/b/*
Now I want to disable all {HostName}/api/a/* APIs(But remain /api/b/*). When client call /api/a/* APIs, they get a 404 error. When client call /api/b/* APIs, response normally.
Is there any way to do this in c# ASP.NET application?
There are several approaches a can take to disable certain actions or routes as mentioned in the comments.
1.) [NonAction] attribute
The [NonAction] attribute from System.Web.Http can be applied for ApiController actions. If such a method is called then the server returns the HTTP Code 404 (Method not found). The attribute can only be applied on method level and not on classes. So every single method has to be decorated with this attribute.
2.) Writing a custom action filter
This approach gives you more control. Your filter can be applied on class level and you can implement some more advanced logic in which conditions your controller is accessible or not (depending on dates, licences, feature toggles and so forth)
public class MyNoActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (IfDisabledLogic(actionContext))
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.NotFound);
}
else
base.OnActionExecuting(actionContext);
}
}
[MyNoActionFilter]
public class ValuesController : ApiController
{
// web api controller logic...
}
3.) Route Configuration in WebApiConfig.cs
You can add a web api route for the inaccessible controllers in the WebApiConfig and map this route to a non existant controller. Then the framework takes this route, does not find the controller and sends a 404 return code to the client. It is important to place these routes at the beginning in order to avoid undesired execution.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
config.Routes.MapHttpRoute(
name: "DisabledApi",
routeTemplate: "api/b/{id}",
defaults: new { controller = "DoesNotExist", id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Because you stated not to use attributes because of the amount of work I recommend the third option, because the route configuration defines a single place for this. And if you want to enable the route in the future again you have to remove only one route definition.
Might be a hack, but works fine for me:
Changing scope of the Controller from public to internal hides all actions from that Controller class. So:
internal class AController : ApiController
{
[...]
}
Requests to http://host/api/a/* then will fail with
"No type was found that matches the controller named 'a'."
Feature flags were built for this purpose
How to enable/disable controllers/routes using feature flags: Tutorial for using feature flags in a .NET Core app | Microsoft Docs
To enable this in your app:
Add the Microsoft.FeatureManagement.AspNetCore nuget package to your project.
Add the feature management service to your builder in Program.cs: builder.Services.AddFeatureManagement();
Add settings for the features you want to turn on and off in appsettings.json:
{ "FeatureManagement": {    
"AdminAccess": true,
"AnonymousAccess": false
}
Add the FeatureGate decorator to the controllers/actions you want to control access to: [FeatureGate("AnonymousAccess")]

Overriding / Extending an MVC Controller / Area

I'm currently working on an MVC project and I'm trying to figure out how I might go about extending the routes of an existing Controller within an Area, specifically from another project.
For instance, I have a Controller with an area that looks like the following :
namespace MyProject.Areas.Foo.Controllers
{
[Authorize]
public class FooController : ApplicationController
{
//code
}
}
And what I would like to do, is be able to define another Controller, within a separate project that could extend this like so :
namespace MyOtherProject.Areas.Foo.Custom.Controllers
{
public class FooController : ApplicationController
{
public string Bar()
{
return "Bar";
}
}
}
Basically, I would like the controllers to almost function as if I was using the partial keyword (so that I could call any of the actions in the original or the new one).
The Main Problem
What I am really trying to accomplish is that I have a main project with several areas and another area of my solution with various client folders. I want to be able to essentially extend the base controllers for my main project and add client-specific actions within these client folders so that they can be used in the main project. I'm already doing this with certain MVC Views, but I was hoping I could accomplish it with controllers as well.
What I've tried
I tried using the partial keyword on both declarations of the class, but since they are in different projects / assemblies, I don't think that works.
I defined a build event that would move the custom DLL into the bin directory of the main MVC project, but that didn't seem to work as expected.
I've tried various approaches for inheritance, hoping the new class would get picked up, but those didn't work (received the duplicate controller declaration error).
I've read about trying to use a custom ControllerFactory but I wasn't sure how to implement it.
I've tried defining custom namespace routing parameters in the AreaRegistration section to pick up the new controller like the following example.
Routing Example (AreaRegistration)
context.MapRoute(
AreaName,
String.Format("{0}/{{action}}/{{id}}", AreaName),
new { controller = AreaName, action = "Index", id = UrlParameter.Optional },
new[] {
String.Format("MyProject.Areas.{0}.Controllers", AreaName),
String.Format("MyOtherProject.Areas.{0}.Custom.Controllers", AreaName)
}
);
Update
I attempted an approach seen here as per some of the comments discussion that involved simply handling this via inheritance :
// Main Project
namespace MyProject.Areas.Foo.Controllers
{
[Authorize]
public class FooController : ApplicationController
{
public ActionResult Index()
{
return View();
}
}
}
// This is in another project / namespace / assembly
namespace MyOtherProject.Foo.Controllers
{
public class CustomFooController : MyProject.Areas.Foo.Controllers.FooController
{
[Route("Foo/Bar")]
public string Bar()
{
return "Bar";
}
}
}
So my current steps are as follows :
Inherited from the base FooController in the main project within another project / solution.
Set up attribute routing to access the custom controller to avoid conflicting routes from the main project.
Created a Build Event that moves the custom DLL into the main project when built (so it will be accessible) from the new custom project.
This didn't seem to make any difference. I tried going to the Foo/Bar url but it just threw a 404 as if it didn't see it at all. The CustomFooController.cs file is in it's own separate project and is just a class file and not an MVC project. Is this correct? Do I need to set the routing rules in the main project?
Controller Inheritance
Using inheritance as Chris mentioned in the comments section will likely be the best way of going about this as well. This is especially true if you are already deriving from another base controller class like ApplicationController in your example :
// ProjectA is assumed to be your "main" MVC application
public class CustomFooController : ProjectA.Controllers.FooController
{
[Route("Foo/Bar")]
public ActionResult Bar()
{
return Content("Bar");
}
}
The attribute routing here is extremely important as you don't want your existing routes to confuse your two controllers or overlook them.
Registering Attribute Routes
Since you are using attribute routing via the [Route] attribute within your ProjectB section, you'll want to ensure that you explicitly set it within the RouteConfig.cs of your ProjectA project so that it can properly identify it through the Routes.MapMvcAttributeRoutes() method as seen below :
public static void RegisterRoutes(RouteCollection routes)
{
// This is important to set up your Route Attributes
routes.MapMvcAttributeRoutes();
// Route declarations omitted for brevity
}
Likewise, if you are using Areas, you'll want to configure this within the respective AreaRegistration.cs file as well :
public override void RegisterArea(AreaRegistrationContext context)
{
// Wire up any attribute based routing
context.Routes.MapMvcAttributeRoutes();
// Area routing omitted for brevity
}
Scoping Routes
Finally, the last thing you'll want to make sure to do is properly "scope" your routes to prioritize your main namespace within the RouteConfig.cs of your main ProjectA application :
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Foo", action = "Index", id = UrlParameter.Optional },
// This will prioritize your existing Controllers so they work as expected
namespaces: new[] { "ProjectA.Controllers"}
);
}
Getting References Across
You mentioned using a Build Event to copy over the DLL from your ProjectB project into your main ProjectA project, which should be fine in this case. You will basically need some way to access it and a simply xcopy like the following should be fine in most scenarios :
xcopy /E /Y /S "$(ProjectName).dll" "$(SolutionDir)\ProjectA\Bin\"
Putting It All Together
If you have wired up all of these steps correctly, you should be able to Clean / Rebuild your existing solution. After doing so, double-check to ensure that you have the appropriate DLL within your ProjectA bin directory :
If that is there, then you are on the right track and should be able to run your main application and navigate to ~/Foo to see the following :
Likewise, navigating to ~/Foo/Bar should pick up the appropriate attribute route that was defined in your other Controller and serve the proper content :

Web API 2 default routing scheme

This question suddenly pops up in my mind.
In Startup.cs I have:
HttpConfiguration config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
When I have a method like this one:
[RoutePrefix("api/Order")]
public class OrderController : ApiController
{
// don't use any attribute here
public IHttpActionResult HelloWorld()
{
...
return Ok();
}
}
Is it possible to access HelloWorld()?
Should a GET or POST or whatever action be sent?
You can access to HttpWorld() using GET if you rename your method as: GetHelloWorld().
The same with POST renaming to PostHelloWorld().
But I prefer using [HttpGet], [HttpPost], ... attributes, even if my action methods has the "Get" or "Post" characters in the name, to avoid possible errors.
Updated
After making some tests, I realised that my comments about that is not possible to call the HelloWorld are not correct.
Indeed it is possible to call your HelloWorld() method if you make a POST call to http://<YourProjectUrl>/order.
So, the default method will be POST and, as you haven't configured any Route for your action method (take in account that RoutePrefix is just a prefix, so needs a Route attribute to be considered), it will get your controller name without "Controller" (OrderController -> Order).

Routing Undefined Controllers

How would you set up the routing in ASP.NET MVC in order to route paths with undefined controllers to a specific controller.
For example, I have a UserController, so I would want http://example.com/user to route to the UserController, but I would want http://example.com/supercoolproject to route to the ProjectController in order to find the Super Cool Project item
I believe you shouldn't use http://example.com/supercoolproject address. Instead, it should be http://example.com/project/supercool, but if you want to use address in http://example.com/{projectname}project format, you can define rule in global.asax like this:
routes.MapRoute(
"ProductByNameRule",
"{projectName}project",
new { controller = "Project", action = "ByName" }
);
and have
public ActionResult ByName(string projectName) {
}
in ProjectController.
You can create a custom controller factory to do this.
http://keyvan.io/custom-controller-factory-in-asp-net-mvc
In the CreateController method of the custom controller factory's IControllerFactory implementation first try to create the controller using DefaultControllerFactory, then if this fails create an instance of your fallback controller and return it instead.

Categories