Webapi inherited controllers are ignoring Route attributes - c#

I want to have a Controller Hierarchy assembly at webapi 2 website to handle common CRUD service operation.
I Started with an abstract webapi class like this (I removed unnecesary code and injection for simplicity):
public abstract class EntityController<T> : ApiController {
[Route("GetAll")]
[HttpGet]
public IHttpActionResult GetAll()
{
//code
}
[Route("Add")]
[HttpPost]
public IHttpActionResult Add(T entity)
{
//code
}
[Route("Delete")]
[HttpDelete]
public IHttpActionResult Delete(T entity)
{
//code
}
[Route("Edit")]
[HttpPost]
public IHttpActionResult Edit(T entity)
{
//code
}
}
First, I noticed that if we have controllers in external assemblies, the routes are not resolved properly. I found this post:
https://www.strathweb.com/2012/06/using-controllers-from-an-external-assembly-in-asp-net-web-api/
and when I apply the proposed changes, the routes dont use the "route" attribute. I.E. if I have the following derived class:
[RoutePrefix("api/thebooks")]
public class BookController : EntityController<Book>
{
public BookController() : base()
{
}
}
The controller prefix is api/book instead api/thebooks and if I try to call "Add" I have the exception:
Multiple actions were found that match the request:
Add on type [...]
Edit on type [...]
Find on type [...]
How can I indicate that I will use the Route and RoutePrefix attribute on inherited Controllers?
Thanks in advance!!!

Both Route and RoutePrefix are not inherited because Inherited is set to false as shown below:
[Inherited=false)]
public class RoutePrefixAttribute : Attribute
You can read more about Inherited here.
You will need to decorate your subclasses again unfortunately.

It is possible to override the inherited false of the Route and RoutePrefix with a bit of jiggery pokery.
Firstly inherit from DefaultDirectRouteProvider - notice the inherit true inside GetCustomAttributes
public class InheritanceDirectRouteProvider : DefaultDirectRouteProvider
{
protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
{
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(true);
}
}
Then in you application startup class (WebApiConfig or Startup) register it against your HttpConfiguration as an extension, then change the line in the startup from config.MapHttpAttributeRoutes(); to config.MapInheritedAttributeRoutes();
public static class HttpConfigurationExtensions
{
public static void MapInheritedAttributeRoutes(this HttpConfiguration config)
{
config.MapHttpAttributeRoutes(new InheritanceDirectRouteProvider());
}
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
config.MapInheritedAttributeRoutes();
app.UseWebApi(config);
}
}
Taken from this article https://www.strathweb.com/2016/06/inheriting-route-attributes-in-asp-net-web-api/

Related

.net core routing, understand what application called with a generic route

So what I have is a base controller that the following [Route] definition
[Route("{application}/api/[controller]")]
public class BaseController
{
}
All of my current controllers inherit from BaseController.
What I am trying to achieve is that two different application can call my controllers and my code to be aware of what 'application' is calling it.
Application 1 should be able to call /Application1/Api/MyController
Application 2 should be able to call /Application2/Api/MyController
and both requests should go to the same controller but my code should be aware of which application called it.
I thought about having some sort of Middleware and then work out the application from the Request.Path, and then store it in something like HttpContext.Current.Items but that doesn't seem like the correct way to do it.
My personal preference here would be to pass the value as an HTTP header rather than a route parameter, especially if you want it everywhere. It means you don't need a Route attribute and a different URL per application. Using a custom ActionFilterAttribute, there's a bunch of ways you can pass this detail into your action. For example:
public class ApplicationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.Headers.TryGetValue("Application", out var values))
{
// Method 1: This allows you to specify a parameter on your action
context.ActionArguments.Add("application", values.First());
// Method 2: This adds the value into the route data
context.RouteData.Values.Add("Application", values.First());
// Method 3: This will set a property on your controller
if (context.Controller is BaseApplicationController baseController)
{
baseController.Application = values.First();
}
}
base.OnActionExecuting(context);
}
}
And apply it to action methods or your controller:
[Application]
public class FooController : Controller
{
}
Method 1 Usage:
public IActionResult Index(string application)
{
// do something with the parameter passed in
}
Method 2 Usage:
public IActionResult Index(string application)
{
var application = (string)RouteData.Values["Application"];
}
Method 3 Usage:
First, create a base controller that contains the property:
public abstract class BaseApplicationController : Controller
{
public string Application { get; set; }
}
Then make sure your controller inherits from it:
[Application]
public class FooController : BaseApplicationController
{
}
Now you can access the property on your controller:
public IActionResult Index(string application)
{
var application = this.Application;
}
Bonus Method 4:
As an aside, you could use this method to use the URL route value, using the base controller from method 3, modify the attribute to look like this:
public class ApplicationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.Controller is BaseApplicationController baseController)
{
baseController.Application = (string)context.RouteData.Values["application"];
}
base.OnActionExecuting(context);
}
}
Add a route attribute to your controller:
[Route("{application}/api/[controller]/[action]")]
And now you should have the property value on the controller set.
You could move the route template into action and then each action would technically be aware of its caller context by your proposed convention:
[Route("api/[controller]")]
public class YourController : BaseController
{
[HttpGet("{application}")]
public IActionResult Get(string application)
{
if (application == "Application1")
{
...Application1 called
}
if (application == "Application2")
{
...Application2 called
}
...
}
}
Of course, this is your proposed convention and it is not enforced through some custom application authentication in any way so you will have to trust that your callers will correctly identify themselves through this convention.
Another approach, could be to have a base class variable and set that after inspecting the route.
[Route("{application}/api/[controller]")
public class BaseController: Controller
{
protected string CallingApp { get; set; }
public override void OnActionExecuting(ActionExecutingContext ctx)
{
CallingApp = ctx.RouteData.Values["application"];
base.OnActionExecuting(ctx);
}
}

Dotnet Core MVC - Apply action filter to specific controllers only

I have worked in MVC on .Net 4 and I had my action filters overridden in the controller class. That way I can have multiple controllers, each with a unique OnActionExecuting method.
Now I'm working on Dotnet Core 2.1 and things are a little different so I can't get the same methodology to work.
I have a controller for an API that inherits ControllerBase and then another controller for serving pages, that one inherits Controller.
I created my action filter class that derives from ActionFilterAttribute.
I then registered my action filter by using this line:
services.AddMvc(config => { config.Filters.Add(typeof(MyActionFilter)); })
The problem is that the filter now applies to the API controller as well as the pages controller. Is there a way the action filter can be applied to specific controllers only?
I used this page as a reference for my initial action filter implementation.
I know that inside the OnActionExecuting method I can use context.Controller.GetType() to compare it to the types of my individual controllers, but I would prefer to avoid that, and instead specify which controllers should use which action filters.
Thanks in advance.
services.AddMvc(config => { config.Filters.Add(typeof(MyActionFilter)); })
The above source code is used when you want to apply the global filter that mean every API will have the affected.
So to specific particular Controller you just simply add [MyActionFilter] atribute to you controller
Example:
[MyActionFilter]
public class HomeController : Controller {
public ViewResult Index(){..}
}
First of All Register Your filter in your startup.cs
services.AddScoped<"---FilterNameHere----">();
Put Your Filter in controller like
[ServiceFilter(typeof("----FilterNameHere---"))]
public class HomeController : ControllerBase
{
public IActionResult Index()
{//do your code here}
}
Was looking for an answer too. Both above confused me. Went to the page of the documentation...
Two ways of doing this in a clean way.
An action filter for all actions of all controllers.
public class YourActionFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
throw new System.NotImplementedException();
}
public void OnActionExecuting(ActionExecutingContext context)
{
throw new System.NotImplementedException();
}
}
And in startup do
services.AddControllers(c => { c.Filters.Add(new YourActionFilter()); })
An Attribute that you want to use only on specific actions which implements TypeFilterAttribute and has an private class that implemements an action filter.
public class YourActionFilterAttribute : TypeFilterAttribute
{
public YourActionFilterAttribute() : base(typeof(TheActionFilter)) { }
private class TheActionFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
throw new System.NotImplementedException();
}
public void OnActionExecuting(ActionExecutingContext context)
{
throw new System.NotImplementedException();
}
}
}
Now you only need to add it to a whole controller or to a specific action
[YourActionFilter]
public class HomeController : ControllerBase
{
[YourActionFilter]
public IActionResult Index()
{//do your code here}
}

Attribute Routing for querystring

I have below route URL:-
www.domanname.com/subroute/GetInfo?param1=somestring&param2=somestring
I have function in webapi as:-
public class HomeController : ApiController
{
public object GetInfo(string param1,string param2)
{}
}
To apply route:-
[RoutePrefix("subroute")]
public class HomeController : ApiController
{
[Route("GetInfo?param1={param1:string}&param2={param2:string}")]
public object GetInfo(string param1,string param2)
{}
}
But after applying above URL:-
www.domanname.com/subroute/GetInfo?param1=somestring&param2=somestring
It is not able to find that URL
How can I design this particular route?
You need to modify the routes a bit as query string are not normally used in attribute routes. They tend to be used for inline route parameters.
[RoutePrefix("subroute")]
public class HomeController : ApiController {
//Matches GET subroute/GetInfo?param1=somestring&param2=somestring
[HttpGet]
[Route("GetInfo")]
public IHttpActionResult GetInfo(string param1, string param2) {
//...
}
}
Also
Enabling Attribute Routing
To enable attribute routing, call MapHttpAttributeRoutes during configuration. This extension method is
defined in the System.Web.Http.HttpConfigurationExtensions class.
using System.Web.Http;
namespace WebApplication
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
// Other Web API configuration not shown.
}
}
}
Reference Attribute Routing in ASP.NET Web API 2
[RoutePrefix("subroute")]
public class HomeController : ApiController {
[HttpGet]
[Route("GetInfo/{param1}/{param2}")]
public IHttpActionResult GetInfo(string param1, string param2) {
//...
}
}
Calling
//Matches GET subroute/GetInfo/Hello/World

MapHttpAttributeRoutes not working with areas - every route 404s

I'm completely stuck on this - after commenting out almost everything to do with startup configuration leaving only the following, I still can't get the WebApi Attribute routing to work correctly.
I have the following Global.asax.cs:
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
The following controller - located in an area called "v2":
[RoutePrefix("v2/businesses")]
public class BusinessesController : ApiController
{
// GET: v2/businesses/{id}
[HttpGet]
[Route("{id:int}")]
public ApiResponse<ApiBusiness> Index(int id)
{
return new ApiResponse<ApiBusiness>(new ApiBusiness());
}
}
The following area registration:
public class V2AreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "v2";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.MapMvcAttributeRoutes();
}
}
And the following WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
}
}
When I try to access the endpoint URL businesses/1 or v2/businesses/1 or v2/v2/businesses/1 all three return a 404, I was sure one of those would be correct.
Note that a different controller outside of the "v2" area works perfectly fine:
[RoutePrefix("abc")]
public class TestingController : ApiController
{
[HttpGet]
[Route("123")]
public int Test()
{
return 2;
}
}
Sure enough, 2 is returned when I access that endpoint (abc/123).
Can anyone explain why this isn't working?
In this situation you are saying that "v2" is both and mvc route AND a web api route; they can't coexist.
I mean, they can, but what happens here is that the requests to the web API will be mistakenly handled as if they were an MVC action. That's why you get a 404.
In less words, Web API doesn't really support areas. What you can do here is try to register the web api routing before the mvc routing or explicitly handle the web api rounting using MapHttpRoute. For example:
context.Routes.MapHttpRoute("AreaName_WebApiRoute",
"AreaName/Api/{controller}/{id}",
new { id = RouteParameter.Optional });
Check this SO answer for more details!

Web Api ignores CreateErrorResponse in Attribute

I have a base controller:
public abstract class EntityController<T> : ApiController
{
[HttpPost]
[ValidateModel]
public abstract IHttpActionResult Create(T dto);
[HttpPut]
[ValidateModel]
public abstract IHttpActionResult Update(T dto);
[HttpDelete]
public abstract IHttpActionResult Delete(int id);
}
And a derived controller:
public class CustomerTypeController : EntityController<CustomerTypeDTO>
{
[ApiAuthorize(Right = Rights.CustomerType, Action = Action.Create)]
public override IHttpActionResult Create(CustomerTypeDTO customerType)
{
return Save(customerType);
}
[ApiAuthorize(Right = Rights.CustomerType, Action = Action.Update)]
public override IHttpActionResult Update(CustomerTypeDTO customerType)
{
return Save(customerType);
}
private IHttpActionResult Save(CustomerTypeDTO customerType)
{
//service layer save customer type
}
}
The base controller has a [ValidateModel] on the actions, whilst the derived controller sets it's own [ApiAuthorize] attribute. The attributes are fired in the right order (first ApiAuthorize and then ValidateModel). I have not set the order and I don't know if it's possible.
The ApiAuthorize looks like this:
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false)]
public class ApiAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
base.OnAuthorization(actionContext);
//Do some checks...
if (!authorized)
actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, new CustomNotAuthorizedException());
}
}
However the problem is that even although the attribute creates an error response, the action still proceeds further by going into ValidateModel and then into the action itself.
How is it possible to stop the api from processing the request further at the Authorize and ValidateModel levels?
The problem was that the Authorize attribute code was not setting the response. It has to be like this:
if (!authorized)
{
var response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, new CustomNotAuthorizedException());
actionContent.Response = response;
}

Categories