Problem: We are upgrading from a legacy system, so solutions are constrained. I am trying to route to an unauthorized controller if a specific query string is present. If it is not present, the user is routed to the authorized controller. This is on ASP.Net Core 2.1.
Is it possible to set the controller to route based on query string? I've tried
[/home/[action]?query={query}] -> Leads to runtime error due to '?'
[/home/[action]/{query}] - > maps to /home/index/1 (not what I need)
Thanks for any help!
Edit: Alternatively, is it possible to have a separate controller Action that depends on the query parameter?
public IActionResult Index(){}
public IActionResult Index([FromQuery]string query){}
Routing doesn't seem to distinguish between these two.
You can use IActionConstraint and IParameterModelConvention interfaces for that. In short, create an IActionConstraint like this:
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;
}
}
If a matching parameter is not found on the request's query string, then it will return false from the Accept method.
Than create RequiredFromQueryAttribute class like this:
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));
}
}
}
Than you could decorate your mandatory query string parameters with this attribute:
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet("{id}")]
public string Get(int id, [RequiredFromQuery]string foo, [RequiredFromQuery]string bar)
{
return id + " " + foo + " " + bar;
}
}
From now than, only the following URL GET api/values/5?foo=a&bar=b would lead into the action above, all other combinations of parameters would result in response with status 404, which you can eventually replace with what you want.
You can find more info at this link https://www.strathweb.com/2016/09/required-query-string-parameters-in-asp-net-core-mvc/
Related
I have an ASP.NET Core web API controller with (among others) two methods that have the same signature.
Shortened down, this looks as follows:
[Route("my/route")]
public class MyApiController : ApiController
{
[HttpGet("{*id}", Order = 2)]
[Route("{*id}", Order = 2)]
public MyObject Load([FromUri] String id) => new MyObject();
[HttpDelete("{*id}", Order = 1)]
[Route("{*id}", Order = 1)]
public void Delete([FromUri] String id)
{
}
}
Now, I am issuing a call:
GET my/route/123/456
Shockingly, this call ends up in the Delete method. I literally have a breapoint in the first line of my (in real life, non-empty) Delete method, and the Immediate window in VS tells me HttpContext.Request.Method is "GET", yet I end up in the method explicitly marked as HttpDelete.
What is going on here? Luckily, my call happened from within an automated test to test the web API, but if someone had issued that call to retrieve actual data from the DB, they would have ended up deleting that data instead. Is there any misunderstanding on my side with respect to the [HttpDelete] attribute?
You don't have to use route attribute and order parameter. It might be cause this situation.
[Route("my/route")]
public class MyApiController : ApiController
{
[HttpGet("{*id}")]
public MyObject Load([FromUri] String id) => new MyObject();
[HttpDelete("{*id}")]
public void Delete([FromUri] String id)
{
}
}
If you have an [ApiController] attribute, you have to remove it since you will need full explicit route attribute. Or it is much better to use explicit routes
[Route("my/[action]")]
public class MyApiController : ApiController
{
[HttpGet("my/load/{id}")]
public MyObject Load(string id) => new MyObject();
[HttpDelete("my/delete/{id}")]
[HttpGet("my/delete/{id}")]
public void Delete(string id)
{
}
}
I've got this controller with 2 actions, both GETs, the same name.
[ApiVersion("2")]
[Route("v{version:apiVersion}/[controller]")]
public class FooController : BaseController
And the two actions are like:
public IActionResult Get([FromServices]IBarService barService)
public IActionResult Get([FromServices]IBarService barService, string someParameter)
But I do need to distinguish those two actions as different operations with inputs and even outputs.
Also I cannot change the behaviour of the current API, it means, the final user should have access to the following listed paths:
GET v2/Foo
GET v2/Foo?someParameter={someParameter}
In the first place I had to add this line to make it work:
o.ResolveConflictingActions((apiDescriptions) => apiDescriptions.First());
It works, but when the OpenApiOperation are listed it had only the first one (for obvious reasons)
How can I add the query string parameters to make them work like 2 different paths?
Here is an alternative:
[RoutePrefix("foo/widgets")]
public class RoutePrefixController : ApiController
{
[Route("")]
public string Get()
{
return "value";
}
[Route("{id}")]
public string GetById(string id)
{
return "value " + id;
}
[Route("{id}/{name}")]
public string GetByIdByName(string id, string name)
{
return "value " + id + " " + name;
}
That uses RoutePrefix and Route your final enpoints are:
GET /foo/widgets
GET /foo/widgets/{id}
GET /foo/widgets/{id}/{name}
Here is how that looks like on swagger:
http://swagger-net-test.azurewebsites.net/swagger/ui/index?filter=RoutePrefix
I tried to convert ASP.NET WEB API to ASP.NET CORE WEB API and have errors
My code in ASP.NET WebAPI
public class TestController : ApiController
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
public object Get(string id)
{
return id;
}
// GET /test?id={id}&anyParam={anyParam}
public object Get(string id, string anyParam)
{
return id + anyParam;
}
}
config.Routes.MapHttpRoute("Controller", "{controller}");
Try to convert it to ASP.NET Core 2.1 / 3.0
[ApiController]
[Route("{controller}")]
public class TestController : ControllerBase
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
public object Get(string id)
{
return id;
}
// GET /test?id={id}&anyParam={anyParam}
public object Get(string id, string anyParam)
{
return id + anyParam;
}
}
services.AddControllers();
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
And i have in ASP.NET Core
AmbiguousMatchException: The request matched multiple endpoints
The sensible solution is just have one method that takes three parameters.
But, sensible solutions don't make for the most interesting stackoverflow answers, so here is how you can do this with two custom attributes, one which states the parameters that are required, and another which states which parameters are excluded:
public class RequireRequestParameterAttribute : ActionMethodSelectorAttribute
{
private readonly string[] _requiredNames;
public RequireRequestParameterAttribute(params string[] names)
{
this._requiredNames = names;
}
public override bool IsValidForRequest(
RouteContext routeContext,
ActionDescriptor action
) =>
this._requiredNames
.All(
routeContext
.HttpContext
.Request
.Query
.ContainsKey
);
}
public class DisallowRequestParameterAttribute : ActionMethodSelectorAttribute
{
private readonly string[] _forbiddenNames;
public DisallowRequestParameterAttribute(params string[] names)
{
this._forbiddenNames = names;
}
public override bool IsValidForRequest(
RouteContext routeContext,
ActionDescriptor action
) =>
!(this._forbiddenNames
.Any(
routeContext
.HttpContext
.Request
.Query
.ContainsKey
)
);
}
Now you can apply the attributes as follows:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
// GET test
public object Get()
{
return "Get";
}
// GET test?id={id}
[RequireRequestParameter("id")]
[DisallowRequestParameter("anyParam")]
public object Get(string id)
{
return id;
}
// GET test?id={id}&anyParam={anyParam}
[RequireRequestParameter("id", "anyParam")]
public object Get(string id, string anyParam)
{
return $"{id}: {anyParam}";
}
}
This means if you add another method with a third parameter, you have the maintenance burden of adding or modifying the DisallowRequestParameter attribute on the other methods.
I look your generated urls on actions and they are both /test which cause AmbiguousMatchException because your parameters are GET and are optional.
I think you can have same names on actions but you need define different ROUTE attribute (diff urls) on actions. Eg. you can not use default route with polymorphism on controller actions.
[Route("Home/About")]
MVC controllers Mapping of controllers now takes place inside
UseEndpoints.
Add MapControllers if the app uses attribute routing.
Source
https://learn.microsoft.com/cs-cz/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.0#attribute-routing
Thanks to daremachine with his answer I was able to find information on Google
First step in ASP.NET Core we need class which inherit ActionMethodSelectorAttribute
public class RequireRequestValueAttribute : ActionMethodSelectorAttribute
{
public RequireRequestValueAttribute(string name, string value = null)
{
Name = name;
Value = value;
}
public string Name { get; }
public string Value { get; }
public StringComparison ComparisonType { get; } = StringComparison.OrdinalIgnoreCase;
private bool ValueIsValid(object value)
{
return ValueIsValid(value?.ToString());
}
private bool ValueIsValid(string value)
{
if (Value == null)
{
return true;
}
return string.Equals(value, Value, ComparisonType);
}
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
var value = default(object);
if (routeContext.RouteData.Values.TryGetValue(Name, out value) && ValueIsValid(value))
return true;
if (routeContext.RouteData.DataTokens.TryGetValue(Name, out value) && ValueIsValid(value))
return true;
if (routeContext.HttpContext.Request.Query.ContainsKey(Name))
{
var values = routeContext.HttpContext.Request.Query[Name];
if (values.Count <= 0)
{
if (ValueIsValid(null))
return true;
}
else if (values.Any(v => ValueIsValid(v)))
return true;
}
return false;
}
}
Then we can add to question methods [RequireRequestValue("")], the controller will look like this
[ApiController]
[Route("{controller}")]
public class TestController : ControllerBase
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
[RequireRequestValue("id")]
public object Get(string id)
{
return id;
}
}
But it can't polymorphism two similar fields, type id in my question
For asp net core 2. If you try to implement the same logic as was in web api controllers then use Microsoft.AspNetCore.Mvc.WebApiCompatShim. This nuget package provides compatibility in ASP.NET Core MVC with ASP.NET Web API 2 to simplify migration of existing Web API implementations. Please check this answer. Starting with ASP.NET Core 3.0, the Microsoft.AspNetCore.Mvc.WebApiCompatShim package is no longer available.
I would like to pass information from the action filter (database) to the Action function.
Is it secure to use ActionContext Request.Properties.Add to store the data?
is there any chance that the information will be seen by the WEBAPI client or its safe as it safe to store information in the Cache\Session?
Is it a better way to do it?
The client will not see request properties unless you explicitly serialize them. They completely remain on the server side.
To answer your followup question here are two other ways to do it. There is no "Best" way per se. It all depends on how far you want the information to flow, and how generic you want your filter to be. My personal preference is using the controller object, but again it is just a preference.
For the sample here is a simple values controller and a POCO class:
[MyActionfilter]
public class ValuesController : ApiController
{
public string Foo { get; set; }
public User Get(User user)
{
if (Foo != null && user != null)
{
user.FamilyName = Foo;
}
return user;
}
}
public class User
{
public string FirstName { get; set; }
public string FamilyName { get; set; }
}
The action filter below is naively implementing access to the controller object or the method parameters. Note that it's up to you to either apply the filter sparingly or do type checks/dictionary checks.
public class MyActionfilter : ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
controller = actionContext.ControllerContext.Controller;
// Not safe unless applied only to controllers deriving
// from ValuesController
((ValuesController)controller).Foo = "From filter";
// Not safe unless you know the user is on the signature
// of the action method.
actionContext.ActionArguments["user"] = new User()
{
FirstName = "From filter"
};
}
}
If a requested resource is not found by the Service Layer returning null to the Web API controller; what is the best way to throw a HttpStatusCode.NotFound response back to the client without hard coding it in the controller, and by checking if it's null?
Personally I would just do the checks in the controllers as per Oppositional's comment but what you are asking for is perfectly reasonable. Again using action filters either attached per controller (or registered globally) you could do something along these lines:
Example Model:
public class Foo
{
public string Bar { get; set; }
}
Example Controller:
public class FoosController : ApiController
{
[NullObjectActionFilter]
public Foo Get(string id)
{
// - Returns model and 200
//return new Foo() { Bar = "TEST" };
// - Returns 404
//return null;
}
}
The Filter:
public class NullObjectActionFilter : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
object outValue = null;
actionExecutedContext.Response.TryGetContentValue<object>(out outValue);
if (outValue == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
base.OnActionExecuted(actionExecutedContext);
}
}
I agree with Mark that the ActionFilter is the way to go - small action methods are a good smell.
However, HttpActionExecutedContext.Response can be null when an exception occurs; and the NullObjectActionFilter class shown above can obscure error HTTP status codes. You're better off checking for successful exit and a successful HTTP code.
Here's an action filter I use:
/// <summary>
/// Converts <c>null</c> return values into an HTTP 404 return code.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class NullResponseIs404Attribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if ((actionExecutedContext.Response != null) && actionExecutedContext.Response.IsSuccessStatusCode)
{
object contentValue = null;
actionExecutedContext.Response.TryGetContentValue<object>(out contentValue);
if (contentValue == null)
{
actionExecutedContext.Response = actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "Object not found");
}
}
}
}