I am trying to support multiple Get() methods per controller, as well as just specially named methods accessible through web api. I have done this in MVC 5, but can't seem to figure out how it is done in MVC 6. Any ideas? Thanks.
You cannot have multiple Get methods with same url pattern. You can use attribute routing and setup multiple GET method's for different url patterns.
[Route("api/[controller]")]
public class IssuesController : Controller
{
// GET: api/Issues
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "item 1", "item 2" };
}
// GET api/Issues/5
[HttpGet("{id}")]
public string Get(int id)
{
return "request for "+ id;
}
// GET api/Issues/special/5
[HttpGet("special/{id}")]
public string GetSpecial(int id)
{
return "special request for "+id;
}
// GET another/5
[HttpGet("~/another/{id}")]
public string AnotherOne(int id)
{
return "request for AnotherOne method with id:" + id;
}
// GET api/special2/5
[HttpGet()]
[Route("~/api/special2/{id}")]
public string GetSpecial2(int id)
{
return "request for GetSpecial2 method with id:" + id;
}
}
You can see that i used both HttpGet and Route attributes for defining the route patterns.
With the above configuration, you you will get the below responses
Request Url : yourSite/api/issues/
Result ["value1","value2"]
Request Url : yourSite/api/issues/4
Result request for 4
Request Url : yourSite/api/special2/6
Result request for GetSpecial2 method with id:6
Request Url : yourSite/another/3
Result request for AnotherOne method with id:3
You can use attribute routing link this -
[Route("api/[controller]")] /* this is the defualt prefix for all routes, see line 20 for overridding it */
public class ValuesController : Controller
{
[HttpGet] // this api/Values
public string Get()
{
return string.Format("Get: simple get");
}
[Route("GetByAdminId")] /* this route becomes api/[controller]/GetByAdminId */
public string GetByAdminId([FromQuery] int adminId)
{
return $"GetByAdminId: You passed in {adminId}";
}
[Route("/someotherapi/[controller]/GetByMemberId")] /* note the / at the start, you need this to override the route at the controller level */
public string GetByMemberId([FromQuery] int memberId)
{
return $"GetByMemberId: You passed in {memberId}";
}
[HttpGet]
[Route("IsFirstNumberBigger")] /* this route becomes api/[controller]/IsFirstNumberBigger */
public string IsFirstNumberBigger([FromQuery] int firstNum, int secondNum)
{
if (firstNum > secondNum)
{
return $"{firstNum} is bigger than {secondNum}";
}
return $"{firstNum} is NOT bigger than {secondNum}";
}
}
See here for more detail - http://nodogmablog.bryanhogan.net/2016/01/asp-net-5-web-api-controller-with-multiple-get-methods/
Related
I've been trying to use Namespace routing to build some APIs dynamically without the need to worry about hardcoding the routes. However, I did find an example from MSDN to use namespaces and folder structure as your API structure. Here's the sample that I have to use Namespace routing:
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
private readonly string _baseNamespace;
public NamespaceRoutingConvention(string baseNamespace)
{
_baseNamespace = baseNamespace;
}
public void Apply(ControllerModel controller)
{
var hasRouteAttributes = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
return;
}
var namespc = controller.ControllerType.Namespace;
if (namespc == null) return;
var templateParts = new StringBuilder();
templateParts.Append(namespc, _baseNamespace.Length + 1, namespc.Length - _baseNamespace.Length - 1);
templateParts.Replace('.', '/');
templateParts.Append("/[controller]/[action]/{environment}/{version}");
var template = templateParts.ToString();
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template
};
}
}
}
And here's the controller:
namespace Backend.Controllers.Api.Project.Core
{
public class UserController : ApiBaseController
{
public UserController()
{
}
[HttpPost]
public IActionResult Login(LoginInput loginInput) // <-- loginInput properties return null
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
}
}
in Startup.cs
namespace Backend
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Let's use namespaces as the routing default way for our APIs
services.AddControllers(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention(typeof(Startup).Namespace + ".Controllers"));
});
}
}
}
Everything works ok except that when I trigger a POST api call to Login action the LoginInput doesn't get populated the values I'm sending through Postman i.e. {"username": "value", "password": "sample"} and it always returns null value. I'm not sure what am I doing wrong with the NamespaceRoutingConvention. Bear in mind if I remove it and hard-code the route in the controller like:
[ApiController]
[Route("api/project/core/[controller]/[action]/proda/v1")]
It works as expected. Any ideas?
Try to use this instead:
[HttpPost]
public IActionResult Login([FromBody]LoginInput loginInput)
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
I think that by setting AttributeRouteModel, you're preventing the middleware invoked by having ApiControllerAttribute in the Controller to do its job, and so the defaults of treating object parameters as body is not applied.
This is a guess though, I haven't been able to find the corresponding code in the source code.
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
Steps to reproduce:
Create a new Web API project
Create a UsersController with the following code
.
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id:int}", Name = nameof(GetUserByIdAsync))]
public async Task<ActionResult<object>> GetUserByIdAsync([FromRoute] int id)
{
object user = null;
return Ok(user);
}
[HttpPost]
public async Task<ActionResult<object>> CreateUserAsync()
{
object user = null;
return CreatedAtAction(nameof(GetUserByIdAsync), new { id = 1 }, user);
}
}
Call the url POST https://localhost:5001/users
You will get the exception
System.InvalidOperationException: No route matches the supplied
values.
Rename both methods by removing the Async from the method names, the methods should look like
[HttpGet("{id:int}", Name = nameof(GetUserById))]
public async Task<ActionResult<object>> GetUserById([FromRoute] int id)
{
// ...
}
[HttpPost]
public async Task<ActionResult<object>> CreateUser()
{
// ...
}
Call the url POST https://localhost:5001/users again
You will receive an empty 201 response
So I'm assuming the error occurs with the method names, any ideas?
System.InvalidOperationException: No route matches the supplied values.
To fix above error, you can try to set SuppressAsyncSuffixInActionNames option to false, like below.
services.AddControllers(opt => {
opt.SuppressAsyncSuffixInActionNames = false;
});
Or apply the [ActionName] attribute to preserve the original name.
[HttpGet("{id:int}", Name = nameof(GetUserByIdAsync))]
[ActionName("GetUserByIdAsync")]
public async Task<ActionResult<object>> GetUserByIdAsync([FromRoute] int id)
{
I have a controller that has multiple routes.
I am trying to call an endpoint stated as
GET: api/lookupent/2020-03-17T13:28:37.627691
but this results in this error
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:
Controllers.RecordController.Get (API)
Controllers.RecordController.GetRecordRegisteredAt (API)
but I am not sure I understand why this makes sense since this code
// GET: api/{RecordName}/{id}
[HttpGet("{RecordName}/{id}", Name = "GetRecord")]
public ActionResult Get(string RecordName, long id)
// GET: api/{RecordName}/{timestamp}
[HttpGet("{RecordName}/{timestamp}", Name = "GetRecordRegisteredAt")]
public ActionResult GetRecordRegisteredAt(string RecordName, string timestamp)
why does the input match with these endpoints?
You can fix this using route constraints.
Take a look at https://learn.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
Here's their example:
[Route("users/{id:int}")]
public User GetUserById(int id) { ... }
[Route("users/{name}")]
public User GetUserByName(string name) { ... }
The problem you have is that your controller has the same routing for 2 different methods receiving different parameters.
Let me illustrate it with a similar example, you can have the 2 methods like this:
Get(string entityName, long id)
Get(string entityname, string timestamp)
So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request.
You can change the routing which is one solution.
This solution gives you the ability to map your input to a complex type as well, otherwise use Route constraint for simple types
Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example
public class IntDto
{
public int i { get; set; }
}
public class StringDto
{
public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
dynamic model = null;
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;
var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];
if (contentType == "application/myContentType.json")
{
model = new StringDto{i = val};
}
else model = new IntDto{ i = int.Parse(val)};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
return new MyModelBinder();
return null;
}
and register it into the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
});
}
So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes, int order)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
Order = order;
}
public int Order { get; set; }
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}
Here is your final change:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
Now the error is gone if you run your app. But how you pass the parameters?:
This one is going to hit this method:
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Run it and let me know
I had the same issue for these two methods:
[HttpPost]
public async Task<IActionResult> PostFoos(IEnumerable<FooModelPostDTO> requests)
[HttpPost]
public async Task<IActionResult> GetFoos(GetRequestDTO request)
The first one is for getting entities (using Post) and the second one is for posting new entities in DB (again using Post).
One possible solution is to distinguish between them by their's method names (../[action]) with the Route attribute:
[Route("api/[controller]/[action]")]
[ApiController]
public class FoosController : ControllerBase
I have a Web API Controller with a method called GetHeroes() and it doesn't get called by the front end. I can get a simple Get() method to work but there doesn't seem to be a way to name methods and have these methods called.
CharactersController.cs
[Route("api/{controller}/{action}")]
public class CharactersController : Controller
{
private readonly ICharacterRepository _characterRepository;
public CharactersController(ICharacterRepository characterRepository)
{
_characterRepository = characterRepository;
}
[HttpGet]
public IEnumerable<Character> GetHeroes()
{
return _characterRepository.ListAll().OrderBy(x => x.Name);
}
}
data.service.ts
getItems() {
this.http.get('api/characters/getheroes').map((res: Response) => res.json()).subscribe(items => {
this._collectionObserver.next(items);
});
}
You can specify route and parameters in HttpGet attribute. Have you tried something like this?
[Route("api/[controller]")]
public class CharactersController : Controller
{
...
[HttpGet("GetHeroes")] // Here comes method name
public IEnumerable<Character> GetHeroes()
{
return _characterRepository.ListAll().OrderBy(x => x.Name);
}
}
Here is a good article about routing:
Custom Routing and Action Method Names in ASP.NET 5 and ASP.NET MVC 6
This works for ASP.NET Core:
[HttpGet("{id}", Name = "GetHero")]
public IActionResult GetById(string id)
{
var hero = Heroes.Find(id);
if (hero == null)
{
return NotFound();
}
return new ObjectResult(hero);
}