i'm making a Web API and i want to retrieve the object created by the CreateCommand Method.
To do that, I'm using the CreateAtRoute function to call the GetCommandById function ,with the id of the created Command as parameter, but i'm getting the following error:
" System.InvalidOperationException: No route matches the supplied
values."
This is my controller:
[Route("api/commands")]
[ApiController]
public class CommandsController : Controller
{
private readonly ICommanderRepo _repository;
private readonly IMapper _mapper;
public CommandsController(ICommanderRepo repository,IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
[HttpGet]
public ActionResult <IEnumerable<CommandReadDto>> GetAllCommands()
{
var commands = _repository.GetAllCommands();
return Ok(_mapper.Map<IEnumerable<CommandReadDto>>(commands));
}
[HttpGet("{id}")]
public ActionResult <CommandReadDto> GetCommandById(int id)
{
var command = _repository.GetCommandById(id);
if(command != null)
{
return Ok(_mapper.Map<CommandReadDto>(command));
}
return NotFound();
}
[HttpPost]
public ActionResult <CommandReadDto> CreateCommand(CommandCreateDto commandCreateDto)
{
var commandModel = _mapper.Map<Command>(commandCreateDto);
_repository.CreateCommand(commandModel);
_repository.SaveChanges();
var commandReadDto = _mapper.Map<CommandReadDto>(commandModel);
var x = nameof(GetCommandById);
return CreatedAtRoute(nameof(GetCommandById), new { id = commandReadDto.Id }, commandReadDto);
}
I have already tried this (which didn't resolve the problem):
Check if the parameters of both functions match
Added to my Startup.cs : services.AddControllers(options => options.SuppressAsyncSuffixInActionNames = false); ( i saw this on a post here in stackoverflow
What might be the problem?
When using CreatedAtRoute, you'll need something like shown below. Please note the addition of a route name, and use of that route name in CreatedAtRoute.
[HttpGet("{id}", Name="GetCommand")]
public ActionResult <CommandReadDto> GetCommandById(int id)
{
... // your code here
}
[HttpPost]
public ActionResult <CommandReadDto> CreateCommand(CommandCreateDto commandCreateDto)
{
... // your code here
return CreatedAtRoute("GetCommand", new { commandReadDto.Id }, commandReadDto);
}
An alternative, is to use CreatedAtAction like shown below. With this approach, a route name is not required.
return CreatedAtAction("GetCommandById", new { commandReadDto.Id }, commandReadDto);
You should derive your API-controllers from ControllerBase instead of Controller. The latter is targeting MVC-controllers.
And I believe you should remove options.SuppressAsyncSuffixInActionNames until you actually need it.
Related
I´m trying to make a simple api on .net core 3.1, I deleted the wheather thing that comes from default. I put a new starting point but despite many efforts and changes, I still get an 404
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
In properties, start explorer:
api/Servicio/GetServicio/tecnico/Pedro/semanaDelAno/8
Controller
[Route("api/Servicio")]
public class ServicioController : Controller
{
private readonly ApplicationDbContext _context;
public ServicioController(ApplicationDbContext context)
{
_context = context;
}
// POST: api/PostServicio
[HttpPost]
public async Task<ActionResult<Servicio>> PostServicio(Servicio servicio)
{
_context.Servicio.Add(servicio);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetServicio), new { id = servicio.Id }, servicio);
}
// GET: api/GetServicio/5
//[HttpGet("{tecnico}/{semanaDelAno}")]
[HttpGet("GetServicio/{tecnico}/{semanaDelAno}")]
public async Task<ActionResult<Servicio>> GetServicio(string tecnico, int semanaDelAno)
{
var servicio = await _context.Servicio.FirstOrDefaultAsync(i => i.Tecnico == tecnico && i.SemanaDelAno == semanaDelAno);
if (servicio == null)
{
return NotFound();
}
return servicio;
}
Assuming tecnico=Pedro and semanaDelAno=8, you have to use this url
~/api/Servicio/GetServicio/Pedro/8
for GetServicio action:
[HttpGet("GetServicio/{tecnico}/{semanaDelAno}")]
public async Task<ActionResult<Servicio>> GetServicio(string tecnico, int semanaDelAno)
{
.... your code
}
There are huge numbers of questions about the "No route matches the supplied values" error, but I have not yet found any solutions among the answers :(
Here is my controller:
[ApiVersion("0.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class WidgetsController : ControllerBase
{
private readonly IRepository _repository;
public WidgetsController(IRepository repository)
{
_repository = repository;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult Add([FromBody] AddWidgetRequest request)
{
WidgetDetails details;
try
{
details = request.ToWidgetDetails();
}
catch (AddWidgetRequest.BadRequest e)
{
return BadRequest(e.Message);
}
var id = _repository.AddWidget(details);
return CreatedAtAction(nameof(GetById), new {id = id}, details.WithId(id));
}
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
if (_repository.TryGetWidget(id, out var details))
{
return Ok(details.WithId(id));
}
else
{
return NotFound();
}
}
}
When POSTing to /api/v0/Widgets, the new entry is added to the database, but HTTP 500 is returned, with message "System.InvalidOperationException: No route matches the supplied values.". My code is almost identical to the example in https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-3.1, I'm at a loss as to what the issue could be.
You need specify the api version in the CreatedAtAction method like below:
public IActionResult Add([FromBody] AddWidgetRequest request,ApiVersion version)
{
return CreatedAtAction(nameof(GetById), new { id = 1, version = version.ToString() }, details.WithId(id));
}
I want to call an ASP.NET Core 2.1.0 Web API with a controller's method.
I tried following but I get an error
Cannot GET /api/remote/NewAc/test1
Code:
[Route("api/remote/{action}")]
//[Route("api/[controller]")]
[ApiController]
public class RemoteController : ControllerBase
{
private readonly MyContext _context;
public RemoteValsController(MyContext context)
{ _context = context; }
[HttpGet]
public async Task<OkObjectResult> NewAc()
{
var r = await _context.TypeOfAccounts.AnyAsync();
return Ok(new { r = true });
}
[HttpGet("{id}")]
public async Task<OkObjectResult> NewAc([FromRoute] string AccountType)
{
var r = await _context.TypeOfAccounts.AnyAsync(o => o.AccountType.ToUpper() == AccountType.ToUpper());
return Ok(new { r = !r });
}
}
Startup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
I tried both [HttpPost] and [HttpGet] but in vain.
Re-check the defined routes for the controller.
[Route("api/remote/[action]")] //<-- NOTE Token replacement in route templates
[ApiController]
public class RemoteController : ControllerBase {
private readonly MyContext _context;
public RemoteController(MyContext context) {
_context = context;
}
//GET api/remote/NewAc
[HttpGet]
public async Task<IActionResult> NewAc() {
var r = await _context.TypeOfAccounts.AnyAsync();
return Ok(new { r = true });
}
//GET api/remote/NewAc/test1
[HttpGet("{accountType}")]
public async Task<IActionResult> NewAc(string accountType) {
var r = await _context.TypeOfAccounts.AnyAsync(o => o.AccountType.ToUpper() == accountType.ToUpper());
return Ok(new { r = !r });
}
}
Reference Routing to controller actions in ASP.NET Core
First, mapped routes and attribute routing are an either/or affair. If you have route attributes involved, the route definition in your Startup is not applicable at all.
Second, you can't just throw [FromRoute] in front a param and magically have it in the route. In fact, that attribute isn't necessary at all unless there's some ambiguity about where the param value is actually supposed to come from. If you want it from the route, then it needs to be part of your route template. Simply:
[HttpGet("{id}/{AccountType}")]
public async Task<OkObjectResult> NewAc(string AccountType)
{
var r = await _context.TypeOfAccounts.AnyAsync(o => o.AccountType.ToUpper() == AccountType.ToUpper());
return Ok(new { r = !r });
}
I'm trying to create multiple level routes in asp.net core such as:
api/cities
api/cities/{id}
api/cities/date/{date}
The problem is, when I try and use anything longer than the api/cities/{id} I just get a 404.
My controller:
[Route("api/[controller]")]
public class CitiesController : Controller
{
private ICityRepository _repository;
public CitiesController(ICityRepository repository)
{
_repository = repository;
}
// GET: api/cities
[HttpGet]
public IEnumerable<City> Get()
{
IEnumerable<City> results = _repository.GetCities();
return results;
}
//api/cities/date/{date}
[HttpGet]
[Route("date/{date}")]
public IEnumerable<City> Get2(string date)
{
return _repository.GetCitiesByDate(date);
}
// GET api/cities/5
[HttpGet("{id: int}")]
public string Get(int id)
{
return "value";
}
}
What do I need to do to get longer routes to work under this controller?
Edit:
I see the documentation here: https://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
It says that you can have routes like:
/orders/1
/orders/pending
/orders/2013/06/16
And have all three route separately. But it doesn't seem to provide example for how you do that specifically.
I can see the problem with the route as you cannot have "/" as part of the string.
Try passing the date as 2013_06_16 not 2013/06/16 or change the route to have date/{year}/{month}/{day}
example:
[Route("api/[controller]")]
public class CitiesController : Controller
{
private ICityRepository _repository;
public CitiesController(ICityRepository repository)
{
_repository = repository;
}
// GET: api/cities
[HttpGet]
public IEnumerable<City> Get()
{
IEnumerable<City> results = _repository.GetCities();
return results;
}
//api/cities/date/2016/06/16
[HttpGet]
[Route("date/{year}/{month}/{day}")]
public IEnumerable<City> Get2(string year,string month,string day)
{
string date = year + "/" + month + "/" + day;
return _repository.GetCitiesByDate(date);
}
// GET api/cities/5
[Route("{id: int}")]
[HttpGet]
public string Get(int id)
{
return "value";
}
//api/cities/pending
[Route("{text}"]
[HttpGet]
public string Get(string text)
{
return "value";
}
}
Hope this help.
I've got a pretty basic controller method that returns a list of Customers. I want it to return the List View when a user browses to it, and return JSON to requests that have application/json in the Accept header.
Is that possible in ASP.NET Core MVC 1.0?
I've tried this:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
But that returns JSON by default, even if it's not in the Accept list. If I hit "/customers" in my browser, I get the JSON output, not my view.
I thought I might need to write an OutputFormatter that handled text/html, but I can't figure out how I can call the View() method from an OutputFormatter, since those methods are on Controller, and I'd need to know the name of the View I wanted to render.
Is there a method or property I can call to check if MVC will be able to find an OutputFormatter to render? Something like the following:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
if(Response.WillUseContentNegotiation)
{
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
else
{
return View(customers.Select(c => new { c.Id, c.Name }));
}
}
I think this is a reasonable use case as it would simplify creating APIs that return both HTML and JSON/XML/etc from a single controller. This would allow for progressive enhancement, as well as several other benefits, though it might not work well in cases where the API and Mvc behavior needs to be drastically different.
I have done this with a custom filter, with some caveats below:
public class ViewIfAcceptHtmlAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Request.Headers["Accept"].ToString().Contains("text/html"))
{
var originalResult = context.Result as ObjectResult;
var controller = context.Controller as Controller;
if(originalResult != null && controller != null)
{
var model = originalResult.Value;
var newResult = controller.View(model);
newResult.StatusCode = originalResult.StatusCode;
context.Result = newResult;
}
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
}
which can be added to a controller or action:
[ViewIfAcceptHtml]
[Route("/foo/")]
public IActionResult Get(){
return Ok(new Foo());
}
or registered globally in Startup.cs
services.AddMvc(x=>
{
x.Filters.Add(new ViewIfAcceptHtmlAttribute());
});
This works for my use case and accomplishes the goal of supporting text/html and application/json from the same controller. I suspect isn't the "best" approach as it side-steps the custom formatters. Ideally (in my mind), this code would just be another Formatter like Xml and Json, but that outputs Html using the View rendering engine. That interface is a little more involved, though, and this was the simplest thing that works for now.
I haven't tried this, but could you just test for that content type in the request and return accordingly:
var result = customers.Select(c => new { c.Id, c.Name });
if (Request.Headers["Accept"].Contains("application/json"))
return Json(result);
else
return View(result);
I liked Daniel's idea and felt inspired, so here's a convention based approach as well. Because often the ViewModel needs to include a little bit more 'stuff' than just the raw data returned from the API, and it also might need to check different stuff before it does its work, this will allow for that and help in following a ViewModel for every View principal. Using this convention, you can write two controller methods <Action> and <Action>View both of which will map to the same route. The constraint applied will choose <Action>View if "text/html" is in the Accept header.
public class ContentNegotiationConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
if (action.ActionName.ToLower().EndsWith("view"))
{
//Make it match to the action of the same name without 'view', exa: IndexView => Index
action.ActionName = action.ActionName.Substring(0, action.ActionName.Length - 4);
foreach (var selector in action.Selectors)
//Add a constraint which will choose this action over the API action when the content type is apprpriate
selector.ActionConstraints.Add(new TextHtmlContentTypeActionConstraint());
}
}
}
public class TextHtmlContentTypeActionConstraint : ContentTypeActionConstraint
{
public TextHtmlContentTypeActionConstraint() : base("text/html") { }
}
public class ContentTypeActionConstraint : IActionConstraint, IActionConstraintMetadata
{
string _contentType;
public ContentTypeActionConstraint(string contentType)
{
_contentType = contentType;
}
public int Order => -10;
public bool Accept(ActionConstraintContext context) =>
context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().Contains(_contentType);
}
which is added in startup here:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o => { o.Conventions.Add(new ContentNegotiationConvention()); });
}
In you controller, you can write method pairs like:
public class HomeController : Controller
{
public ObjectResult Index()
{
//General checks
return Ok(new IndexDataModel() { Property = "Data" });
}
public ViewResult IndexView()
{
//View specific checks
return View(new IndexViewModel(Index()));
}
}
Where I've created ViewModel classes meant to take the output of API actions, another pattern which connects the API to the View output and reinforces the intent that these two represent the same action:
public class IndexViewModel : ViewModelBase
{
public string ViewOnlyProperty { get; set; }
public string ExposedDataModelProperty { get; set; }
public IndexViewModel(IndexDataModel model) : base(model)
{
ExposedDataModelProperty = model?.Property;
ViewOnlyProperty = ExposedDataModelProperty + " for a View";
}
public IndexViewModel(ObjectResult apiResult) : this(apiResult.Value as IndexDataModel) { }
}
public class ViewModelBase
{
protected ApiModelBase _model;
public ViewModelBase(ApiModelBase model)
{
_model = model;
}
}
public class ApiModelBase { }
public class IndexDataModel : ApiModelBase
{
public string Property { get; internal set; }
}