I need a custom IHttpControllerSelector which should be applied to a specific route only. All other web api routes should use the default implementation of IHttpControllerSelector.
While researching I found the following code that is meant to replace the IHttpControllerSelector at application start, but it replaces the default controller selector completely, which causes that all routes in the application use my custom controller selector:
config.Services.Replace(typeof(IHttpControllerSelector),
new CustomControllerSelector(config));
Is there a way to configure the IHttpControllerSelector for a single route?
You can assign a per-route message handler to the route that needs to use a different controller selection logic. This handler would mark the HttpRequestMessage with a flag that this request needs to be treated differently.
Then simply make the CustomControllerSelector inherit from DefaultHttpControllerSelector and inspect that flag:
if it's set, continue with your custom logic
if it's not set, return to base (DefaultHttpControllerSelector)
Here is the code:
1) message handler, setting the flag
public class RouteSpecificHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Properties["UseCustomSelector"] = true;
return base.SendAsync(request, cancellationToken);
}
}
2) assigning per route message handler to the specific route only (do not run for other routes)
config.Routes.MapHttpRoute(
name: "MyRoute",
routeTemplate: "api/dummy/{id}",
defaults: new {controller = "Dummy", id = RouteParameter.Optional},
constraints: null,
handler: new RouteSpecificHandler { InnerHandler = new HttpControllerDispatcher(config) }
);
3) custom selector respecting the flag:
public class CustomSelector : DefaultHttpControllerSelector
{
public CustomSelector(HttpConfiguration configuration) : base(configuration)
{
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
if (request.Properties.ContainsKey("UseCustomSelector") &&
request.Properties["UseCustomSelector"] as bool? == true)
{
//your logic goes here
}
return base.SelectController(request);
}
}
4) registering the selector:
config.Services.Replace(typeof(IHttpControllerSelector), new CustomSelector(config));
edit
If you wish to not inherit from DefaultHttpControllerSelector - then implement IHttpControllerSelector directly, and instead of calling the base.SelectController(request) save the old selector as a field/property in your class
public class CustomSelector : IHttpControllerSelector
{
private HttpConfiguration _config;
public IHttpControllerSelector PreviousSelector {get; set;}
public CustomSelector(HttpConfiguration configuration)
{
_config = configuration;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
if (request.Properties.ContainsKey("UseCustomSelector") &&
request.Properties["UseCustomSelector"] as bool? == true)
{
//your logic goes here
}
return PreviousSelector.SelectController(request);
}
}
Then just change the registration:
var previousSelector = config.Services.GetService(typeof(IHttpControllerSelector)) as IHttpControllerSelector;
config.Services.Replace(typeof(IHttpControllerSelector), new CustomSelector(config) { PreviousSelector = previousSelector});
Related
I have an ASP.NET Web API, .Net 4.6.1 project where I need to capture some info in the middleware and then retrieve it in a code that will be called from controller. In .Net core it is very easy with registering my custom context class as Scoped and resolving it in different stages of message processing. In .Net Framework, what looked similar to it was Autofac's InstancePerRequest so I tried but it does not work as I expected. Apparently every time I do BeginScope() it returns a new instance even if I am within same reuest? I am implementing the IAutofacContinuationActionFilter interface where I resolve my service, registered with InstancePerRequest, then later in controller I try to resolve it again and get new instance. What am I missing here?
Oh and in the controller below, both instances of IHomeService: injected via constructor and resolved manually are creating new instances.
UPDATE:
The code above is oversimplification of the real situation. The call where I need the information passed from filter is in a separate class and call happens through a series of autogenerated code. The constructor injection is not an option for me so I was hoping to have a solution similar to .Net Core DI.
my WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
var container = MyContainerBuilder.Build(config);
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
my container builder class:
public class MyContainerBuilder
{
public static IContainer Build(HttpConfiguration config)
{
var builder = new ContainerBuilder();
builder.RegisterWebApiFilterProvider(config);
builder
.Register(c => new MyCustomFilter())
.AsWebApiActionFilterForAllControllers()
.InstancePerRequest();
// var assembly = typeof(IHomeService).Assembly;
// builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces().InstancePerRequest();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()).InstancePerRequest();
builder
.RegisterType<HomeService>()
.As<IHomeService>()
.InstancePerRequest();
return builder.Build();
}
}
filter:
public class MyCustomFilter : IAutofacContinuationActionFilter
{
public async Task<HttpResponseMessage> ExecuteActionFilterAsync(
HttpActionContext actionContext,
CancellationToken cancellationToken,
Func<Task<HttpResponseMessage>> next)
{
using (var scope = GlobalConfiguration.Configuration.DependencyResolver.BeginScope().GetRequestLifetimeScope())
{
var hs = scope.Resolve<IHomeService>();
++hs.Counter;
var hs1 = scope.Resolve<IHomeService>();
++hs1.Counter;
var r = next().Result;
return await Task.FromResult(r);
}
}
}
controller:
[Route("home")]
public class HomeController : ApiController
{
public IHomeService HomeService { get; set; }
public HomeController(IHomeService homeService)
{
HomeService = homeService;
}
[HttpGet]
[Route("")]
public string Index()
{
var dr = GlobalConfiguration.Configuration.DependencyResolver as AutofacWebApiDependencyResolver;
using (var scope = dr.GetRequestLifetimeScope())
// This does not work either, returns new instance:
// using (var scope = GlobalConfiguration.Configuration.DependencyResolver.BeginScope().GetRequestLifetimeScope())
{
var hs = scope.Resolve<IHomeService>();
++hs.Counter;
}
return "Home";
}
}
service class I try to resolve:
public interface IHomeService
{
int Counter { get; set; }
}
public class HomeService : IHomeService
{
public HomeService()
{
Console.WriteLine("Yet another instance of HomeService!!!");
}
public int Counter { get; set; }
}
Thanks in advance
You can't create your own request scope, you need to get it from the request message. In the filter, that's like:
var scope = actionContext.Request.GetDependencyScope();
An example filter showing this is in the Autofac docs
However, since you're using Autofac interfaces, they're injected by Autofac for each request - if your filter needs a per-request service, it's better to make it a constructor parameter on the filter. You only need to do service location if you're not using Autofac filter interfaces.
If you seriously need service location in the filter, you can still use the constructor to make things easier - add an ILifetimeScope parameter to the filter constructor and you'll get the request scope as a parameter.
For the controller, same thing: inject what you need in the constructor rather than using service location. If you need the request scope because you can't escape service location, either inject an ILifetimeScope into the controller constructor or get the request lifetime off the HttpRequestMessage.
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>()
So I already have 1 Web API set up and working great, but now that I am trying to set up my own admin panel ( which I did ), I need to use the DeleteUser() function from the Web API named AdminApi but I can't seem to get it working. I keep getting 404 error while giving the path that the API should be at.
Web Api Config:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Global :
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//Create the custom role and user
RoleActions roleActions = new RoleActions();
roleActions.AddUserAndRole();
}
AdminApi :
[Authorize(Roles = "admin")]
public class AdminApiController : ApiController
{
public string test()
{
return "test";
}
[HttpPost]
public string DeleteUser(string id)
{
ApplicationDbContext db = new ApplicationDbContext();
var user = db.Users.Find(id);
if (user != null)
{
string email = user.Email;
db.Users.Remove(user);
return "Succesfully deleted user : " + email;
}
else
return "Failed to delete user.";
}
}
Ajax :
function deleteUser (id)
{
$.ajax({
url: '../api/AdminApi/DeleteUser',
type: 'POST',
contentType: "application/json",
dataType: 'json',
data: JSON.stringify(id),
success: function (data) {
alert(data);
},
error: function (x, y, z) {
alert(x + '\n' + y + '\n' + z);
}
});
}
The ajax function is called on the page /Admin/AdminPage
so to get to the web api -> ../api/AdminApi
and the function to delete users is DeleteUser
-> ../api/AdminApi/DeleterUser
I don't get why I get a 404 error. I can understand if my function DeleteUser is not working since I haven't tested it, but I can't test it if I can't get in the function.
The issue is related to how you use the attribute:
[Route("DeleteUser")]
If you use the Attribute Route. at Method level what it does is to define new route or more routes therefore the way you should use it is like [Route('Url/path1/route1')]:
As an example of how it works:
//GET api/customer/GetMetaData
[Route('/api/customer/GetMetaData')]
public string Get2(){
//your code goes here
}
If you will be declaring several Routes in your class then you can use RoutePrefix attribute like [RoutePrefix('url')] at class level. This will set a new base URL for all methods your in Controller class.
For example:
[RoutePrefix("api2/some")]
public class SomeController : ApiController
{
// GET api2/some
[Route("")]
public IEnumerable<Some> Get() { ... }
// POST api2/some/DeleteUser/5
[HttpPost]
[Route("DeleteUser/{id:int}")]
public Some DeleteUser(int id) { ... }
}
Update
By default Web API looks at the routing URL first, what is in your [Route] I mean and it tries to match it against your post. However if your method has a complex object as parameter WebApi can't get the values from the request URI because the parameter is a complex type Web API uses a media-type formatter to read the value from the request body.
Since your string id is not a complex object and it is part of your Route WebApi expects it as part of your URL not the body. Try this instead:
[HttpPost]
public string DeleteUser([FromBody]string anotherName)
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}"