I don't know enough about C#, .NET, or the MVC pattern to know exactly what is relevant to include here, but I'm pulling my hair out with a very simple change I'm working on.
I have a controller with a Search action (method?) that looks like:
public string Search(int id)
{
return $"The id was {id}";
}
and when I hit the route I get the expected response, e.g.
$ curl https://localhost:7180/Players/Search/1
The id was 1
but when I change the variable name from id to anything else, the behavior changes and the value goes to 0 for some reason.
public string Search(int thing)
{
return $"The thing was {thing}";
}
$ curl https://localhost:7180/Players/Search/1
The thing was 0
I thought maybe it had to do with the Model itself, because the model code at least has an Id attribute
public class Player
{
public int Id { get; set; }
public string? Name { get; set; }
}
but renaming that variable to name (which seems analogous) also doesn't help.
So what concept am I missing here? Why can't I just rename that variable to whatever I want? Thanks in advance!
(I don't know how better to communicate all the different aspects of the code, so here is a link to the line in question, inside the project)
By default MVC registers (see either Program or Startup) next default route, so it can bind id parameter of method as positional part of path:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
You can change the parameter name for example using attribute routing:
[Route("[controller]/search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
Or using HTTP verb templates:
[HttpGet("[controller]/search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
Check the linked docs for other options/details.
I believe this has to do with the way you've defined your route in Program.cs:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
You'll want to add a new definition like this:
app.MapControllerRoute(
name: "default",
pattern: "Players/Search/{thing?}");
or, you could use attribute-based route definitions to move the route pattern definition closer to the actual code. See the MSFT docs for details. Basically, add app.MapControllers(); to Program.cs, then for your individual routes, do something like this:
[Route("Players/Search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
You can decorate the method and define the parameter.
// GET api/values/5
[HttpGet("{id}")]
public virtual async Task<ActionResult<IEntity>> Get(string id)
{
var entity = await Repository.GetEntity(x => x.Id == id);
if (entity == null) return NotFound();
return Ok(entity);
}
Here is another example of an API Controller
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly GameDataContext context;
public UserController(GameDataContext context)
{
this.context = context;
}
// GET: /<controller>/
// GET api/user
[HttpGet]
public ActionResult<IEnumerable<User>> Get()
{
return context.Users.Where(x => x.IsDeleted == false).ToArray();
}
// GET api/user/5
[HttpGet("{id}", Name = "GetUser")]
public ActionResult<User> Get(int id)
{
var user = context.Users.FirstOrDefault(x => x.Id == id && x.IsDeleted == false);
if (user != null)
{
return user;
}
return NotFound();
}
// GET api/user/username/5
[HttpGet("username/{id}", Name = "GetUserByGameId")]
public ActionResult<User> GetByUser(string gameId)
{
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user != null)
{
return user;
}
return NotFound();
}
// POST api/user
[HttpPost]
public async Task<ActionResult> Post([FromBody] User value)
{
if (value == null)
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(value.UserGameId) && x.IsDeleted == false);
if (user != null)
{
return BadRequest("User already exists!");
}
context.Users.Add(value);
await context.SaveChangesAsync();
return CreatedAtRoute("GetUser", new { id = value.Id }, value);
}
// PUT api/user/steamId
[HttpPut("{gameId}")]
public async Task<ActionResult> Put(string gameId, [FromBody] User value)
{
if (value == null || !value.UserGameId.Equals(gameId))
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user == null)
{
return NotFound();
}
user.UserGameId = value.UserGameId;
user.FirstName = value.FirstName;
user.MiddleName = value.MiddleName;
user.LastName = value.LastName;
user.Email = value.Email;
context.Users.Update(user);
await context.SaveChangesAsync();
return new NoContentResult();
}
// DELETE api/user/steamId
[HttpDelete("{gameId}")]
public async Task<ActionResult> Delete(string gameId)
{
if (string.IsNullOrEmpty(gameId))
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user == null)
{
return NotFound();
}
user.IsDeleted = true;
context.Users.Update(user);
var scores = context.Scores.Where(x => x.UserId == user.Id);
foreach (var score in scores)
{
score.IsDeleted = true;
context.Scores.Update(score);
}
await context.SaveChangesAsync();
return new NoContentResult();
}
}
Related
I use ASP.NET Core 5 MVC and I have an interesting issue. When I want load the Index method of any controller, then I have to write index action name into URL too. If i write only controller name into the URL, then it load nothing. I don't get any exception. I haven't folder in root directory with name what matching with any controller name. I use default routing, so i don't understand what is the problem. But i noticed an interesting stuff while debugging. I tried set a custom route path with data-annotation. When I renamed the controller, then I get an AmbiguousMatch exception.
"AmbiguousMatchException: The request matched multiple endpoints.
Matches:
CanteenFeedback2._0.Controllers.AdminPanelController.Login
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.EditQuestion
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.Statistic
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.GetStatistic
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.Index
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.AuthError
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.HomeController.Index
(CanteenFeedback2.0)"
A detail from AdminPanelController:
public class AdminPanelController : Controller
{
private readonly DatabaseContext DB;
public AdminPanelController(DatabaseContext dB)
{
DB = dB;
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
string userName = User.Claims.Where(claim => claim.Type == ClaimTypes.NameIdentifier).First().Value;
string lastName = User.Claims.Where(claim => claim.Type == ClaimTypes.GivenName).First().Value;
bool exists = false;
string userId = User.Claims.Where(claim => claim.Type == ClaimTypes.NameIdentifier).First().Value;
if (DB.IdmAccounts.FirstOrDefault(i => i.AccountId == userId) != null)
{
exists = true;
}
else
{
exists = false;
}
IEnumerable<Category> categories;
IEnumerable<Subcategory> subcategories;
IEnumerable<Question> questions;
try
{
categories = DB.Categories;
subcategories = DB.Subcategories;
questions = DB.Questions
.Where(q => q.isArchived == false)
.Include(q => q.Categories)
.Include(q => q.Subcategories)
.ThenInclude(sc => sc.Category);
}
catch (SqlException)
{
return Content("Nem sikerült betölteni a kérdéseket");
}
catch (Exception)
{
return Content("Nem sikerült betölteni a kérdéseket");
}
ViewBag.lastName = lastName;
ViewBag.exists = exists;
ViewBag.categories = categories;
ViewBag.subcategories = subcategories;
ViewBag.questions = questions;
return View();
}
[HttpPost]
public IActionResult SessionSave(string token)
{
HttpContext.Session.SetString("JWToken", token);
return Redirect("http://localhost:5000/AdminPanel/Index");
}
[HttpGet]
public IActionResult Login()
{
return View();
}
public IActionResult AuthError()
{
return View();
}
}
And a detail from HomeController:
public class HomeController : Controller
{
public IActionResult Index()
{
// Cookie options
CookieOptions cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Now.AddDays(365);
// Add default cookie
Response.Cookies.Append("lang_cookie", Languages.HU.ToString(), cookieOptions);
// Setting language to view
Language language = new Language(Pages.Home, Languages.HU);
return View(language.GetPageTexts());
}
[Route("{changedLan}")]
public IActionResult Index(Languages changedLan)
{
// Cookie options
CookieOptions cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Now.AddDays(365);
// Update cookie
Response.Cookies.Append("lang_cookie", changedLan.ToString(), cookieOptions);
Language language = new Language(Pages.Home, changedLan);
return View(language.GetPageTexts());
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
I have a C# .NET 5.0 ASP.NET Core Web API application with "Enable OpenAPI support" selected. I want to hide the optional parameter in the below example from what shows up on the swagger page. I have found numerous posts about hiding a property or the controller but none of these solutions seem to work for just the parameter in the given code:
[HttpGet]
[Route("search")]
[Authorize]
public async Task<IActionResult> Search(string query, string optional = "")
{
return OK();
}
You can create a custom attibute and an operation filter inhering from Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter to exclude the desired parameters from swagger.json generation
public class OpenApiParameterIgnoreAttribute : System.Attribute
{
}
public class OpenApiParameterIgnoreFilter : Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter
{
public void Apply(Microsoft.OpenApi.Models.OpenApiOperation operation, Swashbuckle.AspNetCore.SwaggerGen.OperationFilterContext context)
{
if (operation == null || context == null || context.ApiDescription?.ParameterDescriptions == null)
return;
var parametersToHide = context.ApiDescription.ParameterDescriptions
.Where(parameterDescription => ParameterHasIgnoreAttribute(parameterDescription))
.ToList();
if (parametersToHide.Count == 0)
return;
foreach (var parameterToHide in parametersToHide)
{
var parameter = operation.Parameters.FirstOrDefault(parameter => string.Equals(parameter.Name, parameterToHide.Name, System.StringComparison.Ordinal));
if (parameter != null)
operation.Parameters.Remove(parameter);
}
}
private static bool ParameterHasIgnoreAttribute(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription parameterDescription)
{
if (parameterDescription.ModelMetadata is Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata metadata)
{
return metadata.Attributes.ParameterAttributes.Any(attribute => attribute.GetType() == typeof(OpenApiParameterIgnoreAttribute));
}
return false;
}
}
Put it in your controller's parameter
[HttpGet]
[Route("search")]
[Authorize]
public async Task<IActionResult> Search(string query, [OpenApiParameterIgnore] string optional = "")
{
return Ok();
}
Then configure it in Status.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API Title", Version = "v1" });
c.OperationFilter<OpenApiParameterIgnoreFilter>();
});
I have created the following web service and can access it by:
https://localhost:44311/valores/1
but I want to access it with a url like:
https://localhost:44311/usuario/1
using usuario in the url
[HttpGet("{id:int}",Name ="usuario")]
public ActionResult<Usuario> Get(int id)
{
using (var db = new prueba2Context())
{
var usuario = db.Usuario.Where(x => x.Id == id).FirstOrDefault();
if (usuario == null)
{
return NotFound();
}
return Ok(usuario);
}
}
I am new to c#, I appreciate if you indicate what I am doing wrong and how to correct it.
This is the structure of my folder.
It looks like you are using ASP.NET Core. A typical endpoint will be set up like this:
[ApiController, Route("api/[controller]")]
public class ComputationController : ControllerBase
{
// ...
[HttpPost, Route("beginComputation")]
[ProducesResponseType(StatusCodes.Status202Accepted, Type = typeof(JobCreatedModel))]
public async Task<IActionResult> BeginComputation([FromBody] JobParametersModel obj)
{
return Accepted(
await _queuedBackgroundService.PostWorkItemAsync(obj).ConfigureAwait(false));
}
[HttpGet, Route("computationStatus/{jobId}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(JobModel))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(string))]
public async Task<IActionResult> GetComputationResultAsync(string jobId)
{
var job = await _computationJobStatusService.GetJobAsync(jobId).ConfigureAwait(false);
if(job != null)
{
return Ok(job);
}
return NotFound($"Job with ID `{jobId}` not found");
}
// ...
}
The [ProducesResponseType] attribute is for documentation frameworks such as Swagger.
I always use the [Route] attribute to define the endpoint path.
In your case, I would set it up as so:
[HttpGet, Route("usuario/{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Usuario))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetUser(int id)
{
using (var db = new prueba2Context())
{
var usuario = await db.Usuario.Where(x => x.Id == id).FirstOrDefault();
if (usuario == null)
{
return NotFound();
}
return Ok(usuario);
}
}
Is more common use "Route"
like that
`[Route("usuario")]
public ActionResult<Usuario> Get(int id)
{
using (var db = new prueba2Context())
{
var usuario = db.Usuario.Where(x => x.Id == id).FirstOrDefault();
if (usuario == null)
{
return NotFound();
}
return Ok(usuario);
}
}
`
I have a controller that is made with ASP.NET and I really want to simplify that thing with quick view:
// REST representation of Storage
// There is always at least two options to view them
// Data as is or Quick view at metrics averages
[Route("metrics")]
public class MetricsController : Controller
{
// Get raw Storage object
[HttpGet]
public IActionResult GetStorageView()
{
// TODO: do not use in production
WSManModule.HyperVMetric.test(false);
//
var response = MetricsService.Instance.GetRawMetrics();
if (response == null)
{
return NotFound();
}
if (Request.QueryString.Value == "?q=quick")
{
return Ok(new StorageQuickView(response));
}
return Ok(response);
}
// Get metrics for specific device
[HttpGet("{deviceName}")]
public IActionResult GetDeviceView(string deviceName)
{
var response = MetricsService.Instance.GetDeviceMetrics(deviceName);
if (response == null)
{
return NotFound();
}
if (Request.QueryString.Value == "?q=quick")
{
return Ok(new DeviceQuickView(response));
}
return Ok(response);
}
// Get metrics for specific component within the device
[HttpGet("{deviceName}/{componentName}")]
public IActionResult GetComponentView(string deviceName, string componentName)
{
var response = MetricsService.Instance.GetComponentMetrics(deviceName, componentName);
if (response == null)
{
return NotFound();
}
if (Request.QueryString.Value == "?q=quick")
{
return Ok(new ComponentQuickView(response));
}
return Ok(response);
}
}
now it does have a lot of repetition and I don't like it.
Is there any way to do it right with optional parameters like {quick?} or something similar?
Simply: I want to perform different operations if we have /quick at the end of the route or no.
Just accept the q parameter with your actions:
// Get raw Storage object
[HttpGet]
public IActionResult GetStorageView(string q)
{
// TODO: do not use in production
WSManModule.HyperVMetric.test(false);
//
var response = MetricsService.Instance.GetRawMetrics();
if (response == null)
{
return NotFound();
}
if (q == "quick")
{
return Ok(new StorageQuickView(response));
}
return Ok(response);
}
// Get metrics for specific device
[HttpGet("{deviceName}")]
public IActionResult GetDeviceView(string deviceName, string q)
{
var response = MetricsService.Instance.GetDeviceMetrics(deviceName);
if (response == null)
{
return NotFound();
}
if (q == "quick")
{
return Ok(new DeviceQuickView(response));
}
return Ok(response);
}
The action method parameters are not just derived from routes. The values come from Value Providers, and one of the default providers parses the query string. So, you only need to add the query string value to your action method parameters rather than parsing or comparing the query string manually.
you can create a private method like this:
private IAction ProcessResponse<T>(IMyResponseType response)
{
if(response == null)
{
return NotFound();
}
if (Request.QueryString.Value == "?q=quick")
{
var okInstance = (T) Activator.CreateInstance(typeof (T), response);
return Ok(okInstance);
}
return Ok(response);
}
and use it like this:
// Get metrics for specific component within the device
[HttpGet("{deviceName}/{componentName}")]
public IActionResult GetComponentView(string deviceName, string componentName)
{
var response = MetricsService.Instance.GetComponentMetrics(deviceName, componentName);
return ProcessResponse<ComponentQuickView>(response);
}
// Get raw Storage object
[HttpGet]
public IActionResult GetStorageView()
{
// TODO: do not use in production
WSManModule.HyperVMetric.test(false);
//
var response = MetricsService.Instance.GetRawMetrics();
return ProcessResponse<StorageQuickView>(response);
}
I created a simple Web API with ASP.NET Core. I have the following API:
GET /api/messages - get all messages
GET /api/messages/{id} - get a message by id
POST /api/messages - add a new message
PUT /api/messages/{id} - update an existing message
DELETE /api/messages/{id} - delete a message
Now, I want another API to get all messages by message owner's name.
What I tried:
I tried to create this API, but it doesn't work because it conflicts with GET /api/messages/{id}:
GET /api/messages/{name} <- (doesn't work due to conflicting API)
// GET: api/messages/{name}
[HttpGet("{name}")]
public IEnumerable<Message> GetMessagesByName(string name)
{
return _repository.GetMessages().Where(m => m.Owner == name);
}
Here is my Message model Message.cs:
public class Message
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string Owner { get; set; }
public string Text { get; set; }
}
Here is my Messages controller MessagesController.cs:
[Route("api/[controller]")]
public class MessagesController : Controller
{
private readonly IMessageRepository _repository;
public MessagesController(IMessageRepository repository)
{
_repository = repository;
}
// GET: api/messages
[HttpGet]
public IEnumerable<Message> Get()
{
return _repository.GetMessages();
}
// GET api/messages/{id}
[HttpGet("{id}", Name = "GetMessage")]
public IActionResult GetById(long id)
{
var message = _repository.GetMessage(id);
if (message == null)
{
return NotFound();
}
return new ObjectResult(message);
}
// POST api/messages
[HttpPost]
public IActionResult Post([FromBody]Message message)
{
if (message == null)
{
return BadRequest();
}
_repository.AddMessage(message);
return CreatedAtRoute("GetMessage", new { id = message.Id }, message);
}
// PUT api/messages/{id}
[HttpPut("{id}")]
public IActionResult Put(long id, [FromBody]Message message)
{
if (message == null || message.Id != id)
{
return BadRequest();
}
var messageToUpdate = _repository.GetMessage(id);
if (messageToUpdate == null)
{
return NotFound();
}
messageToUpdate.Owner = message.Owner;
messageToUpdate.Text = message.Text;
_repository.UpdateMessage(messageToUpdate);
return new NoContentResult();
}
// DELETE api/messages/{id}
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var message = _repository.GetMessage(id);
if (message == null)
{
return NotFound();
}
_repository.RemoveMessage(id);
return new NoContentResult();
}
}
Question:
How can I create an API method to get all messages by message owner's name?
Ideally, I would like the API to look like GET /api/messages/{name}, but don't think its possible since it conflicts with GET /api/messages/{id}.
I'm thinking of creating the API like this, but I'm not sure how.
GET /api/messages/name/{name} <- (or something along that line)
Solution:
To have GET /api/messages/{name} working without conflicting with GET /api/messages/{id}, change attribute [HttpGet("{id}", Name="GetMessage")] to [HttpGet("{id:long}", Name="GetMessage")] for public IActionResult GetById(long id) method.
To also have GET /api/messages/name/{name} working, add [Route("name/{name}")] attribute to public IEnumerable<Message> GetMessagesByName(string name) method.
you can put parameter type in route, so your code method should be look like that:
// GET api/messages/{id}
[HttpGet("{id:long}", Name = "GetMessage")]
public IActionResult GetById(long id)
{
var message = _repository.GetMessage(id);
if (message == null)
{
return NotFound();
}
return new ObjectResult(message);
}
I think, web api is ignoring parameters types in routes if they are not typed explicitly, so in your example it has two routes like this: api/messages/{object} and when you put explicit type, they are like this: api/messages/{object} and api/messages/{long}