I have the following base controller class:
public class ApiControllerBase : ControllerBase
{
[ApiExplorerSettings(IgnoreApi = true)]
protected void IsAccess(int carrierId)
{
if (condition)
{
ControllerContext.HttpContext.Response.StatusCode = 401;
}
}
and then I call it from my controller, inherited from ApiControllerBase:
public IActionResult GetTabletListByGroup(...)
{
IsAccess(55555);
I want to return Forbid() if condition is true. But set StatusCode is not enough for it. How to do it correctly?
First, the status code indicated by Forbid() is 403 instead of 401.
Secondly, the Forbid() method needs to rely on the authentication stack to respond. If you don't have any authentication handlers in your pipeline, you can't use Forbid(). Instead, you should use return StatusCode(403).
You can refer to this.
I have made a simple demo, you can refer to it:
Update
ApiController:
[Route("api/[controller]")]
[ApiController]
public class ApiControllerBase : ControllerBase
{
[ApiExplorerSettings(IgnoreApi = true)]
protected IActionResult IsAccess(int carrierId)
{
if (carrierId >= 1)
{
ControllerContext.HttpContext.Response.StatusCode = 403;
return StatusCode(403);
}
else
{
return Ok();
}
}
}
TestBaseController :
public class TestBaseController : ApiControllerBase
{
public IActionResult GetTabletListByGroup()
{
return IsAccess(55555);
}
}
Here is the test result:
Related
I need to automatically add api/ prefix to every end point in my asp .net core web API. How to do that?
You can custom MvcOptionsExtensions to set route prefix globally instead of change the route attribute manually.
1.custom MvcOptionsExtensions:
public static class MvcOptionsExtensions
{
public static void UseRoutePrefix(this MvcOptions opts, IRouteTemplateProvider routeAttribute)
{
opts.Conventions.Add(new RoutePrefixConvention(routeAttribute));
}
public static void UseRoutePrefix(this MvcOptions opts, string
prefix)
{
opts.UseRoutePrefix(new RouteAttribute(prefix));
}
}
public class RoutePrefixConvention : IApplicationModelConvention
{
private readonly AttributeRouteModel _routePrefix;
public RoutePrefixConvention(IRouteTemplateProvider route)
{
_routePrefix = new AttributeRouteModel(route);
}
public void Apply(ApplicationModel application)
{
foreach (var selector in application.Controllers.SelectMany(c => c.Selectors))
{
if (selector.AttributeRouteModel != null)
{
selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel);
}
else
{
selector.AttributeRouteModel = _routePrefix;
}
}
}
}
2:Register in Startup.cs(version before .Net6) or in Program.cs(version beyond .Net 6):
services.AddControllers(o =>{
o.UseRoutePrefix("api");
});
Or:
builder.Services.AddControllers(o =>{
o.UseRoutePrefix("api");
});
Make your controller constructor with Route Prefix "api/"
For example lets say your controller class name is CustomerController
[Route("api/[controller]")]
public class CustomerController : ControllerBase
{
}
// This will become api/customer
[HttpGet]
public async Task<ActionResult> GetCustomers()
{
// Code to get Customers
}
// This will become api/customer/{id}
[HttpGet]
[Route("{id}")]
public async Task<ActionResult> GetCustomerById(int id)
{
// Code to get Customer by Id
}
we can simply add that in top of the controller like this
[Route("api/[controller]")]
public class TestController : ControllerBase
{
[HttpGet("version")]
public IActionResult Get()
{
return new OkObjectResult("Version One");
}
[HttpGet("Types")]
public IActionResult GetTypes()
{
return new OkObjectResult("Type One");
}
}
so that you can access like below
....api/Test/version
....api/Test/Types
Seems you can use a constant.
public static class Consts
{
public const string DefaultRoute = "api/[controller]";
}
and re-use it everywhere. If you need to change the default route everywhere - just change the constant.
[Route(Consts.DefaultRoute)]
public class TestController : ControllerBase
{
...
}
I am migrating controllers from .NET Framework to .NET Core and I want to be compatibility with API calls from previous version. I have problem with handling multiple routes from Query Params.
My example controller:
[Route("/api/[controller]")]
[Route("/api/[controller]/[action]")]
public class StaticFileController : ControllerBase
{
[HttpGet("{name}")]
public HttpResponseMessage GetByName(string name)
{
}
[HttpGet]
public IActionResult Get()
{
}
}
Calling api/StaticFile?name=someFunnyName will lead me to Get() action instead of expected GetByName(string name).
What I want to achieve:
Calling GET api/StaticFile -> goes to Get() action
Calling GET
api/StaticFile?name=someFunnyName -> goes to GetByName() action
My app.UseEndpoints() from Startup.cs have only these lines:
endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();
If I use [HttpGet] everywhere and add ([FromQuery] string name) it gets me AmbiguousMatchException: The request matched multiple endpoints
Thank you for your time to helping me (and maybe others)
What I want to achieve:
Calling GET api/StaticFile -> goes to Get() action
Calling GET api/StaticFile?name=someFunnyName -> goes to GetByName() action
To achieve above requirement of matching request(s) to expected action(s) based on the query string, you can try to implement a custom ActionMethodSelectorAttribute and apply it to your actions, like below.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class QueryStringConstraintAttribute : ActionMethodSelectorAttribute
{
public string QueryStingName { get; set; }
public bool CanPass { get; set; }
public QueryStringConstraintAttribute(string qname, bool canpass)
{
QueryStingName = qname;
CanPass = canpass;
}
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
StringValues value;
routeContext.HttpContext.Request.Query.TryGetValue(QueryStingName, out value);
if (QueryStingName == "" && CanPass)
{
return true;
}
else
{
if (CanPass)
{
return !StringValues.IsNullOrEmpty(value);
}
return StringValues.IsNullOrEmpty(value);
}
}
}
Apply to Actions
[Route("api/[controller]")]
[ApiController]
public class StaticFileController : ControllerBase
{
[HttpGet]
[QueryStringConstraint("name", true)]
[QueryStringConstraint("", false)]
public IActionResult GetByName(string name)
{
return Ok("From `GetByName` Action");
}
[HttpGet]
[QueryStringConstraint("name", false)]
[QueryStringConstraint("", true)]
public IActionResult Get()
{
return Ok("From `Get` Action");
}
}
Test Result
The parameter for HttpGet sets the route, not query string parameter name.
You should add FromQuery attribute for action parameter and use HttpGet without "{name}":
[HttpGet]
public HttpResponseMessage GetByName([FromQuery] string name)
{
// ...
}
You can also set different name for query parameter:
[HttpGet]
public HttpResponseMessage GetByName([FromQuery(Name = "your_query_parameter_name")] string name)
{
// ...
}
But now you have two actions matching same route so you will get exception. The only way to execute different logic based on query string part only (the route is the same) is to check query string inside action:
[HttpGet]
public IActionResult Get([FromQuery] string name)
{
if (name == null)
{
// execute code when there is not name in query string
}
else
{
// execute code when name is in query string
}
}
So you have only one action which handles both cases using same route.
I got my solution from https://www.strathweb.com/2016/09/required-query-string-parameters-in-asp-net-core-mvc/
public class RequiredFromQueryAttribute : FromQueryAttribute, IParameterModelConvention
{
public void Apply(ParameterModel parameter)
{
if (parameter.Action.Selectors != null && parameter.Action.Selectors.Any())
{
parameter.Action.Selectors.Last().ActionConstraints.Add(new RequiredFromQueryActionConstraint(parameter.BindingInfo?.BinderModelName ?? parameter.ParameterName));
}
}
}
public class RequiredFromQueryActionConstraint : IActionConstraint
{
private readonly string _parameter;
public RequiredFromQueryActionConstraint(string parameter)
{
_parameter = parameter;
}
public int Order => 999;
public bool Accept(ActionConstraintContext context)
{
if (!context.RouteContext.HttpContext.Request.Query.ContainsKey(_parameter))
{
return false;
}
return true;
}
}
For example, if using [RequiredFromQuery] in StaticFileController we are able to call /api/StaticFile?name=withoutAction and /api/StaticFile/GetByName?name=wAction but not /api/StaticFile/someFunnyName (?name= and /)
Workaround solution for that is to create separate controller action to handle such requests
I am implementing a resource filter to store invalid requests in database and override returned BadRequest response.
I stored invalid requests successfully but I am struggling with overriding response, I tried the following:
public class MyFilter : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
;
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
if (!context.ModelState.IsValid)
{
//store request in data base
context.Result= new BadRequestObjectResult(new MyErrorModel(){ID = "1",FriendlyMessage = "Your request was invalid"});
}
}
}
public class MyErrorModel
{
public string FriendlyMessage { get; set; }
public string ID { get; set; }
}
But the returned response is not being overridden.
Is there a way to override the response inside Resource filters?
P.S: I am using [ApiController] attribute.
As we all kown , the IResourceFilter runs immediately after the authorization filter and is suitable for short-circular .
However , you will make no influence on the result by setting Result=new BadRequestObjectResult() when the result execution has finished .
See the workflow as below :
According to the workflow above , we should run the MyFilter after the stage of model binding and before the stage of result filter . In other words , we should put the logic into a action filter . Since there's already a ActionFilterAttribute out of box , just create a MyFilterAttribute which inherits from the ActionFilterAttribute :
public class MyFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
//store request in data base
context.Result = new BadRequestObjectResult(new MyErrorModel() { ID = "1", FriendlyMessage = "Your request was invalid" });
}
}
}
Here's a screenshot the filter works :
[Edit]:
The code of controller decorated with [ApiController]:
namespace App.Controllers
{
[ApiController]
[Route("Hello")]
public class HelloController : Controller
{
[MyFilter]
[HttpGet("index")]
public IActionResult Index(int x)
{
var y =ModelState.IsValid;
return View();
}
}
}
I am calling a Web API method like this:
$.post("/api/attendances", { gigId: button.attr("data-gig-id") })
.done(function() {
button.removeAttr("btn-default")
.addClass("btn-primary")
.text("going");
})
.fail(function() {
alert("something went wrong");
});
And the Web API class looks like this:
[Authorize]
public class AttendancesController : ApiController
{
private ApplicationDbContext _context;
public AttendancesController()
{
_context = new ApplicationDbContext();
}
[HttpPost]
public IHttpActionResult SaveAttenance(AttendanceDto dto)
{
string userId = User.Identity.GetUserId();
if (_context.Attendances.Any(a => a.GigId == dto.GigId && a.AttendeeId == userId))
{
return BadRequest();
}
_context.Attendances.Add(new Attendance()
{
GigId = dto.GigId,
AttendeeId = userId
});
_context.SaveChanges();
return Ok();
}
}
I am testing the call with anonymous user,when calling the method, I get status code 200 back which is not what I am expecting. I am also receiving this:
responseText :"{"Message":"Authorization has been denied for this
request."}"
status:200
statusText : "OK"
Why isn't the Authorize attribute returning a status code that matches the responseText? In my case, the JavaScript code inside the .done function will execute regardless if the user is authorized or not. Any guidance is appreciated.
Update: Here's a link to my web.config if it helps: https://pastebin.com/B26QGjv8
That's because Although you are using the [Authorize] attribute, You are not doing anything with the result.
The method works as expected, you are issuing a request, you are not authorized but you continue on with your work in the controller.
Handle your exception in the controller:
Override the On Exception method and create an exception attribute:
public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is NotImplementedException)
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
}
}
}
And in your controller call it like this:
public class ProductsController : ApiController
{
[NotImplExceptionFilter]
public Contact GetContact(int id)
{
throw new NotImplementedException("This method is not implemented");
}
}
In your WebApiConfig.cs add:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());
// Other configuration code...
}
}
Use This as reference, All snippets are taken from here:
Handling exceptions in Web Api.
I have a custom ActionFilterAttribute. For the sake of this question let's assume it's as follows:
public class CustomActionFilterAttribute : ActionFilterAttribute {
public bool success { get; private set };
public override void OnActionExecuting(HttpActionContext actionContext) {
//Do something and set success
success = DoSomething(actionContext);
}
}
My controller is then decorated with CustomActionFilter. What I am looking for is a way (in my controller method) to do something like:
[CustomActionFilter]
public class MyController : ApiController {
public ActionResult MyAction() {
//How do I get the 'success' from my attribute?
}
}
If there is a more accepted way of doing this please let me know.
I discovered I could do the following to satisfy my problem:
[CustomActionFilter]
public class MyController : ApiController {
public ActionResult MyAction() {
var myAttribute = ControllerContext
.ControllerDescriptor
.GetCustomAttributes<CustomActionFilter>()
.Single();
var success = myAttribute.success;
}
}